snapshot_coordinator.go RawHistoryFolder

1package daemon
2
3// quarantineSnapshotCoordinator prevents a deleted session from starting any
4// further publication. The returned join channel is closed after an already
5// started repository call returns; callers must wait without daemon or session
6// locks before deleting repository state.
7func quarantineSnapshotCoordinator(sess *session) <-chan struct{} {
8	return quarantineSnapshotCoordinatorWithOptions(sess, false)
9}
10
11// quarantineSnapshotCoordinatorRetainingQueuedCapture is used after a forced
12// shutdown checkpoint times out. Its queued capture is logical retry state, not
13// a repository operation: startSnapshotPublication rejects it while
14// quarantined, and the worker eventually discards it after its blocked call
15// returns.
16func quarantineSnapshotCoordinatorRetainingQueuedCapture(sess *session) <-chan struct{} {
17	return quarantineSnapshotCoordinatorWithOptions(sess, true)
18}
19
20func quarantineSnapshotCoordinatorWithOptions(sess *session, retainQueuedCapture bool) <-chan struct{} {
21	if sess == nil {
22		done := make(chan struct{})
23		close(done)
24		return done
25	}
26	sess.snapshotMu.Lock()
27	sess.snapshotQuarantined = true
28	sess.snapEligible.Store(false)
29	if sess.snapshotPublicationCancel != nil {
30		sess.snapshotPublicationCancel()
31	}
32	if sess.snapshotQueuedCapture != nil && !retainQueuedCapture {
33		// A rename or purge must discard a capture under the old identity. The
34		// global queue may still contain it, but startSnapshotPublication
35		// compares identity and will discard it even if this session is renamed.
36		sess.snapshotQueuedCapture.coordinatorDiscarded = true
37		sess.snapshotQueuedCapture = nil
38		if sess.snapshotPendingCaptures > 0 {
39			sess.snapshotPendingCaptures--
40		}
41		sess.snapshotPending = sess.snapshotPendingCaptures > 0 || sess.snapshotInFlightCapture != nil
42	}
43	done := sess.snapshotPublicationDone
44	if done == nil {
45		done = make(chan struct{})
46		close(done)
47	}
48	sess.signalSnapshotChangedLocked()
49	sess.snapshotMu.Unlock()
50	return done
51}
52
53// rollbackSnapshotQuarantine reverses quarantineSnapshotCoordinator when a
54// teardown aborts before it owns any destructive repository operation and the
55// session therefore stays live and registered. The quarantine cancelled the
56// publication context and flipped eligibility, so a surviving session would
57// otherwise never checkpoint again: this discards the cancelled context so the
58// next schedule allocates a fresh one, restores the captured pre-quarantine
59// eligibility, and marks the session dirty so the mutations its ineligible fast
60// path suppressed during the quarantine window are captured by a later
61// publication. A queued capture the quarantine discarded stays discarded:
62// startSnapshotPublication rejects it by identity and the forced dirty
63// generation guarantees a replacement capture.
64func rollbackSnapshotQuarantine(sess *session, wasEligible bool) {
65	if sess == nil {
66		return
67	}
68	sess.snapshotMu.Lock()
69	sess.snapshotQuarantined = false
70	sess.snapshotPublicationContext = nil
71	sess.snapshotPublicationCancel = nil
72	sess.snapshotMu.Unlock()
73	sess.snapEligible.Store(wasEligible)
74	if wasEligible {
75		markSnapshotDirty(sess)
76	}
77}
78
79func markSnapshotDirty(sess *session) {
80	if sess == nil || !sess.snapEligible.Load() {
81		return
82	}
83	sess.snapshotMu.Lock()
84	sess.snapshotGeneration++
85	sess.snapDirty.Store(true)
86	sess.signalSnapshotChangedLocked()
87	wake := sess.snapshotWake
88	sess.snapshotMu.Unlock()
89	if wake != nil {
90		select {
91		case wake <- struct{}{}:
92		default:
93		}
94	}
95}
96
97// snapshotChangeLocked returns a channel closed by the next snapshot state
98// transition. snapshotMu must be held.
99func (sess *session) snapshotChangeLocked() chan struct{} {
100	if sess.snapshotChanged == nil {
101		sess.snapshotChanged = make(chan struct{})
102	}
103	return sess.snapshotChanged
104}
105
106// signalSnapshotChangedLocked wakes waiters after a snapshot state transition.
107// snapshotMu must be held.
108func (sess *session) signalSnapshotChangedLocked() {
109	close(sess.snapshotChangeLocked())
110	sess.snapshotChanged = make(chan struct{})
111}
112
113// waitForSnapshotGeneration waits only for the bounded terminal checkpoint
114// deadline. It observes successful publication of the requested mutation
115// revision, rather than dirty state, so a concurrent later mutation cannot
116// turn a completed forced checkpoint into a false timeout.
117func (d *Daemon) waitForSnapshotGeneration(sess *session, generation uint64) bool {
118	if d == nil || sess == nil || generation == 0 {
119		return true
120	}
121	deadline := newSnapshotShutdownDeadline(d.clock)
122	defer deadline.stop()
123	return d.waitForSnapshotGenerationWithDeadline(sess, generation, deadline)
124}
125
126// waitForSnapshotGenerationWithDeadline waits for a forced checkpoint without
127// allocating another shutdown interval. Serve shares one deadline across every
128// session and the subsequent worker drain.
129func (d *Daemon) waitForSnapshotGenerationWithDeadline(sess *session, generation uint64, deadline *snapshotShutdownDeadline) bool {
130	if d == nil || sess == nil || generation == 0 {
131		return true
132	}
133	if deadline == nil {
134		return d.waitForSnapshotGeneration(sess, generation)
135	}
136	for {
137		// Once the shared deadline is observable, timeout wins even if publication
138		// completion becomes ready concurrently. This preserves the timed-out
139		// session identity and avoids nondeterministic success after the budget.
140		select {
141		case <-deadline.Done():
142			return false
143		default:
144		}
145		sess.snapshotMu.Lock()
146		published := sess.snapshotPublishedMutationRevision >= generation
147		changed := sess.snapshotChangeLocked()
148		sess.snapshotMu.Unlock()
149		if published {
150			return true
151		}
152		select {
153		case <-changed:
154		case <-deadline.Done():
155			return false
156		}
157	}
158}
159
160// startSnapshotPublication moves a retained capture from the producer queue to
161// the repository worker. Quarantine and this transition share snapshotMu, so a
162// repository call cannot begin after destructive teardown has quarantined the
163// session.
164func startSnapshotPublication(capture *snapshotCapture) bool {
165	if capture == nil || capture.session == nil {
166		return false
167	}
168	sess := capture.session
169	sess.snapshotMu.Lock()
170	defer sess.snapshotMu.Unlock()
171	if sess.snapshotQuarantined || sess.snapshotQueuedCapture != capture {
172		return false
173	}
174	sess.snapshotQueuedCapture = nil
175	sess.snapshotInFlightCapture = capture
176	sess.snapshotPublicationDone = make(chan struct{})
177	sess.snapshotPending = true
178	sess.signalSnapshotChangedLocked()
179	return true
180}
181
182func (d *Daemon) finishSnapshotCapture(capture *snapshotCapture, succeeded bool) {
183	if capture == nil {
184		return
185	}
186	capture.finishOnce.Do(func() {
187		var shouldScheduleForcedSuccessor bool
188		capture.session.snapshotMu.Lock()
189		// A capture the quarantine discarded already dropped its pending count when
190		// it was detached from the producer queue; the worker still calls finish on
191		// it after rejecting the stale identity. Decrementing again here would
192		// under-count a replacement capture scheduled after a rollback, so a
193		// discarded capture must not decrement twice.
194		if !capture.coordinatorDiscarded && capture.session.snapshotPendingCaptures > 0 {
195			capture.session.snapshotPendingCaptures--
196		}
197		if capture.session.snapshotQueuedCapture == capture {
198			capture.session.snapshotQueuedCapture = nil
199		}
200		if capture.session.snapshotInFlightCapture == capture {
201			capture.session.snapshotInFlightCapture = nil
202			if capture.session.snapshotPublicationDone != nil {
203				close(capture.session.snapshotPublicationDone)
204				capture.session.snapshotPublicationDone = nil
205			}
206		}
207		capture.session.snapshotPending = capture.session.snapshotPendingCaptures > 0 ||
208			capture.session.snapshotQueuedCapture != nil || capture.session.snapshotInFlightCapture != nil
209		if !capture.coordinatorDiscarded && capture.attemptKind == snapshotAttemptRoutine && d.snapshotRepository != nil {
210			// The interval is measured from completion, including a failed
211			// attempt. A forced teardown checkpoint deliberately leaves it alone.
212			capture.session.snapshotNextEligibleAt = d.clock.Now().Add(snapshotInterval)
213		}
214		mutationRevision := capture.mutationRevision
215		// Older unit fixtures created captures before mutation revisions were
216		// recorded explicitly. Production captures always set it at scheduling;
217		// retaining this fallback keeps those fixtures representative.
218		if mutationRevision == 0 {
219			mutationRevision = capture.generation
220		}
221		if succeeded {
222			if capture.generation >= capture.session.snapshotPublishedGeneration &&
223				capture.checkpoint.Generation == capture.generation && capture.checkpoint.ManifestDigest != ([32]byte{}) {
224				checkpoint := capture.checkpoint
225				capture.session.snapshotPublishedCheckpoint = &checkpoint
226			}
227			capture.session.snapshotPublishedGeneration = max(capture.session.snapshotPublishedGeneration, capture.generation)
228			capture.session.snapshotPublishedMutationRevision = max(capture.session.snapshotPublishedMutationRevision, mutationRevision)
229		}
230		if succeeded && capture.session.snapshotGeneration == mutationRevision {
231			capture.session.snapDirty.Store(false)
232		} else if !succeeded {
233			capture.session.snapDirty.Store(true)
234		}
235		if succeeded && capture.session.snapshotForcedGeneration != 0 &&
236			mutationRevision >= capture.session.snapshotForcedGeneration {
237			capture.session.snapshotForcedGeneration = 0
238		}
239		// A failed forced publication remains dirty for ordinary retry rather
240		// than recursively creating forced jobs. A later forced request records
241		// a newer intent and can create its one bounded successor.
242		shouldScheduleForcedSuccessor = capture.session.snapshotForcedGeneration != 0 &&
243			mutationRevision < capture.session.snapshotForcedGeneration &&
244			capture.session.snapshotQueuedCapture == nil && capture.session.snapshotInFlightCapture == nil &&
245			(succeeded || capture.attemptKind != snapshotAttemptForced)
246		capture.session.signalSnapshotChangedLocked()
247		wake := capture.session.snapshotWake
248		capture.session.snapshotMu.Unlock()
249		if capture.normalWorkerAdmitted {
250			d.snapshots.mu.Lock()
251			delete(d.snapshots.admitted, capture)
252			d.snapshots.mu.Unlock()
253		}
254		if wake != nil {
255			select {
256			case wake <- struct{}{}:
257			default:
258			}
259		}
260		if shouldScheduleForcedSuccessor {
261			// This runs after publication completion and outside snapshotMu. It
262			// bypasses routine eligibility while preserving the single queued and
263			// single in-flight capture bound.
264			_ = d.scheduleFinalSnapshot(capture.session)
265		}
266		if succeeded {
267			d.clearSnapshotFailure()
268		}
269	})
270}

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