| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "reasonix/internal/event" |
| 5 | "reasonix/internal/jobs" |
| 6 | ) |
| 7 | |
| 8 | type controllerBackground struct { |
| 9 | scope *jobs.SessionBackgroundScope |
| 10 | recorder jobs.TaskRecorder |
| 11 | sink event.Sink |
| 12 | release func() |
| 13 | rollback func() |
| 14 | publish func() |
| 15 | candidate bool |
| 16 | retired bool |
| 17 | } |
| 18 | |
| 19 | func (c *Controller) BackgroundScope() *jobs.SessionBackgroundScope { return c.background.scope } |
| 20 | |
| 21 | // ModelReplacementBlocked retains the strict foreground/interaction guards, |
| 22 | // but session-owned processes need not stop for a model-only replacement. |
| 23 | func ModelReplacementBlocked(ctrl interface{ RuntimeStatus() RuntimeStatus }) bool { |
| 24 | if ctrl == nil { |
| 25 | return false |
| 26 | } |
| 27 | if c, ok := ctrl.(*Controller); ok && c.background.scope != nil { |
| 28 | c.mu.Lock() |
| 29 | busy := c.closed || c.bodyActiveLocked() || c.cancelRequestedLocked() || c.maintenance != nil || c.finalizingLocked() || c.recoveryRequiredLocked() |
| 30 | c.mu.Unlock() |
| 31 | return busy || c.PendingPrompt() || len(c.jobs.BlockingJobs(c.parentSessionID())) > 0 |
| 32 | } |
| 33 | status := ctrl.RuntimeStatus() |
| 34 | return status.Running || status.PendingPrompt || status.BackgroundJobs > 0 |
| 35 | } |
| 36 | |
| 37 | func (c *Controller) ModelReplacementJobs() []jobs.View { |
| 38 | if c.background.scope == nil { |
| 39 | return c.Jobs() |
| 40 | } |
| 41 | return c.jobs.BlockingJobs(c.parentSessionID()) |
| 42 | } |
| 43 | |
| 44 | // StageBackgroundReplacement transfers the reservation to the candidate. It |
| 45 | // remains sealed until publication or candidate disposal, not merely Build. |
| 46 | func (c *Controller) StageBackgroundReplacement(release func()) { |
| 47 | c.mu.Lock() |
| 48 | c.background.release = release |
| 49 | c.mu.Unlock() |
| 50 | } |
| 51 | |
| 52 | func (c *Controller) finishBackgroundReplacement(publish bool) { |
| 53 | c.mu.Lock() |
| 54 | release := c.background.release |
| 55 | rollback := c.background.rollback |
| 56 | publishRuntime := c.background.publish |
| 57 | c.background.release = nil |
| 58 | c.background.rollback = nil |
| 59 | c.background.publish = nil |
| 60 | c.mu.Unlock() |
| 61 | if publish && publishRuntime != nil { |
| 62 | publishRuntime() |
| 63 | } |
| 64 | if publish && c.background.scope != nil { |
| 65 | c.background.scope.Bind(c.background.sink, c.background.recorder) |
| 66 | c.mu.Lock() |
| 67 | c.background.candidate = false |
| 68 | c.mu.Unlock() |
| 69 | } |
| 70 | if !publish && rollback != nil { |
| 71 | rollback() |
| 72 | } |
| 73 | if release != nil { |
| 74 | release() |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func (c *Controller) PublishBackgroundScope() { c.finishBackgroundReplacement(true) } |
| 79 | |
| 80 | func (c *Controller) StageReplacementRollback(rollback func()) { |
| 81 | c.mu.Lock() |
| 82 | c.background.rollback = rollback |
| 83 | c.mu.Unlock() |
| 84 | } |
| 85 | |
| 86 | // StageReplacementPublication commits prepared, nonblocking runtime metadata |
| 87 | // only after execution ownership transfers. It must not perform I/O or invoke |
| 88 | // external callbacks while the host's compare-and-publish lock is held. |
| 89 | func (c *Controller) StageReplacementPublication(publish func()) { |
| 90 | c.mu.Lock() |
| 91 | c.background.publish = publish |
| 92 | c.mu.Unlock() |
| 93 | } |
| 94 | |
| 95 | func (c *Controller) isBackgroundCandidate() bool { |
| 96 | c.mu.Lock() |
| 97 | defer c.mu.Unlock() |
| 98 | return c.background.candidate |
| 99 | } |
| 100 | |
| 101 | func (c *Controller) receivesBackgroundRuntimeEvents() bool { |
| 102 | c.mu.Lock() |
| 103 | inactive := c.background.candidate || c.background.retired || c.closed |
| 104 | c.mu.Unlock() |
| 105 | return !inactive && (c.background.scope == nil || !c.background.scope.Manager.ReplacementInProgress()) |
| 106 | } |
| 107 |