| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sync/atomic" |
| 6 | "time" |
| 7 | |
| 8 | "reasonix/internal/event" |
| 9 | ) |
| 10 | |
| 11 | // turnStallThreshold is the silence after which a running turn is reported as |
| 12 | // possibly stuck. It only warns: the user decides whether to stop, because a |
| 13 | // legitimately long tool and a wedged one look identical from here. |
| 14 | var turnStallThreshold atomic.Int64 |
| 15 | |
| 16 | func init() { turnStallThreshold.Store(int64(10 * time.Minute)) } |
| 17 | |
| 18 | // turnLiveness remembers the last event a running turn produced so a silent |
| 19 | // stretch can be surfaced instead of leaving "working" unexplained. |
| 20 | type turnLiveness struct { |
| 21 | lastEvent atomic.Int64 |
| 22 | warned atomic.Bool |
| 23 | } |
| 24 | |
| 25 | func (l *turnLiveness) reset(now time.Time) { |
| 26 | l.lastEvent.Store(now.UnixNano()) |
| 27 | l.warned.Store(false) |
| 28 | } |
| 29 | |
| 30 | func (l *turnLiveness) observe(e event.Event, now time.Time) { |
| 31 | if e.Kind == event.Notice && e.Code == event.NoticeCodeTurnStalled { |
| 32 | return |
| 33 | } |
| 34 | l.lastEvent.Store(now.UnixNano()) |
| 35 | l.warned.Store(false) |
| 36 | } |
| 37 | |
| 38 | // stalledFor claims the single warning for the current silence. |
| 39 | func (l *turnLiveness) stalledFor(now time.Time) (time.Duration, bool) { |
| 40 | last := l.lastEvent.Load() |
| 41 | if last == 0 { |
| 42 | return 0, false |
| 43 | } |
| 44 | silence := now.Sub(time.Unix(0, last)) |
| 45 | if silence < time.Duration(turnStallThreshold.Load()) { |
| 46 | return 0, false |
| 47 | } |
| 48 | return silence, l.warned.CompareAndSwap(false, true) |
| 49 | } |
| 50 | |
| 51 | func (c *Controller) warnIfTurnStalled(now time.Time) { |
| 52 | c.mu.Lock() |
| 53 | running := c.bodyActiveLocked() |
| 54 | c.mu.Unlock() |
| 55 | if !running { |
| 56 | return |
| 57 | } |
| 58 | silence, ok := c.liveness.stalledFor(now) |
| 59 | if !ok { |
| 60 | return |
| 61 | } |
| 62 | c.sink.Emit(event.Event{ |
| 63 | Kind: event.Notice, |
| 64 | Code: event.NoticeCodeTurnStalled, |
| 65 | Level: event.LevelWarn, |
| 66 | Text: fmt.Sprintf("No progress for %s. The turn is still running; press Stop if it looks stuck.", silence.Round(time.Minute)), |
| 67 | }) |
| 68 | } |
| 69 |