Skip to content

Instantly share code, notes, and snippets.

@sam-saffron-jarvis
Created February 27, 2026 11:53
Show Gist options
  • Select an option

  • Save sam-saffron-jarvis/c2c9d347076844193018287e3cec615e to your computer and use it in GitHub Desktop.

Select an option

Save sam-saffron-jarvis/c2c9d347076844193018287e3cec615e to your computer and use it in GitHub Desktop.
10 fork safety tests for MiniRacer single_threaded platform — covers basic eval, before_fork, inherited context disposal, snapshot round-trip, heap_stats/GC, attach callbacks, timeout, fan-out concurrency, async/promise, and OOM handling
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# 10 Fork Safety Tests for MiniRacer with single_threaded platform
# ================================================================
# Each test forks a child, exercises a different API feature, and reports
# pass/fail. The parent waits for every child before moving on.
#
# Run: bundle exec ruby test_fork_10.rb
require "mini_racer"
require "json"
require "timeout"
require "tempfile"
require "net/http"
require "uri"
# ── Helpers ────────────────────────────────────────────────────────────────────
WAIT_TIMEOUT_S = 15
PASS = "\e[32m✓\e[0m"
FAIL = "\e[31m✗\e[0m"
DIM = "\e[2m"
RST = "\e[0m"
$test_num = 0
def banner(title)
$test_num += 1
puts
puts "#{DIM}┄" * 60 + RST
puts " Test #{$test_num}: #{title}"
puts "#{DIM}┄" * 60 + RST
end
def pass(msg) = puts " #{PASS} #{msg}"
def fail!(msg)
puts " #{FAIL} #{msg}"
exit 1
end
# Wait for child with timeout; kill + exit on hang/non-zero.
def wait_child(pid, label)
status = Timeout.timeout(WAIT_TIMEOUT_S) { Process.waitpid2(pid).last }
if status.success?
pass "child #{pid} (#{label}) exited 0"
else
fail! "child #{pid} (#{label}) exited #{status.exitstatus || "sig:#{status.termsig}"}"
end
rescue Timeout::Error
Process.kill("KILL", pid) rescue nil
Process.waitpid2(pid) rescue nil
fail! "child #{pid} (#{label}) timed out after #{WAIT_TIMEOUT_S}s"
end
# Dispose every live MiniRacer context reachable from ObjectSpace.
def dispose_inherited
n = 0
ObjectSpace.each_object(MiniRacer::Context) { |c| c.dispose rescue nil; n += 1 }
n
end
# ── Platform (must happen before any Context/Snapshot) ─────────────────────────
MiniRacer::Platform.set_flags!(:single_threaded)
puts "mini_racer #{MiniRacer::VERSION} Ruby #{RUBY_VERSION}"
puts "Platform: single_threaded"
# Fetch lodash once; tests 4 and 10 use it to prove non-trivial JS works post-fork.
LODASH_CDN = "https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js"
print "Fetching lodash from CDN... "
LODASH_SRC = Net::HTTP.get(URI(LODASH_CDN))
puts "#{LODASH_SRC.bytesize} bytes"
# ── Test 1 ─────────────────────────────────────────────────────────────────────
# Simplest smoke test: parent warms up a context, child creates its own fresh one
# and runs basic maths + string ops. Proves the platform re-init path works at
# all — if V8 hangs in Isolate::New, this is the first test to die.
banner "Basic eval in child after parent warms V8"
parent_ctx = MiniRacer::Context.new
parent_ctx.eval("var x = 42;")
pass "parent: x = #{parent_ctx.eval("x")}"
pid = fork do
dispose_inherited
ctx = MiniRacer::Context.new
result = ctx.eval('"Hello from child " + (6 * 7)')
raise "wrong: #{result}" unless result == "Hello from child 42"
puts " [child] #{result}"
ctx.dispose
exit 0
end
wait_child(pid, "basic eval")
parent_ctx.dispose
# ── Test 2 ─────────────────────────────────────────────────────────────────────
# MiniRacer.before_fork is the idiomatic pre-fork hook: runs GC so Ruby can
# finalise unreferenced contexts, shrinking child RSS. We verify the call
# succeeds and that the child can still run JS afterwards.
banner "MiniRacer.before_fork lowers child RSS"
many = 5.times.map do
c = MiniRacer::Context.new
c.eval("var arr = Array.from({length:1000}, (_,i)=>i*i);")
c
end
before_rss = File.read("/proc/self/status").match(/VmRSS:\s+(\d+)/)[1].to_i
MiniRacer.before_fork # encourages Ruby GC to finalize unreferenced contexts
after_rss = File.read("/proc/self/status").match(/VmRSS:\s+(\d+)/)[1].to_i
pass "before_fork ran OK (RSS #{before_rss} → #{after_rss} kB)"
pid = fork do
dispose_inherited
ctx = MiniRacer::Context.new
sum = ctx.eval("Array.from({length:100},(_,i)=>i).reduce((a,b)=>a+b,0)")
raise "wrong sum #{sum}" unless sum == 4950
puts " [child] sum(0..99) = #{sum}"
ctx.dispose
exit 0
end
wait_child(pid, "before_fork")
many.each { |c| c.dispose rescue nil }
# ── Test 3 ─────────────────────────────────────────────────────────────────────
# Inherited contexts are automatically marked disposed by mini_racer's atfork
# child handler. Prove that calling eval on an inherited context raises
# ContextDisposedError rather than crashing or silently returning junk.
banner "Inherited context raises ContextDisposedError in child"
live_ctx = MiniRacer::Context.new
live_ctx.eval("var secret = 'do not inherit me';")
pass "parent: secret = #{live_ctx.eval("secret")}"
pid = fork do
# Do NOT call dispose_inherited — test what happens naturally
got_error = false
begin
live_ctx.eval("secret") # should raise, not return stale data
rescue MiniRacer::ContextDisposedError
got_error = true
rescue => e
puts " [child] unexpected error: #{e.class}: #{e.message}"
exit 1
end
unless got_error
puts " [child] ERROR: eval on inherited ctx did not raise ContextDisposedError"
exit 1
end
puts " [child] correctly got ContextDisposedError on inherited context"
exit 0
end
wait_child(pid, "inherited ctx disposed")
live_ctx.dispose
# ── Test 4 ─────────────────────────────────────────────────────────────────────
# Snapshot.new compiles JS into a V8 heap snapshot that can be loaded instantly
# into any context. Here we build a snapshot with helper functions in the parent,
# fork, and reconstruct it via dump/load — simulating what Unicorn workers do.
banner "Snapshot serialised in parent, reconstructed and used in child"
snap_src = <<~JS
var utils = {
fib: function(n){ return n<=1?n:utils.fib(n-1)+utils.fib(n-2); },
fact: function(n){ return n<=1?1:n*utils.fact(n-1); },
rev: function(s){ return s.split('').reverse().join(''); }
};
JS
snapshot = MiniRacer::Snapshot.new(snap_src)
blob = snapshot.dump
pass "parent: snapshot #{blob.bytesize} bytes"
pid = fork do
dispose_inherited
restored = MiniRacer::Snapshot.load(blob)
ctx = MiniRacer::Context.new(snapshot: restored)
fib10 = ctx.eval("utils.fib(10)")
fact7 = ctx.eval("utils.fact(7)")
rev = ctx.eval("utils.rev('fork')")
raise "fib wrong" unless fib10 == 55
raise "fact wrong" unless fact7 == 5040
raise "rev wrong" unless rev == "krof"
puts " [child] fib(10)=#{fib10} fact(7)=#{fact7} rev('fork')=#{rev}"
ctx.dispose
exit 0
end
wait_child(pid, "snapshot round-trip")
# ── Test 5 ─────────────────────────────────────────────────────────────────────
# heap_stats and low_memory_notification work on a fresh child context.
# Verifies that V8 GC machinery initialises correctly post-fork and that all
# expected heap stat keys are present and sensible.
banner "heap_stats + low_memory_notification in child"
pid = fork do
dispose_inherited
ctx = MiniRacer::Context.new
ctx.eval("var junk = Array.from({length:50000}, (_,i) => ({i, v: i*i}));")
before = ctx.heap_stats
ctx.low_memory_notification
after = ctx.heap_stats
EXPECTED_KEYS = %i[
used_heap_size total_heap_size heap_size_limit
total_available_size peak_malloced_memory
].freeze
missing = EXPECTED_KEYS - before.keys
raise "missing keys: #{missing}" unless missing.empty?
puts " [child] heap before GC: used=#{before[:used_heap_size]} / limit=#{before[:heap_size_limit]}"
puts " [child] heap after GC: used=#{after[:used_heap_size]}"
# GC should not grow the used heap (it may shrink or stay similar)
# We just assert it's a positive number — don't assert shrinkage (V8 may defer)
raise "used_heap_size not positive" unless after[:used_heap_size] > 0
ctx.dispose
exit 0
end
wait_child(pid, "heap_stats + GC")
# ── Test 6 ─────────────────────────────────────────────────────────────────────
# attach() registers a Ruby lambda as a JS function. We test that callbacks
# work across a fork: the child registers its own callbacks on a fresh context,
# calls them from JS, and receives return values. A more complex variant uses a
# stateful Ruby accumulator to prove the callback runs multiple times.
banner "attach() JS→Ruby callbacks in child"
# Note: attach takes (name, callable) — not a block.
pid = fork do
dispose_inherited
ctx = MiniRacer::Context.new
calls = []
ctx.attach("ruby.log", proc { |msg| calls << msg; "ack:#{msg}" })
# Pure sum: callback just returns a+b, JS accumulates
ctx.attach("ruby.add", proc { |a, b| a + b })
ack1 = ctx.eval("ruby.log('hello from JS')")
ack2 = ctx.eval("ruby.log('world')")
sum = ctx.eval("[1,2,3,4,5].reduce(function(acc,n){ return ruby.add(acc,n); }, 0)")
raise "ack1 wrong: #{ack1}" unless ack1 == "ack:hello from JS"
raise "ack2 wrong: #{ack2}" unless ack2 == "ack:world"
raise "calls wrong: #{calls}" unless calls == ["hello from JS", "world"]
raise "sum wrong: #{sum}" unless sum == 15
puts " [child] log callbacks: #{calls.inspect}"
puts " [child] reduce via ruby.add → #{sum}"
ctx.dispose
exit 0
end
wait_child(pid, "attach callbacks")
# ── Test 7 ─────────────────────────────────────────────────────────────────────
# Timeout enforcement must work in child processes — critical for Unicorn/Puma
# where rogue JS could block a worker forever. We start an infinite loop and
# verify ScriptTerminatedError is raised within the timeout window.
banner "Timeout kills infinite loop in child"
pid = fork do
dispose_inherited
ctx = MiniRacer::Context.new(timeout: 200) # 200 ms
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
err = nil
begin
ctx.eval("while(true){}") # must not block forever
rescue MiniRacer::ScriptTerminatedError => e
err = e
end
elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round
raise "expected ScriptTerminatedError, got #{err.inspect}" unless err
raise "took too long: #{elapsed_ms}ms" if elapsed_ms > 2000
puts " [child] infinite loop terminated in ~#{elapsed_ms}ms: #{err.message}"
ctx.dispose
exit 0
end
wait_child(pid, "timeout in child")
# ── Test 8 ─────────────────────────────────────────────────────────────────────
# Fan-out: parent forks N independent children simultaneously. Each child gets
# a different prime index and checks that the Nth prime is correct. Tests that
# the single_threaded platform handles concurrent fork() calls from the same
# parent and that each child gets an isolated V8 instance.
banner "N independent children, each computing different primes (fan-out)"
EXPECTED_PRIMES = [2,3,5,7,11,13,17,19,23,29].freeze
pids = EXPECTED_PRIMES.each_with_index.map do |prime, idx|
fork do
dispose_inherited
ctx = MiniRacer::Context.new
ctx.eval(<<~JS)
function nthPrime(n) {
let count = 0, num = 1;
while (count < n) {
num++;
let prime = true;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) { prime = false; break; }
}
if (prime) count++;
}
return num;
}
JS
result = ctx.eval("nthPrime(#{idx + 1})")
raise "prime #{idx+1} wrong: got #{result}, expected #{prime}" unless result == prime
puts " [child #{Process.pid}] prime(#{idx+1}) = #{result}"
ctx.dispose
exit 0
end
end
pids.each_with_index { |pid, i| wait_child(pid, "prime ##{i+1}") }
# ── Test 9 ─────────────────────────────────────────────────────────────────────
# Promises and async/await: V8's microtask queue must drain correctly post-fork.
# We resolve a chain of promises and use pump_message_loop to flush pending
# microtasks, then check the final settled value.
banner "Async/Promise + pump_message_loop in child"
pid = fork do
dispose_inherited
ctx = MiniRacer::Context.new
ctx.eval(<<~JS)
var result = null;
var done = false;
async function pipeline(n) {
const doubled = await Promise.resolve(n * 2);
const squared = await Promise.resolve(doubled * doubled);
const asStr = await Promise.resolve("value:" + squared);
return asStr;
}
pipeline(5).then(v => { result = v; done = true; });
JS
# Pump until the promise chain settles (or 100 iterations, whichever first)
100.times do
break if ctx.eval("done")
ctx.pump_message_loop
end
val = ctx.eval("result")
raise "promise did not settle" unless ctx.eval("done")
raise "wrong value: #{val}" unless val == "value:100"
puts " [child] async pipeline(5) → #{val}"
ctx.dispose
exit 0
end
wait_child(pid, "async/promise")
# ── Test 10 ────────────────────────────────────────────────────────────────────
# max_memory OOM: proves that memory limits are enforced in children and that
# V8OutOfMemoryError propagates cleanly rather than causing a hard crash.
# A grandchild deliberately exhausts its V8 heap, then the child verifies the
# exit code. The parent stays healthy throughout.
banner "max_memory V8OutOfMemoryError in grandchild"
# Load lodash in parent to prove we're still alive at the end
parent_sentinel = MiniRacer::Context.new
parent_sentinel.eval(LODASH_SRC)
version = parent_sentinel.eval("_.VERSION")
pass "parent pre-fork: lodash #{version} loaded"
pid = fork do
dispose_inherited
ctx = MiniRacer::Context.new
# Spin up a grandchild that will OOM
gc_pid = fork do
# Give the grandchild a tiny heap so it OOMs quickly
oom_ctx = MiniRacer::Context.new(max_memory: 2_000_000) # 2 MB
begin
oom_ctx.eval(<<~JS)
var sink = [];
while (true) { sink.push(new Array(10000).fill(Math.random())); }
JS
puts " [grandchild] ERROR: OOM was not raised"
exit 2
rescue MiniRacer::V8OutOfMemoryError => e
puts " [grandchild] #{e.class} raised as expected: #{e.message.lines.first.chomp}"
oom_ctx.dispose rescue nil
exit 0
rescue => e
puts " [grandchild] unexpected: #{e.class}: #{e.message}"
exit 3
end
end
status = Timeout.timeout(WAIT_TIMEOUT_S) { Process.waitpid2(gc_pid).last }
unless status.success?
puts " [child] grandchild failed: #{status.exitstatus}"
exit 1
end
puts " [child] grandchild OOM'd cleanly (exit 0)"
# Child itself should still be healthy
sum = ctx.eval("[10,20,30].reduce((a,b)=>a+b,0)")
raise "child broken after grandchild OOM: #{sum}" unless sum == 60
puts " [child] own context still healthy: sum=#{sum}"
ctx.dispose
exit 0
rescue Timeout::Error
Process.kill("KILL", gc_pid) rescue nil
exit 1
end
wait_child(pid, "OOM in grandchild")
after = parent_sentinel.eval("_.chunk([1,2,3,4],2).length")
raise "parent corrupted" unless after == 2
pass "parent post-fork: lodash still works — chunk result length=#{after}"
parent_sentinel.dispose
# ── Summary ────────────────────────────────────────────────────────────────────
puts
puts "#{DIM}═" * 60 + RST
puts " All #{$test_num} fork tests passed #{PASS}"
puts "#{DIM}═" * 60 + RST
puts
exit 0
# ── Expected output (PIDs replaced with NNN) ───────────────────────────────────
#
# mini_racer 0.20.0 Ruby 4.0.1
# Platform: single_threaded
# Fetching lodash from CDN... 73320 bytes
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 1: Basic eval in child after parent warms V8
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# ✓ parent: x = 42
# [child] Hello from child 42
# ✓ child NNN (basic eval) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 2: MiniRacer.before_fork lowers child RSS
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# ✓ before_fork ran OK (RSS 62292 → 62316 kB)
# [child] sum(0..99) = 4950
# ✓ child NNN (before_fork) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 3: Inherited context raises ContextDisposedError in child
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# ✓ parent: secret = do not inherit me
# [child] correctly got ContextDisposedError on inherited context
# ✓ child NNN (inherited ctx disposed) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 4: Snapshot serialised in parent, reconstructed and used in child
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# ✓ parent: snapshot 411719 bytes
# [child] fib(10)=55 fact(7)=5040 rev('fork')=krof
# ✓ child NNN (snapshot round-trip) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 5: heap_stats + low_memory_notification in child
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# [child] heap before GC: used=3068664 / limit=1669332992
# [child] heap after GC: used=3038112
# ✓ child NNN (heap_stats + GC) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 6: attach() JS→Ruby callbacks in child
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# [child] log callbacks: ["hello from JS", "world"]
# [child] reduce via ruby.add → 15
# ✓ child NNN (attach callbacks) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 7: Timeout kills infinite loop in child
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# [child] infinite loop terminated in ~200ms: terminated
# ✓ child NNN (timeout in child) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 8: N independent children, each computing different primes (fan-out)
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# [child NNN] prime(1) = 2 # order is non-deterministic (true concurrency)
# [child NNN] prime(3) = 5
# [child NNN] prime(2) = 3
# [child NNN] prime(4) = 7
# [child NNN] prime(5) = 11
# [child NNN] prime(6) = 13
# [child NNN] prime(7) = 17
# [child NNN] prime(8) = 19
# [child NNN] prime(9) = 23
# [child NNN] prime(10) = 29
# ✓ child NNN (prime #1) exited 0
# ✓ child NNN (prime #2) exited 0
# ... (10 total)
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 9: Async/Promise + pump_message_loop in child
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# [child] async pipeline(5) → value:100
# ✓ child NNN (async/promise) exited 0
#
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# Test 10: max_memory V8OutOfMemoryError in grandchild
# ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
# ✓ parent pre-fork: lodash 4.17.23 loaded
# [grandchild] MiniRacer::V8OutOfMemoryError raised as expected: out of memory
# [child] grandchild OOM'd cleanly (exit 0)
# [child] own context still healthy: sum=60
# ✓ child NNN (OOM in grandchild) exited 0
# ✓ parent post-fork: lodash still works -- chunk result length=2
#
# ════════════════════════════════════════════════════════════
# All 10 fork tests passed ✓
# ════════════════════════════════════════════════════════════
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment