loki-mode 8.0.3 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/events/emit.sh CHANGED
@@ -44,36 +44,79 @@ safe_append_event_jsonl() {
44
44
  if command -v flock >/dev/null 2>&1; then
45
45
  # flock path: bind FD 9 to the sentinel file (created if absent),
46
46
  # take an exclusive lock, append, release on subshell exit.
47
+ #
48
+ # `-w 5` is LOAD-BEARING: a bare `flock -x 9` blocks FOREVER. A holder
49
+ # that is killed mid-write (a reaped CI run, a Ctrl-C'd build) leaves
50
+ # every subsequent emit hung, and because emit.sh is spawned per CLI
51
+ # invocation those hangs ACCUMULATE. Measured 2026-07-29: 59 orphaned
52
+ # emit.sh processes, the oldest alive 21 HOURS, on a machine at 0% idle.
53
+ # Observability must never outlive the thing it observes.
47
54
  (
48
- flock -x 9
55
+ flock -w 5 -x 9 || exit 1
49
56
  printf '%s\n' "$line" >> "$events_path"
50
57
  ) 9>"$lock_target"
51
- return $?
58
+ local frc=$?
59
+ if [ "$frc" -ne 0 ]; then
60
+ # Lock unavailable within the timeout: append unlocked rather than
61
+ # block. A rare interleaved line is strictly better than a hung CLI.
62
+ printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
63
+ fi
64
+ return 0
52
65
  fi
53
66
 
54
- # Fallback: mkdir-based mutex. mkdir is atomic on POSIX.
67
+ # Fallback: mkdir-based mutex. mkdir is atomic on POSIX. macOS ships NO
68
+ # flock(1), so this is the path real users take -- it must be the robust one.
55
69
  local lock_dir="${events_path}.lockdir"
56
70
  local attempts=0
57
- local max_attempts=500 # ~5s at 10ms sleep
71
+ local max_attempts=100 # ~1s at 10ms sleep
72
+ local stale_after=30
58
73
  while ! mkdir "$lock_dir" 2>/dev/null; do
74
+ # COUNT EVERY ITERATION, before any `continue` can skip the increment.
75
+ #
76
+ # This was the v8.1.0 fix's blind spot and it kept the P0 alive. Both
77
+ # `continue` paths below (lock vanished mid-stat; stale lock reclaimed)
78
+ # jumped PAST the increment that used to live at the bottom of the loop,
79
+ # so `max_attempts` was unreachable on those paths and the loop spun
80
+ # forever. Under concurrent emits the stale-reclaim path fires over and
81
+ # over, which is precisely the pathological case.
82
+ #
83
+ # MEASURED 2026-07-30, AFTER the v8.1.0 fix shipped: 63 orphaned
84
+ # emit.sh processes, oldest alive 10h51m, each burning ~5% CPU, machine
85
+ # at load 63 on 14 cores. Every orphan started after the fix landed.
86
+ # An unbounded wait whose only exit is a counter must increment that
87
+ # counter on EVERY path, or the bound is decorative.
59
88
  attempts=$((attempts + 1))
60
89
  if [ "$attempts" -ge "$max_attempts" ]; then
61
- # Stale lock: if the dir is older than 30s, force-remove it.
62
- local age
63
- age=$(( $(date +%s) - $(stat -f%m "$lock_dir" 2>/dev/null \
64
- || stat -c%Y "$lock_dir" 2>/dev/null \
65
- || echo 0) ))
66
- if [ "$age" -gt 30 ]; then
67
- rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null || true
68
- attempts=0
69
- continue
70
- fi
71
- # Give up -- best-effort write so observability never blocks.
90
+ # Give up fast -- best-effort write so observability never blocks.
72
91
  printf '%s\n' "$line" >> "$events_path" 2>/dev/null || true
73
- return 1
92
+ return 0
93
+ fi
94
+ # Check staleness EVERY iteration, not only after exhausting attempts.
95
+ # The old code waited for all 500 attempts before its first staleness
96
+ # check, so a stale lock cost ~5s AND ~500 forked sleep helpers per
97
+ # event; under concurrent emits the locks kept going stale and the
98
+ # processes piled up. Measured before this fix: 10s and ~500 forks for
99
+ # a SINGLE append against an already-stale lock.
100
+ local age=0
101
+ local mtime
102
+ mtime=$(stat -f%m "$lock_dir" 2>/dev/null || stat -c%Y "$lock_dir" 2>/dev/null || echo "")
103
+ if [ -n "$mtime" ]; then
104
+ age=$(( $(date +%s) - mtime ))
105
+ else
106
+ # Lock vanished between the failed mkdir and the stat: retry at once.
107
+ continue
74
108
  fi
75
- # Sleep ~10ms (perl avoids `sleep 0.01` portability issues).
76
- perl -e 'select(undef,undef,undef,0.01)' 2>/dev/null || sleep 1
109
+ if [ "$age" -gt "$stale_after" ]; then
110
+ rmdir "$lock_dir" 2>/dev/null || rm -rf "$lock_dir" 2>/dev/null || true
111
+ continue
112
+ fi
113
+ # (attempt counting and the give-up branch moved to the TOP of the loop
114
+ # so no `continue` can bypass them)
115
+ # Sleep ~10ms WITHOUT forking when the shell supports fractional sleep
116
+ # (bash's `read -t` needs no external process). perl/sleep are fallbacks.
117
+ read -r -t 0.01 _unused_ < /dev/null 2>/dev/null \
118
+ || perl -e 'select(undef,undef,undef,0.01)' 2>/dev/null \
119
+ || sleep 1
77
120
  done
78
121
  # Critical section.
79
122
  printf '%s\n' "$line" >> "$events_path"
@@ -90,6 +133,50 @@ fi
90
133
 
91
134
  set -euo pipefail
92
135
 
136
+ #-----------------------------------------------------------------------------
137
+ # SELF-REAPER: telemetry must never outlive the thing it observes.
138
+ #-----------------------------------------------------------------------------
139
+ # WHY A WATCHDOG AND NOT ANOTHER LOCK FIX. v8.1.0 fixed a specific unbounded
140
+ # wait (bare `flock -x`, and a staleness check that ran only after 500 attempts).
141
+ # It was a real fix and it was not sufficient: MEASURED 2026-07-30, AFTER that
142
+ # release, 63 orphaned emit.sh processes were alive on this machine, the oldest
143
+ # 10h51m, each burning ~5% CPU, contributing to load 63 on 14 cores. Every one
144
+ # of them started AFTER the fix landed, so whatever wedges emit.sh is not (only)
145
+ # the path that was fixed.
146
+ #
147
+ # The lesson is that patching each discovered hang is an arms race we keep
148
+ # losing, because a hang anywhere in this script has the same user-visible cost.
149
+ # emit.sh is FIRE-AND-FORGET telemetry: nothing waits on its result, and a
150
+ # dropped event is strictly cheaper than a wedged process. So instead of proving
151
+ # no path can block, we cap the lifetime of EVERY path.
152
+ #
153
+ # A background timer SIGKILLs this process after LOKI_EMIT_MAX_SECONDS (default
154
+ # 10). It is deliberately blunt: no cleanup hook, no graceful drain, because the
155
+ # failure mode being defended against is precisely "graceful paths did not run".
156
+ # The killer is disowned so it never becomes a job the parent shell waits on,
157
+ # and it exits immediately when the main process finishes normally.
158
+ #
159
+ # Set LOKI_EMIT_MAX_SECONDS=0 to disable (useful only when debugging emit.sh
160
+ # itself -- an operator who disables it is choosing the orphan risk knowingly).
161
+ _emit_max="${LOKI_EMIT_MAX_SECONDS:-10}"
162
+ case "$_emit_max" in ''|*[!0-9]*) _emit_max=10 ;; esac
163
+ if [ "$_emit_max" -gt 0 ]; then
164
+ _emit_target=$$
165
+ (
166
+ # Poll rather than one long sleep so the watchdog exits promptly on the
167
+ # normal path instead of lingering for the full window.
168
+ _waited=0
169
+ while [ "$_waited" -lt "$_emit_max" ]; do
170
+ sleep 1
171
+ kill -0 "$_emit_target" 2>/dev/null || exit 0
172
+ _waited=$((_waited + 1))
173
+ done
174
+ kill -9 "$_emit_target" 2>/dev/null || true
175
+ ) >/dev/null 2>&1 &
176
+ # Disown so the watchdog is not a tracked job of the caller's shell.
177
+ disown 2>/dev/null || true
178
+ fi
179
+
93
180
  # Configuration
94
181
  LOKI_DIR="${LOKI_DIR:-.loki}"
95
182
  EVENTS_DIR="$LOKI_DIR/events/pending"