Skip to content

Repository files navigation

🏦 DST Demo: Bank App

What happens to a bank when you fast-forward its clock into 2038?

This repository is a hands-on demonstration of Deterministic Simulation Testing (DST), built around a small bank application that communicates over TCP. The original motivation was a fun one: expose the epochalypse bug without waiting for the calendar to get there.

The bank is deliberately simple. Clients create and void transactions, query balances, and check that the server is still responding. The interesting part is the world around it: simulated time, networking, filesystem operations, and scheduling let us explore what happens when requests overlap, messages arrive late, or the server restarts.

The same bank implementation runs against real infrastructure or inside the simulator, powered by Switchy and Simvar. The goal isn't to build a production bank. It's to make subtle time, concurrency, and recovery bugs easier to trigger, replay, and understand.

Why deterministic simulation?

A timing bug is much easier to investigate when you can make it happen again. Instead of depending on a lucky thread interleaving or a badly timed network delay, the simulator controls those conditions and generates workloads from a seed.

That gives this little bank a useful testing laboratory:

  • Time travel: move the clock to an interesting boundary instead of waiting years for it.
  • Overlapping clients: exercise transaction operations under different simulated schedules and message delays.
  • Failure and recovery: restart the server while clients are interacting with it.
  • Replay: preserve the seed and configuration, then revisit the same simulated scenario while investigating an assertion failure.

Assertions turn assumptions such as “transaction timestamps never go backwards” into something the simulator can challenge. A passing run covers one experiment; a failing run gives you a concrete scenario to investigate.

The epochalypse experiment

Unix timestamps count seconds from January 1, 1970. Store that count in a signed 32-bit integer and it runs out of positive values at 03:14:08 UTC on January 19, 2038. A narrowing cast can turn a future timestamp into a negative number, breaking assumptions that looked perfectly reasonable when tested with today's date.

That's the appeal of the original demo: bring the bug to the test, rather than waiting for the bug to arrive in production. Set the simulated epoch near the boundary, let the bankers make transactions, and use the bank's timestamp assertions to expose the problem. With the seed and configuration recorded, the failure becomes an experiment you can replay instead of a one-off surprise.

The current bank uses u64 timestamps. The quick start below crosses the signed 32-bit boundary successfully; it is a working simulation example, not a failing reproduction of the original bug.

What it demonstrates

  • A TCP service supporting create, void, get, list, balance, and health requests.
  • Seeded banker workloads generated through InteractionPlan.
  • Controlled time, message latency, and task scheduling through Switchy.
  • Simulation orchestration and restartable hosts through Simvar.
  • Transaction-ID/order, timestamp, response, and health assertions.

The fault injector generates sleeps and server restarts. It does not implement an explicit network-partition scenario. A void creates a compensating transaction rather than deleting the original.

Meet the simulated clients

  • 💼 Banker: follows a generated InteractionPlan of transaction operations and checks the responses.
  • 💥 Fault injector: sleeps, then asks the harness to restart the server, putting recovery behavior into the workload.
  • 🩺 Health checker: sends health requests and checks that the server responds as expected.

Together, these clients turn a small TCP service into a way to explore more than the happy path. See the simulator guide for how the actors fit together.

Run a bounded simulation

Install stable Rust and Git:

git clone https://github.com/BSteffaniak/dst-demo.git
cd dst-demo
NO_TUI=1 \
SIMULATOR_SEED=123 \
SIMULATOR_DURATION=1000 \
SIMULATOR_RUNS=1 \
SIMULATOR_MAX_PARALLEL=1 \
SIMULATOR_BANKER_COUNT=3 \
SIMULATOR_EPOCH_OFFSET=2147483646000 \
SIMULATOR_STEP_MULTIPLIER=10 \
RUST_LOG=info \
cargo run --locked -p dst_demo_server_simulator

This bounded smoke run starts just before the signed 32-bit Unix timestamp boundary and finishes with successful=true and exit code 0 on the current implementation. It is a smoke check, not proof that every generated operation or a restart occurred. Increase the duration and inspect debug logs for broader scenario coverage.

Replay and interpretation

Keep the source revision, Cargo.lock, environment values, and toolchain when reporting a result. Repeat the same command to replay. Compare the seed, generated operations, assertions, and outcome—not wall-clock log timestamps or elapsed performance measurements.

  • SIMULATOR_SEED: RNG seed.
  • SIMULATOR_DURATION: step budget in the pinned harness; bare values are parsed as milliseconds and compared to steps. Use the explicit value above rather than assuming wall-clock seconds.
  • SIMULATOR_STEP_MULTIPLIER: simulated milliseconds advanced per step.
  • SIMULATOR_EPOCH_OFFSET: starting Unix epoch offset in milliseconds.
  • SIMULATOR_RUNS, SIMULATOR_MAX_PARALLEL: number of runs and worker limit.
  • SIMULATOR_BANKER_COUNT: number of generated banker clients.
  • NO_TUI=1: disables the pinned harness's TUI at compile time; set it on the Cargo invocation, not only when executing an already-built binary.

Without a duration, the simulator may run indefinitely. A failed simulation returns a nonzero exit code. Preserve its configuration and failing assertion for a bug report.

Run the real service

In one terminal:

ADDR=127.0.0.1 PORT=3000 RUST_LOG=info cargo run --locked -p dst_demo_server

In another:

cargo run --locked -p dst_demo_tcp_client -- 127.0.0.1:3000

Enter CREATE_TRANSACTION, VOID_TRANSACTION, GET_TRANSACTION, LIST_TRANSACTIONS, or GET_BALANCE; follow any server prompts. HEALTH checks responsiveness.

The server otherwise binds to 0.0.0.0:3000. It has no production authentication/TLS layer; use loopback for evaluation. Real-mode transaction storage is separate from simulator state. Review the server implementation before using a non-disposable working directory.

Source map

Area Responsibility
server/src/bank.rs Transaction storage, balance updates, and invariants
server/src/lib.rs TCP protocol and request handling
simulator/src/main.rs Simulation lifecycle and exit status
simulator/src/client Banker, health-checker, and restart plans
tcp_client Manual protocol client

cargo test --locked --workspace checks compilation/test targets but currently runs no unit tests. It does not run the simulator. CI invokes the simulation executable separately. See the simulator guide.

Limitations

Simulation covers the behavior represented by these abstractions, not every OS, network, disk-flush, or power-loss failure. Health assertions are workload checks, not a proof of liveness. Generated workload coverage and successful runs are evidence, not proofs of correctness.

Further reading

For more background on the ideas behind the demo:

These are related projects and references; Switchy and Simvar power this repository.

Contributing and license

Report a minimal replay command, revision, and assertion through GitHub Issues. Do not include private transaction data. Licensed under MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages