Add the ProxyAdminService proto - #258
Merged
Merged
Conversation
liam-lowe
force-pushed
the
liam-lowe/proxyadmin-service-proto
branch
from
August 21, 2026 00:55
279d226 to
3e4f0c0
Compare
liam-lowe
force-pushed
the
liam-lowe/proxyadmin-service-proto
branch
9 times, most recently
from
August 24, 2026 22:10
4a6b862 to
b4d1ef4
Compare
liam-lowe
force-pushed
the
liam-lowe/proxyadmin-service-proto
branch
2 times, most recently
from
August 24, 2026 22:49
8ccc7d4 to
b6525a5
Compare
liam-lowe
force-pushed
the
liam-lowe/proxyadmin-service-proto
branch
5 times, most recently
from
August 25, 2026 04:36
2b490f6 to
7298853
Compare
JayChung0258
approved these changes
Aug 28, 2026
JayChung0258
left a comment
Contributor
There was a problem hiding this comment.
How do we determine CONNECTED at the group level?
For example, suppose muxCount is configured as 30 and the expected deployment has three pods. The happy path would be:
3 pods × 10 sessions = 30 connected / 30 total / 30 target
Would two pods with 15 sessions each still be considered CONNECTED because the aggregate is 30 / 30 / 30? What about one pod holding all 30 sessions?
We have seen cases where one pod holds all sessions but replication still gets stuck. Should CONNECTED also require the expected number of members and a healthy session distribution across them, rather than only matching the aggregate target?
The schema, the generated stubs, the make targets and the drift workflow are one unit: CI regenerates and diffs api/, so a change to any of them without the others fails the gate. Requests carry no routing fields. Scope and target travel as gRPC metadata, because an interceptor cannot read a request field without reflection, so with fields every listener's limits would have to be re-checked inside every handler and any RPC added later would be exposed until someone remembered. The response is one list. Cluster connections carry their members, and each member row nests the member that reported it, so reading a member's version next to its session counts needs no join. A member appears once per connection it reports on and those facts repeat, which is the cost of not having a second list. Nothing describes the group as a whole. The responder is the member marked self. Session counts carry a target as well as a total. total is what a member holds now, target is what it was configured to hold. Without target a connection holding three sessions of a configured ten reports connected == total, which is the condition this endpoint exists to reveal. ConnectionState is UNSPECIFIED, CONNECTED and ERROR, and will grow. Partial connectivity is ERROR rather than CONNECTED: three sessions of a configured ten is the condition this endpoint exists to reveal, not a healthy link. UNSPECIFIED means no state was observed, which covers a field nobody set and a connection this API does not describe, such as one that is not multiplexed. A member that answered nothing has no row to appear in. It shows up only where every member that did answer was connected, because a connection that would otherwise read CONNECTED is held at ERROR instead. On a connection already reading ERROR it leaves no trace, and a three-member group with one member down is then indistinguishable from a two-member group. buf is pinned in develop/buf.mod and run through the Go toolchain rather than installed separately. The codegen plugins were already remote and version pinned in buf.gen.yaml, so the buf version only governed the CLI, and CI and a developer's machine could still disagree about it. buf gets its own module rather than joining tools.mod. Its dependency graph pulls quic-go and docker, whose versions do not resolve against tools.mod's existing pins: adding it there produces a module where buf does not compile. proto-lint and proto-breaking are make targets so the workflow runs what a developer runs. The drift step diffs the whole tree rather than api/ alone, so generation that writes anywhere is caught. Nothing describes the Temporal cluster a connection fronts. That is ServerDescription under another name, it is not topology, and it would make the proxy originate its own DescribeCluster call to the local frontend. Field 5 on ClusterConnection is left unused rather than renumbered.
clean: true removes everything under out: before generating, and out: is api/. The setting is safe today: api/ does not exist on main and is generated in full. Anything that ever lands there by another route is deleted on the next generation run without warning.
liam-lowe
force-pushed
the
liam-lowe/proxyadmin-service-proto
branch
from
August 31, 2026 21:25
7298853 to
11dae97
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
ProxyAdminServiceprotobuf service boilerplate.The service starts with one RPC,
DescribeClusterConnections, which reports a proxy and the cluster connections it has to other proxies.Routing
Routing is interesting:
Each proxy (already) defines in config the desired cluster connections it reaches - these will be the connections reported on.
For each cluster connection, we will report on reachability and configuration. is the cluster connection reachable - can we connect, is it stable? how was the proxy configured, what dynamic config does the cluster have, what version is the cluster on? etc.
When hitting the
DescribeClusterConnectionsendpoint, the call will describe itself. Ie -DescribeClusterConnections(target = self). In order to reach another cluster connection (one defined in the self's config)we set a grpc metadata fors2s-proxy-target. This will forward the call to that particular proxy. Ie -DescribeClusterConnections(target = oneOfTheCounterpartyClusterConnections)In order to forward the call, we reach the counterparty proxy through its own existing mux connection. If the endpoint can't be reached, the cluster connection is considered not reachable. We don't expose a public API per proxy because: 1. we already have the mux connection; 2. that would required increased networking and security surface area.
Once a cluster is reached, the response is determined, and returned to the called. Determining the response is interesting. We need to ensure: 1. all pods serving that cluster connection are on the same page (deployments, rollouts, etc) and 2. some response values are aggregate across all pods (ie total connected muxes). Thus, we need to hit all pods in the cluster connection and aggregate their response.
Once a cluster connection is reached by an external cluster connection - it performs a fanout to all proxy pods within its pool. This is controlled by another gRPC metadata -
s2s-proxy-scope. With scope - the pod will either response formember(itself) orgroupwhere it does a fanout to all members (now passing scope asmemberand aggregating the result).I opted for using metadata instead of request attributes - as this functionality will need to be built into more
ProxyAdminServicemethods in the future - and this should scale more easilyTo determine members of a pool, we now provide
memberlistconfiguration on startup. We are keeping this configuration extensible, to support numerous approaches to memberlists. For now, a basic dns discovery will be supported.As diagrams
some diagrams to visualize the routing a little easier:
1.
s2s-proxy-scope: how many members of this group answer.member(just itself) the node hitflowchart LR subgraph M["s2s-proxy-scope: member"] direction LR O1(["operator"]) -->|call| A1["member-1"] A1 -->|"answers for itself"| R1(["one member"]) endgroupvia fanout. with the originally hitmemberaggregating.flowchart LR subgraph G["s2s-proxy-scope: group (the default when absent)"] direction LR O2(["operator"]) -->|call| B1["member-1"] B1 -.->|fan out| B2["member-2"] B1 -.->|fan out| B3["member-3"] B2 -.->|reply| B1 B3 -.->|reply| B1 B1 -->|"one merged answer"| R2(["whole group"]) end2.
s2s-proxy-target: which cluster connection answers the query? is it itself, or forwarded to a counterparty ( a cluster connection listed in the config)s2s-proxy-targetnames a cluster connection. The call is forwarded once to that connection's counterparty, which answers atgroupscope for its own group. grpc metadata is not propagated to outgoing calls - so calls will be forwarded exactly once.DescribeClusterConnectionsThe request is empty - at the moment this call just describes the state and everything the proxy knows.
The response is a single list. Each cluster connection carries its members, and each member reports its respective information.
Worked example
Requested at the default
groupscope against a three-member group with three cluster connections configured.Request. Empty. Everything that varies travels in metadata.
grpcurl -plaintext \ -H 's2s-proxy-scope: group' \ localhost:6061 \ temporal.s2sproxy.proxyadmin.v1.ProxyAdminService/DescribeClusterConnections{}Response. Every connection in config is listed, whether or not it is up.
{ "clusterConnections": [ { "name": "prod-migration", "state": "CONNECTION_STATE_CONNECTED", "muxSessionsConnected": 30, "muxSessionsTotal": 30, "members": [ { "identity": { "id": "s2s-proxy-0", "self": true, "version": "1.42.0", "startTime": "2026-08-24T09:14:02Z" }, "state": "CONNECTION_STATE_CONNECTED", "muxSessionsConnected": 10, "muxSessionsTotal": 10 }, { "identity": { "id": "s2s-proxy-1", "address": "10.4.1.19:9234", "version": "1.42.0", "startTime": "2026-08-24T09:14:07Z" }, "state": "CONNECTION_STATE_CONNECTED", "muxSessionsConnected": 10, "muxSessionsTotal": 10 }, { "identity": { "id": "s2s-proxy-2", "address": "10.4.1.23:9234", "version": "1.41.2", "startTime": "2026-08-21T18:02:55Z" }, "state": "CONNECTION_STATE_CONNECTED", "muxSessionsConnected": 10, "muxSessionsTotal": 10 } ] }, { "name": "dr-standby", "state": "CONNECTION_STATE_ERROR", "muxSessionsTotal": 30, "members": [ { "identity": { "id": "s2s-proxy-0", "self": true, "version": "1.42.0", "startTime": "2026-08-24T09:14:02Z" }, "state": "CONNECTION_STATE_ERROR", "muxSessionsTotal": 10 }, { "identity": { "id": "s2s-proxy-1", "address": "10.4.1.19:9234", "version": "1.42.0", "startTime": "2026-08-24T09:14:07Z" }, "state": "CONNECTION_STATE_ERROR", "muxSessionsTotal": 10 }, { "identity": { "id": "s2s-proxy-2", "address": "10.4.1.23:9234", "version": "1.41.2", "startTime": "2026-08-21T18:02:55Z" }, "state": "CONNECTION_STATE_ERROR", "muxSessionsTotal": 10 } ] }, { "name": "eu-migration", "state": "CONNECTION_STATE_ERROR", "muxSessionsConnected": 20, "muxSessionsTotal": 20, "members": [ { "identity": { "id": "s2s-proxy-0", "self": true, "version": "1.42.0", "startTime": "2026-08-24T09:14:02Z" }, "state": "CONNECTION_STATE_CONNECTED", "muxSessionsConnected": 10, "muxSessionsTotal": 10 }, { "identity": { "id": "s2s-proxy-1", "address": "10.4.1.19:9234", "version": "1.42.0", "startTime": "2026-08-24T09:14:07Z" }, "state": "CONNECTION_STATE_CONNECTED", "muxSessionsConnected": 10, "muxSessionsTotal": 10 }, { "identity": { "id": "s2s-proxy-2", "address": "10.4.1.23:9234", "version": "1.41.2", "startTime": "2026-08-21T18:02:55Z" }, "state": "CONNECTION_STATE_ERROR" } ] } ] }