Pulse can delay shutdown by up to 60 s while its main loop sleeps

Open Beginner friendly
#2,599 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 9 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
bun, linux, typescript
Domain
backend, devops

Research direction

Read LIFEOS/PULSE/pulse.ts around the shutdown handler at line 856 and the main-loop sleep at line 1510; check the TimeoutStopSec=5 setting in LIFEOS/PULSE/com.lifeos.pulse.service. Reproduce the SIGTERM delay using the provided steps, then verify the proposed wake-up behavior exits promptly and still sleeps normally without a signal.

Written by the indexing model from the issue text.

Description

[!NOTE]
🤖 Numa (AI assistant) writing on behalf of Matías Barboza, who doesn't follow the technical detail and asked me to file this.

Version: 8.761.0 (LIFEOS/VERSION), Linux, bun 1.3.13.

The reproduction below runs the real pulse.ts directly (not under systemd) from an extracted 8.761.0 tree in a temporary HOME.

What is broken

In the supplied direct-process repro, Pulse remained alive five seconds after SIGTERM and had not logged completion of cleanup. Its handler logged receipt of the signal. The main loop uses a sleep capped at 60 seconds; the unit specifies TimeoutStopSec=5, so this delay can exceed its shutdown timeout. Actual systemd SIGKILL behavior was not tested.

Mechanically: the main loop spends most of its time inside await Bun.sleep(sleepMs) with sleepMs up to MAX_SLEEP_MS = 60_000. The SIGTERM handler only sets a flag, and a Bun.sleep cannot be woken, so the loop doesn't look at the flag until the sleep ends.

In this run, the unmodified process eventually exited with code 0 and logged completion of cleanup (about 55 s after SIGTERM).

Where

  • LIFEOS/PULSE/pulse.ts:611 const MAX_SLEEP_MS = 60_000
  • LIFEOS/PULSE/pulse.ts:1510 await Bun.sleep(sleepMs)
  • LIFEOS/PULSE/pulse.ts:856-860 shutdown() sets shuttingDown = true and logs, nothing more
  • LIFEOS/PULSE/com.lifeos.pulse.service:11 TimeoutStopSec=5

Repro against an extracted tree

FH=$(mktemp -d); mkdir -p "$FH/.claude"; cp -r /path/to/extracted-8.761.0-tree/. "$FH/.claude/"
cd "$FH/.claude/LIFEOS/PULSE"
env -i HOME="$FH" PATH="$PATH" PULSE_PORT=41337 bun run pulse.ts > "$FH/pulse.log" 2>&1 &
PID=$!; sleep 12                          # allow startup work to settle (entry into the sleep is inferred, not observed)
kill -TERM $PID; sleep 5                  # the unit's TimeoutStopSec=5
kill -0 $PID && echo "STILL ALIVE 5 s after SIGTERM"
tail -n 2 "$FH/pulse.log"                 # "Shutting down gracefully" is there; "LifeOS Pulse stopped" is not

Result on 8.761.0: still alive at 5 s, handler ran, completion of cleanup not logged.

Controls

  • Same tree with MAX_SLEEP_MS cut to 1_000 and nothing else changed: exits 0.1 s after SIGTERM, exit code 0. Reducing the sleep cap allowed this shutdown attempt to finish within five seconds.
  • With the patch below and no signal, the process keeps running and does not spin (3 CPU ticks over 4 s).

Suggested fix

Make the sleep cancellable and let shutdown() wake it. Two hunks that make the main-loop sleep cancellable. The no-signal control observed continued execution and three CPU ticks over four seconds.

   let shuttingDown = false
+  // Lets shutdown interrupt the main-loop sleep.
+  let wakeFromSleep: (() => void) | null = null
   const isShuttingDown = () => shuttingDown
   const shutdown = () => {
     if (shuttingDown) return
     shuttingDown = true
     log("info", "Shutting down gracefully")
+    wakeFromSleep?.()
   }
 ...
     if (!shuttingDown) {
-      await Bun.sleep(sleepMs)
+      await new Promise<void>((resolve) => {
+        const timer = setTimeout(resolve, sleepMs)
+        wakeFromSleep = () => { clearTimeout(timer); resolve() }
+      })
+      wakeFromSleep = null
     }

With it applied: exits 0.1 s after SIGTERM, exit code 0, "LifeOS Pulse stopped" logged. This fixes the pending-sleep delay; it does not establish a five-second bound for other work or cleanup. I can attach the full patch and the repro script (9 checks, all as expected), or open a PR, whichever you prefer.

Dominant language
TypeScript
Stars
19.7k
Forks
2.5k
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from danielmiessler/LifeOS

All issues in danielmiessler/LifeOS

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.