Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions docs/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ replication rounds write it, so the claim path is unchanged.
tombstone no claim holds is purged checkpoint first. Retention is a lower
bound: a purge can run up to an hour late. An archive kept forever (no
`archive_delete_after_seconds`) stays until a client adopts and releases it.
A checkpoint the claims journal may still name is never deleted: after a
failed journal write its delete waits for the next write that lands, and a
replica retirement keeps its tombstone until the hourly sweep after that.
- **Usage journal.** The journal stays per node: an adopted claim's `archive`
is in the old owner's `usage.jsonl`, and its `unarchive`, `release` or
`archive_delete` is in the adopter's or the purging node's. Fold the journals
Expand Down Expand Up @@ -307,19 +310,22 @@ checkpoint store and keeps each claim's registry row on the latest snapshot:
about 185 s. The first round of a claim after a wake or adoption uploads the
whole claim, so that pass is the long pass in this bound.
While rounds fail it is the time since the last one that committed.
- **Lifecycle.** A release consumes the row, then deletes the replica and the
tombstone. A hibernate leaves the row on the last replica. An archive puts its
- **Lifecycle.** A release consumes the row, then deletes the tombstone and the
replica. A hibernate leaves the row on the last replica. An archive puts its
own checkpoint over the row, with its retention deadline, and deletes the
replica. A lease expiry with `on_expire` release drops the row and the replica
replica. A lease expiry with `on_expire` destroy drops the row and the replica
first, and the next reap destroys the claim; with `on_expire` archive, the
archive puts its checkpoint over the row. Turning `replicate_seconds` off
takes a restart, since a reload that changes it is refused; the restarted node
drops the rows and replicas of the pool's claims on its first pass. A claim
that holds a replica when the node restarts with replication off is still
treated as replicated by the fence until the next restart, so during a
registry outage it answers 503 like a replicated claim; that costs
availability only, never a second copy or data. A restart keeps the node's own
live row for a running claim and takes the checkpoint a crashed round left on
availability only, never a second copy or data. Turning `archive_failover`
off stops every registry read and write: a claim that holds a replica then
expires and is released like any other, and leaves its row and replica
checkpoint behind, the same residue as a node that never returns.
A restart keeps the node's own live row for a running claim and takes the checkpoint a crashed round left on
it; a claim whose row another node holds, or whose row a finished adopter
consumed, is dropped with its VM. A replica row has no retention deadline: the
hourly sweep never purges it, because no round refreshes it while the claim is
Expand Down
6 changes: 4 additions & 2 deletions sandboxd/pool/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ func (m *Manager) deleteMarked(ckID string) bool {
}

func (m *Manager) archiveCkPinned(ckID string) bool {
_, pinned := m.pinnedArchiveCks()[ckID]
return pinned
m.mu.Lock()
defer m.mu.Unlock()
_, pending := m.pendingCks[ckID]
return pending || m.store.referencesCheckpoint(ckID)
}
1 change: 0 additions & 1 deletion sandboxd/pool/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,6 @@ func TestArchiveDeleteRetryRechecksWakeRollback(t *testing.T) {
if err := <-woke; err == nil {
t.Fatal("wake succeeded despite a persist failure")
}
<-retried
m.mu.Lock()
archived := sb.ArchiveCk == ck && sb.VMName == ""
m.mu.Unlock()
Expand Down
13 changes: 4 additions & 9 deletions sandboxd/pool/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,20 +340,15 @@ func (m *Manager) sweepExpiredCheckpoints(ctx context.Context) {
if ckpt.CreatedAt.After(cutoff) {
continue
}
if err := m.deleteCkLocked(ctx, ckpt.ID); err != nil {
unlock := m.lockRec(ckpt.ID)
err := m.ckpts.Delete(ctx, ckpt.ID)
unlock()
if err != nil {
logger.Errorf(ctx, err, "expire %s", ckpt.ID)
}
}
}

// deleteCkLocked removes a checkpoint under its record lock, releasing it before eviction.
func (m *Manager) deleteCkLocked(ctx context.Context, ckID string) error {
unlock := m.lockRec(ckID)
err := m.ckpts.Delete(ctx, ckID)
unlock()
return err
}

func (m *Manager) loadCheckpoint(ctx context.Context, ckptID string) (types.Checkpoint, error) {
if !store.CheckpointIDRe.MatchString(ckptID) {
return types.Checkpoint{}, ErrUnknownCheckpoint
Expand Down
Loading