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}