Skip to content

Commit 9e48ebf

Browse files
DaniGlezclaude
andauthored
Fix IOBuffer data race in stdio_loop / take! (#120)
Two background tasks (stdout + stderr) wrote to the same IOBuffer concurrently, and the main test loop called take! without any synchronisation. Julia's IOBuffer is not thread-safe, so this caused torn reads of io.size vs io.data under higher I/O load, manifesting as: DimensionMismatch: Attempted to wrap a MemoryRef of length N with an Array of size dims=(M,) Fix: add a ReentrantLock to PTRWorker; both stdio_loop write tasks and the take! call in runtests hold the lock. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5ad80db commit 9e48ebf

1 file changed

Lines changed: 8 additions & 6 deletions

File tree

src/ParallelTestRunner.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,17 @@ const ID_COUNTER = Threads.Atomic{Int}(0)
2929
struct PTRWorker <: Malt.AbstractWorker
3030
w::Malt.Worker
3131
io::IOBuffer
32+
io_lock::ReentrantLock
3233
id::Int
3334
end
3435

3536
function PTRWorker(; exename=Base.julia_cmd()[1], exeflags=String[], env=String[])
3637
io = IOBuffer()
38+
io_lock = ReentrantLock()
3739
wrkr = Malt.Worker(; exename, exeflags, env, monitor_stdout=false, monitor_stderr=false)
38-
stdio_loop(wrkr, io)
40+
stdio_loop(wrkr, io, io_lock)
3941
id = ID_COUNTER[] += 1
40-
return PTRWorker(wrkr, io, id)
42+
return PTRWorker(wrkr, io, io_lock, id)
4143
end
4244

4345
worker_id(wrkr::PTRWorker) = wrkr.id
@@ -258,19 +260,19 @@ function print_test_crashed(wrkr, test, ctx::TestIOContext)
258260
end
259261

260262
# Adapted from `Malt._stdio_loop`
261-
function stdio_loop(worker::Malt.Worker, io)
263+
function stdio_loop(worker::Malt.Worker, io, io_lock::ReentrantLock)
262264
Threads.@spawn while !eof(worker.stdout) && Malt.isrunning(worker)
263265
try
264266
bytes = readavailable(worker.stdout)
265-
write(io, bytes)
267+
@lock io_lock write(io, bytes)
266268
catch
267269
break
268270
end
269271
end
270272
Threads.@spawn while !eof(worker.stderr) && Malt.isrunning(worker)
271273
try
272274
bytes = readavailable(worker.stderr)
273-
write(io, bytes)
275+
@lock io_lock write(io, bytes)
274276
catch
275277
break
276278
end
@@ -1061,7 +1063,7 @@ function runtests(mod::Module, args::ParsedArgs;
10611063
ex
10621064
end
10631065
test_t1 = time()
1064-
output = String(take!(wrkr.io))
1066+
output = @lock wrkr.io_lock String(take!(wrkr.io))
10651067
push!(results, (test, result, output, test_t0, test_t1))
10661068

10671069
# act on the results

0 commit comments

Comments
 (0)