package host import ( "context" "fmt" "net/http" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/schedule" ) // genWakeBackend is fakeWakeBackend with the generation capability: with // perGen it starts the fake instance on /g.sock, never on rt.Socket, // so a tick that reached the canonical path would find nothing. type genWakeBackend struct { fakeWakeBackend dir string } func (b *genWakeBackend) StartGen(_ context.Context, rt registry.Route, n uint64, perGen bool) (activator.Proc, activator.Gen, error) { sock := rt.Socket if perGen { sock = filepath.Join(b.dir, fmt.Sprintf("g%d.sock", n)) } b.inst.socket = sock if err := b.inst.start(); err != nil { return nil, activator.Gen{}, err } return &fakeInstanceProc{inst: b.inst}, activator.Gen{N: n, Socket: sock, Control: sock + ".control"}, nil } func (b *genWakeBackend) NextDueVia(context.Context, registry.Route, string) (int64, error) { return 0, nil } // genDeliveryFixture is wakeFixture over genWakeBackend, with the // generations switch set before the activator reads it. func genDeliveryFixture(t *testing.T, on bool) (*activator.Service, *fakeInstance, registry.Route) { t.Helper() prev := activator.GenerationsEnabled() activator.SetGenerations(on) t.Cleanup(func() { activator.SetGenerations(prev) }) dd, err := os.MkdirTemp("", "h") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(dd) }) reg, err := registry.Open(filepath.Join(dd, "r.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) inst := &fakeInstance{handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })} rt := registry.Route{Host: "gen.bab.oncarlos.com", Kind: "instance", Account: "bab", App: "app", Status: "ready", Hibernate: true, Socket: filepath.Join(dd, "canonical.sock"), DBPath: filepath.Join(dd, "app.db")} if err := reg.Upsert(rt); err != nil { t.Fatal(err) } svc := activator.New(activator.Config{Bucket: "test-bucket", Idle: time.Hour}, &genWakeBackend{fakeWakeBackend: fakeWakeBackend{inst: inst}, dir: dd}, reg) svc.Start() t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() svc.Shutdown(ctx) }) return svc, inst, rt } func deliverTick(t *testing.T, d *activatorDeliverer, rt registry.Route) int { t.Helper() req, err := schedule.BuildRequest(rt, registry.Timer{Name: "sync", Kind: schedule.KindSchedule, Path: "/jobs/sync", NextAt: 1, DueAt: 1}, "tok", "test") if err != nil { t.Fatal(err) } status, err := d.Deliver(context.Background(), rt, req) if err != nil { t.Fatalf("Deliver: %v", err) } return status } // Scheduled delivery dials AppSocket (spec ยง7.1): the tick wakes the // instance and reaches its generation socket, though nothing listens on the // route's canonical path. func TestScheduledDeliveryReachesTheCurrentGeneration(t *testing.T) { requireUnixSockets(t) svc, inst, rt := genDeliveryFixture(t, true) d := &activatorDeliverer{svc: svc} if got := deliverTick(t, d, rt); got != http.StatusNoContent { t.Fatalf("tick status = %d, want 204 from the instance", got) } cur, ok := svc.AppSocket(rt.Host) if !ok || inst.socket != cur || cur == rt.Socket { t.Fatalf("instance bound %q, AppSocket = %q (%v); want a generation socket, not %q", inst.socket, cur, ok, rt.Socket) } if len(inst.served()) != 1 { t.Fatalf("instance served %d ticks, want 1", len(inst.served())) } if d.genPools == nil || d.genPools.Len() != 1 { t.Fatal("the tick did not go through the leg's generation pools") } } // Flag off: the canonical socket and the cached per-socket proxy, as before. func TestScheduledDeliveryWithGenerationsOffUsesTheCanonicalSocket(t *testing.T) { requireUnixSockets(t) svc, inst, rt := genDeliveryFixture(t, false) d := &activatorDeliverer{svc: svc} if got := deliverTick(t, d, rt); got != http.StatusNoContent { t.Fatalf("tick status = %d, want 204", got) } if inst.socket != rt.Socket { t.Fatalf("instance bound %q, want the canonical %q", inst.socket, rt.Socket) } if d.genPools != nil { t.Fatal("generation pools were made with the switch off") } if _, ok := d.proxies[rt.Socket]; !ok { t.Fatal("the canonical per-socket proxy was not used") } } // The status probe asks about each managed route's current generation, and // about everything else exactly as before. func TestCurrentSocketProbePointsAtTheCurrentGeneration(t *testing.T) { routes := []registry.Route{ {Host: "awake.example.com", Kind: "instance", Hibernate: true, Socket: "/run/carlos/awake.example.com.sock"}, {Host: "asleep.example.com", Kind: "instance", Hibernate: true, Socket: "/run/carlos/asleep.example.com.sock"}, {Host: "svc.example.com", Kind: "service", Socket: "/run/carlos/svc.example.com.sock"}, } appSocket := func(host string) (string, bool) { if host == "awake.example.com" { return "/run/carlos/g/0123456789ab.4.sock", true } return "", false } var asked []string probe := currentSocketProbe(routes, appSocket, func(sock string) *bool { asked = append(asked, sock) v := true return &v }) for _, r := range routes { probe(r.Socket) } want := []string{"/run/carlos/g/0123456789ab.4.sock", "/run/carlos/asleep.example.com.sock", "/run/carlos/svc.example.com.sock"} if fmt.Sprint(asked) != fmt.Sprint(want) { t.Fatalf("probed %v, want %v", asked, want) } } // Flag off (or no activator): the status loop's probe is listeningProbe. func TestStatusSocketProbeIsUnchangedWithGenerationsOff(t *testing.T) { got := statusSocketProbe(nil, nil) if fmt.Sprintf("%p", got) != fmt.Sprintf("%p", listeningProbe) { t.Fatal("with no activator the status probe is not listeningProbe") } svc, _, _ := genDeliveryFixture(t, false) got = statusSocketProbe(nil, svc) if fmt.Sprintf("%p", got) != fmt.Sprintf("%p", listeningProbe) { t.Fatal("with generations off the status probe is not listeningProbe") } }