fix(app): propagate lifecycle release failures
- commit
- c0e00f5d9c4e97cfabb365418a0462811b79b291
- author
- brice · · 2026-07-26 01:32 UTC
- parent
- 1c282c1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +103 -73
M internal/app/run.go +8 -4
| @@ -419,6 +419,10 @@ type lifecycleOwnership interface { | |||
| 419 | 419 | Release() error | |
| 420 | 420 | } | |
| 421 | 421 | ||
| 422 | + | func joinLifecycleReleaseError(retErr *error, owner lifecycleOwnership) { | |
| 423 | + | *retErr = errors.Join(*retErr, owner.Release()) | |
| 424 | + | } | |
| 425 | + | ||
| 422 | 426 | type lifecycleStartupDeps struct { | |
| 423 | 427 | ensurePrivate func(string) error | |
| 424 | 428 | acquire func(context.Context, string, time.Duration) (lifecycleOwnership, error) | |
| @@ -1213,13 +1217,13 @@ func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger | |||
| 1213 | 1217 | ||
| 1214 | 1218 | // runList prints the daemon's session listing. With no daemon running, it | |
| 1215 | 1219 | // falls back to the persisted stopped-session records. | |
| 1216 | - | func runList(ctx context.Context) error { | |
| 1220 | + | func runList(ctx context.Context) (retErr error) { | |
| 1217 | 1221 | transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff) | |
| 1218 | 1222 | if err != nil { | |
| 1219 | 1223 | return fmt.Errorf("vev: waiting for durable session state: %w", err) | |
| 1220 | 1224 | } | |
| 1221 | 1225 | if owner != nil { | |
| 1222 | - | defer func() { _ = owner.Release() }() | |
| 1226 | + | defer joinLifecycleReleaseError(&retErr, owner) | |
| 1223 | 1227 | records, loadErr := persist.LoadReadOnly(platform.StateDir()) | |
| 1224 | 1228 | if loadErr != nil { | |
| 1225 | 1229 | return fmt.Errorf("vev: reading stored sessions: %w", loadErr) | |
| @@ -1340,13 +1344,13 @@ func runOfflineNamedKill(ctx context.Context, name string) (retErr error) { | |||
| 1340 | 1344 | } | |
| 1341 | 1345 | ||
| 1342 | 1346 | // runKill asks the daemon to terminate a named session, every session, or the daemon. | |
| 1343 | - | func runKill(ctx context.Context, name string, all, daemon bool) error { | |
| 1347 | + | func runKill(ctx context.Context, name string, all, daemon bool) (retErr error) { | |
| 1344 | 1348 | transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff) | |
| 1345 | 1349 | if err != nil { | |
| 1346 | 1350 | return fmt.Errorf("vev: waiting for durable session state: %w", err) | |
| 1347 | 1351 | } | |
| 1348 | 1352 | if owner != nil { | |
| 1349 | - | defer func() { _ = owner.Release() }() | |
| 1353 | + | defer joinLifecycleReleaseError(&retErr, owner) | |
| 1350 | 1354 | if name != "" && !all && !daemon { | |
| 1351 | 1355 | if err := runOfflineNamedKill(ctx, name); err != nil { | |
| 1352 | 1356 | return err | |
| @@ -419,6 +419,10 @@ type lifecycleOwnership interface { | |||||
| 419 | Release() error | 419 | Release() error | ||
| 420 | } | 420 | } | ||
| 421 | 421 | ||||
| 422 | + | func joinLifecycleReleaseError(retErr *error, owner lifecycleOwnership) { | |||
| 423 | + | *retErr = errors.Join(*retErr, owner.Release()) | |||
| 424 | + | } | |||
| 425 | + | ||||
| 422 | type lifecycleStartupDeps struct { | 426 | type lifecycleStartupDeps struct { | ||
| 423 | ensurePrivate func(string) error | 427 | ensurePrivate func(string) error | ||
| 424 | acquire func(context.Context, string, time.Duration) (lifecycleOwnership, error) | 428 | acquire func(context.Context, string, time.Duration) (lifecycleOwnership, error) | ||
| @@ -1213,13 +1217,13 @@ func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger | |||||
| 1213 | 1217 | ||||
| 1214 | // runList prints the daemon's session listing. With no daemon running, it | 1218 | // runList prints the daemon's session listing. With no daemon running, it | ||
| 1215 | // falls back to the persisted stopped-session records. | 1219 | // falls back to the persisted stopped-session records. | ||
| 1216 | - | func runList(ctx context.Context) error { | 1220 | + | func runList(ctx context.Context) (retErr error) { |
| 1217 | transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff) | 1221 | transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff) | ||
| 1218 | if err != nil { | 1222 | if err != nil { | ||
| 1219 | return fmt.Errorf("vev: waiting for durable session state: %w", err) | 1223 | return fmt.Errorf("vev: waiting for durable session state: %w", err) | ||
| 1220 | } | 1224 | } | ||
| 1221 | if owner != nil { | 1225 | if owner != nil { | ||
| 1222 | - | defer func() { _ = owner.Release() }() | 1226 | + | defer joinLifecycleReleaseError(&retErr, owner) |
| 1223 | records, loadErr := persist.LoadReadOnly(platform.StateDir()) | 1227 | records, loadErr := persist.LoadReadOnly(platform.StateDir()) | ||
| 1224 | if loadErr != nil { | 1228 | if loadErr != nil { | ||
| 1225 | return fmt.Errorf("vev: reading stored sessions: %w", loadErr) | 1229 | return fmt.Errorf("vev: reading stored sessions: %w", loadErr) | ||
| @@ -1340,13 +1344,13 @@ func runOfflineNamedKill(ctx context.Context, name string) (retErr error) { | |||||
| 1340 | } | 1344 | } | ||
| 1341 | 1345 | ||||
| 1342 | // runKill asks the daemon to terminate a named session, every session, or the daemon. | 1346 | // runKill asks the daemon to terminate a named session, every session, or the daemon. | ||
| 1343 | - | func runKill(ctx context.Context, name string, all, daemon bool) error { | 1347 | + | func runKill(ctx context.Context, name string, all, daemon bool) (retErr error) { |
| 1344 | transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff) | 1348 | transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff) | ||
| 1345 | if err != nil { | 1349 | if err != nil { | ||
| 1346 | return fmt.Errorf("vev: waiting for durable session state: %w", err) | 1350 | return fmt.Errorf("vev: waiting for durable session state: %w", err) | ||
| 1347 | } | 1351 | } | ||
| 1348 | if owner != nil { | 1352 | if owner != nil { | ||
| 1349 | - | defer func() { _ = owner.Release() }() | 1353 | + | defer joinLifecycleReleaseError(&retErr, owner) |
| 1350 | if name != "" && !all && !daemon { | 1354 | if name != "" && !all && !daemon { | ||
| 1351 | if err := runOfflineNamedKill(ctx, name); err != nil { | 1355 | if err := runOfflineNamedKill(ctx, name); err != nil { | ||
| 1352 | return err | 1356 | return err | ||
M internal/app/run_test.go +86 -0
| @@ -33,6 +33,92 @@ type fakeLifecycleOwnership struct { | |||
| 33 | 33 | ||
| 34 | 34 | func (o fakeLifecycleOwnership) Release() error { return o.release() } | |
| 35 | 35 | ||
| 36 | + | type fakeLifecycleProbe struct { | |
| 37 | + | owner lifecycleOwnership | |
| 38 | + | err error | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func (p fakeLifecycleProbe) TryAcquire(string) (lifecycleOwnership, error) { | |
| 42 | + | return p.owner, p.err | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func TestJoinLifecycleReleaseError(t *testing.T) { | |
| 46 | + | operationErr := errors.New("operation failed") | |
| 47 | + | releaseErr := errors.New("release failed") | |
| 48 | + | ||
| 49 | + | tests := []struct { | |
| 50 | + | name string | |
| 51 | + | operationErr error | |
| 52 | + | releaseErr error | |
| 53 | + | wantErrors []error | |
| 54 | + | }{ | |
| 55 | + | { | |
| 56 | + | name: "successful operation preserves release failure", | |
| 57 | + | releaseErr: releaseErr, | |
| 58 | + | wantErrors: []error{releaseErr}, | |
| 59 | + | }, | |
| 60 | + | { | |
| 61 | + | name: "operation and release failures are joined", | |
| 62 | + | operationErr: operationErr, | |
| 63 | + | releaseErr: releaseErr, | |
| 64 | + | wantErrors: []error{operationErr, releaseErr}, | |
| 65 | + | }, | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | for _, tt := range tests { | |
| 69 | + | t.Run(tt.name, func(t *testing.T) { | |
| 70 | + | got := tt.operationErr | |
| 71 | + | joinLifecycleReleaseError(&got, fakeLifecycleOwnership{release: func() error { | |
| 72 | + | return tt.releaseErr | |
| 73 | + | }}) | |
| 74 | + | ||
| 75 | + | for _, wantErr := range tt.wantErrors { | |
| 76 | + | require.ErrorIs(t, got, wantErr) | |
| 77 | + | } | |
| 78 | + | }) | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func TestOfflineCommandsPropagateLifecycleReleaseErrors(t *testing.T) { | |
| 83 | + | releaseErr := errors.New("release failed") | |
| 84 | + | ||
| 85 | + | tests := []struct { | |
| 86 | + | name string | |
| 87 | + | run func(context.Context) error | |
| 88 | + | wantOperationErr string | |
| 89 | + | }{ | |
| 90 | + | { | |
| 91 | + | name: "list joins release failure to success", | |
| 92 | + | run: runList, | |
| 93 | + | }, | |
| 94 | + | { | |
| 95 | + | name: "kill joins release failure to operation failure", | |
| 96 | + | run: func(ctx context.Context) error { | |
| 97 | + | return runKill(ctx, "", false, false) | |
| 98 | + | }, | |
| 99 | + | wantOperationErr: "vev: no daemon running", | |
| 100 | + | }, | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | originalProbe := daemonLifecycleProbe | |
| 104 | + | t.Cleanup(func() { daemonLifecycleProbe = originalProbe }) | |
| 105 | + | for _, tt := range tests { | |
| 106 | + | t.Run(tt.name, func(t *testing.T) { | |
| 107 | + | t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) | |
| 108 | + | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |
| 109 | + | daemonLifecycleProbe = fakeLifecycleProbe{ | |
| 110 | + | owner: fakeLifecycleOwnership{release: func() error { return releaseErr }}, | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | err := tt.run(context.Background()) | |
| 114 | + | require.ErrorIs(t, err, releaseErr) | |
| 115 | + | if tt.wantOperationErr != "" { | |
| 116 | + | require.ErrorContains(t, err, tt.wantOperationErr) | |
| 117 | + | } | |
| 118 | + | }) | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | ||
| 36 | 122 | func TestCatalogueRegistryConstructionPrecedesSocketPublication(t *testing.T) { | |
| 37 | 123 | var events []string | |
| 38 | 124 | _, _, err := constructDaemonBeforeSocketPublication( | |
| @@ -33,6 +33,92 @@ type fakeLifecycleOwnership struct { | |||||
| 33 | 33 | ||||
| 34 | func (o fakeLifecycleOwnership) Release() error { return o.release() } | 34 | func (o fakeLifecycleOwnership) Release() error { return o.release() } | ||
| 35 | 35 | ||||
| 36 | + | type fakeLifecycleProbe struct { | |||
| 37 | + | owner lifecycleOwnership | |||
| 38 | + | err error | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func (p fakeLifecycleProbe) TryAcquire(string) (lifecycleOwnership, error) { | |||
| 42 | + | return p.owner, p.err | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func TestJoinLifecycleReleaseError(t *testing.T) { | |||
| 46 | + | operationErr := errors.New("operation failed") | |||
| 47 | + | releaseErr := errors.New("release failed") | |||
| 48 | + | ||||
| 49 | + | tests := []struct { | |||
| 50 | + | name string | |||
| 51 | + | operationErr error | |||
| 52 | + | releaseErr error | |||
| 53 | + | wantErrors []error | |||
| 54 | + | }{ | |||
| 55 | + | { | |||
| 56 | + | name: "successful operation preserves release failure", | |||
| 57 | + | releaseErr: releaseErr, | |||
| 58 | + | wantErrors: []error{releaseErr}, | |||
| 59 | + | }, | |||
| 60 | + | { | |||
| 61 | + | name: "operation and release failures are joined", | |||
| 62 | + | operationErr: operationErr, | |||
| 63 | + | releaseErr: releaseErr, | |||
| 64 | + | wantErrors: []error{operationErr, releaseErr}, | |||
| 65 | + | }, | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | for _, tt := range tests { | |||
| 69 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 70 | + | got := tt.operationErr | |||
| 71 | + | joinLifecycleReleaseError(&got, fakeLifecycleOwnership{release: func() error { | |||
| 72 | + | return tt.releaseErr | |||
| 73 | + | }}) | |||
| 74 | + | ||||
| 75 | + | for _, wantErr := range tt.wantErrors { | |||
| 76 | + | require.ErrorIs(t, got, wantErr) | |||
| 77 | + | } | |||
| 78 | + | }) | |||
| 79 | + | } | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | func TestOfflineCommandsPropagateLifecycleReleaseErrors(t *testing.T) { | |||
| 83 | + | releaseErr := errors.New("release failed") | |||
| 84 | + | ||||
| 85 | + | tests := []struct { | |||
| 86 | + | name string | |||
| 87 | + | run func(context.Context) error | |||
| 88 | + | wantOperationErr string | |||
| 89 | + | }{ | |||
| 90 | + | { | |||
| 91 | + | name: "list joins release failure to success", | |||
| 92 | + | run: runList, | |||
| 93 | + | }, | |||
| 94 | + | { | |||
| 95 | + | name: "kill joins release failure to operation failure", | |||
| 96 | + | run: func(ctx context.Context) error { | |||
| 97 | + | return runKill(ctx, "", false, false) | |||
| 98 | + | }, | |||
| 99 | + | wantOperationErr: "vev: no daemon running", | |||
| 100 | + | }, | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | originalProbe := daemonLifecycleProbe | |||
| 104 | + | t.Cleanup(func() { daemonLifecycleProbe = originalProbe }) | |||
| 105 | + | for _, tt := range tests { | |||
| 106 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 107 | + | t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) | |||
| 108 | + | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |||
| 109 | + | daemonLifecycleProbe = fakeLifecycleProbe{ | |||
| 110 | + | owner: fakeLifecycleOwnership{release: func() error { return releaseErr }}, | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | err := tt.run(context.Background()) | |||
| 114 | + | require.ErrorIs(t, err, releaseErr) | |||
| 115 | + | if tt.wantOperationErr != "" { | |||
| 116 | + | require.ErrorContains(t, err, tt.wantOperationErr) | |||
| 117 | + | } | |||
| 118 | + | }) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | ||||
| 36 | func TestCatalogueRegistryConstructionPrecedesSocketPublication(t *testing.T) { | 122 | func TestCatalogueRegistryConstructionPrecedesSocketPublication(t *testing.T) { | ||
| 37 | var events []string | 123 | var events []string | ||
| 38 | _, _, err := constructDaemonBeforeSocketPublication( | 124 | _, _, err := constructDaemonBeforeSocketPublication( | ||
M internal/app/spawn.go +4 -4
| @@ -41,12 +41,12 @@ type dialFunc func(ctx context.Context, dir string) (ports.Transport, error) | |||
| 41 | 41 | type spawnFunc func() error | |
| 42 | 42 | ||
| 43 | 43 | type lifecycleProbe interface { | |
| 44 | - | TryAcquire(string) (*lifecycle.Owner, error) | |
| 44 | + | TryAcquire(string) (lifecycleOwnership, error) | |
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | 47 | type osLifecycleProbe struct{} | |
| 48 | 48 | ||
| 49 | - | func (osLifecycleProbe) TryAcquire(runtimeDir string) (*lifecycle.Owner, error) { | |
| 49 | + | func (osLifecycleProbe) TryAcquire(runtimeDir string) (lifecycleOwnership, error) { | |
| 50 | 50 | return lifecycle.TryAcquire(runtimeDir) | |
| 51 | 51 | } | |
| 52 | 52 | ||
| @@ -67,7 +67,7 @@ var defaultBackoff = backoffConfig{ | |||
| 67 | 67 | total: 5 * time.Second, | |
| 68 | 68 | } | |
| 69 | 69 | ||
| 70 | - | func waitForLifecycleAvailability(ctx context.Context, runtimeDir string, cfg backoffConfig) (*lifecycle.Owner, error) { | |
| 70 | + | func waitForLifecycleAvailability(ctx context.Context, runtimeDir string, cfg backoffConfig) (lifecycleOwnership, error) { | |
| 71 | 71 | deadline := time.Now().Add(cfg.total) | |
| 72 | 72 | backoff := cfg.initial | |
| 73 | 73 | for { | |
| @@ -93,7 +93,7 @@ func waitForLifecycleAvailability(ctx context.Context, runtimeDir string, cfg ba | |||
| 93 | 93 | } | |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | - | func waitForDaemonOrLifecycle(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (ports.Transport, *lifecycle.Owner, error) { | |
| 96 | + | func waitForDaemonOrLifecycle(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (ports.Transport, lifecycleOwnership, error) { | |
| 97 | 97 | deadline := time.Now().Add(cfg.total) | |
| 98 | 98 | backoff := cfg.initial | |
| 99 | 99 | for { | |
| @@ -41,12 +41,12 @@ type dialFunc func(ctx context.Context, dir string) (ports.Transport, error) | |||||
| 41 | type spawnFunc func() error | 41 | type spawnFunc func() error | ||
| 42 | 42 | ||||
| 43 | type lifecycleProbe interface { | 43 | type lifecycleProbe interface { | ||
| 44 | - | TryAcquire(string) (*lifecycle.Owner, error) | 44 | + | TryAcquire(string) (lifecycleOwnership, error) |
| 45 | } | 45 | } | ||
| 46 | 46 | ||||
| 47 | type osLifecycleProbe struct{} | 47 | type osLifecycleProbe struct{} | ||
| 48 | 48 | ||||
| 49 | - | func (osLifecycleProbe) TryAcquire(runtimeDir string) (*lifecycle.Owner, error) { | 49 | + | func (osLifecycleProbe) TryAcquire(runtimeDir string) (lifecycleOwnership, error) { |
| 50 | return lifecycle.TryAcquire(runtimeDir) | 50 | return lifecycle.TryAcquire(runtimeDir) | ||
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| @@ -67,7 +67,7 @@ var defaultBackoff = backoffConfig{ | |||||
| 67 | total: 5 * time.Second, | 67 | total: 5 * time.Second, | ||
| 68 | } | 68 | } | ||
| 69 | 69 | ||||
| 70 | - | func waitForLifecycleAvailability(ctx context.Context, runtimeDir string, cfg backoffConfig) (*lifecycle.Owner, error) { | 70 | + | func waitForLifecycleAvailability(ctx context.Context, runtimeDir string, cfg backoffConfig) (lifecycleOwnership, error) { |
| 71 | deadline := time.Now().Add(cfg.total) | 71 | deadline := time.Now().Add(cfg.total) | ||
| 72 | backoff := cfg.initial | 72 | backoff := cfg.initial | ||
| 73 | for { | 73 | for { | ||
| @@ -93,7 +93,7 @@ func waitForLifecycleAvailability(ctx context.Context, runtimeDir string, cfg ba | |||||
| 93 | } | 93 | } | ||
| 94 | } | 94 | } | ||
| 95 | 95 | ||||
| 96 | - | func waitForDaemonOrLifecycle(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (ports.Transport, *lifecycle.Owner, error) { | 96 | + | func waitForDaemonOrLifecycle(ctx context.Context, dir string, dial dialFunc, cfg backoffConfig) (ports.Transport, lifecycleOwnership, error) { |
| 97 | deadline := time.Now().Add(cfg.total) | 97 | deadline := time.Now().Add(cfg.total) | ||
| 98 | backoff := cfg.initial | 98 | backoff := cfg.initial | ||
| 99 | for { | 99 | for { | ||
M internal/usecase/daemon/session.go +0 -1
| @@ -96,7 +96,6 @@ type session struct { | |||
| 96 | 96 | snapshotPublicationContext context.Context | |
| 97 | 97 | snapshotPublicationCancel context.CancelFunc | |
| 98 | 98 | snapshotQuarantined bool | |
| 99 | - | snapshotQuarantineEpoch uint64 | |
| 100 | 99 | // snapshotChunkCache retains encoded sealed history only for this named | |
| 101 | 100 | // session. snapshotMu owns it so cache touches never contend with pane | |
| 102 | 101 | // state or mutate bytes retained by queued/in-flight publications. | |
| @@ -96,7 +96,6 @@ type session struct { | |||||
| 96 | snapshotPublicationContext context.Context | 96 | snapshotPublicationContext context.Context | ||
| 97 | snapshotPublicationCancel context.CancelFunc | 97 | snapshotPublicationCancel context.CancelFunc | ||
| 98 | snapshotQuarantined bool | 98 | snapshotQuarantined bool | ||
| 99 | - | snapshotQuarantineEpoch uint64 | |||
| 100 | // snapshotChunkCache retains encoded sealed history only for this named | 99 | // snapshotChunkCache retains encoded sealed history only for this named | ||
| 101 | // session. snapshotMu owns it so cache touches never contend with pane | 100 | // session. snapshotMu owns it so cache touches never contend with pane | ||
| 102 | // state or mutate bytes retained by queued/in-flight publications. | 101 | // state or mutate bytes retained by queued/in-flight publications. | ||
M internal/usecase/daemon/snapshot_coordinator.go +5 -64
| @@ -1,23 +1,11 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "time" | |
| 6 | - | ) | |
| 7 | - | ||
| 8 | - | // snapshotCoordinatorQuarantine identifies one stop request. A later teardown | |
| 9 | - | // can supersede it, in which case only that later owner may resume publication. | |
| 10 | - | type snapshotCoordinatorQuarantine struct { | |
| 11 | - | done <-chan struct{} | |
| 12 | - | epoch uint64 | |
| 13 | - | } | |
| 14 | - | ||
| 15 | 3 | // quarantineSnapshotCoordinator prevents a deleted session from starting any | |
| 16 | 4 | // further publication. The returned join channel is closed after an already | |
| 17 | 5 | // started repository call returns; callers must wait without daemon or session | |
| 18 | 6 | // locks before deleting repository state. | |
| 19 | 7 | func quarantineSnapshotCoordinator(sess *session) <-chan struct{} { | |
| 20 | - | return quarantineSnapshotCoordinatorWithEpoch(sess).done | |
| 8 | + | return quarantineSnapshotCoordinatorWithOptions(sess, false) | |
| 21 | 9 | } | |
| 22 | 10 | ||
| 23 | 11 | // quarantineSnapshotCoordinatorRetainingQueuedCapture is used after a forced | |
| @@ -26,22 +14,16 @@ func quarantineSnapshotCoordinator(sess *session) <-chan struct{} { | |||
| 26 | 14 | // quarantined, and the worker eventually discards it after its blocked call | |
| 27 | 15 | // returns. | |
| 28 | 16 | func quarantineSnapshotCoordinatorRetainingQueuedCapture(sess *session) <-chan struct{} { | |
| 29 | - | return quarantineSnapshotCoordinatorWithOptions(sess, true).done | |
| 17 | + | return quarantineSnapshotCoordinatorWithOptions(sess, true) | |
| 30 | 18 | } | |
| 31 | 19 | ||
| 32 | - | func quarantineSnapshotCoordinatorWithEpoch(sess *session) snapshotCoordinatorQuarantine { | |
| 33 | - | return quarantineSnapshotCoordinatorWithOptions(sess, false) | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | func quarantineSnapshotCoordinatorWithOptions(sess *session, retainQueuedCapture bool) snapshotCoordinatorQuarantine { | |
| 20 | + | func quarantineSnapshotCoordinatorWithOptions(sess *session, retainQueuedCapture bool) <-chan struct{} { | |
| 37 | 21 | if sess == nil { | |
| 38 | 22 | done := make(chan struct{}) | |
| 39 | 23 | close(done) | |
| 40 | - | return snapshotCoordinatorQuarantine{done: done} | |
| 24 | + | return done | |
| 41 | 25 | } | |
| 42 | 26 | sess.snapshotMu.Lock() | |
| 43 | - | sess.snapshotQuarantineEpoch++ | |
| 44 | - | epoch := sess.snapshotQuarantineEpoch | |
| 45 | 27 | sess.snapshotQuarantined = true | |
| 46 | 28 | sess.snapEligible.Store(false) | |
| 47 | 29 | if sess.snapshotPublicationCancel != nil { | |
| @@ -65,48 +47,7 @@ func quarantineSnapshotCoordinatorWithOptions(sess *session, retainQueuedCapture | |||
| 65 | 47 | } | |
| 66 | 48 | sess.signalSnapshotChangedLocked() | |
| 67 | 49 | sess.snapshotMu.Unlock() | |
| 68 | - | return snapshotCoordinatorQuarantine{done: done, epoch: epoch} | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | // resumeSnapshotCoordinatorForNewIdentity starts a fresh repository lineage | |
| 72 | - | // only after rename committed its new in-memory name. The epoch prevents a | |
| 73 | - | // concurrent teardown from being undone by a late rename completion. | |
| 74 | - | func resumeSnapshotCoordinatorForNewIdentity(sess *session, quarantine snapshotCoordinatorQuarantine) bool { | |
| 75 | - | if sess == nil { | |
| 76 | - | return false | |
| 77 | - | } | |
| 78 | - | sess.snapshotMu.Lock() | |
| 79 | - | if !sess.snapshotQuarantined || sess.snapshotQuarantineEpoch != quarantine.epoch { | |
| 80 | - | sess.snapshotMu.Unlock() | |
| 81 | - | return false | |
| 82 | - | } | |
| 83 | - | if sess.snapshotPublicationCancel != nil { | |
| 84 | - | sess.snapshotPublicationCancel() | |
| 85 | - | } | |
| 86 | - | sess.snapshotPublicationContext, sess.snapshotPublicationCancel = context.WithCancel(context.Background()) | |
| 87 | - | sess.snapshotQuarantined = false | |
| 88 | - | // A repository name owns its own generation stream. Reset all scheduler | |
| 89 | - | // state while eligibility remains false, then atomically make generation 1 | |
| 90 | - | // dirty and wake the scheduler. | |
| 91 | - | sess.snapshotGeneration = 1 | |
| 92 | - | sess.snapshotPublishedGeneration = 0 | |
| 93 | - | sess.snapshotPublishedCheckpoint = nil | |
| 94 | - | sess.snapshotPublishedMutationRevision = 0 | |
| 95 | - | sess.snapshotForcedGeneration = 0 | |
| 96 | - | sess.snapshotNextEligibleAt = time.Time{} | |
| 97 | - | sess.snapshotChunkCache = newSnapshotChunkCache(snapshotChunkCacheLimit) | |
| 98 | - | sess.snapDirty.Store(true) | |
| 99 | - | sess.snapEligible.Store(true) | |
| 100 | - | sess.signalSnapshotChangedLocked() | |
| 101 | - | wake := sess.snapshotWake | |
| 102 | - | sess.snapshotMu.Unlock() | |
| 103 | - | if wake != nil { | |
| 104 | - | select { | |
| 105 | - | case wake <- struct{}{}: | |
| 106 | - | default: | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | return true | |
| 50 | + | return done | |
| 110 | 51 | } | |
| 111 | 52 | ||
| 112 | 53 | func markSnapshotDirty(sess *session) { | |
| @@ -1,23 +1,11 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | - | import ( | |||
| 4 | - | "context" | |||
| 5 | - | "time" | |||
| 6 | - | ) | |||
| 7 | - | ||||
| 8 | - | // snapshotCoordinatorQuarantine identifies one stop request. A later teardown | |||
| 9 | - | // can supersede it, in which case only that later owner may resume publication. | |||
| 10 | - | type snapshotCoordinatorQuarantine struct { | |||
| 11 | - | done <-chan struct{} | |||
| 12 | - | epoch uint64 | |||
| 13 | - | } | |||
| 14 | - | ||||
| 15 | // quarantineSnapshotCoordinator prevents a deleted session from starting any | 3 | // quarantineSnapshotCoordinator prevents a deleted session from starting any | ||
| 16 | // further publication. The returned join channel is closed after an already | 4 | // further publication. The returned join channel is closed after an already | ||
| 17 | // started repository call returns; callers must wait without daemon or session | 5 | // started repository call returns; callers must wait without daemon or session | ||
| 18 | // locks before deleting repository state. | 6 | // locks before deleting repository state. | ||
| 19 | func quarantineSnapshotCoordinator(sess *session) <-chan struct{} { | 7 | func quarantineSnapshotCoordinator(sess *session) <-chan struct{} { | ||
| 20 | - | return quarantineSnapshotCoordinatorWithEpoch(sess).done | 8 | + | return quarantineSnapshotCoordinatorWithOptions(sess, false) |
| 21 | } | 9 | } | ||
| 22 | 10 | ||||
| 23 | // quarantineSnapshotCoordinatorRetainingQueuedCapture is used after a forced | 11 | // quarantineSnapshotCoordinatorRetainingQueuedCapture is used after a forced | ||
| @@ -26,22 +14,16 @@ func quarantineSnapshotCoordinator(sess *session) <-chan struct{} { | |||||
| 26 | // quarantined, and the worker eventually discards it after its blocked call | 14 | // quarantined, and the worker eventually discards it after its blocked call | ||
| 27 | // returns. | 15 | // returns. | ||
| 28 | func quarantineSnapshotCoordinatorRetainingQueuedCapture(sess *session) <-chan struct{} { | 16 | func quarantineSnapshotCoordinatorRetainingQueuedCapture(sess *session) <-chan struct{} { | ||
| 29 | - | return quarantineSnapshotCoordinatorWithOptions(sess, true).done | 17 | + | return quarantineSnapshotCoordinatorWithOptions(sess, true) |
| 30 | } | 18 | } | ||
| 31 | 19 | ||||
| 32 | - | func quarantineSnapshotCoordinatorWithEpoch(sess *session) snapshotCoordinatorQuarantine { | 20 | + | func quarantineSnapshotCoordinatorWithOptions(sess *session, retainQueuedCapture bool) <-chan struct{} { |
| 33 | - | return quarantineSnapshotCoordinatorWithOptions(sess, false) | |||
| 34 | - | } | |||
| 35 | - | ||||
| 36 | - | func quarantineSnapshotCoordinatorWithOptions(sess *session, retainQueuedCapture bool) snapshotCoordinatorQuarantine { | |||
| 37 | if sess == nil { | 21 | if sess == nil { | ||
| 38 | done := make(chan struct{}) | 22 | done := make(chan struct{}) | ||
| 39 | close(done) | 23 | close(done) | ||
| 40 | - | return snapshotCoordinatorQuarantine{done: done} | 24 | + | return done |
| 41 | } | 25 | } | ||
| 42 | sess.snapshotMu.Lock() | 26 | sess.snapshotMu.Lock() | ||
| 43 | - | sess.snapshotQuarantineEpoch++ | |||
| 44 | - | epoch := sess.snapshotQuarantineEpoch | |||
| 45 | sess.snapshotQuarantined = true | 27 | sess.snapshotQuarantined = true | ||
| 46 | sess.snapEligible.Store(false) | 28 | sess.snapEligible.Store(false) | ||
| 47 | if sess.snapshotPublicationCancel != nil { | 29 | if sess.snapshotPublicationCancel != nil { | ||
| @@ -65,48 +47,7 @@ func quarantineSnapshotCoordinatorWithOptions(sess *session, retainQueuedCapture | |||||
| 65 | } | 47 | } | ||
| 66 | sess.signalSnapshotChangedLocked() | 48 | sess.signalSnapshotChangedLocked() | ||
| 67 | sess.snapshotMu.Unlock() | 49 | sess.snapshotMu.Unlock() | ||
| 68 | - | return snapshotCoordinatorQuarantine{done: done, epoch: epoch} | 50 | + | return done |
| 69 | - | } | |||
| 70 | - | ||||
| 71 | - | // resumeSnapshotCoordinatorForNewIdentity starts a fresh repository lineage | |||
| 72 | - | // only after rename committed its new in-memory name. The epoch prevents a | |||
| 73 | - | // concurrent teardown from being undone by a late rename completion. | |||
| 74 | - | func resumeSnapshotCoordinatorForNewIdentity(sess *session, quarantine snapshotCoordinatorQuarantine) bool { | |||
| 75 | - | if sess == nil { | |||
| 76 | - | return false | |||
| 77 | - | } | |||
| 78 | - | sess.snapshotMu.Lock() | |||
| 79 | - | if !sess.snapshotQuarantined || sess.snapshotQuarantineEpoch != quarantine.epoch { | |||
| 80 | - | sess.snapshotMu.Unlock() | |||
| 81 | - | return false | |||
| 82 | - | } | |||
| 83 | - | if sess.snapshotPublicationCancel != nil { | |||
| 84 | - | sess.snapshotPublicationCancel() | |||
| 85 | - | } | |||
| 86 | - | sess.snapshotPublicationContext, sess.snapshotPublicationCancel = context.WithCancel(context.Background()) | |||
| 87 | - | sess.snapshotQuarantined = false | |||
| 88 | - | // A repository name owns its own generation stream. Reset all scheduler | |||
| 89 | - | // state while eligibility remains false, then atomically make generation 1 | |||
| 90 | - | // dirty and wake the scheduler. | |||
| 91 | - | sess.snapshotGeneration = 1 | |||
| 92 | - | sess.snapshotPublishedGeneration = 0 | |||
| 93 | - | sess.snapshotPublishedCheckpoint = nil | |||
| 94 | - | sess.snapshotPublishedMutationRevision = 0 | |||
| 95 | - | sess.snapshotForcedGeneration = 0 | |||
| 96 | - | sess.snapshotNextEligibleAt = time.Time{} | |||
| 97 | - | sess.snapshotChunkCache = newSnapshotChunkCache(snapshotChunkCacheLimit) | |||
| 98 | - | sess.snapDirty.Store(true) | |||
| 99 | - | sess.snapEligible.Store(true) | |||
| 100 | - | sess.signalSnapshotChangedLocked() | |||
| 101 | - | wake := sess.snapshotWake | |||
| 102 | - | sess.snapshotMu.Unlock() | |||
| 103 | - | if wake != nil { | |||
| 104 | - | select { | |||
| 105 | - | case wake <- struct{}{}: | |||
| 106 | - | default: | |||
| 107 | - | } | |||
| 108 | - | } | |||
| 109 | - | return true | |||
| 110 | } | 51 | } | ||
| 111 | 52 | ||||
| 112 | func markSnapshotDirty(sess *session) { | 53 | func markSnapshotDirty(sess *session) { | ||