feat(agentnetwork): record which proxy serves an account

Adds ServingProxyAddress to the agent-network settings row and makes the
synthesized service advertise it as the proxy that serves the account. Empty
-- the value for every existing row -- means the shared proxy named by
Cluster serves the account, which is the previous behaviour exactly.

This is the only mutable column on the row. Cluster, Subdomain and Zone are
fixed once written, but moving an account onto a dedicated proxy, and moving
it back, is exactly one write here. Because mesh DNS peer selection and the
mapping-delivery filter both join on the advertised address, that single write
is what redirects an account's traffic, with no change to either join.

Only the advertised address follows the new column. The placeholder target's
ID stays pinned to Cluster: it names a target the router rewrites per request
and is not a routing decision.

Nothing in this repository writes the column; it is set by whatever process
assigns dedicated proxies. A store setter is included so that assignment can
be a single scoped update rather than a read-modify-write over the whole row,
which would race with unrelated settings changes.
This commit is contained in:
Brad Ison
2026-08-03 20:42:35 +02:00
parent e493efd532
commit 7f3a29d688
8 changed files with 183 additions and 6 deletions

View File

@@ -385,6 +385,24 @@ func (s *SqlStore) CreateAgentNetworkSettings(ctx context.Context, settings *age
return nil
}
// SetAgentNetworkServingProxyAddress points the account's gateway at a specific
// serving proxy, or clears it (address == "") to return the account to the
// shared proxy. Scoped to the one column on purpose: this runs concurrently
// with unrelated settings updates, and a full-row upsert would clobber them.
func (s *SqlStore) SetAgentNetworkServingProxyAddress(ctx context.Context, accountID, address string) error {
result := s.db.Model(&agentNetworkTypes.Settings{}).
Where("account_id = ?", accountID).
Update("serving_proxy_address", address)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to set agent network serving proxy address: %v", result.Error)
return status.Errorf(status.Internal, "failed to set agent network serving proxy address")
}
if result.RowsAffected == 0 {
return status.Errorf(status.NotFound, "agent network settings for account %s not found", accountID)
}
return nil
}
// IncrementAgentNetworkConsumption atomically upserts the consumption
// row keyed on (account, dim_kind, dim_id, window_seconds, window_start)
// and adds the supplied deltas. Concurrent calls from multiple proxy

View File

@@ -364,6 +364,7 @@ type Store interface {
GetAgentNetworkSettingsBySubdomain(ctx context.Context, lockStrength LockingStrength, subdomain string) (*agentNetworkTypes.Settings, error)
SaveAgentNetworkSettings(ctx context.Context, settings *agentNetworkTypes.Settings) error
CreateAgentNetworkSettings(ctx context.Context, settings *agentNetworkTypes.Settings) error
SetAgentNetworkServingProxyAddress(ctx context.Context, accountID, address string) error
IncrementAgentNetworkConsumption(ctx context.Context, accountID string, kind agentNetworkTypes.ConsumptionDimension, dimID string, windowSeconds int64, windowStart time.Time, tokensIn, tokensOut int64, costUSD float64) error
IncrementAgentNetworkConsumptionBatch(ctx context.Context, accountID string, keys []agentNetworkTypes.ConsumptionKey, tokensIn, tokensOut int64, costUSD float64) error
GetAgentNetworkConsumption(ctx context.Context, lockStrength LockingStrength, accountID string, kind agentNetworkTypes.ConsumptionDimension, dimID string, windowSeconds int64, windowStart time.Time) (*agentNetworkTypes.Consumption, error)

View File

@@ -3666,6 +3666,20 @@ func (mr *MockStoreMockRecorder) SaveUsers(ctx, users interface{}) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUsers", reflect.TypeOf((*MockStore)(nil).SaveUsers), ctx, users)
}
// SetAgentNetworkServingProxyAddress mocks base method.
func (m *MockStore) SetAgentNetworkServingProxyAddress(ctx context.Context, accountID, address string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetAgentNetworkServingProxyAddress", ctx, accountID, address)
ret0, _ := ret[0].(error)
return ret0
}
// SetAgentNetworkServingProxyAddress indicates an expected call of SetAgentNetworkServingProxyAddress.
func (mr *MockStoreMockRecorder) SetAgentNetworkServingProxyAddress(ctx, accountID, address interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAgentNetworkServingProxyAddress", reflect.TypeOf((*MockStore)(nil).SetAgentNetworkServingProxyAddress), ctx, accountID, address)
}
// SetFieldEncrypt mocks base method.
func (m *MockStore) SetFieldEncrypt(enc *crypt.FieldEncrypt) {
m.ctrl.T.Helper()

View File

@@ -364,8 +364,9 @@ func (a *Account) privateServiceDomainZone(svc *service.Service) string {
// explicitly: it is server config, so it matches neither the serving
// proxy's address nor any per-account custom-domain row. Checked first so
// the apex stays the zone even once ProxyCluster becomes the tenant
// hostname itself (a private managed proxy), which would otherwise make the
// apex the full hostname and churn the client's zone set on cutover.
// hostname itself (which happens when a dedicated per-account proxy serves
// it), which would otherwise make the apex the full hostname and churn the
// client's zone set when a tenant moves between proxies.
if svc.DNSZone != "" && domainFromSuffix(svc.Domain, svc.DNSZone) {
return svc.DNSZone
}