Skip to content

feat(c-sdk): optional version/size fast-path and concurrent block reads #750

feat(c-sdk): optional version/size fast-path and concurrent block reads

feat(c-sdk): optional version/size fast-path and concurrent block reads #750

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel superseded runs on the same ref (e.g. rapid pushes to a PR).
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Least-privilege default token.
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# Fail the build on any warning across the whole workspace.
RUSTFLAGS: "-D warnings"
RUSTDOCFLAGS: "-D warnings"
jobs:
fmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install toolchain (rustfmt)
run: rustup component add rustfmt
- name: Check formatting
run: cargo fmt --all --check
clippy:
name: clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install toolchain (clippy)
run: rustup component add clippy
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- uses: Swatinem/rust-cache@v2
- name: Lint
run: cargo clippy --workspace --all-targets --all-features
test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --workspace --all-targets --all-features --locked
- name: Test
run: cargo test --workspace --all-features --locked
fuse-mount:
name: fuse mount e2e
runs-on: ubuntu-latest
# The read-path unit/integration tests never touch the kernel (they use mock
# TCP servers). This job mounts a real TalonFuse on /dev/fuse and reads a
# file back through the kernel, asserting byte-exactness end to end — the one
# layer only a real mount can cover. GitHub's Linux runners provide an
# accessible /dev/fuse; macOS and restricted sandboxes do not, which is why
# the test is #[ignore]d and gated behind the `mount` feature by default.
env:
# Turn a missing/unusable /dev/fuse into a hard failure here, so this job
# can never pass without actually exercising the kernel mount path.
TALON_REQUIRE_FUSE: "1"
steps:
- uses: actions/checkout@v4
- name: Capture the FUSE environment
# /dev/fuse existing is necessary but not sufficient (#405). Capture
# everything that can make an unprivileged mount fail, so a failing run
# is diagnosable from its own log rather than from a second run.
run: |
if [ ! -e /dev/fuse ]; then
echo "::error::/dev/fuse is missing on this runner; the FUSE mount test cannot run"
exit 1
fi
echo "--- identity (root would bypass fusermount3 entirely) ---"
id
echo "--- /dev/fuse ---"
ls -l /dev/fuse
echo "--- fusermount3 on PATH, and every copy of it ---"
ls -l "$(command -v fusermount3)" 2>/dev/null || echo "(none on PATH)"
ls -l /usr/bin/fusermount3 /usr/local/bin/fusermount3 2>/dev/null || true
echo "--- /etc/fuse.conf ---"
cat /etc/fuse.conf 2>/dev/null || echo "(absent)"
echo "--- apparmor ---"
aa-status --enabled 2>/dev/null && echo "apparmor enabled" || echo "apparmor not enabled or unavailable"
- name: Ensure an unprivileged FUSE mount is possible
# Some runner images carry a second fusermount3 in /usr/local/bin, owned
# by the runner user and without the setuid bit, which shadows the
# system helper on PATH. An unprivileged process invoking it cannot
# mount, which is #405: every mount test then fails with EPERM in the
# same second while /dev/fuse looks perfectly healthy.
#
# Captured from a failing runner:
# -rwxrwxrwx 1 runner runner /usr/local/bin/fusermount3
# against a passing one:
# -rwsr-xr-x 1 root root /usr/bin/fusermount3
#
# Remove the shadow rather than chmod-ing it. Adding setuid to a
# runner-writable binary would be worse than the bug, and removing it
# keeps the job exercising the real system helper -- which is the only
# reason this test is worth running.
run: |
shadow=/usr/local/bin/fusermount3
if [ -e "$shadow" ] && [ ! -u "$shadow" ]; then
echo "removing non-setuid $shadow which shadows the system helper (#405)"
sudo rm -f "$shadow"
hash -r 2>/dev/null || true
fi
helper="$(command -v fusermount3 || true)"
if [ -z "$helper" ] || [ ! -u "$helper" ]; then
echo "::error::no setuid fusermount3 on PATH (found: ${helper:-none}); unprivileged mounts cannot work"
ls -l "$helper" 2>/dev/null || true
exit 1
fi
ls -l "$helper"
# pjdfstest mounts with allow_other (#419), which needs this enabled.
if ! grep -q '^user_allow_other' /etc/fuse.conf 2>/dev/null; then
echo user_allow_other | sudo tee -a /etc/fuse.conf >/dev/null
fi
grep '^user_allow_other' /etc/fuse.conf
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- uses: Swatinem/rust-cache@v2
- name: Run the real-kernel mount test
run: >-
cargo test -p talon-fuse --features mount --test mount_e2e --locked
-- --ignored --nocapture
docs:
name: doc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- uses: Swatinem/rust-cache@v2
- name: Build docs
run: cargo doc --workspace --no-deps --all-features
docs-book:
name: mdbook build
runs-on: ubuntu-latest
# Ensures the mdBook documentation site always builds. Deployment to GitHub
# Pages happens separately (docs.yml) only on merge to main.
env:
MDBOOK_VERSION: "0.4.52"
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Install mdBook
run: cargo install mdbook --version ${MDBOOK_VERSION} --locked
- name: Build the book
run: mdbook build docs/book
config-docs:
name: reference docs drift
runs-on: ubuntu-latest
# The configuration and REST API references are generated from the code /
# OpenAPI spec. Fail if a committed copy is stale so docs cannot drift.
steps:
- uses: actions/checkout@v4
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- uses: Swatinem/rust-cache@v2
- name: Configuration reference drift
run: |
cargo run -q -p talon-coordinator --features etcd,kubernetes \
--bin talon-gen-config-docs > /tmp/config-ref.md
if ! diff -u docs/reference/configuration.md /tmp/config-ref.md; then
echo "::error::docs/reference/configuration.md is stale; run 'just gen-config-docs'"
exit 1
fi
- name: REST API reference drift
run: |
cargo run -q -p talon-coordinator --bin talon-gen-api-docs > /tmp/api-ref.md
if ! diff -u docs/reference/rest-api.md /tmp/api-ref.md; then
echo "::error::docs/reference/rest-api.md is stale; run 'just gen-api-docs'"
exit 1
fi
docs-quality:
name: docs quality
runs-on: ubuntu-latest
# Documentation hygiene: spelling (typos) and links (lychee). Config lives
# in typos.toml and lychee.toml; run the same checks locally via
# `just spell` and `just linkcheck`.
steps:
- uses: actions/checkout@v4
- name: Spell check
uses: crate-ci/typos@master
- name: Link check
# Offline: validate internal / relative links only. External URLs are
# not fetched, so a slow or rate-limited third-party site (e.g. a
# timeout to etcd.io) can never turn the gate red on a false negative.
uses: lycheeverse/lychee-action@v2
with:
args: >-
--offline --no-progress
README.md DESIGN.md CONTRIBUTING.md BENCHMARKS.md
'docs/**/*.md' '.github/**/*.md'
fail: true
etcd-contract:
name: etcd backend contract
runs-on: ubuntu-latest
# Exercises the production etcd ClusterStateStore backend against a real
# ephemeral etcd, as required by the state-store contract. etcd is launched
# explicitly rather than as a service container because that image's
# healthcheck is unreliable on GitHub-hosted runners.
env:
ETCD_VERSION: v3.5.16
TALON_ETCD_TEST_ENDPOINT: 127.0.0.1:2379
steps:
- uses: actions/checkout@v4
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Start ephemeral etcd
run: |
curl -fsSL -o /tmp/etcd.tar.gz \
"https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-amd64.tar.gz"
tar xzf /tmp/etcd.tar.gz -C /tmp
ETCD_DIR="/tmp/etcd-${ETCD_VERSION}-linux-amd64"
"$ETCD_DIR/etcd" --data-dir /tmp/etcd-data \
--listen-client-urls http://127.0.0.1:2379 \
--advertise-client-urls http://127.0.0.1:2379 &
echo "ETCD_PID=$!" >> "$GITHUB_ENV"
for i in $(seq 1 30); do
if "$ETCD_DIR/etcdctl" --endpoints=127.0.0.1:2379 endpoint health; then
exit 0
fi
sleep 1
done
echo "etcd did not become healthy" >&2
exit 1
- uses: Swatinem/rust-cache@v2
- name: Run etcd backend contract tests
run: >-
cargo test -p talon-coordinator
--features etcd,state-store-testkit
--test etcd_contract --locked
- name: Run etcd metadata-store contract tests
# Same ephemeral etcd, different store. ADR 0003 §7 keeps TMS and
# ClusterStateStore as separate abstractions under separate prefixes,
# and one of these tests asserts the two cannot collide.
run: >-
cargo test -p talon-metadata
--features etcd
--test etcd_backend --locked
docker-build:
name: docker image build
runs-on: ubuntu-latest
# Smoke-build every container image on PRs (no push) so a broken Dockerfile
# fails CI. A merge to main runs the real, publishing build in
# release-images.yml instead — running both would double the most expensive
# job in the repo for no extra signal, so this job is PR-only.
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Validate docker-compose
run: |
docker compose config --quiet
docker compose --profile ha config --quiet
# The latency lab is a standalone stack; validate it parses too.
docker compose -f deploy/testenv/docker-compose.yml config --quiet
docker compose -f deploy/testenv/docker-compose.yml --profile tools config --quiet
# The builds read the main-seeded layer cache (written by
# release-images.yml on every merge) but do not write. This sacrifices
# layer reuse between pushes of one PR (entries written from a PR are
# only readable within that PR) to keep mode=max exports from churning
# the repository's 10GB Actions cache quota, which is already saturated
# by the rust-cache entries every other job depends on.
- name: Build coordinator image
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/coordinator.Dockerfile
push: false
cache-from: type=gha,scope=coordinator
- name: Build worker image
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/worker.Dockerfile
push: false
cache-from: type=gha,scope=worker
- name: Build fuse image
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/fuse.Dockerfile
push: false
cache-from: type=gha,scope=fuse
- name: Build gateway image
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/gateway.Dockerfile
push: false
cache-from: type=gha,scope=gateway
helm:
name: helm chart
runs-on: ubuntu-latest
# Lint and render the Talon chart across every state backend so a broken
# template or invalid values fails CI. No cluster is required — helm renders
# locally and we structurally validate the output.
steps:
- uses: actions/checkout@v4
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: v3.16.3
- name: Lint (all backends)
run: |
for be in memory kubernetes etcd; do
rp=3; [ "$be" = memory ] && rp=1
echo "::group::lint $be"
helm lint deploy/helm/talon --strict \
--set coordinator.backend="$be" --set coordinator.replicas="$rp"
echo "::endgroup::"
done
- name: Render (all backends) and validate YAML
run: |
for be in memory kubernetes etcd; do
rp=3; [ "$be" = memory ] && rp=1
echo "::group::template $be"
helm template t deploy/helm/talon \
--set coordinator.backend="$be" --set coordinator.replicas="$rp" \
| python3 -c "import sys,yaml; list(yaml.safe_load_all(sys.stdin))"
echo "::endgroup::"
done
- name: Reject invalid configurations
run: |
if helm template t deploy/helm/talon --set coordinator.backend=redis >/dev/null 2>&1; then
echo "::error::unknown backend should have failed"; exit 1
fi
if helm template t deploy/helm/talon \
--set coordinator.backend=memory --set coordinator.replicas=3 >/dev/null 2>&1; then
echo "::error::memory backend with HA should have failed"; exit 1
fi
s3-e2e:
name: s3 backend and gateway e2e (localstack)
runs-on: ubuntu-latest
# Exercises the real S3Backend (SigV4 signing + path-style endpoint) against
# LocalStack, reading bytes an object actually contains. The test skips when
# TALON_S3_TEST_ENDPOINT is unset; this job sets it after seeding the bucket.
services:
localstack:
image: localstack/localstack:3.8
ports:
- 4566:4566
env:
SERVICES: s3
options: >-
--health-cmd "curl -sf http://localhost:4566/_localstack/health || exit 1"
--health-interval 5s --health-timeout 5s --health-retries 20
env:
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
TALON_S3_TEST_ENDPOINT: http://127.0.0.1:4566
TALON_S3_TEST_BUCKET: talon-e2e
TALON_S3_TEST_KEY: e2e/object.bin
TALON_S3_TEST_REGION: us-east-1
TALON_S3_SDK_PYTHON: python
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Install S3 client libraries
run: python -m pip install boto3==1.40.0 minio==7.2.16
- name: Seed the bucket with a deterministic object
run: |
# 4096 bytes where byte i == i % 251 (matches the test's expectation).
python3 - <<'PY'
data = bytes((i % 251) for i in range(4096))
open("/tmp/object.bin", "wb").write(data)
PY
aws --endpoint-url http://127.0.0.1:4566 s3 mb s3://talon-e2e
aws --endpoint-url http://127.0.0.1:4566 s3 cp /tmp/object.bin s3://talon-e2e/e2e/object.bin
- uses: Swatinem/rust-cache@v2
- name: Run the S3 backend e2e test
run: cargo test -p talon-backend --test s3_e2e --locked -- --nocapture
- name: Run the S3 SDK gateway conformance test
run: cargo test -p talon-gateway --test s3_sdk_e2e --locked -- --nocapture
gcs-e2e:
name: gcs backend e2e (fake-gcs)
runs-on: ubuntu-latest
# Exercises the real GcsBackend (bearer auth + endpoint override) against
# fake-gcs-server, reading bytes an object actually contains. The test skips
# when TALON_GCS_TEST_ENDPOINT is unset; this job sets it after seeding.
# fake-gcs is launched explicitly (docker run) rather than as a service
# container so we can pass `-scheme http` for a plaintext endpoint.
env:
TALON_GCS_TEST_ENDPOINT: http://127.0.0.1:4443
TALON_GCS_TEST_BUCKET: talon-e2e
TALON_GCS_TEST_KEY: e2e/object.bin
steps:
- uses: actions/checkout@v4
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Start fake-gcs-server and seed a deterministic object
run: |
docker run -d --name fake-gcs -p 4443:4443 \
fsouza/fake-gcs-server:1.49 \
-scheme http -port 4443 -public-host 127.0.0.1:4443
# Wait for it to answer.
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:4443/storage/v1/b >/dev/null 2>&1; then break; fi
sleep 1
done
# 4096 bytes where byte i == i % 251 (matches the test's expectation).
python3 -c "open('/tmp/object.bin','wb').write(bytes(i%251 for i in range(4096)))"
curl -sf -X POST "http://127.0.0.1:4443/storage/v1/b?project=talon" \
-H 'Content-Type: application/json' -d '{"name":"talon-e2e"}' || true
curl -sf -X POST \
"http://127.0.0.1:4443/upload/storage/v1/b/talon-e2e/o?uploadType=media&name=e2e/object.bin" \
-H 'Content-Type: application/octet-stream' \
--data-binary @/tmp/object.bin
- uses: Swatinem/rust-cache@v2
- name: Run the GCS e2e test
run: cargo test -p talon-backend --test gcs_e2e --locked -- --nocapture
azure-e2e:
name: azure backend and gateway e2e (azurite)
runs-on: ubuntu-latest
# Exercises the real AzureBackend (Shared Key signing + path-style endpoint)
# against Azurite, reading bytes an object actually contains. The test skips
# when TALON_AZURE_TEST_ENDPOINT is unset; this job sets it after seeding.
# Azurite is launched explicitly (docker run) so we can pass
# --skipApiVersionCheck: the runner's az CLI sends a newer API version than
# the pinned Azurite supports, which is only relevant to seeding.
env:
TALON_AZURE_TEST_ENDPOINT: http://127.0.0.1:10000
TALON_AZURE_TEST_CONTAINER: talon-e2e
TALON_AZURE_TEST_KEY: e2e/object.bin
TALON_AZURE_SDK_PYTHON: python
# Azurite's well-known dev connection string (public emulator values).
AZURE_STORAGE_CONNECTION_STRING: "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Install the official Azure Storage SDK
run: python -m pip install azure-storage-blob==12.26.0
- name: Start Azurite and seed a deterministic object
run: |
docker run -d --name azurite -p 10000:10000 \
mcr.microsoft.com/azure-storage/azurite:3.33.0 \
azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck
# Wait for the blob endpoint to answer.
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:10000/devstoreaccount1 >/dev/null 2>&1; then break; fi
sleep 1
done
# 4096 bytes where byte i == i % 251 (matches the test's expectation).
python3 -c "open('/tmp/object.bin','wb').write(bytes(i%251 for i in range(4096)))"
az storage container create --name talon-e2e
az storage blob upload --container-name talon-e2e \
--name e2e/object.bin --file /tmp/object.bin --overwrite
- uses: Swatinem/rust-cache@v2
- name: Run the Azure backend e2e test
run: cargo test -p talon-backend --test azure_e2e --locked -- --nocapture
- name: Run the Azure SDK gateway conformance test
run: cargo test -p talon-gateway --test azure_sdk_e2e --locked -- --nocapture
python-client:
name: python client
runs-on: ubuntu-latest
# Builds the wheel and drives it against a real coordinator, worker, and
# blob origin. Mocks would only assert that the mocks behave as written; the
# value of the binding is that it exercises the real protocol path.
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install build and test tooling
run: |
# protoc compiles talon-worker's WAL record schema and the etcd
# client's generated code; both binaries below need it.
sudo apt-get update && sudo apt-get install -y protobuf-compiler
python -m pip install --upgrade maturin pytest
- name: Build the release binaries the tests drive
run: |
cargo build --release --locked \
-p talon-worker --bin talon-worker \
-p talon-coordinator --bin talon-coordinator
- name: Build and install the wheel
run: |
maturin build --release --manifest-path clients/python/Cargo.toml --out dist
python -m pip install --force-reinstall dist/*.whl
- name: Run the client tests
run: python -m pytest clients/python/tests/ -v
java-client:
name: java client
runs-on: ubuntu-latest
# Two suites: conformance vectors, which guard the hand-written codec
# against drift from the Rust implementation, and an end-to-end read against
# a live cluster. The first is what makes a second protocol implementation
# maintainable; the second proves it actually reads bytes.
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-java@v4
with:
distribution: temurin
# Matches the pom's maven.compiler.release, so a source-level feature
# newer than the declared minimum fails here rather than at a user.
java-version: "17"
- name: Install protoc
# java_client_e2e.sh builds talon-worker, whose build script compiles
# the WAL record schema.
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Run conformance vectors and the end-to-end suite
run: scripts/java_client_e2e.sh
bench:
name: bench (informational)
runs-on: ubuntu-latest
# Never blocks a merge: shared runners are too noisy for absolute-time
# gating. This posts the baseline diff for visibility only.
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Benchmark check vs committed baseline
id: bench
run: |
python3 scripts/bench.py check main --soft | tee bench-report.md
- name: Publish report to job summary
if: always()
run: cat bench-report.md >> "$GITHUB_STEP_SUMMARY"
coverage:
name: coverage (informational)
runs-on: ubuntu-latest
# Informational. The workspace sits around 81% line coverage; the value of
# this job is making the *per-file* numbers visible so gaps get noticed
# (a new module landing at 0%, a rewrite dropping a file to half), not
# gating on a single aggregate. A threshold can follow once there is a
# baseline to argue from.
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Install toolchain (llvm-tools)
run: rustup component add llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Measure coverage
run: cargo llvm-cov --workspace --all-features --summary-only | tee coverage.txt
- name: Publish report to job summary
if: always()
run: |
{
echo '### Coverage'
echo '```'
cat coverage.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
audit:
name: dependency advisories
runs-on: ubuntu-latest
# Blocking, unlike coverage: this has a crisp pass/fail. A crate in the tree
# either carries an unacknowledged RUSTSEC advisory or it does not. Known
# ones are acknowledged with a written justification in deny.toml.
#
# A nightly run lives in audit-schedule.yml: an advisory can be published
# against an already-merged crate, so the check needs a heartbeat that does
# not depend on someone opening a PR.
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@cargo-deny
- name: Check advisories, bans, and sources
run: cargo deny check advisories bans sources