Skip to main content

Deterministic simulation testing for Python asyncio: seeded scheduling, virtual time, replayable failures

Project description

simloop

Deterministic simulation testing for Python asyncio: seeded scheduling, virtual time, and a simulated network with fault injection. Any failure simloop finds replays exactly from a seed.

Rust has madsim and turmoil; FoundationDB and TigerBeetle made the technique famous. simloop brings it to asyncio: your real, unmodified networking code runs on a simulated loop where time is virtual, every scheduling decision is seeded, and the network loses, delays, duplicates, and partitions traffic on command.

Install

pip install simloop

Python 3.12+. No runtime dependencies. The pytest plugin ships in the same package and activates automatically.

Find a bug, then replay it

import asyncio
from simloop import sim_test


@sim_test(seeds=200)
async def test_replies_survive_a_lossy_network():
    loop = asyncio.get_running_loop()
    loop.net.set_defaults(latency=(0.001, 0.050))

    async def serve():
        async def handle(reader, writer):
            writer.write((await reader.readline()).upper())
            writer.close()

        server = await asyncio.start_server(handle, port=8080)
        async with server:
            await server.serve_forever()

    loop.net.host("server").create_task(serve())
    await asyncio.sleep(1.0)

    async with asyncio.timeout(30.0):
        reader, writer = await asyncio.open_connection("server", 8080)
        writer.write(b"hello\n")
        assert await reader.readline() == b"HELLO\n"

@sim_test(seeds=200) runs the test under 200 seeds, each on a fresh simulated loop, and stops at the first failure:

simloop: failed at seed 41 (41 seeds passed first)
replay: pytest 'tests/test_echo.py::test_replies_survive_a_lossy_network' --simloop-replay=41

last 20 trace events:
  [t=1.0312] net      seq=812  send driver>server
  ...
pending tasks by host:
  server  Task 'Task-2'  awaiting serve_forever  at ...

The replay command reproduces the failure exactly — same scheduling decisions, same fault decisions, same trace. In CI, crank the search without touching code:

pytest --simloop-seeds=1000

What the simulation gives you

  • Seeded scheduling — the ready queue's execution order comes from a per-run PRNG; a seed pins the entire interleaving.
  • Virtual timeasyncio.sleep(300) costs nothing; timeouts fire in simulated seconds.
  • A simulated networkopen_connection / start_server / datagram endpoints run over an in-memory packet core with per-link latency, drop, and duplication, plus partitions and host crashes:
net = loop.net
net.set_defaults(latency=(0.001, 0.010), drop=0.05)
net.partition({"node1"}, {"node2", "node3"})   # silent blackhole
loop.call_later(5.0, net.heal)                  # heals in virtual time
net.crash("node2")                              # no reset, just silence
  • Replayable traces — every scheduling and fault decision lands in an append-only trace whose hash proves a replay is exact.

Proving it: the jobqueue demo

examples/jobqueue/ is a complete distributed system — an exactly-once job scheduler (leases, fencing tokens, idempotency keys, backoff, and dead-lettering) written in plain asyncio — tested end to end with simloop. Its suite runs hundreds of seeds of partitions, crashes, and poison jobs, and shows that removing any load-bearing safeguard produces a violation the explorer finds and replays from a seed. The bug table lives in examples/jobqueue/README.md.

Honest limits

Code that goes through the event-loop API is supported; code that bypasses it is fenced: threads and executors, raw sockets, subprocesses, signals, TLS, and getaddrinfo raise SimulationFenceError rather than silently breaking determinism. Write-side flow control is not simulated. The full contract is in docs/supported-api.md.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

simloop-0.1.0.tar.gz (19.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

simloop-0.1.0-py3-none-any.whl (23.6 kB view details)

Uploaded Python 3

File details

Details for the file simloop-0.1.0.tar.gz.

File metadata

  • Download URL: simloop-0.1.0.tar.gz
  • Upload date:
  • Size: 19.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for simloop-0.1.0.tar.gz
Algorithm Hash digest
SHA256 942e7c9db83dc39b7aabe3628503f318d896127f494d4fd53ed8530fbd835a12
MD5 8e12e38e69407daaa7126010b7a5dac0
BLAKE2b-256 47468fae4b95277542512fd5577c1e3def79e957792d20bb6c38fa7e75238c08

See more details on using hashes here.

Provenance

The following attestation bundles were made for simloop-0.1.0.tar.gz:

Publisher: release.yml on dhruvl/simloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file simloop-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: simloop-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 23.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for simloop-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5bb4d3021ff524543ff052008ff5a1a40444455e3d900cb280416e78f43338f7
MD5 a4d66fdaeac94eabc8e5a5cc17abbb85
BLAKE2b-256 c3dae9e5b50907f2608e1a4e779a8acf225fed5fbee03507f4ff0471902a5f4d

See more details on using hashes here.

Provenance

The following attestation bundles were made for simloop-0.1.0-py3-none-any.whl:

Publisher: release.yml on dhruvl/simloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page