#async #scenario #thread #deterministic #bug

parcheck

Test permutations of concurrent scenarios

2 releases

0.1.0-alpha.2 Jun 4, 2024
0.1.0-alpha.1 May 21, 2024

#254 in Concurrency

MIT license

47KB
1K SLoC

parcheck

parcheck is a concurrency testing tool for Rust. It helps uncover bugs by running testing scenarios with different thread/task interleavings.

Crates.io Documentation Build status

Usage

Currently in alpha version, interface will change soon, but the idea remains the same.

Add it to your dependencies:

# Cargo.toml
[dependencies]
parcheck = { version = "0.1.0-alpha.2", git = "https://github.com/stepantubanov/parcheck.git" }

[dev-dependencies]
parcheck = { version = "0.1.0-alpha.2", features = ["enable"], git = "https://github.com/stepantubanov/parcheck.git" }

Note that enable feature is enabled only in dev-dependecies. Without this feature calls to parcheck::task and parcheck::operation expand to underlying future without any additional logic, so effect on production code should be as minimal as possible.

In order for the code to be controlled, it has to be instrumented with calls to parcheck::task and parcheck::operation. "Tasks" are threads of execution (currently parcheck does not support intra-task concurrency, but that's coming soon) and "operations" are futures that parcheck will control. Specifically it'll execute operations in a deterministic linearized schedule (when there are multiple tasks running, only a single operation from one of the tasks is selected and allowed to execute according to currently selected schedule, then after it completes, next operation is selected, etc).

async fn handle_http_request() {
    parcheck::task!("name_of_this_request", {
        async {
            // ... code that isn't controlled by parcheck

            let value = parcheck::operation!("name_of_operation", {
                // ... perform an async operation: execute an SQL query, send a message, set/get
                // in redis, etc.
                async {}
            }).await;

            // ... some other code that isn't controlled

            parcheck::operation!("name_of_next_operation", {
                // ... another operation, that will be controlled by parcheck
                async {}
            }).await;
        }
    }).await;
}

We can test interleavings between two concurrent executions of this async function by adding a test case and running it via parcheck::runner.

#[tokio::test]
async fn test_concurrent_scenarios() {
    parcheck::runner()
        .run(["name_of_this_request", "name_of_this_request"], || async {
            join!(
                handle_http_request(),
                handle_http_request(),
            );

            // ... assert that state after 2 concurrent requests is as expected
        }).await;
}

Runner accepts names of tasks to test and starts to control their execution when they are started (parcheck::task called). This test will run 2 concurrent handle_http_request multiple times, each time sequence of operations will be different. If code panics under one of schedules, then parcheck will print that schedule and it can be used to reproduce it again.

Dependencies

~0–6MB
~23K SLoC