1package out
2
3import (
4 "context"
5
6 "github.com/bnema/gordon/internal/domain"
7)
8
9// AppState persists desired/active app state, reservations, intents,
10// operation journals, and ownership records. Implemented by the
11// atomic-file filesystem adapter; consumed by the apps use case.
12// All methods are safe for concurrent use within one process;
13// cross-process coordination is owned by the implementation
14// (store.lock flock). No secret values pass through this boundary.
15type AppState interface {
16 // Close releases the durable store and its process lock.
17 Close() error
18
19 // Recover completes committed-but-unmaterialized apply intents
20 // before any new mutation is accepted (recovery-before-mutation).
21 Recover(ctx context.Context) error
22
23 // LoadRecoveryInhibitions returns every durable recovery-inhibition
24 // record of one app. Boot and periodic recovery must refuse to
25 // start or restart an inhibited container ID.
26 LoadRecoveryInhibitions(ctx context.Context, app string) ([]domain.AppRecoveryInhibition, error)
27
28 // SaveRecoveryInhibition durably records one generation-scoped
29 // recovery inhibition. Repeated calls for the same
30 // (app, service, container ID) replace the existing record.
31 SaveRecoveryInhibition(ctx context.Context, inhibition domain.AppRecoveryInhibition) error
32
33 // ClearRecoveryInhibition drops the inhibition of one exact
34 // generation. Clearing an absent record is not an error.
35 ClearRecoveryInhibition(ctx context.Context, app, service, containerID string) error
36
37 // LoadCheckpoint returns the global reservation checkpoint.
38 LoadCheckpoint(ctx context.Context) (domain.AppStoreCheckpoint, error)
39
40 // AppExists reports whether the app has a live identity: desired
41 // state, active state, or an ownership incarnation. It never creates
42 // state. A retired app whose only remaining record is an operation
43 // journal does not exist (its own key still replays through
44 // ClaimOperation).
45 AppExists(ctx context.Context, app string) (bool, error)
46
47 // RegisterBackendBinds records Gordon-generated loopback backend
48 // binds (owner gordon-backend) in the global checkpoint. It fails
49 // closed on conflict with another app's desired/active/in-flight
50 // claim. Old binds stay claimed until ReleaseBackendBinds after
51 // verified withdrawal; same-app claims never self-conflict.
52 RegisterBackendBinds(ctx context.Context, binds []domain.AppListenerReservation) error
53
54 // ReleaseBackendBinds drops Gordon-generated loopback claims for one
55 // retired container. Unknown claims are ignored.
56 ReleaseBackendBinds(ctx context.Context, app, containerID string) error
57
58 // ListApps returns normalized names of apps with any state.
59 ListApps(ctx context.Context) ([]string, error)
60
61 // LoadDesired returns the latest accepted desired revision.
62 // ok is false when the app has no desired state.
63 LoadDesired(ctx context.Context, app string) (domain.AppDesiredRevision, bool, error)
64
65 // LoadRevision returns one immutable revision by id.
66 LoadRevision(ctx context.Context, app, revision string) (domain.AppDesiredRevision, error)
67
68 // ListRevisions returns revision ids ordered newest first.
69 ListRevisions(ctx context.Context, app string) ([]string, error)
70
71 // LoadActive returns the per-service effective state.
72 // ok is false when the app was never deployed.
73 LoadActive(ctx context.Context, app string) (domain.AppActive, bool, error)
74
75 // LoadIntent returns the durable stopped/running intent.
76 LoadIntent(ctx context.Context, app string) (domain.AppStopIntent, error)
77
78 // SaveIntent persists running/stopped intent.
79 SaveIntent(ctx context.Context, intent domain.AppStopIntent) error
80
81 // LoadOwnership returns the ownership record (zero value when absent).
82 LoadOwnership(ctx context.Context, app string) (domain.AppOwnership, error)
83
84 // SaveOwnership persists the ownership record.
85 SaveOwnership(ctx context.Context, ownership domain.AppOwnership) error
86
87 // StageApply writes a staged intent containing the full candidate.
88 // No reservation or revision is visible until CommitApply.
89 StageApply(ctx context.Context, intent domain.AppApplyIntent) error
90
91 // CommitApply is the single atomic commit point: staged→committed.
92 CommitApply(ctx context.Context, app, intentID string) error
93
94 // MaterializeApply finishes a committed intent: revision file,
95 // desired pointer, reservation checkpoint update, intent→applied.
96 // Idempotent by content hash; safe to replay after a crash.
97 MaterializeApply(ctx context.Context, app, intentID string) error
98
99 // LoadIntent returns one apply intent by id.
100 LoadApplyIntent(ctx context.Context, app, intentID string) (domain.AppApplyIntent, error)
101
102 // ListIntents returns apply intent ids for recovery scans.
103 ListIntents(ctx context.Context, app string) ([]string, error)
104
105 // CollectGarbage removes staged orphans, applied intents, and
106 // unreferenced revisions beyond retention. Referenced revisions
107 // (desired/active/in-flight) are never evicted.
108 CollectGarbage(ctx context.Context, app string, inFlight []string) error
109
110 // SaveOperation persists an operation journal record atomically.
111 SaveOperation(ctx context.Context, op domain.AppOperation) error
112
113 // ClaimOperation is the single atomic check-and-write point for one
114 // mutation request key. Absent key: the candidate is persisted as an
115 // in-flight journal and claimed is true. Same key with the same
116 // request identity: the stored journal is returned and claimed is
117 // false, meaning the caller must not execute the request's effects
118 // again. Same key with a different request identity:
119 // ErrAppStateConflict. Absent key for an app with no desired, active,
120 // or ownership incarnation: ErrAppNotFound, writing nothing. A
121 // concurrent claim of the same key never creates a second journal.
122 ClaimOperation(ctx context.Context, candidate domain.AppOperation) (existing domain.AppOperation, claimed bool, err error)
123
124 // LoadOperation returns one operation journal record.
125 LoadOperation(ctx context.Context, app, opID string) (domain.AppOperation, error)
126
127 // LoadLatestOperation returns the most recently started operation.
128 LoadLatestOperation(ctx context.Context, app string) (domain.AppOperation, bool, error)
129
130 // SaveActive persists the per-service effective state.
131 SaveActive(ctx context.Context, active domain.AppActive) error
132
133 // RetireApp atomically ends an app's incarnation: it archives the
134 // live ownership record with every resource marked retained, drops
135 // the live ownership record, resets the app UUID so a reapply
136 // allocates a new incarnation, and clears desired, active, intent,
137 // staged intents, and revisions. Retained resources stay protected
138 // by the archived record and can never be adopted by a name reuse.
139 RetireApp(ctx context.Context, app string) error
140}
141
142// AppStateReader is the ACTIVE/intent subset read paths need.
143// Narrower than AppState so read paths never gain write access.
144type AppStateReader interface {
145 ListApps(ctx context.Context) ([]string, error)
146 LoadActive(ctx context.Context, app string) (domain.AppActive, bool, error)
147 LoadIntent(ctx context.Context, app string) (domain.AppStopIntent, error)
148}
149
150// AppTrafficRefresher is the app-facing traffic contract: one serialized
151// HTTP/L4 publish boundary that supports both fail-closed withdrawal of
152// one service and full publication of current verified state. Both
153// operations read the latest validated config at call time and are
154// serialized against reload and other rebuilds so a stale config can
155// never win. WithdrawService must leave no forwarding for the named
156// service once it returns nil; failing loudly is preferred to leaving
157// stale L4 forwarding behind.
158type AppTrafficRefresher interface {
159 // RebuildTraffic re-projects verified ACTIVE state and applies the
160 // full HTTP/L4 graph.
161 RebuildTraffic(ctx context.Context) error
162 // WithdrawService publishes the current graph without the named
163 // app service, so a dead or unverified backend stops receiving
164 // traffic. A nil return means stale forwarding is proven disabled.
165 WithdrawService(ctx context.Context, app, service string) error
166 // WithdrawServiceState drops the named service's recorded backend
167 // binds in ACTIVE without applying the graph. It is the canonical
168 // state-only withdrawal: ready-path failures use it when the
169 // caller already owns publication (or none is due), so no second
170 // routing-state write races the serialized WithdrawService path.
171 WithdrawServiceState(ctx context.Context, app, service string) error
172}