diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed088a6f6..0f45173e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -184,6 +184,39 @@ Once Maturin is installed, you can build the crate and install it as a Python mo cd bindings/python && maturin develop ``` +## Antithesis + +Antithesis is a testing platform for finding bugs with reproducibility. In +Limbo, we use Antithesis in addition to our own deterministic simulation +testing (DST) tool for the following: + +- Discovering bugs that the DST did not catch (and improve the DST) +- Discovering bugs that the DST does not cover (for example, non-simulated I/O) + +If you have an Antithesis account, you first need to configure some +environment variables: + +```bash +export ANTITHESIS_USER= +export ANTITHESIS_TENANT= +export ANTITHESIS_PASSWD= +export ANTITHESIS_DOCKER_HOST= +export ANTITHESIS_DOCKER_REPO= +export ANTITHESIS_EMAIL= +``` + +You can then publish a new Antithesis workflow with: + +```bash +scripts/antithesis/publish-workload.sh +``` + +And launch an Antithesis test run with: + +```bash +scripts/antithesis/launch.sh +``` + ## Adding Third Party Dependencies When you want to add third party dependencies, please follow these steps: diff --git a/Cargo.lock b/Cargo.lock index c5af95f10..c05efcd4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,22 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "antithesis_sdk" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201eba73b76341631014baf9c0018e703af204a1e0f15d7664d8a0947f6be74d" +dependencies = [ + "libc", + "libloading", + "linkme", + "once_cell", + "rand 0.8.5", + "rustc_version_runtime", + "serde", + "serde_json", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -1765,6 +1781,17 @@ dependencies = [ "uncased", ] +[[package]] +name = "limbo_stress" +version = "0.0.15" +dependencies = [ + "antithesis_sdk", + "clap", + "limbo", + "serde_json", + "tokio", +] + [[package]] name = "limbo_time" version = "0.0.15" @@ -1786,6 +1813,26 @@ dependencies = [ "uuid", ] +[[package]] +name = "linkme" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2654,6 +2701,16 @@ dependencies = [ "semver", ] +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + [[package]] name = "rustix" version = "0.38.43" @@ -2745,9 +2802,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "indexmap", "itoa", diff --git a/Cargo.toml b/Cargo.toml index 425dc07cb..52eadf082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "macros", "simulator", "sqlite3", + "stress", "tests", ] exclude = ["perf/latency/limbo"] diff --git a/Dockerfile.antithesis b/Dockerfile.antithesis new file mode 100644 index 000000000..056ad0947 --- /dev/null +++ b/Dockerfile.antithesis @@ -0,0 +1,73 @@ +FROM lukemathwalker/cargo-chef:0.1.68-rust-1.84.0-slim-bullseye AS chef +RUN apt update \ + && apt install -y git libssl-dev pkg-config\ + && apt clean \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app + +# +# Cache dependencies +# + +FROM chef AS planner +COPY ./Cargo.lock ./Cargo.lock +COPY ./Cargo.toml ./Cargo.toml +COPY ./bindings/go ./bindings/go/ +COPY ./bindings/java ./bindings/java/ +COPY ./bindings/python ./bindings/python/ +COPY ./bindings/rust ./bindings/rust/ +COPY ./bindings/wasm ./bindings/wasm/ +COPY ./cli ./cli/ +COPY ./core ./core/ +COPY ./extensions ./extensions/ +COPY ./macros ./macros/ +COPY ./simulator ./simulator/ +COPY ./sqlite3 ./sqlite3/ +COPY ./tests ./tests/ +COPY ./stress ./stress/ +COPY ./vendored ./vendored/ +RUN cargo chef prepare --bin limbo_stress --recipe-path recipe.json + +# +# Build the project. +# + +FROM chef AS builder + +ARG antithesis=true + +# Source: https://antithesis.com/assets/instrumentation/libvoidstar.so +COPY stress/libvoidstar.so /opt/antithesis/libvoidstar.so + +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --bin limbo_stress --release --recipe-path recipe.json +COPY --from=planner /app/bindings/rust ./bindings/rust/ +COPY --from=planner /app/core ./core/ +COPY --from=planner /app/extensions ./extensions/ +COPY --from=planner /app/macros ./macros/ +COPY --from=planner /app/stress ./stress/ +COPY --from=planner /app/vendored ./vendored/ + +RUN if [ "$antithesis" = "true" ]; then \ + cp /opt/antithesis/libvoidstar.so /usr/lib/libvoidstar.so && \ + export RUSTFLAGS="-Ccodegen-units=1 -Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=3 -Cllvm-args=-sanitizer-coverage-trace-pc-guard -Clink-args=-Wl,--build-id -L/usr/lib/ -lvoidstar" && \ + cargo build --bin limbo_stress --release; \ + else \ + cargo build --bin limbo_stress --release; \ + fi + +# +# The final image. +# + +FROM debian:bullseye-slim AS runtime +RUN apt-get update && apt-get install -y bash && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +EXPOSE 8080 +COPY --from=builder /usr/lib/libvoidstar.so* /usr/lib/ +COPY --from=builder /app/target/release/limbo_stress /bin/limbo_stress +COPY stress/docker-entrypoint.sh /bin +RUN chmod +x /bin/docker-entrypoint.sh +ENTRYPOINT ["/bin/docker-entrypoint.sh"] +CMD ["/bin/limbo_stress"] diff --git a/scripts/antithesis/launch.sh b/scripts/antithesis/launch.sh new file mode 100755 index 000000000..aab84b649 --- /dev/null +++ b/scripts/antithesis/launch.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +curl --fail -u "$ANTITHESIS_USER:$ANTITHESIS_PASSWD" \ + -X POST https://$ANTITHESIS_TENANT.antithesis.com/api/v1/launch/basic_test \ + -d "{\"params\": { \"antithesis.description\":\"basic_test on main\", + \"antithesis.duration\":\"1\", + \"antithesis.config_image\":\"$ANTITHESIS_DOCKER_REPO/limbo-config:antithesis-latest\", + \"antithesis.images\":\"$ANTITHESIS_DOCKER_REPO/limbo-workload:antithesis-latest\", + \"antithesis.report.recipients\":\"$ANTITHESIS_EMAIL\" + } }" diff --git a/scripts/antithesis/publish-config.sh b/scripts/antithesis/publish-config.sh new file mode 100755 index 000000000..e90cb73e6 --- /dev/null +++ b/scripts/antithesis/publish-config.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +export DOCKER_REPO_URL=$ANTITHESIS_DOCKER_REPO + +export IMAGE_NAME=limbo-config + +export DOCKER_IMAGE_VERSION=antithesis-latest + +export DOCKER_BUILD_ARGS="--build-arg antithesis=true" + +export DOCKERFILE=stress/Dockerfile.antithesis-config + +export DOCKER_DIR=stress + +cat turso.key.json | docker login -u _json_key https://$ANTITHESIS_DOCKER_HOST --password-stdin + +${BASH_SOURCE%/*}/publish-docker.sh diff --git a/scripts/antithesis/publish-docker.sh b/scripts/antithesis/publish-docker.sh new file mode 100755 index 000000000..74de3981b --- /dev/null +++ b/scripts/antithesis/publish-docker.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +if [ -z "$DOCKER_REPO_URL" ]; then + echo "Error: DOCKER_REPO_URL is not set." + exit 1 +fi + +if [ -z "$IMAGE_NAME" ]; then + echo "Error: IMAGE_NAME is not set." + exit 1 +fi + +if [ -z "$DOCKER_IMAGE_VERSION" ]; then + DOCKER_IMAGE_VERSION=$(git rev-parse HEAD) +fi + +DOCKER_IMAGE=$DOCKER_REPO_URL/$IMAGE_NAME:$DOCKER_IMAGE_VERSION + +docker build -f $DOCKERFILE -t $DOCKER_IMAGE $DOCKER_BUILD_ARGS $DOCKER_DIR + +docker push $DOCKER_IMAGE diff --git a/scripts/antithesis/publish-workload.sh b/scripts/antithesis/publish-workload.sh new file mode 100755 index 000000000..62f6ffd92 --- /dev/null +++ b/scripts/antithesis/publish-workload.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -e + +export DOCKER_REPO_URL=$ANTITHESIS_DOCKER_REPO + +export IMAGE_NAME=limbo-workload + +export DOCKER_IMAGE_VERSION=antithesis-latest + +export DOCKER_BUILD_ARGS="--build-arg antithesis=true" + +export DOCKERFILE=Dockerfile.antithesis + +export DOCKER_DIR=. + +cat turso.key.json | docker login -u _json_key https://$ANTITHESIS_DOCKER_HOST --password-stdin + +${BASH_SOURCE%/*}/publish-docker.sh diff --git a/stress/Cargo.toml b/stress/Cargo.toml new file mode 100644 index 000000000..59c4e8256 --- /dev/null +++ b/stress/Cargo.toml @@ -0,0 +1,22 @@ +# Copyright 2025 the Limbo authors. All rights reserved. MIT license. + +[package] +name = "limbo_stress" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "The Limbo stress tester" +publish = false + +[[bin]] +name = "limbo_stress" +path = "main.rs" + +[dependencies] +antithesis_sdk = "0.2.5" +clap = { version = "4.5", features = ["derive"] } +limbo = { path = "../bindings/rust" } +serde_json = "1.0.139" +tokio = { version = "1.29.1", features = ["full"] } diff --git a/stress/Dockerfile.antithesis-config b/stress/Dockerfile.antithesis-config new file mode 100644 index 000000000..87e723cdb --- /dev/null +++ b/stress/Dockerfile.antithesis-config @@ -0,0 +1,2 @@ +FROM scratch +COPY docker-compose.yaml docker-compose.yaml diff --git a/stress/docker-compose.yaml b/stress/docker-compose.yaml new file mode 100644 index 000000000..13b1149a8 --- /dev/null +++ b/stress/docker-compose.yaml @@ -0,0 +1,4 @@ +services: + workload: + image: us-central1-docker.pkg.dev/molten-verve-216720/turso-repository/limbo-workload:antithesis-latest + command: [ "/bin/limbo_stress" ] diff --git a/stress/docker-entrypoint.sh b/stress/docker-entrypoint.sh new file mode 100644 index 000000000..a09822694 --- /dev/null +++ b/stress/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -Eeuo pipefail + +exec "$@" diff --git a/stress/libvoidstar.so b/stress/libvoidstar.so new file mode 100644 index 000000000..84a597b61 Binary files /dev/null and b/stress/libvoidstar.so differ diff --git a/stress/main.rs b/stress/main.rs new file mode 100644 index 000000000..c62714e63 --- /dev/null +++ b/stress/main.rs @@ -0,0 +1,44 @@ +mod opts; + +use antithesis_sdk::*; +use clap::Parser; +use limbo::{Builder, Value}; +use opts::Opts; +use serde_json::json; +use std::sync::Arc; + +#[tokio::main] +async fn main() { + let (num_nodes, main_id) = (1, "n-001"); + let startup_data = json!({ + "num_nodes": num_nodes, + "main_node_id": main_id, + }); + lifecycle::setup_complete(&startup_data); + antithesis_init(); + + let opts = Opts::parse(); + let mut handles = Vec::new(); + + for _ in 0..opts.nr_threads { + // TODO: share the database between threads + let db = Arc::new(Builder::new_local(":memory:").build().await.unwrap()); + let nr_iterations = opts.nr_iterations; + let db = db.clone(); + let handle = tokio::spawn(async move { + let conn = db.connect().unwrap(); + + for _ in 0..nr_iterations { + let mut rows = conn.query("select 1", ()).await.unwrap(); + let row = rows.next().await.unwrap().unwrap(); + let value = row.get_value(0).unwrap(); + assert_always!(matches!(value, Value::Integer(1)), "value is incorrect"); + } + }); + handles.push(handle); + } + for handle in handles { + handle.await.unwrap(); + } + println!("Done."); +} diff --git a/stress/opts.rs b/stress/opts.rs new file mode 100644 index 000000000..392d79448 --- /dev/null +++ b/stress/opts.rs @@ -0,0 +1,16 @@ +use clap::{command, Parser}; + +#[derive(Parser)] +#[command(name = "limbo_stress")] +#[command(author, version, about, long_about = None)] +pub struct Opts { + #[clap(short = 't', long, help = "the number of threads", default_value_t = 8)] + pub nr_threads: usize, + #[clap( + short = 'i', + long, + help = "the number of iterations", + default_value_t = 1000 + )] + pub nr_iterations: usize, +}