refactor(app): switch to incremental snapshots
- commit
- cd4708ea0b795fe8d6fe26c4ad95465cf0f31f31
- author
- brice · · 2026-07-21 21:50 UTC
- parent
- 792ed4c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +36 -7
M internal/app/run.go +10 -3
| @@ -450,7 +450,8 @@ func runDaemon() (retErr error) { | |||
| 450 | 450 | daemonOpts = append(daemonOpts, daemon.WithConfig(cfg)) | |
| 451 | 451 | daemonOpts = append(daemonOpts, daemon.WithBarScriptCommandRunner(shellcmd.New())) | |
| 452 | 452 | daemonOpts = append(daemonOpts, daemon.WithProcessInspector(platform.NewProcessInspector()), daemon.WithDirOrHome(platform.DirOrHome)) | |
| 453 | - | daemonOpts = append(daemonOpts, daemon.WithSnapshotStore(snapshotadapter.NewStore(snapshotDir()))) | |
| 453 | + | snapshotRepository := snapshotadapter.NewRepository(snapshotDir()) | |
| 454 | + | daemonOpts = append(daemonOpts, daemon.WithSnapshotRepository(snapshotRepository, snapshotRepository)) | |
| 454 | 455 | daemonOpts = append(daemonOpts, daemon.WithNoticeStore(noticefile.New(platform.StateDir()))) | |
| 455 | 456 | storePath := persist.StorePath(platform.StateDir()) | |
| 456 | 457 | var storeErr error | |
| @@ -1221,10 +1222,16 @@ func runKill(ctx context.Context, name string, all, daemon bool) error { | |||
| 1221 | 1222 | if closeErr := p.Close(); closeErr != nil { | |
| 1222 | 1223 | return fmt.Errorf("vev: closing stored sessions: %w", closeErr) | |
| 1223 | 1224 | } | |
| 1224 | - | snapshots := snapshotadapter.NewStore(snapshotDir()) | |
| 1225 | - | if deleteErr := snapshots.Delete(name); deleteErr != nil { | |
| 1225 | + | snapshots := snapshotadapter.NewRepository(snapshotDir()) | |
| 1226 | + | if deleteErr := snapshots.Delete(ctx, name); deleteErr != nil { | |
| 1226 | 1227 | return fmt.Errorf("vev: deleting session snapshot: %w", deleteErr) | |
| 1227 | 1228 | } | |
| 1229 | + | // A daemon-less delete may be the first command after upgrade, before | |
| 1230 | + | // startup has had a chance to import v3. Remove that one-way source as | |
| 1231 | + | // well so a later daemon cannot resurrect an explicitly killed session. | |
| 1232 | + | if deleteErr := snapshots.DeleteLegacy(ctx, name); deleteErr != nil { | |
| 1233 | + | return fmt.Errorf("vev: deleting legacy session snapshot: %w", deleteErr) | |
| 1234 | + | } | |
| 1228 | 1235 | printKillSuccess(name, all, daemon) | |
| 1229 | 1236 | return nil | |
| 1230 | 1237 | } | |
| @@ -450,7 +450,8 @@ func runDaemon() (retErr error) { | |||||
| 450 | daemonOpts = append(daemonOpts, daemon.WithConfig(cfg)) | 450 | daemonOpts = append(daemonOpts, daemon.WithConfig(cfg)) | ||
| 451 | daemonOpts = append(daemonOpts, daemon.WithBarScriptCommandRunner(shellcmd.New())) | 451 | daemonOpts = append(daemonOpts, daemon.WithBarScriptCommandRunner(shellcmd.New())) | ||
| 452 | daemonOpts = append(daemonOpts, daemon.WithProcessInspector(platform.NewProcessInspector()), daemon.WithDirOrHome(platform.DirOrHome)) | 452 | daemonOpts = append(daemonOpts, daemon.WithProcessInspector(platform.NewProcessInspector()), daemon.WithDirOrHome(platform.DirOrHome)) | ||
| 453 | - | daemonOpts = append(daemonOpts, daemon.WithSnapshotStore(snapshotadapter.NewStore(snapshotDir()))) | 453 | + | snapshotRepository := snapshotadapter.NewRepository(snapshotDir()) |
| 454 | + | daemonOpts = append(daemonOpts, daemon.WithSnapshotRepository(snapshotRepository, snapshotRepository)) | |||
| 454 | daemonOpts = append(daemonOpts, daemon.WithNoticeStore(noticefile.New(platform.StateDir()))) | 455 | daemonOpts = append(daemonOpts, daemon.WithNoticeStore(noticefile.New(platform.StateDir()))) | ||
| 455 | storePath := persist.StorePath(platform.StateDir()) | 456 | storePath := persist.StorePath(platform.StateDir()) | ||
| 456 | var storeErr error | 457 | var storeErr error | ||
| @@ -1221,10 +1222,16 @@ func runKill(ctx context.Context, name string, all, daemon bool) error { | |||||
| 1221 | if closeErr := p.Close(); closeErr != nil { | 1222 | if closeErr := p.Close(); closeErr != nil { | ||
| 1222 | return fmt.Errorf("vev: closing stored sessions: %w", closeErr) | 1223 | return fmt.Errorf("vev: closing stored sessions: %w", closeErr) | ||
| 1223 | } | 1224 | } | ||
| 1224 | - | snapshots := snapshotadapter.NewStore(snapshotDir()) | 1225 | + | snapshots := snapshotadapter.NewRepository(snapshotDir()) |
| 1225 | - | if deleteErr := snapshots.Delete(name); deleteErr != nil { | 1226 | + | if deleteErr := snapshots.Delete(ctx, name); deleteErr != nil { |
| 1226 | return fmt.Errorf("vev: deleting session snapshot: %w", deleteErr) | 1227 | return fmt.Errorf("vev: deleting session snapshot: %w", deleteErr) | ||
| 1227 | } | 1228 | } | ||
| 1229 | + | // A daemon-less delete may be the first command after upgrade, before | |||
| 1230 | + | // startup has had a chance to import v3. Remove that one-way source as | |||
| 1231 | + | // well so a later daemon cannot resurrect an explicitly killed session. | |||
| 1232 | + | if deleteErr := snapshots.DeleteLegacy(ctx, name); deleteErr != nil { | |||
| 1233 | + | return fmt.Errorf("vev: deleting legacy session snapshot: %w", deleteErr) | |||
| 1234 | + | } | |||
| 1228 | printKillSuccess(name, all, daemon) | 1235 | printKillSuccess(name, all, daemon) | ||
| 1229 | return nil | 1236 | return nil | ||
| 1230 | } | 1237 | } | ||
M internal/usecase/daemon/session.go +26 -4
| @@ -553,7 +553,15 @@ func (d *Daemon) renameSession(sess *session, name string) error { | |||
| 553 | 553 | } | |
| 554 | 554 | } | |
| 555 | 555 | if !wasEphemeral && oldName != name { | |
| 556 | - | if d.snapsEnabled && d.snaps != nil { | |
| 556 | + | if d.snapsEnabled && d.snapshotRepository != nil { | |
| 557 | + | if err := d.snapshotRepository.Delete(context.Background(), oldName); err != nil { | |
| 558 | + | if cleanupErr := d.persist.Delete(name); cleanupErr != nil { | |
| 559 | + | d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name) | |
| 560 | + | } | |
| 561 | + | sess.mu.Unlock() | |
| 562 | + | return err | |
| 563 | + | } | |
| 564 | + | } else if d.snapsEnabled && d.snaps != nil { | |
| 557 | 565 | if err := d.snaps.Delete(oldName); err != nil { | |
| 558 | 566 | if cleanupErr := d.persist.Delete(name); cleanupErr != nil { | |
| 559 | 567 | d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name) | |
| @@ -731,9 +739,15 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||
| 731 | 739 | sess.mu.Lock() | |
| 732 | 740 | name := sess.name | |
| 733 | 741 | sess.mu.Unlock() | |
| 734 | - | if err := d.snaps.Delete(name); err != nil { | |
| 735 | - | snapshotDeleteErr = err | |
| 736 | - | d.log.Warn("deleting session snapshot failed", "err", err, "session", name) | |
| 742 | + | if d.snapshotRepository != nil { | |
| 743 | + | // Repository deletion is intentionally deferred until the render | |
| 744 | + | // coordinator is cancelled and joined below. See client.go's lock | |
| 745 | + | // ordering notes: no coordinator callback may race a purge. | |
| 746 | + | } else if d.snaps != nil { | |
| 747 | + | if err := d.snaps.Delete(name); err != nil { | |
| 748 | + | snapshotDeleteErr = err | |
| 749 | + | d.log.Warn("deleting session snapshot failed", "err", err, "session", name) | |
| 750 | + | } | |
| 737 | 751 | } | |
| 738 | 752 | } else { | |
| 739 | 753 | markSnapshotDirty(sess) | |
| @@ -840,6 +854,14 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||
| 840 | 854 | d.log.Warn("removing clipboard temp file failed", "err", err, "path", path) | |
| 841 | 855 | } | |
| 842 | 856 | } | |
| 857 | + | // The coordinator and all panes are now stopped, so no producer can | |
| 858 | + | // publish another generation after this destructive repository delete. | |
| 859 | + | if !ephemeral && purge && d.snapshotRepository != nil { | |
| 860 | + | if err := d.snapshotRepository.Delete(context.Background(), stoppedName); err != nil { | |
| 861 | + | purgeErr = errors.Join(purgeErr, err) | |
| 862 | + | d.log.Warn("deleting session snapshot failed", "err", err, "session", stoppedName) | |
| 863 | + | } | |
| 864 | + | } | |
| 843 | 865 | if empty { | |
| 844 | 866 | d.doneOnce.Do(func() { close(d.done) }) | |
| 845 | 867 | } else if ringing { | |
| @@ -553,7 +553,15 @@ func (d *Daemon) renameSession(sess *session, name string) error { | |||||
| 553 | } | 553 | } | ||
| 554 | } | 554 | } | ||
| 555 | if !wasEphemeral && oldName != name { | 555 | if !wasEphemeral && oldName != name { | ||
| 556 | - | if d.snapsEnabled && d.snaps != nil { | 556 | + | if d.snapsEnabled && d.snapshotRepository != nil { |
| 557 | + | if err := d.snapshotRepository.Delete(context.Background(), oldName); err != nil { | |||
| 558 | + | if cleanupErr := d.persist.Delete(name); cleanupErr != nil { | |||
| 559 | + | d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name) | |||
| 560 | + | } | |||
| 561 | + | sess.mu.Unlock() | |||
| 562 | + | return err | |||
| 563 | + | } | |||
| 564 | + | } else if d.snapsEnabled && d.snaps != nil { | |||
| 557 | if err := d.snaps.Delete(oldName); err != nil { | 565 | if err := d.snaps.Delete(oldName); err != nil { | ||
| 558 | if cleanupErr := d.persist.Delete(name); cleanupErr != nil { | 566 | if cleanupErr := d.persist.Delete(name); cleanupErr != nil { | ||
| 559 | d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name) | 567 | d.log.Warn("cleaning up renamed persisted session failed", "err", cleanupErr, "session", name) | ||
| @@ -731,9 +739,15 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 731 | sess.mu.Lock() | 739 | sess.mu.Lock() | ||
| 732 | name := sess.name | 740 | name := sess.name | ||
| 733 | sess.mu.Unlock() | 741 | sess.mu.Unlock() | ||
| 734 | - | if err := d.snaps.Delete(name); err != nil { | 742 | + | if d.snapshotRepository != nil { |
| 735 | - | snapshotDeleteErr = err | 743 | + | // Repository deletion is intentionally deferred until the render |
| 736 | - | d.log.Warn("deleting session snapshot failed", "err", err, "session", name) | 744 | + | // coordinator is cancelled and joined below. See client.go's lock |
| 745 | + | // ordering notes: no coordinator callback may race a purge. | |||
| 746 | + | } else if d.snaps != nil { | |||
| 747 | + | if err := d.snaps.Delete(name); err != nil { | |||
| 748 | + | snapshotDeleteErr = err | |||
| 749 | + | d.log.Warn("deleting session snapshot failed", "err", err, "session", name) | |||
| 750 | + | } | |||
| 737 | } | 751 | } | ||
| 738 | } else { | 752 | } else { | ||
| 739 | markSnapshotDirty(sess) | 753 | markSnapshotDirty(sess) | ||
| @@ -840,6 +854,14 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 840 | d.log.Warn("removing clipboard temp file failed", "err", err, "path", path) | 854 | d.log.Warn("removing clipboard temp file failed", "err", err, "path", path) | ||
| 841 | } | 855 | } | ||
| 842 | } | 856 | } | ||
| 857 | + | // The coordinator and all panes are now stopped, so no producer can | |||
| 858 | + | // publish another generation after this destructive repository delete. | |||
| 859 | + | if !ephemeral && purge && d.snapshotRepository != nil { | |||
| 860 | + | if err := d.snapshotRepository.Delete(context.Background(), stoppedName); err != nil { | |||
| 861 | + | purgeErr = errors.Join(purgeErr, err) | |||
| 862 | + | d.log.Warn("deleting session snapshot failed", "err", err, "session", stoppedName) | |||
| 863 | + | } | |||
| 864 | + | } | |||
| 843 | if empty { | 865 | if empty { | ||
| 844 | d.doneOnce.Do(func() { close(d.done) }) | 866 | d.doneOnce.Do(func() { close(d.done) }) | ||
| 845 | } else if ringing { | 867 | } else if ringing { | ||