loki-mode 9.17.0 → 9.18.2

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.
@@ -86,68 +86,6 @@ _get_discord_color() {
86
86
  # Slack Notifications
87
87
  #===============================================================================
88
88
 
89
- # Verdict fields for a Slack card, from the newest Evidence Receipt.
90
- #
91
- # Roadmap item 9: "put the receipt where review already happens". Slack
92
- # notifications carried the event name and the project name and nothing else --
93
- # grep for receipt/verdict/proof in this file returned 0. A team watching a
94
- # channel saw "build finished" and had to go somewhere else to learn whether it
95
- # was verified, which is the surface problem item 9 names: verification nobody
96
- # sees does not build trust.
97
- #
98
- # Emits Slack `fields` entries (the payload already has that array), so this
99
- # adds to the card rather than restructuring it.
100
- #
101
- # SILENT AND EMPTY ON ANY DOUBT. No receipt, unreadable JSON, no python -- emit
102
- # nothing. A notification is a side channel; it must never fail a build, and it
103
- # must never guess a verdict. An absent receipt yields no verdict field at all
104
- # rather than a reassuring default.
105
- _slack_verdict_fields() {
106
- local loki_dir="${LOKI_DIR:-.loki}"
107
- [ -d "$loki_dir/proofs" ] || return 0
108
- command -v python3 >/dev/null 2>&1 || return 0
109
- python3 - "$loki_dir" <<'PY' 2>/dev/null || true
110
- import glob, json, os, sys
111
- d = sys.argv[1]
112
- paths = sorted(glob.glob(os.path.join(d, "proofs", "*", "proof.json")))
113
- if not paths:
114
- sys.exit(0)
115
- try:
116
- r = json.load(open(paths[-1]))
117
- except (OSError, ValueError):
118
- sys.exit(0)
119
-
120
- def esc(s):
121
- return json.dumps(str(s))[1:-1]
122
-
123
- out = []
124
- qg = r.get("quality_gates") or {}
125
- gates = qg.get("gates") or []
126
- if gates:
127
- passed = sum(1 for g in gates if str(g.get("status", "")).startswith("pass"))
128
- out.append(("Gates", f"{passed}/{len(gates)} passed"))
129
-
130
- git = (r.get("facts") or {}).get("git") or {}
131
- # base_sha empty means the receipt cannot be anchored, so it cannot be
132
- # verified. Say that plainly rather than showing a diff count that implies it.
133
- if not git.get("base_sha"):
134
- out.append(("Verified", "no (unanchored receipt)"))
135
- else:
136
- diff = git.get("diff") or {}
137
- if diff.get("count") is not None:
138
- out.append(("Files changed", str(diff["count"])))
139
-
140
- run_id = r.get("run_id")
141
- if run_id:
142
- out.append(("Receipt", esc(run_id)))
143
-
144
- print(",".join(
145
- '{"title": "%s", "value": "%s", "short": true}' % (esc(t), esc(v))
146
- for t, v in out
147
- ))
148
- PY
149
- }
150
-
151
89
  _notify_slack() {
152
90
  local event="$1"
153
91
  local title="$2"
@@ -168,13 +106,6 @@ _notify_slack() {
168
106
  escaped_event="$(_json_escape "$event")"
169
107
  escaped_project="$(_json_escape "$project")"
170
108
 
171
- # Receipt-derived fields. Empty on any doubt, and the ${var:+,...} expansion
172
- # below means an empty value adds no trailing comma -- a malformed payload
173
- # would make Slack reject the whole card, so the failure mode of "no receipt"
174
- # must be a card without verdict fields, never a card that does not send.
175
- local verdict_fields
176
- verdict_fields="$(_slack_verdict_fields 2>/dev/null || true)"
177
-
178
109
  # Build Slack payload with attachment
179
110
  local payload
180
111
  payload=$(cat <<PAYLOAD
@@ -185,7 +116,7 @@ _notify_slack() {
185
116
  "text": "$escaped_message",
186
117
  "fields": [
187
118
  {"title": "Event", "value": "$escaped_event", "short": true},
188
- {"title": "Project", "value": "$escaped_project", "short": true}${verdict_fields:+,$verdict_fields}
119
+ {"title": "Project", "value": "$escaped_project", "short": true}
189
120
  ],
190
121
  "footer": "Loki Mode",
191
122
  "ts": $(date +%s)
@@ -30,30 +30,11 @@
30
30
  # - Only redis and file are shipped. SQS, Pub/Sub, RabbitMQ, Kafka, etc. are
31
31
  # BRING-YOUR-OWN: override queue.command in values.yaml with your own
32
32
  # consumer. They are documented, not implemented here.
33
- # - The redis backend is AT-LEAST-ONCE by default. LMOVE (RPOPLPUSH pre-6.2)
34
- # pops and records the item in <key>:processing atomically, and the ack
35
- # (LREM) runs only after a zero-exit build. A crashed worker leaves its item
36
- # in <key>:processing for re-drive -- the same shape as the file backend's
37
- # processing/ directory.
38
- # It was at-most-once (LPOP-then-run) until this was fixed; a crash after
39
- # the pop lost the build with no record anywhere.
40
- # A VISIBILITY TIMEOUT now exists: `queue-consumer.sh --reap` requeues
41
- # in-flight items whose claim is older than LOKI_QUEUE_VISIBILITY_SEC
42
- # (default 7200). Run it on a schedule (a CronJob) or by hand. It is a
43
- # separate mode, not folded into the consume loop, because a consumer that
44
- # reaped on every poll would race its peers on a busy queue.
45
- # `--reap` works on BOTH backends. It was redis-only until this was fixed,
46
- # which left the file backend with no crash recovery at all: a dead worker's
47
- # item sat in processing/ forever and an unattended fleet stalled on it.
48
- # A FRESH claim is never reaped -- requeuing a live item would build the
49
- # user's work twice -- and an in-flight item with NO claim is treated as
50
- # infinitely old, since that means the worker died between the LMOVE and the
51
- # claim stamp, which is exactly the case that must be recoverable.
52
- # LOKI_QUEUE_ACK=0 restores the legacy lossy path.
53
- # The file backend's claim time is the item file's MTIME, stamped on claim,
54
- # since a flat directory carries no claim hash. The limit is real and worth
55
- # stating: a consumer that rewrote its own item file would reset that clock
56
- # and look fresh forever. See file_reap.
33
+ # - The redis backend is at-most-once (LPOP-then-run). It has no visibility
34
+ # timeout / dead-letter requeue. If a build crashes after the item is popped,
35
+ # that item is lost from the queue. For at-least-once delivery use the file
36
+ # backend (a crashed build leaves the item in processing/ for manual
37
+ # re-drive) or bring a real broker.
57
38
  # - The file backend's atomicity relies on `mv` being atomic within a single
58
39
  # filesystem (true for a normal PVC). Two consumers racing the same pending
59
40
  # dir is safe (mv either wins or fails-and-skips), but is not load-balanced.
@@ -74,8 +55,6 @@
74
55
  # LOKI_QUEUE_POLL_SEC loop-mode empty-poll wait, seconds (default: 5)
75
56
  # LOKI_QUEUE_BLOCK_SEC redis BLPOP block timeout, seconds (default: 5)
76
57
  # LOKI_TERMINAL_EXIT run.sh terminal-failure exit code (default: 20)
77
- # LOKI_QUEUE_VISIBILITY_SEC --reap requeues in-flight items older than this,
78
- # both backends, seconds (default: 7200)
79
58
  #===============================================================================
80
59
 
81
60
  set -uo pipefail
@@ -197,136 +176,17 @@ redis_cli() {
197
176
  redis-cli -u "$QUEUE_URL" "$@"
198
177
  }
199
178
 
200
- # Pop one item from the redis list, ATOMICALLY RECORDING IT AS IN-FLIGHT.
201
- #
202
- # THE DEFECT THIS FIXES, proven on a real redis 8.6.3 before it was written:
203
- #
204
- # RPUSH loki-builds '{"spec":"build a todo app"}' -> LLEN 1
205
- # LPOP loki-builds -> worker holds the item
206
- # <worker is OOM-killed / node evicted / kill -9>
207
- # LLEN loki-builds -> 0
208
- #
209
- # The user's build is gone and NOTHING anywhere records that it existed. For a
210
- # product whose entire thesis is "we hand you a receipt you can check", silently
211
- # losing the work is the worst failure mode available: there is no receipt, no
212
- # error, and no queue entry to retry.
213
- #
214
- # LMOVE (redis 6.2+) pops and pushes to a processing list in ONE atomic step, so
215
- # a crash between the two is impossible. A dead worker leaves its item in
216
- # <key>:processing where `loki queue reap` (or an operator) can re-drive it.
217
- # RPOPLPUSH is the pre-6.2 equivalent and is tried automatically.
218
- #
219
- # Kept OPT-OUT rather than opt-in (LOKI_QUEUE_ACK=0 restores LPOP). At-least-once
220
- # is the safer default: its failure mode is a duplicate build, which the
221
- # idempotence work already handles, versus silent data loss. But an operator
222
- # running a broker that already guarantees delivery should be able to turn it
223
- # off rather than maintain two in-flight records.
224
- #
225
- # In loop mode with acking we use a short-poll LMOVE plus a sleep instead of
226
- # BLMOVE, because BLMOVE blocks the connection and the stop-flag check has to
227
- # stay responsive; the empty-poll backoff below already bounds the cost.
179
+ # Pop one item from the redis list. In loop mode use BLPOP (blocks up to
180
+ # BLOCK_SEC, then returns empty so we can check the stop flag); in one-shot use
181
+ # LPOP (non-blocking, exits immediately on an empty queue).
182
+ # Prints the popped item to stdout, or nothing if the queue was empty.
228
183
  redis_pop() {
229
- if [ "${LOKI_QUEUE_ACK:-1}" = "0" ]; then
230
- # Legacy at-most-once path, explicitly requested.
231
- if [ "$ONESHOT" = "1" ]; then
232
- redis-cli -u "$QUEUE_URL" --no-raw LPOP "$QUEUE_KEY" 2>/dev/null | _redis_unquote
233
- else
234
- redis-cli -u "$QUEUE_URL" BLPOP "$QUEUE_KEY" "$BLOCK_SEC" 2>/dev/null | sed -n '2p'
235
- fi
236
- return 0
237
- fi
238
-
239
- # RAW output, not --no-raw. `--no-raw` escapes the INNER quotes of a JSON
240
- # payload ({"spec":"x"} comes back as "{\"spec\":\"x\"}"), and
241
- # _redis_unquote only strips the outer pair -- so the item reaching
242
- # extract_spec was mangled AND the string handed to LREM no longer matched
243
- # what redis stored, silently acking nothing. Both were caught by running
244
- # against a real redis 8.6.3; neither is visible in a mock.
245
- local out
246
- out="$(redis-cli -u "$QUEUE_URL" LMOVE "$QUEUE_KEY" "${QUEUE_KEY}:processing" LEFT RIGHT 2>/dev/null)"
247
- # Pre-6.2 servers reject LMOVE as an unknown command (stderr, empty stdout),
248
- # so fall back rather than reporting an empty queue.
249
- if [ -z "$out" ]; then
250
- out="$(redis-cli -u "$QUEUE_URL" RPOPLPUSH "$QUEUE_KEY" "${QUEUE_KEY}:processing" 2>/dev/null)"
251
- fi
252
- # Record WHEN this item was claimed. A bare list carries no time, so nothing
253
- # could tell a stale entry (dead worker) from a running one (long build) --
254
- # and requeuing a live item duplicates the user's build. The claim hash is
255
- # what makes the reaper below safe to run at all.
256
- #
257
- # Written AFTER the move, so a crash between them leaves the item in-flight
258
- # with no claim time. The reaper treats a missing claim as INFINITELY OLD
259
- # rather than infinitely young: an item nothing is tracking is exactly the
260
- # case that must be recoverable.
261
- if [ -n "$out" ]; then
262
- redis-cli -u "$QUEUE_URL" HSET "${QUEUE_KEY}:claims" "$out" "$(date +%s)" >/dev/null 2>&1 || true
184
+ if [ "$ONESHOT" = "1" ]; then
185
+ redis-cli -u "$QUEUE_URL" --no-raw LPOP "$QUEUE_KEY" 2>/dev/null | _redis_unquote
186
+ else
187
+ # BLPOP returns two lines: the key name, then the value. Take the value.
188
+ redis-cli -u "$QUEUE_URL" BLPOP "$QUEUE_KEY" "$BLOCK_SEC" 2>/dev/null | sed -n '2p'
263
189
  fi
264
- printf '%s' "$out"
265
- }
266
-
267
- # Remove a completed item from the in-flight list. Called ONLY after the build
268
- # finished; until then the item stays recoverable.
269
- #
270
- # LREM with count 0 removes every equal element. Two identical specs queued twice
271
- # would both clear on the first ack -- accepted deliberately: the alternative is
272
- # a per-item token, which needs a producer change, and the failure mode here is
273
- # one duplicate re-drive rather than a lost build.
274
- redis_ack() {
275
- [ "${LOKI_QUEUE_ACK:-1}" = "0" ] && return 0
276
- [ -n "${1:-}" ] || return 0
277
- redis-cli -u "$QUEUE_URL" LREM "${QUEUE_KEY}:processing" 0 "$1" >/dev/null 2>&1 || true
278
- # Drop the claim too, or the hash grows without bound and every completed
279
- # item looks like a candidate for reaping forever.
280
- redis-cli -u "$QUEUE_URL" HDEL "${QUEUE_KEY}:claims" "$1" >/dev/null 2>&1 || true
281
- }
282
-
283
- # Requeue in-flight items whose claim is older than the visibility timeout.
284
- #
285
- # THE GAP THIS CLOSES. LMOVE made a crashed worker's item RECOVERABLE, but
286
- # nothing recovered it: re-drive was an operator action, so an unattended fleet
287
- # still stalled on every dead worker. That was documented rather than claimed;
288
- # this closes it.
289
- #
290
- # SAFETY IS THE WHOLE DESIGN. Requeuing an item a worker is still building
291
- # duplicates the user's build, so the timeout must exceed the longest legitimate
292
- # build. Default 2h (LOKI_MAX_DURATION territory), tunable.
293
- #
294
- # An item with NO claim entry is treated as INFINITELY OLD, not young: it means
295
- # the worker died between the LMOVE and the HSET, which is precisely the case
296
- # that must be recoverable. Erring the other way would strand exactly the items
297
- # this exists to rescue.
298
- #
299
- # Reports what it did on stdout. Requeues nothing when the timeout has not
300
- # elapsed, and says so rather than printing a silent zero.
301
- redis_reap() {
302
- [ "${LOKI_QUEUE_ACK:-1}" = "0" ] && { echo "reap: acking disabled, nothing to reap"; return 0; }
303
- local timeout now items requeued=0 kept=0
304
- timeout="${LOKI_QUEUE_VISIBILITY_SEC:-7200}"
305
- now="$(date +%s)"
306
- items="$(redis-cli -u "$QUEUE_URL" LRANGE "${QUEUE_KEY}:processing" 0 -1 2>/dev/null)"
307
- [ -n "$items" ] || { echo "reap: nothing in flight"; return 0; }
308
- while IFS= read -r it; do
309
- [ -n "$it" ] || continue
310
- local claimed age
311
- claimed="$(redis-cli -u "$QUEUE_URL" HGET "${QUEUE_KEY}:claims" "$it" 2>/dev/null)"
312
- if [ -z "$claimed" ]; then
313
- age="$((timeout + 1))" # unclaimed == infinitely old, see above
314
- else
315
- age="$((now - claimed))"
316
- fi
317
- if [ "$age" -gt "$timeout" ]; then
318
- # LREM then RPUSH, not LMOVE: the item may appear more than once and
319
- # LREM 0 clears every copy, so the queue cannot gain duplicates.
320
- redis-cli -u "$QUEUE_URL" LREM "${QUEUE_KEY}:processing" 0 "$it" >/dev/null 2>&1 || true
321
- redis-cli -u "$QUEUE_URL" HDEL "${QUEUE_KEY}:claims" "$it" >/dev/null 2>&1 || true
322
- redis-cli -u "$QUEUE_URL" RPUSH "$QUEUE_KEY" "$it" >/dev/null 2>&1 || true
323
- requeued=$((requeued + 1))
324
- log "reaped stale item (age ${age}s > ${timeout}s), requeued"
325
- else
326
- kept=$((kept + 1))
327
- fi
328
- done <<< "$items"
329
- echo "reap: requeued $requeued, left $kept in flight (timeout ${timeout}s)"
330
190
  }
331
191
 
332
192
  # --no-raw LPOP wraps the value in quotes; strip a single surrounding pair and
@@ -347,21 +207,10 @@ redis_consume_one() {
347
207
  if [ -z "$item" ]; then
348
208
  return 100 # sentinel: queue empty
349
209
  fi
350
- local spec rc
210
+ local spec
351
211
  spec="$(extract_spec "$item")"
352
212
  run_build "$spec"
353
- rc=$?
354
- # ACK ONLY ON SUCCESS, and the order is load-bearing: acking before the
355
- # build would reintroduce the exact data loss this exists to prevent, and
356
- # acking a FAILED build would discard work that a re-drive could complete.
357
- # A failed item stays in <key>:processing so `loki queue reap` can requeue
358
- # it -- the same shape as the file backend leaving it in processing/.
359
- if [ "$rc" -eq 0 ]; then
360
- redis_ack "$item"
361
- else
362
- log "item left in ${QUEUE_KEY}:processing for re-drive (exit $rc)"
363
- fi
364
- return "$rc"
213
+ return $?
365
214
  }
366
215
 
367
216
  # =============================================================================
@@ -392,18 +241,6 @@ file_claim_oldest() {
392
241
  base="$(basename "$f")"
393
242
  dest="$QUEUE_DIR/processing/$base"
394
243
  if mv "$f" "$dest" 2>/dev/null; then
395
- # Stamp the CLAIM TIME. This is the file-backend analogue of the
396
- # `HSET <key>:claims` that redis_pop does right after its LMOVE, and
397
- # file_reap below depends on it being here.
398
- #
399
- # Load-bearing: `mv` is a rename(2), which PRESERVES mtime. An item
400
- # that sat in pending/ behind a backlog for longer than the
401
- # visibility timeout would arrive in processing/ already looking
402
- # stale, and the reaper would requeue it while a worker was actively
403
- # building it -- the exact duplicate build the timeout exists to
404
- # prevent, firing on precisely the busy queues that need a reaper.
405
- # Verified on darwin: mv of a file stamped 2020 kept the 2020 mtime.
406
- touch "$dest" 2>/dev/null || true
407
244
  printf '%s' "$dest"
408
245
  return 0
409
246
  fi
@@ -435,110 +272,13 @@ file_consume_one() {
435
272
  mv "$claimed" "$QUEUE_DIR/failed/$base" 2>/dev/null || log "WARN: could not move $base to failed/"
436
273
  log "item $base TERMINAL-FAILED (exit $rc); moved to failed/, not acked"
437
274
  else
438
- # Transient crash: leave it in processing/ for re-drive. We do not
439
- # requeue INLINE (no retry counter in a flat dir, so an instant retry
440
- # could hot-loop a poison item). `--reap` picks it up once the
441
- # visibility timeout has elapsed; see file_reap.
275
+ # Transient crash: leave it in processing/ for manual re-drive. We do NOT
276
+ # auto-requeue (no retry counter in a flat dir); honest at-least-once.
442
277
  log "item $base crashed (exit $rc); left in processing/ for re-drive"
443
278
  fi
444
279
  return "$rc"
445
280
  }
446
281
 
447
- # Requeue in-flight items whose claim is older than the visibility timeout.
448
- #
449
- # THE GAP THIS CLOSES. The redis backend got a reaper; the file backend did not,
450
- # so an item left in processing/ by a dead worker stayed there FOREVER and an
451
- # unattended fleet on the file backend stalled on every crash. `--reap` answered
452
- # "redis-only" and changed nothing.
453
- #
454
- # CLAIM TIME IS THE FILE MTIME. The file backend has no claim hash, so the mtime
455
- # stamped by file_claim_oldest stands in for one.
456
- #
457
- # THE LIMIT, stated plainly: mtime is whatever last WROTE the file. A build that
458
- # rewrites its own item file resets the clock and would look fresh forever, so
459
- # it would never be reaped. Nothing here writes back to a claimed item, but a
460
- # custom consumer that did would silently opt out of recovery. The redis claim
461
- # hash does not have this weakness; the file backend accepts it rather than
462
- # maintain a second sidecar file whose own staleness would need reaping too.
463
- #
464
- # There is also no retry counter (a flat dir carries none), so a poison item can
465
- # be reaped repeatedly. That is the same shape the backend already documents for
466
- # manual re-drive, not a new failure introduced here.
467
- #
468
- # SAFETY IS THE WHOLE DESIGN, mirroring redis_reap:
469
- # - a FRESH item is NEVER requeued; a live build requeued is the user's work
470
- # built twice, which is worse than the stranding this fixes
471
- # - it NEVER deletes an item -- a name collision in pending/ leaves the item
472
- # in processing/ and says so, rather than clobbering the pending copy
473
- # - it reports counts on stdout; per-item detail goes to stderr via log()
474
- file_reap() {
475
- local timeout now requeued=0 kept=0 seen=0 f base mt age
476
- timeout="${LOKI_QUEUE_VISIBILITY_SEC:-7200}"
477
- now="$(date +%s)"
478
- if [ ! -d "$QUEUE_DIR/processing" ]; then
479
- echo "reap: nothing in flight"
480
- return 0
481
- fi
482
- mkdir -p "$QUEUE_DIR/pending" 2>/dev/null || true
483
- # Glob, not `ls`: a filename with a space or newline survives it.
484
- for f in "$QUEUE_DIR"/processing/*; do
485
- [ -e "$f" ] || continue # unmatched glob stays literal
486
- [ -f "$f" ] || continue
487
- seen=$((seen + 1))
488
- base="$(basename "$f")"
489
- # GNU FIRST, and the order is load-bearing. `stat -f` means
490
- # --file-system on GNU coreutils and EXITS 0, printing a mount point
491
- # rather than an mtime -- so a `-f`-first probe never falls through on
492
- # Linux and hands back garbage. BSD `stat -c` has no such ambiguity: it
493
- # is an illegal option and exits 1 (verified on darwin), so `-f %m` is
494
- # reached only where it means what we want.
495
- # `find -printf` and `-mmin` are GNU-only / minute-granular, and the
496
- # fresh-vs-stale distinction needs seconds.
497
- mt="$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null)"
498
- case "$mt" in
499
- ''|*[!0-9]*)
500
- # Unreadable or non-numeric claim time. KEEP the item, and note
501
- # the ASYMMETRY with redis_reap deliberately: a missing redis
502
- # claim is DIAGNOSTIC (the worker died between LMOVE and HSET),
503
- # but a file that exists ALWAYS has an mtime, so failing to read
504
- # one means our stat invocation is wrong -- a portability bug,
505
- # not a dead worker.
506
- # MEASURED, not assumed: with a `stat` that prints a non-number
507
- # and exits 0, the un-guarded arithmetic aborts the reaper under
508
- # `set -uo pipefail` (rc=1, nothing requeued). So the observed
509
- # failure is a DEAD REAPER on the affected platform, not a
510
- # duplicate build. Keeping the item is correct either way, and
511
- # the WARN names the reason instead of exiting silently.
512
- kept=$((kept + 1))
513
- log "WARN: cannot read mtime for $base (got '$mt'); left in processing/ rather than guess a claim time"
514
- continue
515
- ;;
516
- esac
517
- age="$((now - mt))"
518
- if [ "$age" -le "$timeout" ]; then
519
- kept=$((kept + 1))
520
- continue
521
- fi
522
- if [ -e "$QUEUE_DIR/pending/$base" ]; then
523
- kept=$((kept + 1))
524
- log "stale item $base NOT requeued: pending/$base already exists (never overwrite an item)"
525
- continue
526
- fi
527
- if mv "$f" "$QUEUE_DIR/pending/$base" 2>/dev/null; then
528
- requeued=$((requeued + 1))
529
- log "reaped stale item $base (age ${age}s > ${timeout}s), requeued"
530
- else
531
- kept=$((kept + 1))
532
- log "WARN: could not requeue $base; left in processing/"
533
- fi
534
- done
535
- if [ "$seen" -eq 0 ]; then
536
- echo "reap: nothing in flight"
537
- return 0
538
- fi
539
- echo "reap: requeued $requeued, left $kept in flight (timeout ${timeout}s)"
540
- }
541
-
542
282
  # =============================================================================
543
283
  # Driver
544
284
  # =============================================================================
@@ -567,18 +307,6 @@ main() {
567
307
  return 2
568
308
  fi
569
309
 
570
- # --reap requeues stale in-flight items and exits. Kept a SEPARATE mode
571
- # rather than folded into the consume loop: a reaper wants its own cadence
572
- # (a CronJob, or an operator hand-running it), and a consumer that reaped on
573
- # every poll would race its own peers on a busy queue.
574
- if [ "${1:-}" = "--reap" ] || [ "${LOKI_QUEUE_REAP:-0}" = "1" ]; then
575
- case "$backend" in
576
- redis) redis_reap ;;
577
- file) file_init_dirs && file_reap ;;
578
- esac
579
- return $?
580
- fi
581
-
582
310
  if [ "$ONESHOT" = "1" ]; then
583
311
  log "mode=oneshot backend=$backend (serverless: process one item then exit)"
584
312
  consume_one "$backend"