From 45cf87a0cb6e11e5a5f9cb4ee4e840811d6e1a1e Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Thu, 23 Apr 2026 19:42:33 +1000 Subject: [PATCH] feat(caddy): order local machine upstreams first in generated Caddyfile --- internal/machine/caddyconfig/caddyfile.go | 29 ++++++++++++++----- .../machine/caddyconfig/caddyfile_test.go | 19 ++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/internal/machine/caddyconfig/caddyfile.go b/internal/machine/caddyconfig/caddyfile.go index 292e8a75..06c471ff 100644 --- a/internal/machine/caddyconfig/caddyfile.go +++ b/internal/machine/caddyconfig/caddyfile.go @@ -112,17 +112,23 @@ func NewCaddyfileGenerator( func (g *CaddyfileGenerator) Generate( ctx context.Context, records []store.ContainerRecord, includeCustom bool, ) (string, error) { + // Sort records by local machine first, then by service name and creation time. Placing containers on the local + // machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send + // traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when + // the local one is unhealthy. + // The service name and creation time tiebreakers keep the generated Caddyfile stable across regenerations. + slices.SortStableFunc(records, func(a, b store.ContainerRecord) int { + return cmp.Or( + g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID), + strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()), + a.Container.CreatedTime().Compare(b.Container.CreatedTime()), + ) + }) + containers := make([]api.ServiceContainer, len(records)) for i, cr := range records { containers[i] = cr.Container } - // Sort containers by service name and creation time to generate a stable Caddyfile. - slices.SortStableFunc(containers, func(a, b api.ServiceContainer) int { - return cmp.Or( - strings.Compare(a.ServiceName(), b.ServiceName()), - a.CreatedTime().Compare(b.CreatedTime()), - ) - }) caddyfile, err := g.generateBaseFromPorts(containers) if err != nil { @@ -243,6 +249,15 @@ func (g *CaddyfileGenerator) Generate( return caddyfileHeader + "\n" + caddyfile, nil } +// localMachineRank returns 0 if the given machineID matches the local machine and 1 otherwise. +// Useful for sorting containers running locally first. +func (g *CaddyfileGenerator) localMachineRank(machineID string) int { + if g.machineID == machineID { + return 0 + } + return 1 +} + func (g *CaddyfileGenerator) generateBaseFromPorts(containers []api.ServiceContainer) (string, error) { httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromPorts(containers) diff --git a/internal/machine/caddyconfig/caddyfile_test.go b/internal/machine/caddyconfig/caddyfile_test.go index 5b89534e..447333df 100644 --- a/internal/machine/caddyconfig/caddyfile_test.go +++ b/internal/machine/caddyconfig/caddyfile_test.go @@ -159,6 +159,25 @@ https://secure.example.com { } log } +`, + }, + { + name: "local machine upstreams listed first", + containers: []store.ContainerRecord{ + newContainerRecord(newContainer("10.210.0.2", "app.example.com:8080/http"), "mach1"), + newContainerRecord(newContainer("10.210.1.2", "app.example.com:8080/http"), "test-machine-id"), + newContainerRecord(newContainer("10.210.2.2", "app.example.com:8080/http"), "mach2"), + newContainerRecord(newContainer("10.210.1.3", "app.example.com:8080/http"), "test-machine-id"), + }, + want: testCaddyfileHeader + ` +# Sites generated from service ports. + +http://app.example.com { + reverse_proxy 10.210.1.2:8080 10.210.1.3:8080 10.210.0.2:8080 10.210.2.2:8080 { + import common_proxy + } + log +} `, }, {