main 5041353

runtime_residency.go RawHistoryFolder

1package usecase
2
3import "time"
4
5// ResidencyAction is the single decision a RuntimeResidency event produces.
6// The UI boundary maps it to scheduler operations: clock and timer handles
7// stay outside this controller, which never sleeps.
8type ResidencyAction int
9
10const (
11	// ResidencyNone requires no scheduler change.
12	ResidencyNone ResidencyAction = iota
13	// ResidencyArm starts (or restarts) the idle deadline.
14	ResidencyArm
15	// ResidencyCancel drops a previously armed idle deadline.
16	ResidencyCancel
17	// ResidencyQuit ends the process through the orderly shutdown path.
18	ResidencyQuit
19)
20
21// ResidencyDecision is the outcome of one residency event.
22type ResidencyDecision struct {
23	Action     ResidencyAction
24	Deadline   time.Time
25	Generation uint64
26}
27
28// RuntimeResidency is a testable bounded-residency policy: after the last
29// user window closes it arms a deadline instead of quitting immediately, so
30// a fast reopen can reuse the retained runtime. It tracks only engine-neutral
31// counts; native GTK holds, timers, and quiescence signals live in UI
32// adapters. The zero timeout preserves the existing last-window exit.
33type RuntimeResidency struct {
34	timeout    time.Duration
35	windows    int
36	activeWork int
37	shutdown   bool
38	armed      bool
39	deadline   time.Time
40	generation uint64
41}
42
43// NewRuntimeResidency returns a policy with the given idle timeout. Negative
44// values clamp to zero (disabled). The accepted configuration range is
45// 0..300000ms; enforcement of that range belongs to config validation.
46func NewRuntimeResidency(timeout time.Duration) *RuntimeResidency {
47	if timeout < 0 {
48		timeout = 0
49	}
50	return &RuntimeResidency{timeout: timeout}
51}
52
53// WindowOpened records a user window (including native browser/auth popups
54// that still belong to the application) and cancels any armed deadline.
55func (r *RuntimeResidency) WindowOpened(_ time.Time) ResidencyDecision {
56	r.windows++
57	if r.armed {
58		return r.cancel()
59	}
60	return ResidencyDecision{Action: ResidencyNone, Generation: r.generation}
61}
62
63// WindowClosed records a user-window close. Duplicate closes are idempotent
64// and never drive the count negative.
65func (r *RuntimeResidency) WindowClosed(now time.Time) ResidencyDecision {
66	if r.windows > 0 {
67		r.windows--
68	}
69	return r.evaluateIdle(now)
70}
71
72// OpenFailed records a failed reopen attempt. Idle eligibility is restored
73// rather than stranding the policy: a failed open can never leak the
74// application hold indefinitely.
75func (r *RuntimeResidency) OpenFailed(now time.Time) ResidencyDecision {
76	if r.windows > 0 {
77		r.windows--
78	}
79	return r.evaluateIdle(now)
80}
81
82// WorkStarted records newly accepted native work. An armed deadline is
83// canceled: expiry must never fire while required work is outstanding.
84func (r *RuntimeResidency) WorkStarted(_ time.Time) ResidencyDecision {
85	r.activeWork++
86	if r.armed {
87		return r.cancel()
88	}
89	return ResidencyDecision{Action: ResidencyNone, Generation: r.generation}
90}
91
92// WorkFinished records settled work and re-evaluates idle eligibility.
93func (r *RuntimeResidency) WorkFinished(now time.Time) ResidencyDecision {
94	if r.activeWork > 0 {
95		r.activeWork--
96	}
97	return r.evaluateIdle(now)
98}
99
100// ShutdownRequested records an explicit quit or signal. It always decides
101// Quit immediately and never waits for the idle deadline.
102func (r *RuntimeResidency) ShutdownRequested() ResidencyDecision {
103	r.shutdown = true
104	r.armed = false
105	return ResidencyDecision{Action: ResidencyQuit, Generation: r.generation}
106}
107
108// IdleArmed reports whether an idle deadline is currently armed.
109func (r *RuntimeResidency) IdleArmed() bool {
110	return r.armed
111}
112
113// HandleExpiry processes a fired idle timer. Stale generations (superseded by
114// a later arm/cancel) are ignored. Expiry quits only when still quiescent;
115// otherwise it stands down and lets the next event re-arm.
116func (r *RuntimeResidency) HandleExpiry(_ time.Time, generation uint64) ResidencyDecision {
117	if !r.armed || generation != r.generation {
118		return ResidencyDecision{Action: ResidencyNone, Generation: r.generation}
119	}
120	if r.windows > 0 || r.activeWork > 0 || r.shutdown {
121		r.armed = false
122		if r.shutdown {
123			return ResidencyDecision{Action: ResidencyQuit, Generation: r.generation}
124		}
125		return ResidencyDecision{Action: ResidencyNone, Generation: r.generation}
126	}
127	r.armed = false
128	return ResidencyDecision{Action: ResidencyQuit, Generation: r.generation}
129}
130
131// Armed reports whether an idle deadline is currently armed. The UI boundary
132// uses it only for diagnostics; scheduling follows decisions.
133func (r *RuntimeResidency) Armed() bool { return r.armed }
134
135// Generation returns the current timer generation for expiry correlation.
136func (r *RuntimeResidency) Generation() uint64 { return r.generation }
137
138func (r *RuntimeResidency) cancel() ResidencyDecision {
139	r.armed = false
140	r.generation++
141	return ResidencyDecision{Action: ResidencyCancel, Generation: r.generation}
142}
143
144func (r *RuntimeResidency) evaluateIdle(now time.Time) ResidencyDecision {
145	if r.shutdown {
146		r.armed = false
147		return ResidencyDecision{Action: ResidencyQuit, Generation: r.generation}
148	}
149	if r.windows > 0 {
150		if r.armed {
151			return r.cancel()
152		}
153		return ResidencyDecision{Action: ResidencyNone, Generation: r.generation}
154	}
155	if r.timeout <= 0 {
156		r.armed = false
157		return ResidencyDecision{Action: ResidencyQuit, Generation: r.generation}
158	}
159	if r.activeWork > 0 {
160		return ResidencyDecision{Action: ResidencyNone, Generation: r.generation}
161	}
162	r.armed = true
163	r.deadline = now.Add(r.timeout)
164	r.generation++
165	return ResidencyDecision{Action: ResidencyArm, Deadline: r.deadline, Generation: r.generation}
166}

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 8

Tags 42

↑↓ moveenter openesc close