package activator import ( "context" "net/http" "net/http/httptest" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/seeds" ) // Final review F4: an import's row is cached at the edge; the rollback // closes the gate under its fence, puts the legacy address-backed route // back and lifts the fence; a verify request then arrives before the edge // reloads. The wake refreshes the row, finds the legacy route, and stops: // no lease is taken and nothing is restored. The edge is never reloaded. func TestAVerifyOnACachedImportRowAfterItsRollbackWakesNothing(t *testing.T) { t.Parallel() a, b, _, reg := testActivator(t, time.Hour) a.Routes = reg const host = "old.shop.example.com" if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Addr: "127.0.0.1:8401", App: "shop", Account: "bab"}); err != nil { t.Fatal(err) } original, err := reg.Get(host) if err != nil || original == nil { t.Fatalf("the legacy route: %+v, %v", original, err) } id, err := seeds.NewID() if err != nil { t.Fatal(err) } token := "s3cret-token" imported := registry.Route{Host: host, Kind: "instance", App: "shop", Account: "bab", Provisioned: true, Hibernate: true, Socket: "/run/carlos/instances/old/app.sock", DBPath: "/data/instances/old/" + host + ".db", ImportGate: seeds.HashToken(token), SeedSource: seeds.RowSeed{ID: id, Kind: seeds.KindImport, State: seeds.StateSwapped}.Encode()} if err := reg.SwapForImport(registry.ImportJournal{Host: host, SeedID: id, Original: *original, RecordKey: "apps/bab/shop/instances/" + host + ".json", At: 1}, imported); err != nil { t.Fatal(err) } edge, err := newTestRouter(reg, a) // the edge caches the gated import row if err != nil { t.Fatal(err) } // The rollback, steps 1 to 5. const gen = 7 if err := reg.CloseImportGate(host, id, gen); err != nil { t.Fatal(err) } fctx, cancel := context.WithTimeout(context.Background(), time.Second) _, _ = a.FenceAndStopIfUnfenced(fctx, host, gen) // nothing is running: the stop is unproven, the fence is held cancel() if a.FenceGeneration(host) != gen { t.Fatal("the rollback's fence was not installed") } if err := reg.RestoreImportRoute(host, id); err != nil { t.Fatal(err) } if !a.LiftFence(host, gen) { t.Fatal("the rollback's fence was not lifted") } req := httptest.NewRequest(http.MethodGet, "http://"+host+"/up", nil) req.Header.Set("Carlos-Import-Verify", token) rec := httptest.NewRecorder() edge.ServeHTTP(rec, req) if rec.Code == http.StatusOK { t.Fatalf("the verify was answered %d through the restored legacy route's wake", rec.Code) } b.mu.Lock() acquires, restores := b.acquires, b.restores b.mu.Unlock() if acquires != 0 || restores != 0 { t.Fatalf("the wake took %d leases and ran %d restores for a route the activator no longer manages", acquires, restores) } if got, _ := reg.Get(host); got == nil || got.Addr != original.Addr || got.ImportGate != "" { t.Fatalf("the legacy route after the refused wake = %+v", got) } } // The refreshed row is compared with the row the wake began from: a row // the activator does not manage, or one that is another route (account, // app, address, import gate, seed), is refused. Paths, versions and a // seed's own state moving on are not. func TestWakeRouteChanged(t *testing.T) { id, _ := seeds.NewID() other, _ := seeds.NewID() began := testRoute("h.example.com") began.Account, began.App = "bab", "shop" began.SeedSource = seeds.RowSeed{ID: id, Kind: seeds.KindReplica, State: seeds.StateReady}.Encode() for _, c := range []struct { name string edit func(*registry.Route) refused bool }{ {"unchanged", func(*registry.Route) {}, false}, {"paths moved", func(r *registry.Route) { r.DBPath, r.Socket = "/data/instances/h/h.db", "/run/carlos/instances/h/app.sock" }, false}, {"the seed placed", func(r *registry.Route) { r.SeedSource = seeds.RowSeed{ID: id, Kind: seeds.KindReplica, State: seeds.StatePlaced}.Encode() }, false}, {"not hibernating", func(r *registry.Route) { r.Hibernate = false }, true}, {"no socket", func(r *registry.Route) { r.Socket, r.Addr = "", "127.0.0.1:8401" }, true}, {"unit-backed", func(r *registry.Route) { r.Backing = "unit" }, true}, {"another account", func(r *registry.Route) { r.Account = "cdef" }, true}, {"another app", func(r *registry.Route) { r.App = "blog" }, true}, {"an address", func(r *registry.Route) { r.Addr = "127.0.0.1:8401" }, true}, {"gated", func(r *registry.Route) { r.ImportGate = registry.ImportGateClosed }, true}, {"another seed", func(r *registry.Route) { r.SeedSource = seeds.RowSeed{ID: other, Kind: seeds.KindImport, State: seeds.StateSwapped}.Encode() }, true}, {"no seed", func(r *registry.Route) { r.SeedSource = "" }, true}, } { now := began c.edit(&now) if err := wakeRouteChanged(began, now, false); (err != nil) != c.refused { t.Errorf("%s: %v, want refused %v", c.name, err, c.refused) } } } // Re-review finding 9: a standalone sidecar wakes through WakeWorker on the // same path, with Routes wired to the registry as production has it. Its // row is no ActivatorManaged instance (no socket, kind sidecar), and it // must still wake; a sidecar whose identity changed under the wake is still // refused before any lease. func TestWakeWorkerWithTheRegistryWired(t *testing.T) { t.Parallel() a, b, _, reg := testActivator(t, 10*time.Second) a.Routes = reg rt := testWorkerRoute("bot.shop.example.com") rt.Account, rt.App = "bab", "shop" if err := reg.Upsert(rt); err != nil { t.Fatal(err) } if err := a.WakeWorker(rt); err != nil { t.Fatalf("WakeWorker of an unchanged sidecar: %v", err) } if got := b.events(); !contains(got, "startWorker:bot.shop.example.com") { t.Fatalf("events %v; want the sidecar started", got) } moved := testWorkerRoute("bot2.shop.example.com") moved.Account, moved.App = "bab", "shop" if err := reg.Upsert(moved); err != nil { t.Fatal(err) } stale := moved stale.App = "blog" // the caller's copy names another app b.mu.Lock() before := b.acquires b.mu.Unlock() if err := a.WakeWorker(stale); err == nil { t.Fatal("a sidecar whose identity changed under the wake woke") } b.mu.Lock() after := b.acquires b.mu.Unlock() if after != before { t.Fatalf("the refused wake took %d leases", after-before) } } // The activator is handed instance rows that are not ActivatorManaged too, // with Routes wired as production has it: a unit-backed row (SplitBackend's // Unit half) and a row with no socket column. A wake that began for one // goes on while the row keeps its shape, and is refused when the shape or // the identity changed under it. A row that began ActivatorManaged is held // to that (TestWakeRouteChanged). func TestWakeRouteChangedForARowOutsideActivatorManaged(t *testing.T) { unit := testRoute("u.example.com") unit.Account, unit.App, unit.Backing = "bab", "shop", registry.BackingUnit bare := registry.Route{Host: "bare.example.com", Kind: "instance", Hibernate: true, Account: "bab", App: "shop"} for _, began := range []registry.Route{unit, bare} { if ActivatorManaged(began) { t.Fatalf("%s: the fixture is ActivatorManaged", began.Host) } if err := wakeRouteChanged(began, began, false); err != nil { t.Fatalf("%s: an unchanged row: %v", began.Host, err) } for name, edit := range map[string]func(*registry.Route){ "another backing": func(r *registry.Route) { if r.Backing == "" { r.Backing = registry.BackingUnit } else { r.Backing = "" } }, "not hibernating": func(r *registry.Route) { r.Hibernate = false }, "another kind": func(r *registry.Route) { r.Kind = "service" }, "a tunnel": func(r *registry.Route) { r.Tunnel = "t1" }, "the socket came or went": func(r *registry.Route) { if r.Socket == "" { r.Socket = "/run/carlos/x.sock" } else { r.Socket = "" } }, "another account": func(r *registry.Route) { r.Account = "cdef" }, "another app": func(r *registry.Route) { r.App = "blog" }, "an address": func(r *registry.Route) { r.Addr = "127.0.0.1:8401" }, "gated": func(r *registry.Route) { r.ImportGate = registry.ImportGateClosed }, } { now := began edit(&now) if err := wakeRouteChanged(began, now, false); err == nil { t.Errorf("%s, %s: a row that changed under its wake was not refused", began.Host, name) } } } } // The role decides the shape: a worker entry needs a sidecar row, an // instance entry an ActivatorManaged one; the identity checks hold for both. func TestWakeRouteChangedByRole(t *testing.T) { sidecar := testWorkerRoute("bot.shop.example.com") sidecar.Account, sidecar.App = "bab", "shop" if err := wakeRouteChanged(sidecar, sidecar, true); err != nil { t.Fatalf("an unchanged sidecar for a worker entry: %v", err) } if err := wakeRouteChanged(sidecar, sidecar, false); err == nil { t.Fatal("a sidecar row passed for an instance entry") } inst := testRoute("h.example.com") if err := wakeRouteChanged(inst, inst, true); err == nil { t.Fatal("an instance row passed for a worker entry") } other := sidecar other.Account = "cdef" if err := wakeRouteChanged(sidecar, other, true); err == nil { t.Fatal("a worker entry woke a sidecar of another account") } }