carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Ported from plan 3 task 2 (isolated-exec design §8.1): the classification
2// that decides whether a route's paths are today's flat layout, the nested
3// per-instance layout, or something an operator needs to look at.
4package activator
5
6import (
7 "fmt"
8 "log"
9 "path/filepath"
10
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15// Layout is the classification of an activator-managed route's paths
16// (isolated-exec design §8.1). It decides the spawn path: flat and foreign
17// rows run today's exec child; nested rows run as units; fenced and
18// conflict rows refuse to wake.
19type Layout string
20
21const (
22 LayoutFlat Layout = "flat"
23 LayoutNested Layout = "nested"
24 LayoutFenced Layout = "fenced"
25 LayoutConflict Layout = "conflict"
26 LayoutForeign Layout = "foreign"
27)
28
29// LayoutInputs are the box facts Classify needs beyond the row: where the
30// data dir is, whether a migration journal exists for a host (plan 4
31// writes them), and the other rows on the box (for identifier conflicts).
32// Nil seams mean "never fenced" and "never conflict", which is what unit
33// tests without a box want.
34type LayoutInputs struct {
35 DataDir string
36 // SocketDir is where flat sockets live and under which the nested
37 // instances directory sits. Empty means provision.SocketDir; a test
38 // points it at a temp dir so a route can classify as nested without
39 // writing to /run/carlos.
40 SocketDir string
41 Journal func(host string) bool
42 Peers func() ([]registry.Route, error)
43 // SameID reports whether two hosts share an instance identifier. Nil
44 // means provision.InstanceID equality; tests inject a collision.
45 SameID func(a, b string) bool
46}
47
48// sockets returns the flat and nested socket paths for host under the
49// configured socket directory.
50func (in LayoutInputs) sockets(host string) (flat, nested string) {
51 dir := in.SocketDir
52 if dir == "" {
53 dir = provision.SocketDir
54 }
55 return filepath.Join(dir, host+".sock"), filepath.Join(dir, "instances", provision.InstanceID(host), "app.sock")
56}
57
58// ActivatorManaged is the activator's own predicate for a route it owns
59// (activator.go's per-host gate and edge.go's proxy decision): an instance
60// route, hibernate-flagged, with a socket, not unit-backed, not
61// tunnel-served. An unflagged socket route is an externally supervised
62// process and is nobody's to migrate or isolate (design §8.2 step 1).
63func ActivatorManaged(rt registry.Route) bool {
64 return rt.Kind == "instance" && rt.Hibernate && rt.Socket != "" && rt.Backing == "" && rt.Tunnel == ""
65}
66
67// Classifier lets doWake ask a backend for a route's layout before it does
68// anything the layout might forbid: acquiring the lease or restoring a
69// database. A backend that does not implement it (a test double, say) is
70// simply not asked, and the in-Start/in-Seed refusal still catches a fenced
71// or conflicting row after the fact.
72type Classifier interface {
73 Layout(rt registry.Route) (Layout, string)
74}
75
76// Classify returns the layout and, for anything but flat and nested, the
77// reason a member or operator sees.
78func Classify(rt registry.Route, in LayoutInputs) (Layout, string) {
79 if in.Journal != nil && in.Journal(rt.Host) {
80 return LayoutFenced, "layout migration in flight; see " + provision.LayoutJournalPath(in.DataDir, rt.Host)
81 }
82 if in.Peers != nil {
83 same := in.SameID
84 if same == nil {
85 same = func(a, b string) bool { return provision.InstanceID(a) == provision.InstanceID(b) }
86 }
87 peers, err := in.Peers()
88 if err != nil {
89 // Fail open, not the fenced/conflict refusal itself: a registry
90 // read that fails here would otherwise silently skip the
91 // conflict check with no trace that it never ran. Loud, so a
92 // registry problem during a wake shows up in carlos logs rather
93 // than only in a collision nobody could explain later.
94 log.Printf("activator: %s: peer list for conflict check: %v (proceeding)", rt.Host, err)
95 } else {
96 for _, p := range peers {
97 if p.Host != rt.Host && ActivatorManaged(p) && same(p.Host, rt.Host) {
98 return LayoutConflict, fmt.Sprintf("instance identifier collides with %s; neither host wakes until one is removed", p.Host)
99 }
100 }
101 }
102 }
103 flatSock, nestedSock := in.sockets(rt.Host)
104 switch {
105 case rt.DBPath == provision.DefaultDBPath(in.DataDir, rt.Host) && rt.Socket == flatSock:
106 return LayoutFlat, ""
107 case rt.DBPath == provision.NestedDBPath(in.DataDir, rt.Host) && rt.Socket == nestedSock:
108 return LayoutNested, ""
109 }
110 return LayoutForeign, fmt.Sprintf("db %q socket %q are neither the flat nor the nested defaults", rt.DBPath, rt.Socket)
111}
112