Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*
!Cargo.toml
!Cargo.lock
!rust-toolchain.toml
!src/
!src/**
!benches/
!benches/**
!LICENSE
96 changes: 95 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -27,8 +27,102 @@ jobs:
- run: cargo fmt --all -- --check
- run: cargo clippy --all-targets --all-features --locked --offline -- -D warnings
- run: cargo test --all-targets --all-features --locked --offline
- run: cargo test --doc --all-features --locked --offline
- run: cargo run --locked --offline -- --version
- name: Process crash recovery
env:
FTWDB_POWER_CUT_ITERATIONS: 32
FTWDB_POWER_CUT_SEED: 20260907
run: cargo test --test power_cut --locked --offline always_recovers_every_acknowledged_batch_after_sigkill -- --ignored
# The privileged Linux NBD smoke script stays outside this portable job.
- run: cargo build --manifest-path bench/sd-card-emulator/Cargo.toml --all-targets --locked --offline
- run: cargo fmt --manifest-path bench/sd-card-emulator/Cargo.toml -- --check
- run: cargo clippy --manifest-path bench/sd-card-emulator/Cargo.toml --all-targets --locked --offline -- -D warnings
- run: cargo test --manifest-path bench/sd-card-emulator/Cargo.toml --all-targets --locked --offline

packages:
uses: ./.github/workflows/packages.yml

dependencies:
name: Dependency advisories
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: 1.97.1
- run: cargo install cargo-audit --locked --version 0.22.2
- run: cargo audit --deny warnings
- run: cargo audit --deny warnings --file bench/sd-card-emulator/Cargo.lock

storage-faults:
name: Linux filesystem faults
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: 1.97.1
- name: Run the ext4 fault and recovery checks on a test NBD device
run: |
set -euo pipefail
# Keep the healthy card's cache/power-loss model and full workload.
# Artificial timing belongs in SD performance runs, not this gate.
python3 - <<'PY'
import json
from pathlib import Path
profiles = Path("bench/sd-card-emulator/profiles")
card = json.loads((profiles / "healthy.json").read_text())
fast = json.loads((profiles / "full-disk-64m.json").read_text())
card.update(name="ci-durability", read=fast["read"], write=fast["write"])
Path("bench-results").mkdir(exist_ok=True)
Path("bench-results/ci-profile.json").write_text(json.dumps(card, indent=2))
PY
sudo modprobe nbd nbds_max=1
test ! -e /sys/block/nbd0/pid
for scenario in smoke full-disk mid-commit; do
sudo env "PATH=$PATH" "CARGO_HOME=$HOME/.cargo" "RUSTUP_HOME=$HOME/.rustup" \
"FTW_NBD_OUTPUT=$PWD/bench-results/linux-nbd-smoke" \
"FTW_NBD_FULL_OUTPUT=$PWD/bench-results/linux-nbd-full-disk" \
"FTW_NBD_MID_OUTPUT=$PWD/bench-results/linux-nbd-mid-commit" \
"FTW_NBD_PROFILE=$PWD/bench-results/ci-profile.json" \
"FTW_SD_EMULATOR_COMMIT=$GITHUB_SHA" \
bash "bench/sd-card-emulator/linux-$scenario.sh"
done
- name: Collect reports without walking private database snapshots
if: always()
run: |
sudo env "FTW_REPORT_UID=$(id -u)" "FTW_REPORT_GID=$(id -g)" python3 - <<'PY'
import os
from pathlib import Path
import shutil
root = Path("bench-results")
owner = (int(os.environ["FTW_REPORT_UID"]), int(os.environ["FTW_REPORT_GID"]))
reports = root / "evidence"
reports.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chown(reports, *owner)
sources = list(root.glob("linux-nbd-*"))
for source in sources:
destination = reports / source.name
destination.mkdir(mode=0o700)
os.chown(destination, *owner)
# Only direct report files: never traverse store/backup trees.
for item in source.iterdir():
if item.is_file() and item.suffix in (".json", ".jsonl", ".txt", ".log", ".stdout", ".stderr"):
target = destination / item.name
shutil.copy2(item, target)
os.chown(target, *owner)
profile = root / "ci-profile.json"
if profile.is_file():
shutil.copy2(profile, reports / profile.name)
os.chown(reports / profile.name, *owner)
PY
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: linux-filesystem-faults
path: bench-results/evidence/
if-no-files-found: warn
retention-days: 14
45 changes: 45 additions & 0 deletions .github/workflows/packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Packages

on:
workflow_call:

permissions:
contents: read

jobs:
package:
name: package (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: 1.97.1
- name: Build Linux binaries on the supported Debian baseline
if: runner.os == 'Linux'
run: |
set -euo pipefail
docker build -t ftwdb-package .
bash packaging/check-container.sh ftwdb-package
container=$(docker create ftwdb-package)
trap 'docker rm -f "$container" >/dev/null' EXIT
mkdir -p target/release
docker cp "$container":/usr/local/bin/. target/release/
- name: Build macOS binaries
if: runner.os == 'macOS'
run: cargo build --release --locked --bins
- name: Pack and run all three archived binaries
run: python3 packaging/build-archive.py --require-clean
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-${{ matrix.os }}
path: |
dist/*.tar.gz
dist/*.sha256
if-no-files-found: error
retention-days: 14
42 changes: 6 additions & 36 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ concurrency:
jobs:
verify:
name: verify release
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: 1.97.1
Expand All @@ -27,6 +29,7 @@ jobs:
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$ ]]; then
echo "invalid release tag: ${tag}" >&2
exit 1
Expand Down Expand Up @@ -57,42 +60,8 @@ jobs:
- run: cargo package --locked --offline

build:
name: build (${{ matrix.os }})
needs: verify
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: 1.97.1
- run: cargo build --release --locked
- name: Build archive
id: archive
shell: bash
run: |
set -euo pipefail
host="$(rustc -vV | sed -n 's/^host: //p')"
name="ftw-${GITHUB_REF_NAME}-${host}"
mkdir -p "dist/${name}"
cp target/release/ftw README.md CHANGELOG.md LICENSE "dist/${name}/"
tar -C dist -czf "dist/${name}.tar.gz" "${name}"
(
cd dist
shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256"
)
echo "name=${name}" >> "${GITHUB_OUTPUT}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-${{ steps.archive.outputs.name }}
path: |
dist/${{ steps.archive.outputs.name }}.tar.gz
dist/${{ steps.archive.outputs.name }}.tar.gz.sha256
if-no-files-found: error
retention-days: 14
uses: ./.github/workflows/packages.yml

publish:
name: publish GitHub release
Expand All @@ -116,6 +85,7 @@ jobs:
run: |
set -euo pipefail
sort -k2 dist/*.sha256 > dist/SHA256SUMS
(cd dist && sha256sum -c SHA256SUMS)
git for-each-ref --format='%(contents)' \
"refs/tags/${GITHUB_REF_NAME}" > dist/RELEASE_NOTES.md
if [[ ! -s dist/RELEASE_NOTES.md ]]; then
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target/
/dist/
/bench/sd-card-emulator/target/
/bench-results/
__pycache__/
Expand Down
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,84 @@ steps in [docs/releases.md](docs/releases.md).

## [Unreleased]

## [0.1.0-alpha.2]

Candidate for bounded shadow collection alongside FTW beta. Not yet published.

### Added

- A 512 MiB sidecar store limit and a 512 MiB free-disk reserve reject new
writes before append. Exact stored retries still work at either limit;
health reports degraded state and the client receives a retryable error.
- Native Linux ARM64 and AMD64 archives now include all three tools, private
service examples, recovery docs, source identity, and per-binary checksums.
- A Debian 12 container runs as UID 100 and GID 101 with only a Unix socket.
- CI runs native ARM64 tests, container and archive checks, process crashes,
and Linux ext4/NBD full-disk and mid-commit recovery checks.

- Sealed raw segments are published through the store manifest and used on the
query path. `Store::seal_and_reclaim` rewrites `active.wlog` to catalog,
identity receipts, and the unsealed tail so reopen does not reload every
historical point.
- Salvage recovers sealed `.wseg` coverage and the live `active.wlog` tail
instead of refusing stores that have already sealed raw history. A missing
or unreadable sealed segment fails closed.
- Ordered ingress frames now store source, sequence, commit ID, and transaction
as one checked unit. Exact retries compare the original bytes and return the
original receipt across reopen.
- A bounded single-writer runtime separates request errors from storage faults
and tracks accepted and durable progress per source.
- A draft, hand-written metadata wire codec and local Unix shadow sidecar
carry catalog, run, plan, point, and outcome data without joining FTW's
control path.
- The sidecar checks each Unix peer's effective UID and drains the writer on
SIGTERM or SIGINT before it removes the socket and exits.
- Checked systemd and launchd examples keep the store and socket private and
give the sidecar time to finish a clean stop.
- An offline reconcile command compares exact source commit frames with stored
receipts, catalog state, and point bits and emits a bounded JSON summary.
- Reconciliation binds every receipt to its exact canonical payload, caps raw
points scanned before time filtering, and bounds regular-file inputs.
- A clean client EOF at a frame boundary no longer raises the sidecar's client
error count; partial frames still do.

### Limits of this candidate

- Collection keeps SQLite/Parquet authoritative and is opt-in. The sidecar
does not establish complete replication or serve production reads.
- Bounded collection runs without automatic rollups, sealing, or retention.
It stops at its storage limit. Preserve or replace its store as an operator
action; do not delete the current store while the sidecar runs.
- Physical SD-card power cuts, target-box soak, resource measurements, and an
off-card backup/restore drill still gate a standalone FTWDB beta release.
- Older binaries cannot open the new ingress or reclaimed receipt format.
Rollback needs a pre-upgrade snapshot or a fresh disposable shadow store.

### Fixed

- Reconciliation charges full overlapping blocks before decoding and stops at
its scan/output limits across sealed history and the live tail.
- Manifest v3 binds sealed raw file contents, counts, and time bounds. Open,
integrity checks, and salvage reject valid but unrelated replacements.
Open streams all sealed bytes for verification. Published alpha.1 stores
still load; unpublished v2 stores with sealed raw segments need a pre-seal
snapshot or a fresh shadow store.

- Postcard no longer enables an unused embedded heapless backend, removing
the archived atomic-polyfill dependency from the lockfile.

- Release publication now reads notes from the annotated tag through a file,
which works with an explicit GitHub repository target.
- A live or later-sealed correction now wins when all three time keys match an
older sealed point.
- Log reclaim sorts ingress receipts and retains their exact bytes. CRC32
equality alone can no longer accept a changed retry.
- This version still reads the old exact-receipt index, but older binaries
cannot read a store after the new writer reclaims it.
- Catalog compaction rejects run and plan cycles instead of writing a partial
catalog.
- Store paths, segment links, manifest order, reserved fields, rollup values,
and SD-card ACK evidence now fail on invalid input.

## [0.1.0-alpha.1] - 2026-07-21

Expand Down
Loading