microsandbox-rb 0.5.11 → 0.5.12

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2d0013cf55f89aa21492847859554937ff838d5d38d4f5f34d0e8da473c9554
4
- data.tar.gz: 95bd9d994905567facd356597a8fee6e7f25715a485edbbc48bf98bb09f90eb3
3
+ metadata.gz: 82e3bca57a41c8b7a0b03e92baf14dc84f413e89e438616a0ea48c95ea237063
4
+ data.tar.gz: 4b3498e04b1ea1ff2f03e706bcc401bd9a216f622b38c9bf6e2e33f96a9dfaf9
5
5
  SHA512:
6
- metadata.gz: 337de8ed90dfea5b44d959579a16dafdd736e33fbfff7e10ed8ad4678184f84bdf417f33d85a7a8f11ab69a4785a91c381ea719600769b804d9a71c1c048adb1
7
- data.tar.gz: 672d7c7945e3a5561ad9934bc682b5f9764d1873d4235105e33ca1494a0079d11e574d520ed0e1f90bb66049264cdc783180448d2f456c78185024d9f18b186d
6
+ metadata.gz: cfed8fb4ae35f54dbb099fb306899c0e669f6ac7a607c283480637125f57575707534607a92604be78b8ca0135b6e1ec15869c369cfb75b809f14a70847f42d7
7
+ data.tar.gz: 1583a6b01336edb76312cde96b32bca2e124f2f2c815ddb26f5273930fe5e0e40f3b761e44e763a440a0d8ff368ca30cee30ee65991c1518110ad938b23ef6ba
data/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ upstream microsandbox runtime.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.12] - 2026-06-23
10
+
11
+ ### Fixed
12
+
13
+ - **fork-safe tokio runtime.** The process-wide multi-threaded runtime is now
14
+ tagged with the pid it was built under and rebuilt automatically after a
15
+ `fork(2)`. A forking host (Solid Queue / Resque job servers, clustered Puma)
16
+ used to inherit a runtime whose worker + I/O-driver threads do not survive the
17
+ fork — `block_on` could still drive the calling thread, but background I/O (e.g.
18
+ the agent-relay connection that streams `exec_stream` output) never ran, so
19
+ long-lived operations stalled or the connection dropped mid-stream in the child.
20
+ `runtime()` now detects the pid change and builds a fresh runtime for the child
21
+ (the stale one is leaked, never dropped — dropping a runtime whose threads
22
+ vanished across fork can hang on the shutdown join). No API change.
23
+
9
24
  ## [0.5.11] - 2026-06-23
10
25
 
11
26
  ### Added
data/Cargo.lock CHANGED
@@ -3249,7 +3249,7 @@ dependencies = [
3249
3249
 
3250
3250
  [[package]]
3251
3251
  name = "microsandbox_rb"
3252
- version = "0.5.11"
3252
+ version = "0.5.12"
3253
3253
  dependencies = [
3254
3254
  "chrono",
3255
3255
  "futures",
@@ -7,7 +7,7 @@ description = "Ruby SDK native extension for microsandbox — secure, fast micro
7
7
  # Must equal Microsandbox::VERSION (lib/microsandbox/version.rb) — Native.version
8
8
  # returns this via env!("CARGO_PKG_VERSION") and version_spec.rb asserts equality.
9
9
  # The core-crate dependency below stays pinned at its own tag (v0.5.8).
10
- version = "0.5.11"
10
+ version = "0.5.12"
11
11
  authors = ["Super Rad Company <development@superrad.company>"]
12
12
  repository = "https://github.com/superradcompany/microsandbox"
13
13
  license = "Apache-2.0"
@@ -9,7 +9,8 @@
9
9
  use std::ffi::c_void;
10
10
  use std::future::Future;
11
11
  use std::panic::{catch_unwind, AssertUnwindSafe};
12
- use std::sync::OnceLock;
12
+ use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
13
+ use std::sync::Mutex;
13
14
 
14
15
  use magnus::Ruby;
15
16
  use tokio::runtime::Runtime;
@@ -21,17 +22,60 @@ pub fn ruby() -> Ruby {
21
22
  Ruby::get().expect("microsandbox: not on a Ruby thread")
22
23
  }
23
24
 
24
- static RUNTIME: OnceLock<Runtime> = OnceLock::new();
25
+ // The process-wide tokio runtime, guarded by the pid it was built under. A
26
+ // multi-threaded runtime owns worker + I/O-driver threads, and `fork(2)` copies
27
+ // ONLY the calling thread — so a child that inherits this runtime has a runtime
28
+ // whose threads are gone: `block_on` can still drive the current thread, but the
29
+ // background I/O that keeps e.g. the agent-relay connection alive never runs, so
30
+ // connections stall/drop mid-use. This is hit in practice by forking job servers
31
+ // (Solid Queue, Resque) and clustered web servers (Puma workers).
32
+ //
33
+ // Fix: store the runtime behind a leaked raw pointer tagged with the building
34
+ // pid. If `runtime()` is called in a process whose pid differs (we forked), build
35
+ // a FRESH runtime for this process and swap the pointer. The stale runtime is
36
+ // LEAKED, never dropped — dropping a tokio runtime whose worker threads vanished
37
+ // across fork can hang on the shutdown thread-join. One runtime is leaked per
38
+ // process that uses the SDK (bounded; equivalent to the old set-once behavior in
39
+ // the common no-fork case).
40
+ static RUNTIME_PTR: AtomicPtr<Runtime> = AtomicPtr::new(std::ptr::null_mut());
41
+ static RUNTIME_PID: AtomicU32 = AtomicU32::new(0);
42
+ static RUNTIME_LOCK: Mutex<()> = Mutex::new(());
25
43
 
26
- /// The process-wide multi-threaded tokio runtime, built on first use.
44
+ fn build_runtime() -> Runtime {
45
+ tokio::runtime::Builder::new_multi_thread()
46
+ .enable_all()
47
+ .thread_name("microsandbox-rb")
48
+ .build()
49
+ .expect("microsandbox: failed to build tokio runtime")
50
+ }
51
+
52
+ /// The multi-threaded tokio runtime for THIS process, built on first use and
53
+ /// rebuilt after a `fork(2)` (fork-safe — see the note above).
27
54
  pub fn runtime() -> &'static Runtime {
28
- RUNTIME.get_or_init(|| {
29
- tokio::runtime::Builder::new_multi_thread()
30
- .enable_all()
31
- .thread_name("microsandbox-rb")
32
- .build()
33
- .expect("microsandbox: failed to build tokio runtime")
34
- })
55
+ let cur = std::process::id();
56
+ let ptr = RUNTIME_PTR.load(Ordering::Acquire);
57
+ if !ptr.is_null() && RUNTIME_PID.load(Ordering::Acquire) == cur {
58
+ // SAFETY: ptr was produced by Box::leak (valid for 'static) and matches
59
+ // this process's pid, so its runtime threads are live in this process.
60
+ return unsafe { &*ptr };
61
+ }
62
+
63
+ let _guard = RUNTIME_LOCK
64
+ .lock()
65
+ .expect("microsandbox: runtime lock poisoned");
66
+ // Re-check under the lock (another thread may have just built it).
67
+ let ptr = RUNTIME_PTR.load(Ordering::Acquire);
68
+ if !ptr.is_null() && RUNTIME_PID.load(Ordering::Acquire) == cur {
69
+ return unsafe { &*ptr };
70
+ }
71
+
72
+ // Build a fresh runtime for this process and publish it. The previous pointer
73
+ // (if any) belonged to a parent process and is intentionally leaked, not
74
+ // dropped (its threads are gone post-fork; Drop would block on join).
75
+ let rt: &'static Runtime = Box::leak(Box::new(build_runtime()));
76
+ RUNTIME_PTR.store(rt as *const Runtime as *mut Runtime, Ordering::Release);
77
+ RUNTIME_PID.store(cur, Ordering::Release);
78
+ rt
35
79
  }
36
80
 
37
81
  /// Run `f` with the Ruby GVL released.
@@ -5,5 +5,5 @@ module Microsandbox
5
5
  # the pinned core-crate tag); the patch segment advances for gem-only revisions
6
6
  # that add bindings atop the same core. Must equal the native ext's Cargo crate
7
7
  # version (`Native.version`), enforced by spec/unit/version_spec.rb.
8
- VERSION = "0.5.11"
8
+ VERSION = "0.5.12"
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: microsandbox-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.11
4
+ version: 0.5.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao