// Ported from plan 3 task 2 (isolated-exec design §8.1): the classification // that decides whether a route's paths are today's flat layout, the nested // per-instance layout, or something an operator needs to look at. package activator import ( "fmt" "log" "path/filepath" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // Layout is the classification of an activator-managed route's paths // (isolated-exec design §8.1). It decides the spawn path: flat and foreign // rows run today's exec child; nested rows run as units; fenced and // conflict rows refuse to wake. type Layout string const ( LayoutFlat Layout = "flat" LayoutNested Layout = "nested" LayoutFenced Layout = "fenced" LayoutConflict Layout = "conflict" LayoutForeign Layout = "foreign" ) // LayoutInputs are the box facts Classify needs beyond the row: where the // data dir is, whether a migration journal exists for a host (plan 4 // writes them), and the other rows on the box (for identifier conflicts). // Nil seams mean "never fenced" and "never conflict", which is what unit // tests without a box want. type LayoutInputs struct { DataDir string // SocketDir is where flat sockets live and under which the nested // instances directory sits. Empty means provision.SocketDir; a test // points it at a temp dir so a route can classify as nested without // writing to /run/carlos. SocketDir string Journal func(host string) bool Peers func() ([]registry.Route, error) // SameID reports whether two hosts share an instance identifier. Nil // means provision.InstanceID equality; tests inject a collision. SameID func(a, b string) bool } // sockets returns the flat and nested socket paths for host under the // configured socket directory. func (in LayoutInputs) sockets(host string) (flat, nested string) { dir := in.SocketDir if dir == "" { dir = provision.SocketDir } return filepath.Join(dir, host+".sock"), filepath.Join(dir, "instances", provision.InstanceID(host), "app.sock") } // ActivatorManaged is the activator's own predicate for a route it owns // (activator.go's per-host gate and edge.go's proxy decision): an instance // route, hibernate-flagged, with a socket, not unit-backed, not // tunnel-served. An unflagged socket route is an externally supervised // process and is nobody's to migrate or isolate (design §8.2 step 1). func ActivatorManaged(rt registry.Route) bool { return rt.Kind == "instance" && rt.Hibernate && rt.Socket != "" && rt.Backing == "" && rt.Tunnel == "" } // Classifier lets doWake ask a backend for a route's layout before it does // anything the layout might forbid: acquiring the lease or restoring a // database. A backend that does not implement it (a test double, say) is // simply not asked, and the in-Start/in-Seed refusal still catches a fenced // or conflicting row after the fact. type Classifier interface { Layout(rt registry.Route) (Layout, string) } // Classify returns the layout and, for anything but flat and nested, the // reason a member or operator sees. func Classify(rt registry.Route, in LayoutInputs) (Layout, string) { if in.Journal != nil && in.Journal(rt.Host) { return LayoutFenced, "layout migration in flight; see " + provision.LayoutJournalPath(in.DataDir, rt.Host) } if in.Peers != nil { same := in.SameID if same == nil { same = func(a, b string) bool { return provision.InstanceID(a) == provision.InstanceID(b) } } peers, err := in.Peers() if err != nil { // Fail open, not the fenced/conflict refusal itself: a registry // read that fails here would otherwise silently skip the // conflict check with no trace that it never ran. Loud, so a // registry problem during a wake shows up in carlos logs rather // than only in a collision nobody could explain later. log.Printf("activator: %s: peer list for conflict check: %v (proceeding)", rt.Host, err) } else { for _, p := range peers { if p.Host != rt.Host && ActivatorManaged(p) && same(p.Host, rt.Host) { return LayoutConflict, fmt.Sprintf("instance identifier collides with %s; neither host wakes until one is removed", p.Host) } } } } flatSock, nestedSock := in.sockets(rt.Host) switch { case rt.DBPath == provision.DefaultDBPath(in.DataDir, rt.Host) && rt.Socket == flatSock: return LayoutFlat, "" case rt.DBPath == provision.NestedDBPath(in.DataDir, rt.Host) && rt.Socket == nestedSock: return LayoutNested, "" } return LayoutForeign, fmt.Sprintf("db %q socket %q are neither the flat nor the nested defaults", rt.DBPath, rt.Socket) }