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")
}
}