
Show HN: Ploom, find thread bugs in Python by trying every interleaving
Find thread bugs in Python. Replay the schedule that failed.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
Find thread bugs in Python. Replay the schedule that failed.
A thread bug depends on which thread runs next. A test can pass a thousand times and still miss the bad order. Ploom runs your test under a controlled scheduler. It tries every order of ploom operations, stops at the first failure, and saves that order as a JSON trace. Replay the trace to get the same failure again.
Documentation · Guide · Guarantees · Python API · Development
uv add --dev ploom
or
pip install ploom
Example: two withdrawals from one account
Mathieu and Lucas share an account that holds 100. They both withdraw 100 at the same time. Only one withdrawal must succeed.
import ploom
def scenario(): balance = ploom.SharedCell(100)
def withdraw(): if balance.read() >= 100: balance.write(balance.read() - 100)
mathieu = ploom.Thread(target=withdraw) lucas = ploom.Thread(target=withdraw) mathieu.start() lucas.start() mathieu.join() lucas.join() assert balance.read() >= 0, "the account is overdrawn"
ploom.check(scenario)
SharedCell holds a value that the threads share. ploom.Thread is a thread that ploom schedules. With threading.Thread, this code passes almost every time. Ploom tries the possible orders and finds the bad one in a few seconds:
ploom.exploration.ModelCheckError: builtins.AssertionError: the account is overdrawn Original trace: .ploom/f981…-original.json Minimized trace: .ploom/f981…-minimized.json
The minimized trace shows the bad order:
To fix the bug, hold a lock during the check and the withdrawal:
lock = ploom.Lock()
def withdraw(): with lock: if balance.read() >= 100: balance.write(balance.read() - 100)
Now all 127 orders pass, and ploom.check returns the exploration statistics. examples/bank_account.py contains the version without the lock.
The ploom_check fixture explores a scenario inside a test:
def test_withdraw(ploom_check): ploom_check(scenario)
When the test fails, ploom prints the command that replays the failure:
ploom first failure: .ploom/ID-original.json Replay: pytest tests/test_bank.py::test_withdraw --ploom-replay=.ploom/ID-original.json
Replay a failure
Replay runs one saved schedule. It does not search. Run the printed command:
uv run pytest tests/test_bank.py::test_withdraw --ploom-replay=.ploom/ID-original.json
A module-level scenario can also use the command line:
uv run python -m ploom check examples.bank_account:scenario uv run python -m ploom replay .ploom/ID-minimized.json
Replay stops with ReplayDivergenceError if the scenario's source file, the Python minor version, or the recorded operations changed.
Each ploom operation is a scheduling point. Only one task runs at a time, so the result does not depend on the GIL.
A depth-first search tries each runnable task at each scheduling point.
Each schedule runs in a fresh Python process. Python state from one schedule does not leak into the next. All processes use the same hash seed, so set order does not change between schedules.
At the first failure, ploom saves the trace. Then it looks for the same failure with fewer preemptions and saves a minimized trace. A preemption is a switch away from a task that could continue.
A search limit raises IncompleteExplorationError. A limit never counts as a pass.
A fresh process costs about 0.1 seconds per schedule. Keep scenarios small. For larger scenarios, set Config(max_preemptions=2) to explore every schedule with at most two preemptions.
Area Supported
Threads Thread(target, args, kwargs), start, join, is_alive
Locks Lock: blocking and nonblocking acquire, release, locked, with
Shared state SharedCell reads and writes of immutable values, yield_now()
Entry points Python API, pytest fixture, python -m ploom
Interpreters Standard CPython 3.13 and 3.14, free-threaded CPython 3.14
Ploom sees only ploom operations, and it models them as sequentially consistent. It does not see threading locks, Event, Queue, I/O, or weak-memory effects. It does not detect data races. Your assertions find the bugs.
Ploom does not roll back files, network calls, or other external effects between schedules.
uv sync --locked make check
Run make docs to build the documentation. The development guide covers benchmarks and CI.
Ploom is inspired by Loom, the concurrency testing library for Rust. Loom showed how to explore schedules and reproduce failures. Ploom defines its own Python operations. It does not implement Loom's C11 memory model or its partial-order reduction.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。