Skip to content

Commit 8b9c5bf

Browse files
authored
Add location-aware routing (#221)
* Add location-aware routing for proxy targets and provisioning Ling proxy now prefers targets on kings matching its preferred location, falling back to all targets when none match. Council provisioner prefers kings matching a service's preferred location when assigning ports. Closes #14 * Fix lint issues: golines, wsl, unparam
1 parent 4c1a607 commit 8b9c5bf

4 files changed

Lines changed: 257 additions & 22 deletions

File tree

internal/council/provisioner.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ func collectUsedPorts(king *state.King, currentState *state.State) map[int]bool
8282
return used
8383
}
8484

85-
// ProvisionService assigns the first available king port to a service.
85+
// ProvisionService assigns the first available king port to a service,
86+
// preferring kings that match the service's PreferredLocation.
8687
func ProvisionService(currentState *state.State, service *state.Service) {
8788
available := AvailableKingPorts(currentState)
8889
if len(available) == 0 {
@@ -91,19 +92,30 @@ func ProvisionService(currentState *state.State, service *state.Service) {
9192
return
9293
}
9394

94-
first := available[0]
95-
remotePort := first.Ports[0]
95+
selected := available[0]
9696

97-
service.Host = &first.King.Host
97+
if service.PreferredLocation != "" {
98+
for _, candidate := range available {
99+
if candidate.King.Location == service.PreferredLocation {
100+
selected = candidate
101+
102+
break
103+
}
104+
}
105+
}
106+
107+
remotePort := selected.Ports[0]
108+
109+
service.Host = &selected.King.Host
98110
service.RemotePort = &remotePort
99-
service.BindPort = &first.King.BindPort
111+
service.BindPort = &selected.King.BindPort
100112

101113
currentState.Revision++
102114

103115
slog.Info("Provisioned service",
104116
"name", service.Name,
105-
"host", first.King.Host,
106-
"bind_port", first.King.BindPort,
117+
"host", selected.King.Host,
118+
"bind_port", selected.King.BindPort,
107119
"remote_port", remotePort,
108120
)
109121
}

internal/council/provisioner_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,101 @@ func TestProvision_NoReprovisioning_WhenNoAvailableKing(t *testing.T) {
568568
t.Fatalf("expected KingReady to be false")
569569
}
570570
}
571+
572+
func newTestKingWithLocation(
573+
host, ports string, bindPort int, location string,
574+
) state.King {
575+
return state.King{
576+
BindPort: bindPort,
577+
Host: host,
578+
Ports: ports,
579+
ShuttingDown: false,
580+
Beat: 0,
581+
Location: location,
582+
CertPEM: "",
583+
}
584+
}
585+
586+
func TestProvisionService_PrefersMatchingLocation(t *testing.T) {
587+
t.Parallel()
588+
589+
currentState := &state.State{
590+
Revision: 0,
591+
Kings: []state.King{
592+
newTestKingWithLocation(testHostA, "5000-5001", 2333, "us-east"),
593+
newTestKingWithLocation(testHostB, "6000-6001", 2334, "eu-west"),
594+
},
595+
Services: []state.Service{
596+
{
597+
ServiceID: "svc-1",
598+
Name: "alpha",
599+
Token: "",
600+
LingID: "",
601+
PreferredLocation: "eu-west",
602+
LingReady: false,
603+
KingReady: false,
604+
Host: nil,
605+
BindPort: nil,
606+
RemotePort: nil,
607+
},
608+
},
609+
Lings: nil,
610+
}
611+
612+
council.ProvisionService(currentState, &currentState.Services[0])
613+
614+
svc := currentState.Services[0]
615+
assertServiceHost(t, svc, testHostB)
616+
assertServiceBindPort(t, svc, 2334)
617+
assertServiceRemotePort(t, svc, 6000)
618+
}
619+
620+
func TestProvisionService_FallsBackWhenPreferredLocationFull(t *testing.T) {
621+
t.Parallel()
622+
623+
hostB := testHostB
624+
bindPortB := 2334
625+
port6000 := 6000
626+
627+
currentState := &state.State{
628+
Revision: 0,
629+
Kings: []state.King{
630+
newTestKingWithLocation(testHostA, "5000-5001", 2333, "us-east"),
631+
newTestKingWithLocation(testHostB, "6000-6000", 2334, "eu-west"),
632+
},
633+
Services: []state.Service{
634+
{
635+
ServiceID: "existing",
636+
Name: "existing",
637+
Token: "",
638+
LingID: "",
639+
PreferredLocation: "",
640+
LingReady: false,
641+
KingReady: false,
642+
Host: &hostB,
643+
BindPort: &bindPortB,
644+
RemotePort: &port6000,
645+
},
646+
{
647+
ServiceID: "svc-1",
648+
Name: "alpha",
649+
Token: "",
650+
LingID: "",
651+
PreferredLocation: "eu-west",
652+
LingReady: false,
653+
KingReady: false,
654+
Host: nil,
655+
BindPort: nil,
656+
RemotePort: nil,
657+
},
658+
},
659+
Lings: nil,
660+
}
661+
662+
council.ProvisionService(currentState, &currentState.Services[1])
663+
664+
svc := currentState.Services[1]
665+
assertServiceHost(t, svc, testHostA)
666+
assertServiceBindPort(t, svc, 2333)
667+
assertServiceRemotePort(t, svc, 5000)
668+
}

internal/ling/ling.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func run(
253253
stateMutex.Unlock()
254254

255255
OnStateChanged(
256-
ctx, stateSnapshot, lingID,
256+
ctx, stateSnapshot, lingID, preferredLocation,
257257
tunnelMap, tunnelCli, tcpProxies,
258258
)
259259
},
@@ -435,13 +435,14 @@ func OnStateChanged(
435435
ctx context.Context,
436436
stateSnapshot *state.State,
437437
lingID string,
438+
preferredLocation string,
438439
tunnelMap map[string]string,
439440
tunnelCli *TunnelClient,
440441
tcpProxies map[string]*TCPProxy,
441442
) {
442443
kings := buildKingIndex(stateSnapshot)
443444
updateTunnelConnections(ctx, stateSnapshot, lingID, tunnelMap, tunnelCli, kings)
444-
updateProxyTargets(stateSnapshot, tcpProxies, kings)
445+
updateProxyTargets(stateSnapshot, preferredLocation, tcpProxies, kings)
445446
}
446447

447448
// KingGroup holds the connection details for a group of services on a single king.
@@ -454,16 +455,21 @@ type KingGroup struct {
454455

455456
// KingIndex stores health and certificate info for a king address.
456457
type KingIndex struct {
457-
healthy bool
458-
certPEM string
458+
healthy bool
459+
certPEM string
460+
location string
459461
}
460462

461463
func buildKingIndex(stateSnapshot *state.State) map[string]KingIndex {
462464
index := make(map[string]KingIndex, len(stateSnapshot.Kings))
463465

464466
for _, king := range stateSnapshot.Kings {
465467
addr := net.JoinHostPort(king.Host, strconv.Itoa(king.BindPort))
466-
index[addr] = KingIndex{healthy: !king.ShuttingDown, certPEM: king.CertPEM}
468+
index[addr] = KingIndex{
469+
healthy: !king.ShuttingDown,
470+
certPEM: king.CertPEM,
471+
location: king.Location,
472+
}
467473
}
468474

469475
return index
@@ -551,12 +557,13 @@ func updateTunnelConnections(
551557

552558
func updateProxyTargets(
553559
stateSnapshot *state.State,
560+
preferredLocation string,
554561
tcpProxies map[string]*TCPProxy,
555562
kings map[string]KingIndex,
556563
) {
557564
for proxyName, proxy := range tcpProxies {
558565
targets := collectProxyTargets(
559-
stateSnapshot, proxyName, kings,
566+
stateSnapshot, proxyName, preferredLocation, kings,
560567
)
561568
proxy.updateTargets(targets)
562569
}
@@ -589,20 +596,36 @@ func isServiceEligibleForProxy(
589596
func collectProxyTargets(
590597
stateSnapshot *state.State,
591598
proxyName string,
599+
preferredLocation string,
592600
kings map[string]KingIndex,
593601
) []ProxyTarget {
594-
targets := make([]ProxyTarget, 0, len(stateSnapshot.Services))
602+
var allTargets []ProxyTarget
603+
604+
var preferredTargets []ProxyTarget
595605

596606
for _, svc := range stateSnapshot.Services {
597607
if !isServiceEligibleForProxy(svc, proxyName, kings) {
598608
continue
599609
}
600610

601-
targets = append(targets, ProxyTarget{
611+
target := ProxyTarget{
602612
host: *svc.Host,
603613
remotePort: *svc.RemotePort,
604-
})
614+
}
615+
616+
allTargets = append(allTargets, target)
617+
618+
if preferredLocation != "" {
619+
kingAddr := net.JoinHostPort(*svc.Host, strconv.Itoa(*svc.BindPort))
620+
if king, exists := kings[kingAddr]; exists && king.location == preferredLocation {
621+
preferredTargets = append(preferredTargets, target)
622+
}
623+
}
624+
}
625+
626+
if len(preferredTargets) > 0 {
627+
return preferredTargets
605628
}
606629

607-
return targets
630+
return allTargets
608631
}

0 commit comments

Comments
 (0)