Created
February 27, 2026 11:53
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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