Skip to content

fix: a devpod up no longer outlives the dl that holds its lock - #668

Merged
blooop merged 19 commits into
mainfrom
fix/orphaned-devpod-up
Oct 2, 2026
Merged

blooop merged 19 commits into
mainfrom
fix/orphaned-devpod-up

Conversation

@blooop

@blooop blooop commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

What this changes

On a host, two devpod up processes kept devpod's workspace lock for four hours. Their aid --boot-up parent had died, and init became their parent. Every later dl <ws>, dl rme and devpod delete then printed Trying to lock workspace and waited with no end.

  • A devpod up dies with its dl. On Linux, the runner sets PR_SET_PDEATHSIG in pre_exec for a child that leads its own group (ends_with_this_process, devlaunch-runner/src/interrupt.rs). The devpod up child gets SIGKILL. The aid --boot-up child gets SIGINT, so its own handler still removes its token file. After prctl, the child compares getppid() with the pid that the parent read before the fork. If the parent died first, the child exits. On other platforms the function does nothing.
  • rm, rme and --rm clear an orphan that holds the lock. The delete path now calls kill::release_the_lock (flows/lifecycle/delete.rs), the same sweep that a launch uses. Before, it printed advice to run dl <ws> kill and continued to wait. A holder whose parent is alive is spared.
  • The sweep runs again during a long wait. A launch and a delete sweep again every 12 lock lines, about once a minute (LockWait, clients/devpod.rs). A holder that the first sweep spared can lose its parent later. A later sweep prints a line only when it signals something.
  • The interrupt handler sends SIGKILL after SIGTERM. The handler sends SIGTERM to the devpod up group and waits up to 2 s for the group leader. Then it sends SIGKILL to the group. Before, it sent SIGTERM and exited at once. Without the wait, the kernel SIGKILL from the first bullet would land right after the SIGTERM, and devpod up would get no time to stop cleanly.
  • CHANGELOG.md, README.md and docs/cli.md describe the new behavior. The two public-api snapshots list ends_with_this_process.

Where this fits

 dl (or aid --boot-up) starts devpod up in its own process group
+  child: prctl(PR_SET_PDEATHSIG) ; if getppid() != parent pid: _exit
 dl waits on devpod up
   devpod prints "Trying to lock workspace"
-    up: sweep orphans one time ; rm/rme: print "run dl <ws> kill"
+    up and rm/rme: sweep orphans now, then again every 12 lines
 dl gets SIGINT / SIGTERM / SIGHUP
   killpg(group, SIGTERM)
+  wait up to 2 s for the leader ; killpg(group, SIGKILL)
   _exit
 dl gets SIGKILL (no handler runs)
-  devpod up lives on and keeps the lock
+  the kernel sends the death signal to devpod up

How I checked it

Before → after, each test fails on origin/main and passes on this branch:

  • devlaunch-runner/tests/parent_death.rs: start a parent that spawns an own-group child, SIGKILL the parent. Before: the child lives on. After: the child is gone.
  • dl/tests/up_blocked.rs: SIGKILL dl during devpod up. Before: the fake devpod up lives on. After: it is gone. Other tests in this file cover a sweep that repeats, and a later sweep that finds a holder the first sweep spared.
  • aid/tests/interactive.rs a_sigkilled_aid_takes_its_boot_down_and_the_token_with_it: SIGKILL aid. After: the boot child stops and removes its token.
  • flows/lifecycle/tests.rs and dl/tests/lifecycle.rs: a delete behind an orphan sweeps it and reports the sweep. A delete behind a live build signals nothing. A delete that stays blocked sweeps again.
  • dl/tests/interrupt.rs: Ctrl-C during devpod up gives devpod its 2 s grace, then the group gets SIGKILL.

Each test that guards a branch also failed when I mutated the code that it guards.

Commands I ran on the host:

  • cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings: clean.
  • cargo test --workspace --no-fail-fast: 2923 passed and 5 failed. The same 5 fail on origin/main: 4 in devlaunch-core flows::lifecycle::tests (for example a_remote_that_cannot_be_reached_leaves_the_refusal_standing) and dl/tests/lifecycle.rs::a_commit_pushed_by_url_does_not_stop_rm. They look like a host git-remote setup problem.
  • pixi run test: 834 passed, 7 skipped.
  • prek run --from-ref origin/main --to-ref HEAD: clean.

I did not compile for macOS. No Darwin target is installed here, and CI has no macOS job.

Merge danger

Door: two-way. A revert brings back the old behavior. No stored data or file format changes.

Blast radius: small. If the sweep is wrong, it can signal a devpod up that another live dl waits on. The sweep spares a holder whose parent is alive, and a test covers that case. A Ctrl-C now takes up to 2 s longer, but only when devpod up ignores SIGTERM. A normal Ctrl-C took 0.29 s in the test.

Review notes

  • No test covers the getppid race branch, because a test cannot cause that race on demand.
  • A subreaper hides an orphan: the holder's parent is then the subreaper, not PID 1. The comment at kill.rs:203 says that the sweep covers this case, and that is not true. This gap was already on main, and the new sweeps have the same gap.
  • The launch path still says that dl <ws> kill ends a wait behind a live build. That is false, and it is already on main. This PR fixes the same text on the delete path only.
  • The release commit 6de25f9 is the bump to 0.59.3, so a merge publishes the fix.

🤖 Generated with Claude Code

Summary by Sourcery

Prevent workspace-locking processes from becoming orphaned and make launches and deletes recover automatically from abandoned lock holders.

Bug Fixes:

  • Prevent devpod up and background boot processes from outliving the dl or aid process that started them.
  • Allow blocked deletes to clear orphaned workspace-lock holders and retry lock cleanup during prolonged waits.
  • Ensure interrupted devpod up process groups receive a graceful termination period before being forcefully killed.

Enhancements:

  • Improve lock-wait reporting to distinguish initial and repeated cleanup sweeps and avoid unnecessary repeated notices.

Build:

  • Bump the project version and release metadata to 0.59.3.

Documentation:

  • Document process lifetime handling, orphan cleanup, repeated lock sweeps, and interrupt behavior in the changelog, README, and CLI guide.

Tests:

  • Add coverage for parent-death cleanup, orphaned lock recovery, repeated lock sweeps, delete behavior, and graceful interrupt escalation.

blooop added 12 commits October 2, 2026 16:10
…s handler

The own-group child (devpod up) now gets PR_SET_PDEATHSIG(SIGKILL) in its
pre_exec, and checks getppid against the pid read before the fork so a parent
that died in between is not missed. aid's --boot-up child gets the same with
SIGINT, so the boot's own handler still kills its group and unlinks its token.
Linux only; a no-op elsewhere.
…ng kill

A delete blocked on devpod's workspace lock now runs the sweep a launch runs
(kill::release_the_lock): orphans holding the workspace are signalled, a
holder somebody is waiting on is spared, and the line says what the sweep
found. Both the launch and the delete sweep again every 12 lock lines (about a
minute), since a holder spared once can lose its parent later; a repeat sweep
speaks only when it signalled something.

Tests: the delete sweeps an orphan (core), the lock-wait count, the launch's
repeat sweep, and the rm binary test now expects the sweep's finding.
… its SIGTERM

The drain sent SIGTERM to the group and _exited at once. A devpod up that
ignored it, or a child of it that did, outlived dl holding the workspace
flock. Now the drain waits up to two seconds for the leader (waitpid WNOHANG
and poll, both async-signal-safe), then SIGKILLs the group. The wait also
gives devpod up its unwind before the kernel's parent-death SIGKILL lands.

Test: a Ctrl-C mid-up kills an up child that ignores SIGTERM.
…KILL

Removing the drain's wait for the group leader left every interrupt test green. The new test's up writes a marker 0.5 s into its SIGTERM unwind; it fails with the wait removed.
Removing the boot's parent-death SIGINT left every aid test green. aid runs under a shell that leads the pty's session and outlives it, so the kernel's hangup cannot stand in for the parent-death signal. The test fails with the ends_with_this_process call removed.
docs/cli.md still said a delete behind devpod's lock answered the first lock line by naming dl <ws> kill, and said it once. It now prints the launch's notice and sweeps on the first line and every twelfth after, and kill's own delete does the same.
No delete test sent more than one sweep's worth of lock lines, so a delete
that swept once and never again passed the suite. This one sends
1 + SWEEP_AGAIN_EVERY lines and counts the process-table reads.
The first sweep spares a holder that a live dl is behind, and a later sweep
takes it once that dl is gone. Nothing tested that the later sweep is said,
so a gate reduced to `if first` passed in both the launch and the delete.
Each now reads a different process table per sweep.
Every delete test fed the sweep an empty table or an orphan, so a sweep that
signalled attended holders too passed the delete suite. This one puts a
devpod up with a live dl behind it on the lock.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @blooop, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 hour and 5 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Prevents devpod up and aid boot processes from becoming orphaned lock holders, adds automatic orphan sweeping for blocked launches and deletes with periodic retries, and makes interrupt cleanup gracefully terminate before forcing process-group shutdown; the behavior is covered by Linux integration tests and documented for the 0.59.3 release.

Sequence diagram for parent-death cleanup of devpod up

sequenceDiagram
    participant DL as dl or aid
    participant Child as devpod up or boot child
    participant Kernel as Linux kernel
    participant Lock as Workspace lock

    DL->>Child: spawn with ends_with_this_process
    Child->>Kernel: prctl(PR_SET_PDEATHSIG)
    Child->>Kernel: getppid() check
    Child->>Lock: acquire workspace lock
    DL--xDL: parent exits or is SIGKILLed
    Kernel-->>Child: SIGKILL for devpod up
    Kernel-->>Child: SIGINT for aid boot
    Child->>Lock: release lock and exit
Loading

Sequence diagram for periodic orphan lock sweeping

sequenceDiagram
    participant Launch as devpod up or delete
    participant Devpod as devpod
    participant LockWait as LockWait
    participant Sweep as release_the_lock
    participant Holder as Lock holder

    Launch->>Devpod: start watched operation
    Devpod-->>Launch: Trying to lock workspace
    Launch->>LockWait: read(lock line)
    LockWait-->>Launch: First
    Launch->>Sweep: release_the_lock
    Sweep->>Holder: signal orphan holder
    Holder-->>Devpod: release workspace lock
    loop Every 12 lock lines
        Devpod-->>Launch: Trying to lock workspace
        Launch->>LockWait: read(lock line)
        LockWait-->>Launch: SweepAgain
        Launch->>Sweep: release_the_lock
        Sweep->>Holder: signal only if now orphaned
    end
Loading

Sequence diagram for graceful interrupt shutdown

sequenceDiagram
    actor User
    participant DL as dl
    participant Group as devpod up process group
    participant Kernel as Kernel

    User->>DL: SIGINT, SIGTERM, or SIGHUP
    DL->>Group: killpg(SIGTERM)
    DL->>Group: wait up to 2 seconds
    alt group leader exits
        Group-->>DL: leader reaped
    else timeout
        DL->>Group: killpg(SIGKILL)
    end
    DL->>Kernel: exit
    Kernel-->>Group: PDEATHSIG SIGKILL if still running
Loading

Flow diagram for blocked delete lock handling

flowchart TD
    A[devpod delete or rm waits for workspace lock] --> B[LockWait reads first lock line]
    B --> C[release_the_lock sweeps orphan holders]
    C --> D{Lock holder has live parent?}
    D -->|Yes| E[Preserve holder and continue waiting]
    D -->|No| F[Signal orphan holder]
    F --> G[Delete continues when lock is released]
    E --> H[Repeat sweep every 12 lock lines]
    H --> C
Loading

File-Level Changes

Change Details Files
Tie child lifetimes to the process that owns the workspace operation using Linux parent-death signals.
  • Add a reusable pre-exec hook that applies PR_SET_PDEATHSIG and closes the fork/parent-death race.
  • Make devpod up terminate with its dl and make aid boot cleanup run on aid death.
  • Keep the behavior a no-op on non-Linux platforms and update API snapshots.
rust/devlaunch-runner/src/interrupt.rs
rust/devlaunch-runner/src/lib.rs
rust/dl/src/lib.rs
rust/aid/src/interactive.rs
rust/devlaunch-runner/public-api.txt
rust/devlaunch-core/public-api.api.txt
Make blocked launches and deletes periodically sweep orphaned workspace-lock holders.
  • Centralize lock-line counting and trigger an initial sweep plus repeat sweeps every 12 lock messages.
  • Reuse release_the_lock for rm, rme, and --rm while sparing holders with a live waiting parent.
  • Report repeat sweeps only when they signal a process and preserve distinct launch/delete messaging.
rust/devlaunch-core/src/clients/devpod.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/lifecycle/delete.rs
rust/devlaunch-core/src/flows/kill.rs
rust/dl/src/commands.rs
rust/dl/src/render.rs
Strengthen interrupt cleanup so process groups get a graceful termination window followed by forced termination.
  • Send SIGTERM, wait up to two seconds for the group leader, then send SIGKILL to the process group.
  • Use async-signal-safe wait and polling operations within the signal-handler cleanup path.
  • Ensure ignored SIGTERM handlers cannot leave devpod up or its children behind.
rust/devlaunch-runner/src/interrupt.rs
rust/dl/tests/interrupt.rs
Add regression coverage for parent death, orphan recovery, repeated sweeps, and interrupt behavior.
  • Test Linux parent-death propagation for dl/devpod up and aid boot, including token cleanup.
  • Test delete and launch sweeps for orphaned, live, and later-orphaned holders.
  • Test SIGTERM grace and SIGKILL escalation for devpod up process groups.
rust/devlaunch-runner/tests/parent_death.rs
rust/dl/tests/up_blocked.rs
rust/aid/tests/interactive.rs
rust/devlaunch-core/src/flows/lifecycle/tests.rs
rust/dl/tests/lifecycle.rs
Document the new cleanup and lock-recovery behavior and publish the release.
  • Update changelog, README, and CLI documentation for parent-linked cleanup, sweeps, and interrupt escalation.
  • Bump the release to 0.59.3.
CHANGELOG.md
README.md
docs/cli.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

All four copies of the version move together: Cargo.toml, Cargo.lock,
and the README conda badge and dl --version transcript. The bump rides
on #668 so the merge that lands the fix is the one that publishes it.
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.05085% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.37%. Comparing base (11d160d) to head (5faba36).

Files with missing lines Patch % Lines
rust/devlaunch-runner/src/interrupt.rs 14.81% 23 Missing ⚠️
rust/devlaunch-core/src/flows/launch.rs 90.00% 9 Missing ⚠️
rust/aid/src/interactive.rs 0.00% 5 Missing ⚠️
rust/dl/src/lib.rs 0.00% 3 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 94.59% <83.05%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 94.59% <83.05%> (-0.07%) ⬇️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

blooop added 6 commits October 2, 2026 17:39
An rm, rme or --rm whose sweep spared somebody's live `devpod up` ended
its line with "'dl <ws> kill' ends it and deletes the workspace". kill
classifies that holder as Standing::ABuild, spares it in the same sweep,
and withholds its delete (kill_delete_withheld), so the advice sent the
reader to a command that does neither.

The delete waiter now says to stop the build in its own terminal and
that this rm deletes the workspace once it lets go. The README and
docs/cli.md copies this branch added are corrected to match. The launch
waiter's sentence is unchanged here.
…n's SIGKILL gone

The fake up was `exec sleep 30`, which pdeathsig kills when dl exits, so
up_alive: false held without the drain's killpg(SIGKILL). The up now forks
a child that records its pid and blocks on it; a forked child does not
inherit pdeathsig, and under the TERM row it inherits the ignored SIGTERM,
so only the drain's SIGKILL ends it.
@blooop
blooop merged commit 319d3b9 into main Oct 2, 2026
15 checks passed
@blooop
blooop deleted the fix/orphaned-devpod-up branch October 2, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant