detached b56b682

refactor(broker): promote durable production storage

commit
b56b682de06aa9c3b697336272ba6ed9d027a63a
author
brice · · 2026-09-20 12:34 UTC
parent
0ab3226
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
12 files · +116 -59
M docs/architecture.md +1 -1
@@ -416,7 +416,7 @@ It is excluded from public help and changes no ordinary command, path, or
416416 factory. The root must be absolute, cleaned, symlink-free, and outside the
417417 production runtime and state directories; the runtime, state, and log paths are
418418 derived only beneath it and secured with `pkg/safedir.EnsurePrivate`, and the
419-durable store is opened with `brokerstore.OpenOffline` over the sandbox state
419+durable store is opened with `brokerstore.Open` over the sandbox state
420420 directory with no legacy inputs.
421421
422422 `internal/adapters/brokerconfig` owns the strict sandbox configuration: a
@@ -416,7 +416,7 @@ It is excluded from public help and changes no ordinary command, path, or
416 factory. The root must be absolute, cleaned, symlink-free, and outside the416 factory. The root must be absolute, cleaned, symlink-free, and outside the
417 production runtime and state directories; the runtime, state, and log paths are417 production runtime and state directories; the runtime, state, and log paths are
418 derived only beneath it and secured with `pkg/safedir.EnsurePrivate`, and the418 derived only beneath it and secured with `pkg/safedir.EnsurePrivate`, and the
419-durable store is opened with `brokerstore.OpenOffline` over the sandbox state419+durable store is opened with `brokerstore.Open` over the sandbox state
420 directory with no legacy inputs.420 directory with no legacy inputs.
421 421
422 `internal/adapters/brokerconfig` owns the strict sandbox configuration: a422 `internal/adapters/brokerconfig` owns the strict sandbox configuration: a
M internal/adapters/brokerconfig/config.go +7 -1
@@ -288,7 +288,13 @@ type Config struct {
288288 // route for the same pair is refused rather than silently dialed or ignored. A
289289 // missing or malformed config is an error, never an empty default.
290290 func Load(layout Layout) (*Config, error) {
291- raw, err := readConfig(filepath.Join(layout.Root, ConfigFileName))
291+ return LoadPath(layout, filepath.Join(layout.Root, ConfigFileName))
292+}
293+
294+// LoadPath parses the broker configuration at an explicit application-owned
295+// path while retaining the route and reserved-path validation from layout.
296+func LoadPath(layout Layout, path string) (*Config, error) {
297+ raw, err := readConfig(path)
292298 if err != nil {
293299 return nil, err
294300 }
@@ -288,7 +288,13 @@ type Config struct {
288 // route for the same pair is refused rather than silently dialed or ignored. A288 // route for the same pair is refused rather than silently dialed or ignored. A
289 // missing or malformed config is an error, never an empty default.289 // missing or malformed config is an error, never an empty default.
290 func Load(layout Layout) (*Config, error) {290 func Load(layout Layout) (*Config, error) {
291- raw, err := readConfig(filepath.Join(layout.Root, ConfigFileName))291+ return LoadPath(layout, filepath.Join(layout.Root, ConfigFileName))
292+}
293+
294+// LoadPath parses the broker configuration at an explicit application-owned
295+// path while retaining the route and reserved-path validation from layout.
296+func LoadPath(layout Layout, path string) (*Config, error) {
297+ raw, err := readConfig(path)
292 if err != nil {298 if err != nil {
293 return nil, err299 return nil, err
294 }300 }
M internal/adapters/brokerstore/README.md +1 -1
@@ -1,6 +1,6 @@
11 # Offline broker store (Plan 001 P2.3)
22
3-`OpenOffline` is deliberately not wired into application composition. Supply a
3+`Open` is deliberately not wired into application composition. Supply a
44 private destination directory, immutable copies of any legacy inputs, and an
55 explicit policy for each imported endpoint. An empty input path means no input;
66 a named missing, empty, corrupt, or unsupported file fails closed. The lifetime
@@ -1,6 +1,6 @@
1 # Offline broker store (Plan 001 P2.3)1 # Offline broker store (Plan 001 P2.3)
2 2
3-`OpenOffline` is deliberately not wired into application composition. Supply a3+`Open` is deliberately not wired into application composition. Supply a
4 private destination directory, immutable copies of any legacy inputs, and an4 private destination directory, immutable copies of any legacy inputs, and an
5 explicit policy for each imported endpoint. An empty input path means no input;5 explicit policy for each imported endpoint. An empty input path means no input;
6 a named missing, empty, corrupt, or unsupported file fails closed. The lifetime6 a named missing, empty, corrupt, or unsupported file fails closed. The lifetime
M internal/adapters/brokerstore/legacy_test.go +4 -4
@@ -26,7 +26,7 @@ func TestCacheStrictFailures(t *testing.T) {
2626 o := options(t)
2727 o.LegacyCache = filepath.Join(t.TempDir(), "cache")
2828 require.NoError(t, os.WriteFile(o.LegacyCache, []byte(data), 0600))
29- _, err := OpenOffline(o)
29+ _, err := Open(o)
3030 require.Error(t, err)
3131 })
3232 }
@@ -34,7 +34,7 @@ func TestCacheStrictFailures(t *testing.T) {
3434
3535 func TestRecoveryCorruption(t *testing.T) {
3636 o := options(t)
37- s, err := OpenOffline(o)
37+ s, err := Open(o)
3838 require.NoError(t, err)
3939 require.NoError(t, s.Close())
4040 require.NoError(t, os.Remove(filepath.Join(o.Dir, "state.json")))
@@ -46,13 +46,13 @@ func TestRecoveryCorruption(t *testing.T) {
4646 // Use the production atomic writer without changing its digest metadata.
4747 writer := Store{dir: o.Dir}
4848 require.NoError(t, writer.write("recovery.json", r))
49- _, err = OpenOffline(o)
49+ _, err = Open(o)
5050 require.ErrorContains(t, err, "digest mismatch")
5151 }
5252
5353 func TestDefensiveCopiesAndConcurrentAccess(t *testing.T) {
5454 o := options(t)
55- s, err := OpenOffline(o)
55+ s, err := Open(o)
5656 require.NoError(t, err)
5757 defer s.Close()
5858 h, err := s.LoadHosts()
@@ -26,7 +26,7 @@ func TestCacheStrictFailures(t *testing.T) {
26 o := options(t)26 o := options(t)
27 o.LegacyCache = filepath.Join(t.TempDir(), "cache")27 o.LegacyCache = filepath.Join(t.TempDir(), "cache")
28 require.NoError(t, os.WriteFile(o.LegacyCache, []byte(data), 0600))28 require.NoError(t, os.WriteFile(o.LegacyCache, []byte(data), 0600))
29- _, err := OpenOffline(o)29+ _, err := Open(o)
30 require.Error(t, err)30 require.Error(t, err)
31 })31 })
32 }32 }
@@ -34,7 +34,7 @@ func TestCacheStrictFailures(t *testing.T) {
34 34
35 func TestRecoveryCorruption(t *testing.T) {35 func TestRecoveryCorruption(t *testing.T) {
36 o := options(t)36 o := options(t)
37- s, err := OpenOffline(o)37+ s, err := Open(o)
38 require.NoError(t, err)38 require.NoError(t, err)
39 require.NoError(t, s.Close())39 require.NoError(t, s.Close())
40 require.NoError(t, os.Remove(filepath.Join(o.Dir, "state.json")))40 require.NoError(t, os.Remove(filepath.Join(o.Dir, "state.json")))
@@ -46,13 +46,13 @@ func TestRecoveryCorruption(t *testing.T) {
46 // Use the production atomic writer without changing its digest metadata.46 // Use the production atomic writer without changing its digest metadata.
47 writer := Store{dir: o.Dir}47 writer := Store{dir: o.Dir}
48 require.NoError(t, writer.write("recovery.json", r))48 require.NoError(t, writer.write("recovery.json", r))
49- _, err = OpenOffline(o)49+ _, err = Open(o)
50 require.ErrorContains(t, err, "digest mismatch")50 require.ErrorContains(t, err, "digest mismatch")
51 }51 }
52 52
53 func TestDefensiveCopiesAndConcurrentAccess(t *testing.T) {53 func TestDefensiveCopiesAndConcurrentAccess(t *testing.T) {
54 o := options(t)54 o := options(t)
55- s, err := OpenOffline(o)55+ s, err := Open(o)
56 require.NoError(t, err)56 require.NoError(t, err)
57 defer s.Close()57 defer s.Close()
58 h, err := s.LoadHosts()58 h, err := s.LoadHosts()
M internal/adapters/brokerstore/store.go +6 -5
@@ -1,5 +1,4 @@
1-// Package brokerstore provides offline-only broker persistence. OpenOffline is
2-// never composed into application wiring. Legacy sources are immutable inputs:
1+// Package brokerstore provides broker persistence. Legacy sources are immutable inputs:
32 // the lifetime lock is the same hosts.json.lock the legacy host writer takes,
43 // so a live host writer is excluded, but the legacy catalog cache source has no
54 // lock at all and must be immutable or quiesced. No environment policy is
@@ -83,7 +82,7 @@ func (m manifest) source(label manifestSourceLabel) (manifestSource, bool) {
8382 return manifestSource{}, false
8483 }
8584
86-// Options names a private offline destination and immutable legacy inputs.
85+// Options names a private destination and immutable legacy inputs.
8786 // Policies must explicitly cover every imported endpoint. Fault is a test seam
8887 // called after each durable-write boundary; an error poisons this open handle.
8988 type Options struct {
@@ -113,11 +112,13 @@ type Store struct {
113112
114113 var _ ports.BrokerHostStore = (*Store)(nil)
115114
116-// OpenOffline returns only after validated unified state is durable. The
115+// Open returns only after validated unified state is durable. The
117116 // recovery record is written first and retained forever as the rollback copy;
118117 // restart reuses its generated identities instead of migrating a second time.
119118 // An existing corrupt state is an error, never silently replaced by a backup.
120-func OpenOffline(o Options) (*Store, error) {
119+// Open acquires exclusive lifetime ownership, migrates immutable legacy input
120+// when needed, and returns the durable broker store.
121+func Open(o Options) (*Store, error) {
121122 if err := safedir.EnsurePrivate(o.Dir); err != nil {
122123 return nil, err
123124 }
@@ -1,5 +1,4 @@
1-// Package brokerstore provides offline-only broker persistence. OpenOffline is1+// Package brokerstore provides broker persistence. Legacy sources are immutable inputs:
2-// never composed into application wiring. Legacy sources are immutable inputs:
3 // the lifetime lock is the same hosts.json.lock the legacy host writer takes,2 // the lifetime lock is the same hosts.json.lock the legacy host writer takes,
4 // so a live host writer is excluded, but the legacy catalog cache source has no3 // so a live host writer is excluded, but the legacy catalog cache source has no
5 // lock at all and must be immutable or quiesced. No environment policy is4 // lock at all and must be immutable or quiesced. No environment policy is
@@ -83,7 +82,7 @@ func (m manifest) source(label manifestSourceLabel) (manifestSource, bool) {
83 return manifestSource{}, false82 return manifestSource{}, false
84 }83 }
85 84
86-// Options names a private offline destination and immutable legacy inputs.85+// Options names a private destination and immutable legacy inputs.
87 // Policies must explicitly cover every imported endpoint. Fault is a test seam86 // Policies must explicitly cover every imported endpoint. Fault is a test seam
88 // called after each durable-write boundary; an error poisons this open handle.87 // called after each durable-write boundary; an error poisons this open handle.
89 type Options struct {88 type Options struct {
@@ -113,11 +112,13 @@ type Store struct {
113 112
114 var _ ports.BrokerHostStore = (*Store)(nil)113 var _ ports.BrokerHostStore = (*Store)(nil)
115 114
116-// OpenOffline returns only after validated unified state is durable. The115+// Open returns only after validated unified state is durable. The
117 // recovery record is written first and retained forever as the rollback copy;116 // recovery record is written first and retained forever as the rollback copy;
118 // restart reuses its generated identities instead of migrating a second time.117 // restart reuses its generated identities instead of migrating a second time.
119 // An existing corrupt state is an error, never silently replaced by a backup.118 // An existing corrupt state is an error, never silently replaced by a backup.
120-func OpenOffline(o Options) (*Store, error) {119+// Open acquires exclusive lifetime ownership, migrates immutable legacy input
120+// when needed, and returns the durable broker store.
121+func Open(o Options) (*Store, error) {
121 if err := safedir.EnsurePrivate(o.Dir); err != nil {122 if err := safedir.EnsurePrivate(o.Dir); err != nil {
122 return nil, err123 return nil, err
123 }124 }
M internal/adapters/brokerstore/store_test.go +38 -38
@@ -49,7 +49,7 @@ func TestPriorFixtures(t *testing.T) {
4949 require.NoError(t, err)
5050 beforeC, err := os.ReadFile(o.LegacyCache)
5151 require.NoError(t, err)
52- s, err := OpenOffline(o)
52+ s, err := Open(o)
5353 require.NoError(t, err)
5454 hosts, err := s.LoadHosts()
5555 require.NoError(t, err)
@@ -73,7 +73,7 @@ func TestPriorFixtures(t *testing.T) {
7373 o.LegacyHosts = "missing"
7474 o.LegacyCache = "missing"
7575 o.Policies = nil
76- s, err = OpenOffline(o)
76+ s, err = Open(o)
7777 require.NoError(t, err)
7878 defer s.Close()
7979 again, err := s.LoadHosts()
@@ -107,7 +107,7 @@ func TestMigrationFaultRestart(t *testing.T) {
107107 }
108108 return nil
109109 }
110- s, err := OpenOffline(o)
110+ s, err := Open(o)
111111 require.ErrorIs(t, err, fault)
112112 require.Nil(t, s)
113113 var saved recovery
@@ -116,7 +116,7 @@ func TestMigrationFaultRestart(t *testing.T) {
116116 require.NoError(t, strict(raw, &saved))
117117 }
118118 o.Fault = nil
119- s, err = OpenOffline(o)
119+ s, err = Open(o)
120120 require.NoError(t, err)
121121 defer s.Close()
122122 hosts, err := s.LoadHosts()
@@ -130,7 +130,7 @@ func TestMigrationFaultRestart(t *testing.T) {
130130 }
131131 func TestLockLifetime(t *testing.T) {
132132 if dir := os.Getenv("VEV_BROKERSTORE_LOCK_TEST"); dir != "" {
133- s, err := OpenOffline(Options{Dir: dir})
133+ s, err := Open(Options{Dir: dir})
134134 if s != nil {
135135 s.Close()
136136 }
@@ -140,7 +140,7 @@ func TestLockLifetime(t *testing.T) {
140140 return
141141 }
142142 o := options(t)
143- s, err := OpenOffline(o)
143+ s, err := Open(o)
144144 require.NoError(t, err)
145145 // The lifetime lock carries the legacy host writer's exact file name, so a
146146 // store opened beside a live hosts.json excludes that writer too.
@@ -158,14 +158,14 @@ func TestLockLifetime(t *testing.T) {
158158 wg.Add(1)
159159 go func(i int) {
160160 defer wg.Done()
161- other, e := OpenOffline(o)
161+ other, e := Open(o)
162162 if other != nil {
163163 _ = other.Close()
164- outcomes[i] = fmt.Errorf("concurrent OpenOffline returned a store: %w", ports.ErrBrokerStoreLocked)
164+ outcomes[i] = fmt.Errorf("concurrent Open returned a store: %w", ports.ErrBrokerStoreLocked)
165165 return
166166 }
167167 if !errors.Is(e, ErrLocked) || !errors.Is(e, ports.ErrBrokerStoreLocked) {
168- outcomes[i] = fmt.Errorf("concurrent OpenOffline error = %v, want ErrLocked", e)
168+ outcomes[i] = fmt.Errorf("concurrent Open error = %v, want ErrLocked", e)
169169 }
170170 }(i)
171171 }
@@ -176,7 +176,7 @@ func TestLockLifetime(t *testing.T) {
176176 require.NoError(t, s.Close())
177177 _, err = s.Load()
178178 require.Error(t, err)
179- s, err = OpenOffline(o)
179+ s, err = Open(o)
180180 require.NoError(t, err)
181181 require.NoError(t, s.Close())
182182 }
@@ -185,7 +185,7 @@ func TestLockLifetime(t *testing.T) {
185185 // writer does and proves the store refuses to own the same directory.
186186 func TestLockExcludesLegacyHostWriter(t *testing.T) {
187187 o := options(t)
188- s, err := OpenOffline(o)
188+ s, err := Open(o)
189189 require.NoError(t, err)
190190 require.NoError(t, s.Close())
191191
@@ -194,12 +194,12 @@ func TestLockExcludesLegacyHostWriter(t *testing.T) {
194194 defer legacyLock.Close()
195195 require.NoError(t, syscall.Flock(int(legacyLock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))
196196
197- _, err = OpenOffline(o)
197+ _, err = Open(o)
198198 require.ErrorIs(t, err, ErrLocked)
199199 require.ErrorIs(t, err, ports.ErrBrokerStoreLocked)
200200
201201 require.NoError(t, syscall.Flock(int(legacyLock.Fd()), syscall.LOCK_UN))
202- s, err = OpenOffline(o)
202+ s, err = Open(o)
203203 require.NoError(t, err)
204204 require.NoError(t, s.Close())
205205 }
@@ -213,21 +213,21 @@ func TestStrictCorruption(t *testing.T) {
213213 o := options(t)
214214 o.LegacyHosts = filepath.Join(t.TempDir(), "hosts")
215215 require.NoError(t, os.WriteFile(o.LegacyHosts, []byte(raw), 0600))
216- s, err := OpenOffline(o)
216+ s, err := Open(o)
217217 require.Error(t, err)
218218 require.Nil(t, s)
219219 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))
220220 require.True(t, os.IsNotExist(err))
221221 })
222222 }
223- t.Run("no implicit policy", func(t *testing.T) { o := options(t); o.Policies = nil; _, err := OpenOffline(o); require.Error(t, err) })
223+ t.Run("no implicit policy", func(t *testing.T) { o := options(t); o.Policies = nil; _, err := Open(o); require.Error(t, err) })
224224 t.Run("state never falls back", func(t *testing.T) {
225225 o := options(t)
226- s, err := OpenOffline(o)
226+ s, err := Open(o)
227227 require.NoError(t, err)
228228 require.NoError(t, s.Close())
229229 require.NoError(t, os.WriteFile(filepath.Join(o.Dir, "state.json"), []byte(`{`), 0600))
230- _, err = OpenOffline(o)
230+ _, err = Open(o)
231231 require.Error(t, err)
232232 })
233233 t.Run("source symlink", func(t *testing.T) {
@@ -235,13 +235,13 @@ func TestStrictCorruption(t *testing.T) {
235235 p := filepath.Join(t.TempDir(), "link")
236236 require.NoError(t, os.Symlink(o.LegacyHosts, p))
237237 o.LegacyHosts = p
238- _, err := OpenOffline(o)
238+ _, err := Open(o)
239239 require.Error(t, err)
240240 })
241241 }
242242 func TestDurableStateSentinelMapping(t *testing.T) {
243243 o := options(t)
244- s, err := OpenOffline(o)
244+ s, err := Open(o)
245245 require.NoError(t, err)
246246
247247 // A conflicting authority token is a host conflict, not a generic failure.
@@ -257,7 +257,7 @@ func TestDurableStateSentinelMapping(t *testing.T) {
257257 // Closing releases ownership without discarding the durable files.
258258 require.Error(t, s.ReplaceHosts(1, nil))
259259 require.NoError(t, os.WriteFile(filepath.Join(o.Dir, "state.json"), []byte(`{`), 0600))
260- _, err = OpenOffline(o)
260+ _, err = Open(o)
261261 require.ErrorIs(t, err, ErrInvalidState)
262262 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)
263263 }
@@ -275,7 +275,7 @@ func TestNonRegularInputsAreRejected(t *testing.T) {
275275 require.NoError(t, syscall.Mkfifo(path, 0600))
276276 }
277277 o.LegacyHosts = path
278- s, err := OpenOffline(o)
278+ s, err := Open(o)
279279 require.Error(t, err)
280280 require.Nil(t, s)
281281 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))
@@ -291,7 +291,7 @@ func TestNonRegularInputsAreRejected(t *testing.T) {
291291 func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
292292 t.Run("state", func(t *testing.T) {
293293 o := options(t)
294- s, err := OpenOffline(o)
294+ s, err := Open(o)
295295 require.NoError(t, err)
296296 require.NoError(t, s.Close())
297297
@@ -303,7 +303,7 @@ func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
303303 require.NoError(t, os.Remove(state))
304304 require.NoError(t, os.Symlink(target, state))
305305
306- _, err = OpenOffline(o)
306+ _, err = Open(o)
307307 require.ErrorIs(t, err, ErrInvalidState)
308308 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)
309309 after, err := os.ReadFile(target)
@@ -312,7 +312,7 @@ func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
312312 })
313313 t.Run("recovery", func(t *testing.T) {
314314 o := options(t)
315- s, err := OpenOffline(o)
315+ s, err := Open(o)
316316 require.NoError(t, err)
317317 require.NoError(t, s.Close())
318318
@@ -325,7 +325,7 @@ func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
325325 require.NoError(t, os.Remove(filepath.Join(o.Dir, "state.json")))
326326 require.NoError(t, os.Symlink(target, recoveryPath))
327327
328- _, err = OpenOffline(o)
328+ _, err = Open(o)
329329 require.ErrorIs(t, err, ErrInvalidState)
330330 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)
331331 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))
@@ -351,7 +351,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
351351 absent.LegacyHosts = ""
352352 absent.LegacyCache = ""
353353 absent.Policies = nil
354- absentStore, err := OpenOffline(absent)
354+ absentStore, err := Open(absent)
355355 require.NoError(t, err)
356356 defer absentStore.Close()
357357 hosts, err := absentStore.LoadHosts()
@@ -369,7 +369,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
369369 empty.LegacyCache = ""
370370 empty.LegacyHosts = filepath.Join(t.TempDir(), "hosts")
371371 require.NoError(t, os.WriteFile(empty.LegacyHosts, []byte(`{"version":1,"hosts":[]}`), 0600))
372- emptyStore, err := OpenOffline(empty)
372+ emptyStore, err := Open(empty)
373373 require.NoError(t, err)
374374 defer emptyStore.Close()
375375 stillEmpty, err := emptyStore.LoadHosts()
@@ -401,7 +401,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
401401 require.NoError(t, writer.write("recovery.json", r))
402402 require.NoError(t, os.Remove(filepath.Join(empty.Dir, "state.json")))
403403 require.NoError(t, emptyStore.Close())
404- _, err = OpenOffline(Options{Dir: empty.Dir})
404+ _, err = Open(Options{Dir: empty.Dir})
405405 require.ErrorIs(t, err, ErrInvalidState)
406406 require.ErrorContains(t, err, "absent membership source")
407407 }
@@ -410,7 +410,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
410410 // or accepting an exhausted authority token.
411411 func TestRevisionOverflowIsRefused(t *testing.T) {
412412 o := options(t)
413- s, err := OpenOffline(o)
413+ s, err := Open(o)
414414 require.NoError(t, err)
415415 defer s.Close()
416416 h, err := s.LoadHosts()
@@ -429,7 +429,7 @@ func TestRevisionOverflowIsRefused(t *testing.T) {
429429 // incarnation.
430430 func TestAuthorityGenerationPolicyAndRemoveReAdd(t *testing.T) {
431431 o := options(t)
432- s, err := OpenOffline(o)
432+ s, err := Open(o)
433433 require.NoError(t, err)
434434 defer s.Close()
435435 h, err := s.LoadHosts()
@@ -495,7 +495,7 @@ func TestAuthorityGenerationPolicyAndRemoveReAdd(t *testing.T) {
495495 // as ErrBrokerStoreInvalidState: the durable state is a victim, not the cause.
496496 func TestReplaceHostsRejectsCallerInputBeforeCAS(t *testing.T) {
497497 o := options(t)
498- s, err := OpenOffline(o)
498+ s, err := Open(o)
499499 require.NoError(t, err)
500500 defer s.Close()
501501 h, err := s.LoadHosts()
@@ -548,7 +548,7 @@ func TestReplaceHostsRejectsCallerInputBeforeCAS(t *testing.T) {
548548 // Removed, instead of being rejected as durable-state corruption.
549549 func TestDurableSnapshotDropsProcessLocalTombstones(t *testing.T) {
550550 o := options(t)
551- s, err := OpenOffline(o)
551+ s, err := Open(o)
552552 require.NoError(t, err)
553553 h, err := s.LoadHosts()
554554 require.NoError(t, err)
@@ -567,7 +567,7 @@ func TestDurableSnapshotDropsProcessLocalTombstones(t *testing.T) {
567567 require.Empty(t, stored.Removed)
568568 require.NoError(t, s.Close())
569569
570- s, err = OpenOffline(o)
570+ s, err = Open(o)
571571 require.NoError(t, err)
572572 defer s.Close()
573573 reopened, err := s.Load()
@@ -579,7 +579,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
579579 for _, point := range []string{"write", "sync", "rename", "dirsync"} {
580580 t.Run(point, func(t *testing.T) {
581581 o := options(t)
582- s, err := OpenOffline(o)
582+ s, err := Open(o)
583583 require.NoError(t, err)
584584 h, err := s.LoadHosts()
585585 require.NoError(t, err)
@@ -606,7 +606,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
606606 _, err = s.LoadHosts()
607607 require.Error(t, err)
608608 require.NoError(t, s.Close())
609- s, err = OpenOffline(o)
609+ s, err = Open(o)
610610 require.NoError(t, err)
611611 defer s.Close()
612612 h, err = s.LoadHosts()
@@ -622,7 +622,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
622622 }
623623 t.Run("generation and restart", func(t *testing.T) {
624624 o := options(t)
625- s, err := OpenOffline(o)
625+ s, err := Open(o)
626626 require.NoError(t, err)
627627 h, err := s.LoadHosts()
628628 require.NoError(t, err)
@@ -632,7 +632,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
632632 require.NoError(t, s.ReplaceHosts(h.Revision, h.Hosts))
633633 require.ErrorIs(t, s.Store(snap), ErrStale)
634634 require.NoError(t, s.Close())
635- s, err = OpenOffline(o)
635+ s, err = Open(o)
636636 require.NoError(t, err)
637637 defer s.Close()
638638 got, err := s.Load()
@@ -648,7 +648,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
648648 // daemon observation is process-local state and the store refuses it outright,
649649 // so it can never become durable authority.
650650 func TestLocalDaemonObservationIsNeverDurable(t *testing.T) {
651- s, err := OpenOffline(options(t))
651+ s, err := Open(options(t))
652652 require.NoError(t, err)
653653 defer s.Close()
654654 err = s.Store(ports.BrokerSnapshot{Epoch: 9, Revision: 1, Daemons: []ports.BrokerDaemonObservation{{
@@ -49,7 +49,7 @@ func TestPriorFixtures(t *testing.T) {
49 require.NoError(t, err)49 require.NoError(t, err)
50 beforeC, err := os.ReadFile(o.LegacyCache)50 beforeC, err := os.ReadFile(o.LegacyCache)
51 require.NoError(t, err)51 require.NoError(t, err)
52- s, err := OpenOffline(o)52+ s, err := Open(o)
53 require.NoError(t, err)53 require.NoError(t, err)
54 hosts, err := s.LoadHosts()54 hosts, err := s.LoadHosts()
55 require.NoError(t, err)55 require.NoError(t, err)
@@ -73,7 +73,7 @@ func TestPriorFixtures(t *testing.T) {
73 o.LegacyHosts = "missing"73 o.LegacyHosts = "missing"
74 o.LegacyCache = "missing"74 o.LegacyCache = "missing"
75 o.Policies = nil75 o.Policies = nil
76- s, err = OpenOffline(o)76+ s, err = Open(o)
77 require.NoError(t, err)77 require.NoError(t, err)
78 defer s.Close()78 defer s.Close()
79 again, err := s.LoadHosts()79 again, err := s.LoadHosts()
@@ -107,7 +107,7 @@ func TestMigrationFaultRestart(t *testing.T) {
107 }107 }
108 return nil108 return nil
109 }109 }
110- s, err := OpenOffline(o)110+ s, err := Open(o)
111 require.ErrorIs(t, err, fault)111 require.ErrorIs(t, err, fault)
112 require.Nil(t, s)112 require.Nil(t, s)
113 var saved recovery113 var saved recovery
@@ -116,7 +116,7 @@ func TestMigrationFaultRestart(t *testing.T) {
116 require.NoError(t, strict(raw, &saved))116 require.NoError(t, strict(raw, &saved))
117 }117 }
118 o.Fault = nil118 o.Fault = nil
119- s, err = OpenOffline(o)119+ s, err = Open(o)
120 require.NoError(t, err)120 require.NoError(t, err)
121 defer s.Close()121 defer s.Close()
122 hosts, err := s.LoadHosts()122 hosts, err := s.LoadHosts()
@@ -130,7 +130,7 @@ func TestMigrationFaultRestart(t *testing.T) {
130 }130 }
131 func TestLockLifetime(t *testing.T) {131 func TestLockLifetime(t *testing.T) {
132 if dir := os.Getenv("VEV_BROKERSTORE_LOCK_TEST"); dir != "" {132 if dir := os.Getenv("VEV_BROKERSTORE_LOCK_TEST"); dir != "" {
133- s, err := OpenOffline(Options{Dir: dir})133+ s, err := Open(Options{Dir: dir})
134 if s != nil {134 if s != nil {
135 s.Close()135 s.Close()
136 }136 }
@@ -140,7 +140,7 @@ func TestLockLifetime(t *testing.T) {
140 return140 return
141 }141 }
142 o := options(t)142 o := options(t)
143- s, err := OpenOffline(o)143+ s, err := Open(o)
144 require.NoError(t, err)144 require.NoError(t, err)
145 // The lifetime lock carries the legacy host writer's exact file name, so a145 // The lifetime lock carries the legacy host writer's exact file name, so a
146 // store opened beside a live hosts.json excludes that writer too.146 // store opened beside a live hosts.json excludes that writer too.
@@ -158,14 +158,14 @@ func TestLockLifetime(t *testing.T) {
158 wg.Add(1)158 wg.Add(1)
159 go func(i int) {159 go func(i int) {
160 defer wg.Done()160 defer wg.Done()
161- other, e := OpenOffline(o)161+ other, e := Open(o)
162 if other != nil {162 if other != nil {
163 _ = other.Close()163 _ = other.Close()
164- outcomes[i] = fmt.Errorf("concurrent OpenOffline returned a store: %w", ports.ErrBrokerStoreLocked)164+ outcomes[i] = fmt.Errorf("concurrent Open returned a store: %w", ports.ErrBrokerStoreLocked)
165 return165 return
166 }166 }
167 if !errors.Is(e, ErrLocked) || !errors.Is(e, ports.ErrBrokerStoreLocked) {167 if !errors.Is(e, ErrLocked) || !errors.Is(e, ports.ErrBrokerStoreLocked) {
168- outcomes[i] = fmt.Errorf("concurrent OpenOffline error = %v, want ErrLocked", e)168+ outcomes[i] = fmt.Errorf("concurrent Open error = %v, want ErrLocked", e)
169 }169 }
170 }(i)170 }(i)
171 }171 }
@@ -176,7 +176,7 @@ func TestLockLifetime(t *testing.T) {
176 require.NoError(t, s.Close())176 require.NoError(t, s.Close())
177 _, err = s.Load()177 _, err = s.Load()
178 require.Error(t, err)178 require.Error(t, err)
179- s, err = OpenOffline(o)179+ s, err = Open(o)
180 require.NoError(t, err)180 require.NoError(t, err)
181 require.NoError(t, s.Close())181 require.NoError(t, s.Close())
182 }182 }
@@ -185,7 +185,7 @@ func TestLockLifetime(t *testing.T) {
185 // writer does and proves the store refuses to own the same directory.185 // writer does and proves the store refuses to own the same directory.
186 func TestLockExcludesLegacyHostWriter(t *testing.T) {186 func TestLockExcludesLegacyHostWriter(t *testing.T) {
187 o := options(t)187 o := options(t)
188- s, err := OpenOffline(o)188+ s, err := Open(o)
189 require.NoError(t, err)189 require.NoError(t, err)
190 require.NoError(t, s.Close())190 require.NoError(t, s.Close())
191 191
@@ -194,12 +194,12 @@ func TestLockExcludesLegacyHostWriter(t *testing.T) {
194 defer legacyLock.Close()194 defer legacyLock.Close()
195 require.NoError(t, syscall.Flock(int(legacyLock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))195 require.NoError(t, syscall.Flock(int(legacyLock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB))
196 196
197- _, err = OpenOffline(o)197+ _, err = Open(o)
198 require.ErrorIs(t, err, ErrLocked)198 require.ErrorIs(t, err, ErrLocked)
199 require.ErrorIs(t, err, ports.ErrBrokerStoreLocked)199 require.ErrorIs(t, err, ports.ErrBrokerStoreLocked)
200 200
201 require.NoError(t, syscall.Flock(int(legacyLock.Fd()), syscall.LOCK_UN))201 require.NoError(t, syscall.Flock(int(legacyLock.Fd()), syscall.LOCK_UN))
202- s, err = OpenOffline(o)202+ s, err = Open(o)
203 require.NoError(t, err)203 require.NoError(t, err)
204 require.NoError(t, s.Close())204 require.NoError(t, s.Close())
205 }205 }
@@ -213,21 +213,21 @@ func TestStrictCorruption(t *testing.T) {
213 o := options(t)213 o := options(t)
214 o.LegacyHosts = filepath.Join(t.TempDir(), "hosts")214 o.LegacyHosts = filepath.Join(t.TempDir(), "hosts")
215 require.NoError(t, os.WriteFile(o.LegacyHosts, []byte(raw), 0600))215 require.NoError(t, os.WriteFile(o.LegacyHosts, []byte(raw), 0600))
216- s, err := OpenOffline(o)216+ s, err := Open(o)
217 require.Error(t, err)217 require.Error(t, err)
218 require.Nil(t, s)218 require.Nil(t, s)
219 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))219 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))
220 require.True(t, os.IsNotExist(err))220 require.True(t, os.IsNotExist(err))
221 })221 })
222 }222 }
223- t.Run("no implicit policy", func(t *testing.T) { o := options(t); o.Policies = nil; _, err := OpenOffline(o); require.Error(t, err) })223+ t.Run("no implicit policy", func(t *testing.T) { o := options(t); o.Policies = nil; _, err := Open(o); require.Error(t, err) })
224 t.Run("state never falls back", func(t *testing.T) {224 t.Run("state never falls back", func(t *testing.T) {
225 o := options(t)225 o := options(t)
226- s, err := OpenOffline(o)226+ s, err := Open(o)
227 require.NoError(t, err)227 require.NoError(t, err)
228 require.NoError(t, s.Close())228 require.NoError(t, s.Close())
229 require.NoError(t, os.WriteFile(filepath.Join(o.Dir, "state.json"), []byte(`{`), 0600))229 require.NoError(t, os.WriteFile(filepath.Join(o.Dir, "state.json"), []byte(`{`), 0600))
230- _, err = OpenOffline(o)230+ _, err = Open(o)
231 require.Error(t, err)231 require.Error(t, err)
232 })232 })
233 t.Run("source symlink", func(t *testing.T) {233 t.Run("source symlink", func(t *testing.T) {
@@ -235,13 +235,13 @@ func TestStrictCorruption(t *testing.T) {
235 p := filepath.Join(t.TempDir(), "link")235 p := filepath.Join(t.TempDir(), "link")
236 require.NoError(t, os.Symlink(o.LegacyHosts, p))236 require.NoError(t, os.Symlink(o.LegacyHosts, p))
237 o.LegacyHosts = p237 o.LegacyHosts = p
238- _, err := OpenOffline(o)238+ _, err := Open(o)
239 require.Error(t, err)239 require.Error(t, err)
240 })240 })
241 }241 }
242 func TestDurableStateSentinelMapping(t *testing.T) {242 func TestDurableStateSentinelMapping(t *testing.T) {
243 o := options(t)243 o := options(t)
244- s, err := OpenOffline(o)244+ s, err := Open(o)
245 require.NoError(t, err)245 require.NoError(t, err)
246 246
247 // A conflicting authority token is a host conflict, not a generic failure.247 // A conflicting authority token is a host conflict, not a generic failure.
@@ -257,7 +257,7 @@ func TestDurableStateSentinelMapping(t *testing.T) {
257 // Closing releases ownership without discarding the durable files.257 // Closing releases ownership without discarding the durable files.
258 require.Error(t, s.ReplaceHosts(1, nil))258 require.Error(t, s.ReplaceHosts(1, nil))
259 require.NoError(t, os.WriteFile(filepath.Join(o.Dir, "state.json"), []byte(`{`), 0600))259 require.NoError(t, os.WriteFile(filepath.Join(o.Dir, "state.json"), []byte(`{`), 0600))
260- _, err = OpenOffline(o)260+ _, err = Open(o)
261 require.ErrorIs(t, err, ErrInvalidState)261 require.ErrorIs(t, err, ErrInvalidState)
262 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)262 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)
263 }263 }
@@ -275,7 +275,7 @@ func TestNonRegularInputsAreRejected(t *testing.T) {
275 require.NoError(t, syscall.Mkfifo(path, 0600))275 require.NoError(t, syscall.Mkfifo(path, 0600))
276 }276 }
277 o.LegacyHosts = path277 o.LegacyHosts = path
278- s, err := OpenOffline(o)278+ s, err := Open(o)
279 require.Error(t, err)279 require.Error(t, err)
280 require.Nil(t, s)280 require.Nil(t, s)
281 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))281 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))
@@ -291,7 +291,7 @@ func TestNonRegularInputsAreRejected(t *testing.T) {
291 func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {291 func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
292 t.Run("state", func(t *testing.T) {292 t.Run("state", func(t *testing.T) {
293 o := options(t)293 o := options(t)
294- s, err := OpenOffline(o)294+ s, err := Open(o)
295 require.NoError(t, err)295 require.NoError(t, err)
296 require.NoError(t, s.Close())296 require.NoError(t, s.Close())
297 297
@@ -303,7 +303,7 @@ func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
303 require.NoError(t, os.Remove(state))303 require.NoError(t, os.Remove(state))
304 require.NoError(t, os.Symlink(target, state))304 require.NoError(t, os.Symlink(target, state))
305 305
306- _, err = OpenOffline(o)306+ _, err = Open(o)
307 require.ErrorIs(t, err, ErrInvalidState)307 require.ErrorIs(t, err, ErrInvalidState)
308 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)308 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)
309 after, err := os.ReadFile(target)309 after, err := os.ReadFile(target)
@@ -312,7 +312,7 @@ func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
312 })312 })
313 t.Run("recovery", func(t *testing.T) {313 t.Run("recovery", func(t *testing.T) {
314 o := options(t)314 o := options(t)
315- s, err := OpenOffline(o)315+ s, err := Open(o)
316 require.NoError(t, err)316 require.NoError(t, err)
317 require.NoError(t, s.Close())317 require.NoError(t, s.Close())
318 318
@@ -325,7 +325,7 @@ func TestCommittedStateAndRecoveryAreNeverFollowed(t *testing.T) {
325 require.NoError(t, os.Remove(filepath.Join(o.Dir, "state.json")))325 require.NoError(t, os.Remove(filepath.Join(o.Dir, "state.json")))
326 require.NoError(t, os.Symlink(target, recoveryPath))326 require.NoError(t, os.Symlink(target, recoveryPath))
327 327
328- _, err = OpenOffline(o)328+ _, err = Open(o)
329 require.ErrorIs(t, err, ErrInvalidState)329 require.ErrorIs(t, err, ErrInvalidState)
330 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)330 require.ErrorIs(t, err, ports.ErrBrokerStoreInvalidState)
331 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))331 _, err = os.Stat(filepath.Join(o.Dir, "state.json"))
@@ -351,7 +351,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
351 absent.LegacyHosts = ""351 absent.LegacyHosts = ""
352 absent.LegacyCache = ""352 absent.LegacyCache = ""
353 absent.Policies = nil353 absent.Policies = nil
354- absentStore, err := OpenOffline(absent)354+ absentStore, err := Open(absent)
355 require.NoError(t, err)355 require.NoError(t, err)
356 defer absentStore.Close()356 defer absentStore.Close()
357 hosts, err := absentStore.LoadHosts()357 hosts, err := absentStore.LoadHosts()
@@ -369,7 +369,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
369 empty.LegacyCache = ""369 empty.LegacyCache = ""
370 empty.LegacyHosts = filepath.Join(t.TempDir(), "hosts")370 empty.LegacyHosts = filepath.Join(t.TempDir(), "hosts")
371 require.NoError(t, os.WriteFile(empty.LegacyHosts, []byte(`{"version":1,"hosts":[]}`), 0600))371 require.NoError(t, os.WriteFile(empty.LegacyHosts, []byte(`{"version":1,"hosts":[]}`), 0600))
372- emptyStore, err := OpenOffline(empty)372+ emptyStore, err := Open(empty)
373 require.NoError(t, err)373 require.NoError(t, err)
374 defer emptyStore.Close()374 defer emptyStore.Close()
375 stillEmpty, err := emptyStore.LoadHosts()375 stillEmpty, err := emptyStore.LoadHosts()
@@ -401,7 +401,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
401 require.NoError(t, writer.write("recovery.json", r))401 require.NoError(t, writer.write("recovery.json", r))
402 require.NoError(t, os.Remove(filepath.Join(empty.Dir, "state.json")))402 require.NoError(t, os.Remove(filepath.Join(empty.Dir, "state.json")))
403 require.NoError(t, emptyStore.Close())403 require.NoError(t, emptyStore.Close())
404- _, err = OpenOffline(Options{Dir: empty.Dir})404+ _, err = Open(Options{Dir: empty.Dir})
405 require.ErrorIs(t, err, ErrInvalidState)405 require.ErrorIs(t, err, ErrInvalidState)
406 require.ErrorContains(t, err, "absent membership source")406 require.ErrorContains(t, err, "absent membership source")
407 }407 }
@@ -410,7 +410,7 @@ func TestManifestDistinguishesAbsentFromEmptySources(t *testing.T) {
410 // or accepting an exhausted authority token.410 // or accepting an exhausted authority token.
411 func TestRevisionOverflowIsRefused(t *testing.T) {411 func TestRevisionOverflowIsRefused(t *testing.T) {
412 o := options(t)412 o := options(t)
413- s, err := OpenOffline(o)413+ s, err := Open(o)
414 require.NoError(t, err)414 require.NoError(t, err)
415 defer s.Close()415 defer s.Close()
416 h, err := s.LoadHosts()416 h, err := s.LoadHosts()
@@ -429,7 +429,7 @@ func TestRevisionOverflowIsRefused(t *testing.T) {
429 // incarnation.429 // incarnation.
430 func TestAuthorityGenerationPolicyAndRemoveReAdd(t *testing.T) {430 func TestAuthorityGenerationPolicyAndRemoveReAdd(t *testing.T) {
431 o := options(t)431 o := options(t)
432- s, err := OpenOffline(o)432+ s, err := Open(o)
433 require.NoError(t, err)433 require.NoError(t, err)
434 defer s.Close()434 defer s.Close()
435 h, err := s.LoadHosts()435 h, err := s.LoadHosts()
@@ -495,7 +495,7 @@ func TestAuthorityGenerationPolicyAndRemoveReAdd(t *testing.T) {
495 // as ErrBrokerStoreInvalidState: the durable state is a victim, not the cause.495 // as ErrBrokerStoreInvalidState: the durable state is a victim, not the cause.
496 func TestReplaceHostsRejectsCallerInputBeforeCAS(t *testing.T) {496 func TestReplaceHostsRejectsCallerInputBeforeCAS(t *testing.T) {
497 o := options(t)497 o := options(t)
498- s, err := OpenOffline(o)498+ s, err := Open(o)
499 require.NoError(t, err)499 require.NoError(t, err)
500 defer s.Close()500 defer s.Close()
501 h, err := s.LoadHosts()501 h, err := s.LoadHosts()
@@ -548,7 +548,7 @@ func TestReplaceHostsRejectsCallerInputBeforeCAS(t *testing.T) {
548 // Removed, instead of being rejected as durable-state corruption.548 // Removed, instead of being rejected as durable-state corruption.
549 func TestDurableSnapshotDropsProcessLocalTombstones(t *testing.T) {549 func TestDurableSnapshotDropsProcessLocalTombstones(t *testing.T) {
550 o := options(t)550 o := options(t)
551- s, err := OpenOffline(o)551+ s, err := Open(o)
552 require.NoError(t, err)552 require.NoError(t, err)
553 h, err := s.LoadHosts()553 h, err := s.LoadHosts()
554 require.NoError(t, err)554 require.NoError(t, err)
@@ -567,7 +567,7 @@ func TestDurableSnapshotDropsProcessLocalTombstones(t *testing.T) {
567 require.Empty(t, stored.Removed)567 require.Empty(t, stored.Removed)
568 require.NoError(t, s.Close())568 require.NoError(t, s.Close())
569 569
570- s, err = OpenOffline(o)570+ s, err = Open(o)
571 require.NoError(t, err)571 require.NoError(t, err)
572 defer s.Close()572 defer s.Close()
573 reopened, err := s.Load()573 reopened, err := s.Load()
@@ -579,7 +579,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
579 for _, point := range []string{"write", "sync", "rename", "dirsync"} {579 for _, point := range []string{"write", "sync", "rename", "dirsync"} {
580 t.Run(point, func(t *testing.T) {580 t.Run(point, func(t *testing.T) {
581 o := options(t)581 o := options(t)
582- s, err := OpenOffline(o)582+ s, err := Open(o)
583 require.NoError(t, err)583 require.NoError(t, err)
584 h, err := s.LoadHosts()584 h, err := s.LoadHosts()
585 require.NoError(t, err)585 require.NoError(t, err)
@@ -606,7 +606,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
606 _, err = s.LoadHosts()606 _, err = s.LoadHosts()
607 require.Error(t, err)607 require.Error(t, err)
608 require.NoError(t, s.Close())608 require.NoError(t, s.Close())
609- s, err = OpenOffline(o)609+ s, err = Open(o)
610 require.NoError(t, err)610 require.NoError(t, err)
611 defer s.Close()611 defer s.Close()
612 h, err = s.LoadHosts()612 h, err = s.LoadHosts()
@@ -622,7 +622,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
622 }622 }
623 t.Run("generation and restart", func(t *testing.T) {623 t.Run("generation and restart", func(t *testing.T) {
624 o := options(t)624 o := options(t)
625- s, err := OpenOffline(o)625+ s, err := Open(o)
626 require.NoError(t, err)626 require.NoError(t, err)
627 h, err := s.LoadHosts()627 h, err := s.LoadHosts()
628 require.NoError(t, err)628 require.NoError(t, err)
@@ -632,7 +632,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
632 require.NoError(t, s.ReplaceHosts(h.Revision, h.Hosts))632 require.NoError(t, s.ReplaceHosts(h.Revision, h.Hosts))
633 require.ErrorIs(t, s.Store(snap), ErrStale)633 require.ErrorIs(t, s.Store(snap), ErrStale)
634 require.NoError(t, s.Close())634 require.NoError(t, s.Close())
635- s, err = OpenOffline(o)635+ s, err = Open(o)
636 require.NoError(t, err)636 require.NoError(t, err)
637 defer s.Close()637 defer s.Close()
638 got, err := s.Load()638 got, err := s.Load()
@@ -648,7 +648,7 @@ func TestFencingAndCommitFaults(t *testing.T) {
648 // daemon observation is process-local state and the store refuses it outright,648 // daemon observation is process-local state and the store refuses it outright,
649 // so it can never become durable authority.649 // so it can never become durable authority.
650 func TestLocalDaemonObservationIsNeverDurable(t *testing.T) {650 func TestLocalDaemonObservationIsNeverDurable(t *testing.T) {
651- s, err := OpenOffline(options(t))651+ s, err := Open(options(t))
652 require.NoError(t, err)652 require.NoError(t, err)
653 defer s.Close()653 defer s.Close()
654 err = s.Store(ports.BrokerSnapshot{Epoch: 9, Revision: 1, Daemons: []ports.BrokerDaemonObservation{{654 err = s.Store(ports.BrokerSnapshot{Epoch: 9, Revision: 1, Daemons: []ports.BrokerDaemonObservation{{
M internal/app/broker_offline.go +1 -1
@@ -247,7 +247,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
247247 }
248248 defer func() { retErr = errors.Join(retErr, logCloser.Close()) }()
249249
250- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: layout.State})
250+ store, err := brokerstore.Open(brokerstore.Options{Dir: layout.State})
251251 if err != nil {
252252 return fmt.Errorf("vev: open broker sandbox store: %w", err)
253253 }
@@ -247,7 +247,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
247 }247 }
248 defer func() { retErr = errors.Join(retErr, logCloser.Close()) }()248 defer func() { retErr = errors.Join(retErr, logCloser.Close()) }()
249 249
250- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: layout.State})250+ store, err := brokerstore.Open(brokerstore.Options{Dir: layout.State})
251 if err != nil {251 if err != nil {
252 return fmt.Errorf("vev: open broker sandbox store: %w", err)252 return fmt.Errorf("vev: open broker sandbox store: %w", err)
253 }253 }
M internal/app/broker_offline_local_test.go +1 -1
@@ -255,7 +255,7 @@ func TestBrokerLocalObservationRegistryPublishesLocalFirst(t *testing.T) {
255255
256256 stateDir := filepath.Join(shortTempDir(t, "vevs"), "state")
257257 require.NoError(t, os.MkdirAll(stateDir, 0o700))
258- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: stateDir})
258+ store, err := brokerstore.Open(brokerstore.Options{Dir: stateDir})
259259 require.NoError(t, err)
260260 t.Cleanup(func() { require.NoError(t, store.Close()) })
261261
@@ -255,7 +255,7 @@ func TestBrokerLocalObservationRegistryPublishesLocalFirst(t *testing.T) {
255 255
256 stateDir := filepath.Join(shortTempDir(t, "vevs"), "state")256 stateDir := filepath.Join(shortTempDir(t, "vevs"), "state")
257 require.NoError(t, os.MkdirAll(stateDir, 0o700))257 require.NoError(t, os.MkdirAll(stateDir, 0o700))
258- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: stateDir})258+ store, err := brokerstore.Open(brokerstore.Options{Dir: stateDir})
259 require.NoError(t, err)259 require.NoError(t, err)
260 t.Cleanup(func() { require.NoError(t, store.Close()) })260 t.Cleanup(func() { require.NoError(t, store.Close()) })
261 261
A internal/app/broker_production.go +28 -0
@@ -0,0 +1,28 @@
1+package app
2+
3+import (
4+ "path/filepath"
5+
6+ "github.com/bnema/vev/internal/adapters/brokerconfig"
7+ "github.com/bnema/vev/internal/adapters/ipc"
8+ "github.com/bnema/vev/internal/platform"
9+)
10+
11+// productionBrokerLayout is the single production broker filesystem contract.
12+// Runtime ownership belongs to broker.Supervisor; state persists independently;
13+// configuration remains under the user's vev configuration directory.
14+func productionBrokerConfigPath() string {
15+ return filepath.Join(filepath.Dir(platform.ConfigPath()), "broker.json")
16+}
17+
18+func productionBrokerLayout() brokerconfig.Layout {
19+ root := filepath.Join(platform.StateDir(), "broker")
20+ runtime := filepath.Join(ipc.SocketDir(), "broker")
21+ return brokerconfig.Layout{
22+ Root: root,
23+ Runtime: runtime,
24+ State: filepath.Join(root, brokerconfig.StateDirName),
25+ Log: filepath.Join(root, brokerconfig.LogDirName),
26+ Spawn: filepath.Join(runtime, brokerconfig.SpawnDirName),
27+ }
28+}
@@ -0,0 +1,28 @@
1+package app
2+
3+import (
4+ "path/filepath"
5+
6+ "github.com/bnema/vev/internal/adapters/brokerconfig"
7+ "github.com/bnema/vev/internal/adapters/ipc"
8+ "github.com/bnema/vev/internal/platform"
9+)
10+
11+// productionBrokerLayout is the single production broker filesystem contract.
12+// Runtime ownership belongs to broker.Supervisor; state persists independently;
13+// configuration remains under the user's vev configuration directory.
14+func productionBrokerConfigPath() string {
15+ return filepath.Join(filepath.Dir(platform.ConfigPath()), "broker.json")
16+}
17+
18+func productionBrokerLayout() brokerconfig.Layout {
19+ root := filepath.Join(platform.StateDir(), "broker")
20+ runtime := filepath.Join(ipc.SocketDir(), "broker")
21+ return brokerconfig.Layout{
22+ Root: root,
23+ Runtime: runtime,
24+ State: filepath.Join(root, brokerconfig.StateDirName),
25+ Log: filepath.Join(root, brokerconfig.LogDirName),
26+ Spawn: filepath.Join(runtime, brokerconfig.SpawnDirName),
27+ }
28+}
A internal/app/broker_production_test.go +22 -0
@@ -0,0 +1,22 @@
1+package app
2+
3+import (
4+ "path/filepath"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestProductionBrokerLayoutUsesDedicatedRuntimeStateAndConfig(t *testing.T) {
11+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
12+ t.Setenv("XDG_STATE_HOME", t.TempDir())
13+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
14+
15+ layout := productionBrokerLayout()
16+ require.Equal(t, filepath.Join(layout.Root, "state"), layout.State)
17+ require.Equal(t, filepath.Join(layout.Runtime, "spawn"), layout.Spawn)
18+ require.Equal(t, "broker", filepath.Base(layout.Root))
19+ require.Equal(t, "broker", filepath.Base(layout.Runtime))
20+ require.Equal(t, "broker.json", filepath.Base(productionBrokerConfigPath()))
21+ require.NotEqual(t, layout.Root, layout.Runtime)
22+}
@@ -0,0 +1,22 @@
1+package app
2+
3+import (
4+ "path/filepath"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestProductionBrokerLayoutUsesDedicatedRuntimeStateAndConfig(t *testing.T) {
11+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
12+ t.Setenv("XDG_STATE_HOME", t.TempDir())
13+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
14+
15+ layout := productionBrokerLayout()
16+ require.Equal(t, filepath.Join(layout.Root, "state"), layout.State)
17+ require.Equal(t, filepath.Join(layout.Runtime, "spawn"), layout.Spawn)
18+ require.Equal(t, "broker", filepath.Base(layout.Root))
19+ require.Equal(t, "broker", filepath.Base(layout.Runtime))
20+ require.Equal(t, "broker.json", filepath.Base(productionBrokerConfigPath()))
21+ require.NotEqual(t, layout.Root, layout.Runtime)
22+}
M internal/usecase/broker/registry_durable_test.go +5 -5
@@ -32,7 +32,7 @@ func TestRegistryDurableReplaceHostsRestart(t *testing.T) {
3232 {"add", []ports.BrokerHostRecord{first}}, {"remove", nil}, {"re-add", []ports.BrokerHostRecord{readded}}, {"restart", []ports.BrokerHostRecord{readded}},
3333 } {
3434 t.Run(stage.name, func(t *testing.T) {
35- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir})
35+ store, err := brokerstore.Open(brokerstore.Options{Dir: dir})
3636 require.NoError(t, err)
3737 defer store.Close()
3838 r, err := NewRegistry(ports.BrokerEpoch(i+1), store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)
@@ -70,7 +70,7 @@ func TestRegistryDurableReplaceHostsRestart(t *testing.T) {
7070 func TestRegistryDurableConflictDoesNotMutateProjection(t *testing.T) {
7171 for _, mode := range []string{"stale revision", "policy without new generation"} {
7272 t.Run(mode, func(t *testing.T) {
73- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})
73+ store, err := brokerstore.Open(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})
7474 require.NoError(t, err)
7575 defer store.Close()
7676 record := ports.BrokerHostRecord{Registration: registration(t, "host", 1), Pinned: true, Policy: poolPolicy()}
@@ -138,7 +138,7 @@ func TestRegistryReplaceHostsAfterShutdownIsTyped(t *testing.T) {
138138 // the newest post-CAS publication must always flush; churn cannot leave the
139139 // registry permanently publishing membership that the store will reject.
140140 func TestRegistryDurableChurnFlushesLatestMembership(t *testing.T) {
141- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})
141+ store, err := brokerstore.Open(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})
142142 require.NoError(t, err)
143143 defer store.Close()
144144 r, err := NewRegistry(1, store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)
@@ -174,7 +174,7 @@ func TestRegistryDurableRoundTripObservedIdentity(t *testing.T) {
174174 dir := filepath.Join(t.TempDir(), "broker")
175175 record := ports.BrokerHostRecord{Registration: registration(t, "host", 1), Pinned: true, Policy: poolPolicy()}
176176
177- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir})
177+ store, err := brokerstore.Open(brokerstore.Options{Dir: dir})
178178 require.NoError(t, err)
179179 r, err := NewRegistry(1, store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)
180180 require.NoError(t, err)
@@ -201,7 +201,7 @@ func TestRegistryDurableRoundTripObservedIdentity(t *testing.T) {
201201 r.settle() // flush before reopening
202202 require.NoError(t, store.Close())
203203
204- reopened, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir})
204+ reopened, err := brokerstore.Open(brokerstore.Options{Dir: dir})
205205 require.NoError(t, err)
206206 defer reopened.Close()
207207 persisted, err := reopened.Load()
@@ -32,7 +32,7 @@ func TestRegistryDurableReplaceHostsRestart(t *testing.T) {
32 {"add", []ports.BrokerHostRecord{first}}, {"remove", nil}, {"re-add", []ports.BrokerHostRecord{readded}}, {"restart", []ports.BrokerHostRecord{readded}},32 {"add", []ports.BrokerHostRecord{first}}, {"remove", nil}, {"re-add", []ports.BrokerHostRecord{readded}}, {"restart", []ports.BrokerHostRecord{readded}},
33 } {33 } {
34 t.Run(stage.name, func(t *testing.T) {34 t.Run(stage.name, func(t *testing.T) {
35- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir})35+ store, err := brokerstore.Open(brokerstore.Options{Dir: dir})
36 require.NoError(t, err)36 require.NoError(t, err)
37 defer store.Close()37 defer store.Close()
38 r, err := NewRegistry(ports.BrokerEpoch(i+1), store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)38 r, err := NewRegistry(ports.BrokerEpoch(i+1), store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)
@@ -70,7 +70,7 @@ func TestRegistryDurableReplaceHostsRestart(t *testing.T) {
70 func TestRegistryDurableConflictDoesNotMutateProjection(t *testing.T) {70 func TestRegistryDurableConflictDoesNotMutateProjection(t *testing.T) {
71 for _, mode := range []string{"stale revision", "policy without new generation"} {71 for _, mode := range []string{"stale revision", "policy without new generation"} {
72 t.Run(mode, func(t *testing.T) {72 t.Run(mode, func(t *testing.T) {
73- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})73+ store, err := brokerstore.Open(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})
74 require.NoError(t, err)74 require.NoError(t, err)
75 defer store.Close()75 defer store.Close()
76 record := ports.BrokerHostRecord{Registration: registration(t, "host", 1), Pinned: true, Policy: poolPolicy()}76 record := ports.BrokerHostRecord{Registration: registration(t, "host", 1), Pinned: true, Policy: poolPolicy()}
@@ -138,7 +138,7 @@ func TestRegistryReplaceHostsAfterShutdownIsTyped(t *testing.T) {
138 // the newest post-CAS publication must always flush; churn cannot leave the138 // the newest post-CAS publication must always flush; churn cannot leave the
139 // registry permanently publishing membership that the store will reject.139 // registry permanently publishing membership that the store will reject.
140 func TestRegistryDurableChurnFlushesLatestMembership(t *testing.T) {140 func TestRegistryDurableChurnFlushesLatestMembership(t *testing.T) {
141- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})141+ store, err := brokerstore.Open(brokerstore.Options{Dir: filepath.Join(t.TempDir(), "broker")})
142 require.NoError(t, err)142 require.NoError(t, err)
143 defer store.Close()143 defer store.Close()
144 r, err := NewRegistry(1, store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)144 r, err := NewRegistry(1, store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)
@@ -174,7 +174,7 @@ func TestRegistryDurableRoundTripObservedIdentity(t *testing.T) {
174 dir := filepath.Join(t.TempDir(), "broker")174 dir := filepath.Join(t.TempDir(), "broker")
175 record := ports.BrokerHostRecord{Registration: registration(t, "host", 1), Pinned: true, Policy: poolPolicy()}175 record := ports.BrokerHostRecord{Registration: registration(t, "host", 1), Pinned: true, Policy: poolPolicy()}
176 176
177- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir})177+ store, err := brokerstore.Open(brokerstore.Options{Dir: dir})
178 require.NoError(t, err)178 require.NoError(t, err)
179 r, err := NewRegistry(1, store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)179 r, err := NewRegistry(1, store, newTestProbe(1), newManualClock(time.Unix(100, 0)), nil)
180 require.NoError(t, err)180 require.NoError(t, err)
@@ -201,7 +201,7 @@ func TestRegistryDurableRoundTripObservedIdentity(t *testing.T) {
201 r.settle() // flush before reopening201 r.settle() // flush before reopening
202 require.NoError(t, store.Close())202 require.NoError(t, store.Close())
203 203
204- reopened, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir})204+ reopened, err := brokerstore.Open(brokerstore.Options{Dir: dir})
205 require.NoError(t, err)205 require.NoError(t, err)
206 defer reopened.Close()206 defer reopened.Close()
207 persisted, err := reopened.Load()207 persisted, err := reopened.Load()
M internal/usecase/broker/registry_membership_durable_test.go +2 -2
@@ -35,7 +35,7 @@ func newDurableMutableRegistry(t *testing.T, epoch ports.BrokerEpoch, store port
3535
3636 func openDurableStore(t *testing.T, dir string, fault func(string) error) *brokerstore.Store {
3737 t.Helper()
38- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir, Fault: fault})
38+ store, err := brokerstore.Open(brokerstore.Options{Dir: dir, Fault: fault})
3939 require.NoError(t, err)
4040 t.Cleanup(func() { require.NoError(t, store.Close()) })
4141 return store
@@ -230,7 +230,7 @@ func TestRegistryReplaceHostsRefusesUnpublishableMembershipBeforeCAS(t *testing.
230230 func TestRegistryMutableOutcomeUnknownRequiresReopen(t *testing.T) {
231231 dir := filepath.Join(t.TempDir(), "broker")
232232 fault := errors.New("power loss")
233- // The fault is armed only after OpenOffline so the store can be created and
233+ // The fault is armed only after Open so the store can be created and
234234 // seeded normally; state.json:rename fires after the rename, so the commit
235235 // point was already crossed when the failure is reported.
236236 var armed atomic.Bool
@@ -35,7 +35,7 @@ func newDurableMutableRegistry(t *testing.T, epoch ports.BrokerEpoch, store port
35 35
36 func openDurableStore(t *testing.T, dir string, fault func(string) error) *brokerstore.Store {36 func openDurableStore(t *testing.T, dir string, fault func(string) error) *brokerstore.Store {
37 t.Helper()37 t.Helper()
38- store, err := brokerstore.OpenOffline(brokerstore.Options{Dir: dir, Fault: fault})38+ store, err := brokerstore.Open(brokerstore.Options{Dir: dir, Fault: fault})
39 require.NoError(t, err)39 require.NoError(t, err)
40 t.Cleanup(func() { require.NoError(t, store.Close()) })40 t.Cleanup(func() { require.NoError(t, store.Close()) })
41 return store41 return store
@@ -230,7 +230,7 @@ func TestRegistryReplaceHostsRefusesUnpublishableMembershipBeforeCAS(t *testing.
230 func TestRegistryMutableOutcomeUnknownRequiresReopen(t *testing.T) {230 func TestRegistryMutableOutcomeUnknownRequiresReopen(t *testing.T) {
231 dir := filepath.Join(t.TempDir(), "broker")231 dir := filepath.Join(t.TempDir(), "broker")
232 fault := errors.New("power loss")232 fault := errors.New("power loss")
233- // The fault is armed only after OpenOffline so the store can be created and233+ // The fault is armed only after Open so the store can be created and
234 // seeded normally; state.json:rename fires after the rename, so the commit234 // seeded normally; state.json:rename fires after the rename, so the commit
235 // point was already crossed when the failure is reported.235 // point was already crossed when the failure is reported.
236 var armed atomic.Bool236 var armed atomic.Bool

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close