Skip to content

Commit 0a5f713

Browse files
client_routes: add sticky route tracking
When a host has multiple routes (one per connection), remember which connectionID was used on the first successful TranslateHost call and prefer it on subsequent calls via findPreferredRoute. If the preferred route is removed (e.g. connection pruned), fall back to FindByHostID. This avoids unnecessary connection churn when multiple PrivateLink endpoints serve the same host.
1 parent 3d4734a commit 0a5f713

2 files changed

Lines changed: 79 additions & 4 deletions

File tree

client_routes.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ type ClientRoutesHandler struct {
185185
resolver DNSResolver
186186
sub *eventbus.Subscriber[events.Event]
187187
routes UnresolvedClientRouteList
188+
stickyRoute map[string]string // hostID → preferred connectionID
188189
updateTasks chan updateTask
189190
closeChan chan struct{}
190191
cfg ClientRoutesConfig
@@ -208,6 +209,20 @@ func pickProperPort(pickTLSPorts bool, rec *UnresolvedClientRoute) uint16 {
208209
return rec.CQLPort
209210
}
210211

212+
// findPreferredRoute returns the route for hostID that matches the sticky
213+
// connectionID, falling back to the first route for that host.
214+
// Must be called with p.mu held (at least RLock).
215+
func (p *ClientRoutesHandler) findPreferredRoute(hostID string) *UnresolvedClientRoute {
216+
if preferred, ok := p.stickyRoute[hostID]; ok {
217+
for i := range p.routes {
218+
if p.routes[i].HostID == hostID && p.routes[i].ConnectionID == preferred {
219+
return &p.routes[i]
220+
}
221+
}
222+
}
223+
return p.routes.FindByHostID(hostID)
224+
}
225+
211226
// TranslateHost implements AddressTranslatorV2 interface.
212227
// It resolves DNS on every call rather than caching resolved addresses.
213228
func (p *ClientRoutesHandler) TranslateHost(host AddressTranslatorHostInfo, addr AddressPort) (AddressPort, error) {
@@ -217,7 +232,7 @@ func (p *ClientRoutesHandler) TranslateHost(host AddressTranslatorHostInfo, addr
217232
}
218233

219234
p.mu.RLock()
220-
rec := p.routes.FindByHostID(hostID)
235+
rec := p.findPreferredRoute(hostID)
221236
var route UnresolvedClientRoute
222237
found := rec != nil
223238
if found {
@@ -242,6 +257,10 @@ func (p *ClientRoutesHandler) TranslateHost(host AddressTranslatorHostInfo, addr
242257
return addr, fmt.Errorf("record %s/%s has target port empty", route.HostID, route.ConnectionID)
243258
}
244259

260+
p.mu.Lock()
261+
p.stickyRoute[hostID] = route.ConnectionID
262+
p.mu.Unlock()
263+
245264
return AddressPort{Address: ips[0], Port: port}, nil
246265
}
247266

@@ -393,6 +412,7 @@ func NewClientRoutesAddressTranslator(
393412
updateTasks: make(chan updateTask, 1024),
394413
resolver: resolver,
395414
routes: make(UnresolvedClientRouteList, 0),
415+
stickyRoute: make(map[string]string),
396416
}
397417
}
398418

client_routes_unit_test.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,9 @@ func TestClientRoutesHandlerTranslateHost(t *testing.T) {
119119
})
120120

121121
handler := &ClientRoutesHandler{
122-
resolver: resolver,
123-
routes: make(UnresolvedClientRouteList, 0),
122+
stickyRoute: make(map[string]string),
123+
resolver: resolver,
124+
routes: make(UnresolvedClientRouteList, 0),
124125
}
125126

126127
res, err := handler.TranslateHost(noHost, addr)
@@ -162,14 +163,68 @@ func TestClientRoutesHandlerTranslateHost(t *testing.T) {
162163
resolver: dnsResolverFunc(func(host string) ([]net.IP, error) {
163164
return nil, errors.New("lookup failed")
164165
}),
165-
routes: UnresolvedClientRouteList{{ConnectionID: "c2", HostID: "h2", Address: "host", CQLPort: 9042}},
166+
stickyRoute: make(map[string]string),
167+
routes: UnresolvedClientRouteList{{ConnectionID: "c2", HostID: "h2", Address: "host", CQLPort: 9042}},
166168
}
167169
_, err = errorHandler.TranslateHost(testHostInfo{hostID: "h2"}, addr)
168170
if err == nil {
169171
t.Fatalf("expected resolver error to bubble up")
170172
}
171173
}
172174

175+
func TestTranslateHost_StickyRoute(t *testing.T) {
176+
addr := AddressPort{Address: net.ParseIP("1.1.1.1"), Port: 9042}
177+
resolvedIPs := map[string]net.IP{
178+
"addr-c1": net.ParseIP("10.0.0.1"),
179+
"addr-c2": net.ParseIP("10.0.0.2"),
180+
}
181+
handler := &ClientRoutesHandler{
182+
pickTLSPorts: false,
183+
stickyRoute: make(map[string]string),
184+
resolver: dnsResolverFunc(func(host string) ([]net.IP, error) {
185+
if ip, ok := resolvedIPs[host]; ok {
186+
return []net.IP{ip}, nil
187+
}
188+
return nil, fmt.Errorf("unknown host %s", host)
189+
}),
190+
routes: UnresolvedClientRouteList{
191+
{ConnectionID: "c1", HostID: "h1", Address: "addr-c1", CQLPort: 9042},
192+
{ConnectionID: "c2", HostID: "h1", Address: "addr-c2", CQLPort: 9042},
193+
},
194+
}
195+
196+
// First call picks the first route (c1) and records it as sticky.
197+
res, err := handler.TranslateHost(testHostInfo{hostID: "h1"}, addr)
198+
if err != nil {
199+
t.Fatalf("unexpected error: %v", err)
200+
}
201+
if !res.Address.Equal(net.ParseIP("10.0.0.1")) {
202+
t.Fatalf("expected first route IP 10.0.0.1, got %v", res.Address)
203+
}
204+
205+
// Second call should stick to c1 even though c2 also matches h1.
206+
res, err = handler.TranslateHost(testHostInfo{hostID: "h1"}, addr)
207+
if err != nil {
208+
t.Fatalf("unexpected error: %v", err)
209+
}
210+
if !res.Address.Equal(net.ParseIP("10.0.0.1")) {
211+
t.Fatalf("expected sticky route IP 10.0.0.1, got %v", res.Address)
212+
}
213+
214+
// Remove c1 route; sticky route should fall back to c2.
215+
handler.mu.Lock()
216+
handler.routes = handler.routes[1:]
217+
handler.mu.Unlock()
218+
219+
res, err = handler.TranslateHost(testHostInfo{hostID: "h1"}, addr)
220+
if err != nil {
221+
t.Fatalf("unexpected error: %v", err)
222+
}
223+
if !res.Address.Equal(net.ParseIP("10.0.0.2")) {
224+
t.Fatalf("expected fallback to c2 IP 10.0.0.2, got %v", res.Address)
225+
}
226+
}
227+
173228
func TestGetHostPortMappingFromClusterQuery(t *testing.T) {
174229
tcases := []struct {
175230
name string

0 commit comments

Comments
 (0)