Repository navigation
Conversation
CronRegisterJob and CronRemoveJob kept the +1 ref from to_process as Option<*mut Process> and the file to remove as Option<ZString>, releasing both with the same detach/deref and unlink code repeated in check_finished and in a hand-written Drop impl on each job. The ref is now a HeldProcess (NonNull<Process> that detaches and derefs on drop) and the file an UnlinkOnDrop (ZString that unlinks on drop, with keep() for the macOS plist that launchctl bootstrap takes over), so check_finished just clears the slot and both Drop impls are gone. CronRemoveJob has no temp file on macOS, so its tmp_path field only exists on the other targets now. Option<HeldProcess> is niche-optimized to one pointer and Option<UnlinkOnDrop> keeps ZString's layout; the generated field drops run the same statements in the same order as the removed Drop bodies.
|
Warning Review limit reached
Next review available in: 16 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
Comment |
|
Updated 4:15 PM PT - Aug 11th, 2026
✅ @robobun, your commit f14eb4419b98d406941d06b44e6733e82091d074 passed in 🧪 To try this PR locally: bunx bun-pr 37660That installs a local version of the PR into your bun-37660 --bun |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not the same change, but the overlap is real: #37363 merges the two job structs into a shared |
There was a problem hiding this comment.
LGTM — mechanical RAII refactor, behavior-preserving.
What was reviewed:
HeldProcess::dropandUnlinkOnDrop::dropexecute the same statements as the four removed blocks; field declaration order (processbeforetmp_path) preserves the old manual drop order, anddrop(job)infinish()still runs insideenter()/exit().- Slot assignment in
spawn_cmd_prepare— confirmed the previous value is alwaysNone(cleared bycheck_finishedbeforeAdvance,Noneat construction), so no spurious detach on re-spawn. prepare_bootstrap'stake().map(UnlinkOnDrop::keep)disarms without unlinking (ManuallyDrop + ptr::read), matching the oldtake()semantics; the#[cfg(not(macos))]gate onCronRemoveJob.tmp_pathis correct — onlyprepare_filtered_crontab(already non-macOS) writes it.spawn_cmd_genericstill receives a live*mut Process: the ref is parked inprocess_slot()before the raw pointer is returned.
Extended reasoning...
Overview
Single-file refactor in src/runtime/api/cron.rs introducing two cron-private RAII wrappers: HeldProcess(NonNull<Process>) (Drop = detach() + Process::deref()) and UnlinkOnDrop(ZString) (Drop = sys::unlink; Deref<Target = ZStr>; macOS-only keep() disarm). CronRegisterJob and CronRemoveJob now hold Option<HeldProcess> / Option<UnlinkOnDrop> instead of Option<*mut Process> / Option<ZString>. Four hand-written cleanup sites (two check_finished bodies + two impl Drop) collapse to field-drop glue and self.process = None. SpawnCmdTarget::process_slot return type updated at all three sites. CronRemoveJob.tmp_path gains #[cfg(not(target_os = "macos"))] since it is never written on macOS.
Security risks
None. No untrusted input handling changes; the paths passed to sys::unlink are the same ZString values as before (temp files the job itself created via make_temp_path / plist path). No new FFI surface.
Level of scrutiny
Medium — this touches intrusive refcount lifetime and unsafe blocks around Process, so I traced each obligation:
- Drop order:
processis declared beforetmp_pathin both structs, so Rust's field-drop order matches the deleted manualDropbodies exactly. Both run inside theev.enter()/ev.exit()bracket via the explicitdrop(job)infinish(), unchanged. - Re-spawn drop glue:
*s!().process_slot() = Some(held)would drop a priorHeldProcessif one existed. Verified viamaybe_finished→check_finished(setsself.process = Nonebefore returningAdvance) →advance_state→ next spawn, and initial construction setsNone, so the slot is always empty here. - Raw pointer liveness in
spawn_cmd_generic:heldis stored in the job beforeheld.as_ptr()is returned, so the subsequent(*process).set_exit_handler/watch_or_reapstill see a ref-heldProcess— identical to before. keep(): standardManuallyDrop+ptr::readdisarm; no double-drop, no leak of theZString.- cfg gate:
CronRemoveJobon macOS usesunlink_plist(builds path fresh) and never touchestmp_path; the only writerprepare_filtered_crontabis already#[cfg(not(target_os = "macos"))], and the constructor initializer is gated to match.
Other factors
The change directly implements REVIEW.md's "pair every acquisition with its release at the acquisition site" via RAII, reducing four copies of an unsafe detach/deref block to one. Option<HeldProcess> picks up the NonNull niche. No user-facing API or test changes; existing cron test suite passes per the PR description. Net unsafe surface shrinks. Straightforward, well-documented, and self-contained.
Problem
Bun.cron(path, schedule, title)andBun.cron.remove(title).check_finishedplus a hand-writtenDropper job), so nothing stopped a new stage from dropping the pointer without detaching, or skipping the unlink.Fix
Dropimpls go away.keepwhere the old code took the path out, sincelaunchctl bootstrapneeds the file to stay.Dropbodies ran, in the same order, inside the sameenter()/exit()bracket. The one new drop point, the slot assignment on spawn, always overwritesNone.cargo checkandcargo clippyare clean; the existing cron tests pass (81 pass, 63 skip, 0 fail).Background
Bun.cronhas an in-process callback flavour (untouched) and OS-level jobs that spawncrontab,launchctlorschtasks. Only the latter change.Process, bun's spawned-child handle, is intrusively refcounted.to_processreturns a raw pointer carrying one ref; the holder mustdetach()it (exit callbacks stop reaching the job) andderef()it when done.launchctl bootstraptakes over the plist, and the remove job writes no file on macOS.Drop, so it runs wherever the value is dropped: incheck_finished, on error paths, and when the job is freed.Original description
What
CronRegisterJobandCronRemoveJob(theBun.cron(path, schedule, title)/Bun.cron.remove(title)OS-level jobs insrc/runtime/api/cron.rs) each held the +1 ref returned byto_processasprocess: Option<*mut Process>and the file they have to remove when they go away astmp_path: Option<ZString>. Releasing them was written out four times (check_finishedon both jobs, plus a hand-writtenimpl Dropon each job):Both obligations now live in two small cron-private types, and the fields become
process: Option<HeldProcess>andtmp_path: Option<UnlinkOnDrop>:HeldProcess::adoptis the one place that takes over the ref fromto_process(end ofspawn_cmd_prepare, which still returns the raw pointerspawn_cmd_genericworks with);SpawnCmdTarget::process_slotreturns&mut Option<HeldProcess>; bothcheck_finishedsites becomeself.process = None;. The four sites that arm a file (prepare_install_crontab,prepare_filtered_crontab,prepare_plist,prepare_schtasks_create) wrap the path inUnlinkOnDrop, theFile::openatcalls are unchanged throughDeref, and macOSprepare_bootstrapdisarms withUnlinkOnDrop::keepat the point where it used totake()the path (the plist is handed tolaunchctl bootstrapand must stay). Bothimpl Drop for Cron*Jobblocks are deleted; the two LIFETIMES comments describing the raw pointer go with them.CronRemoveJobnever writes a file on macOS, so itstmp_pathfield is now#[cfg(not(target_os = "macos"))](without the gate the field is never read there anddead_coderejects it).The four copies of the detach/deref
unsafeblock are replaced by one inHeldProcess::drop, plus thenew_uncheckedinadopt, its call site, and theptr::readin the macOS-onlykeep.Why
Who releases the process ref and who unlinks the file is now answered by the field types, a job can no longer drop the pointer without detaching (or unlink without having armed), and a new spawn stage cannot forget either cleanup.
Option<HeldProcess>uses theNonNullniche (one word, down from two) andOption<UnlinkOnDrop>keepsOption<ZString>'s layout; the generated field drops execute exactly the statements the removedDropbodies executed, in the same order (processis declared beforetmp_path), still inside theenter()/exit()bracket infinish(). The only new drop glue is on the slot assignment inspawn_cmd_prepare, whose previous value is alwaysNonebecausecheck_finishedclears it before any further spawn.Part of a series of small type-system hardening changes; each PR stands alone.
Verification
cargo checkandcargo clippyare clean for the touched crates. Debug build succeeds;bun bd test test/js/bun/cron/cron.test.ts test/js/bun/cron/in-process-cron.test.ts: 81 pass, 63 skip, 0 fail (144 tests across 2 files).