Skip to content
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ jobs:
repo-token: ${{ github.token }}
- name: Test
run: cargo test --locked --all-features
- name: Build API documentation
env:
RUSTDOCFLAGS: "-D warnings"
run: cargo doc --locked --all-features --no-deps
- name: Check all targets
run: cargo check --locked --all-targets --all-features
- name: Format
Expand Down
76 changes: 49 additions & 27 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ Unirust is a distributed temporal entity resolution engine. The primary function

### Entity Resolution Must Always Happen

Every ingested record MUST go through entity resolution. Never skip or bypass:
- `linker.link_records_batch_parallel()` - batch links with parallel extraction
- `partitioned.process_batch_optimized()` - optimized partition processing
- `partitioned.ingest_batch()` - distributed batch processing
Every newly accepted record MUST complete entity resolution before its record data
commits. Valid retries of an already resolved source identity remain idempotent.
Preserve the linker calls in every ingest path, including:
- `Unirust::stream_records()` — persistent shard ingestion
- `StreamingLinker::link_records_batch_parallel_with_interner()` — parallel extraction and sequential linking
- `Partition::process_batch_optimized()` — the separate in-memory partition path

Any optimization that skips entity resolution is incorrect and breaks the core value proposition.

Expand Down Expand Up @@ -53,17 +55,23 @@ src/

## Key Entry Points

### Ingest Flow
1. `distributed.rs:ShardNode::ingest_records()` - gRPC entry
2. `distributed.rs:dispatch_ingest_partitioned()` - routes to partitioned processing
3. `partitioned.rs:ParallelPartitionedUnirust::ingest_batch_with_partitions()` - parallel partition dispatch
4. `partitioned.rs:Partition::process_batch_optimized()` - **hot path**: batch insert → parallel extract → sequential link
5. `linker.rs:link_records_batch_parallel()` - parallel key extraction, sequential DSU merges
### Persistent Ingest Flow
1. `distributed.rs:RouterService::ingest_records()` — placement and source-identity reservations
2. `distributed.rs:ShardNode::ingest_records()` — shard RPC and ingest WAL
3. `distributed.rs:dispatch_ingest_records()` — worker dispatch
4. `lib.rs:Unirust::stream_records()` — stage records, link, commit and flush
5. `linker.rs:StreamingLinker` — key extraction, temporal guards and DSU merges

Persistent shards disable partitioned ingestion. `dispatch_ingest_partitioned()`
and `ParallelPartitionedUnirust` use in-memory partition stores and must never
replace the persistent path.

### Query Flow
1. `distributed.rs:RouterService::query_entities()` - gRPC entry
2. `lib.rs:Unirust::query_master_entities()` - query execution
3. `query.rs` - query planning and execution
1. `distributed.rs:RouterService::query_entities()` — concurrent candidate discovery and canonical fragment hydration
2. `lib.rs:Unirust::query_master_entities()` — local candidates and golden descriptors
3. `query.rs` and `graph.rs` — temporal conjunction and mastering

See [DESIGN.md](DESIGN.md) for recovery, reconciliation and matching limits.

## Testing Strategy

Expand All @@ -78,19 +86,31 @@ src/
- Test distributed scenarios (router + shards)

### Load Testing
- Use `unirust_loadtest` binary
- Standard command: `./target/release/unirust_loadtest -r http://127.0.0.1:50060 -c 10000000 --streams 16 --batch 5000`
- Baseline with 5 shards, 10% overlap: **~410K rec/sec, ~12ms batch latency**

Build the load generator explicitly with `--features test-support` (see below).
Use fresh persistent directories and a fixed ontology, topology, seed and workload
for comparisons. Do not change the shard count of an existing dataset to run a
benchmark; durable reservations bind it to its topology.

## Performance Considerations

### Do Not Regress
After any change, verify performance with loadtest. Current baseline with 5 shards:
- **~410K records/second** (10% overlap)
- **~12ms batch latency**

For runtime changes, compare persistent load tests before and after the change,
including acknowledgement counts, failures, throughput and measured RPC latency.
Use focused diagnostics for query or recovery changes. Documentation-only edits
require checking their examples and claims rather than repeating throughput runs.

The September audit measured approximately 43,415 versus 43,939 records/second
on one local five-shard, one-million-record comparison (10% overlap); average RPC
latency was approximately 1,585 versus 1,657 ms. These are observations, not
performance guarantees. The old 410K records/second and 12 ms figures do not
establish a durable production baseline. Workload details and limitations are in
[the audit report](docs/critical-audit-2026-09-05.md).

### Hot Paths
- `partitioned.rs:process_batch_optimized()` - batch insert + parallel extract + sequential link
- `lib.rs:stream_records()` - persistent staging, resolution and commit
- `partitioned.rs:process_batch_optimized()` - separate in-memory partition processing
- `linker.rs:link_records_batch_parallel()` - parallel extraction, sequential DSU
- `linker.rs:link_extracted_record()` - DSU merges with temporal guards
- `dsu.rs:find()` - path compression with root cache
Expand Down Expand Up @@ -126,31 +146,33 @@ After any change, verify performance with loadtest. Current baseline with 5 shar

```bash
# Development
cargo test # Run all tests
cargo clippy --all-targets # Lint
cargo fmt # Format
cargo test --locked --all-features
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo fmt --check
cargo test --doc --locked --all-features

# Benchmarks
cargo bench --bench bench_quick # Fast (~30s)
cargo bench --bench bench_quick # Focused benchmark suite
cargo bench --bench bench_micro # Component benchmarks

# Start cluster (recommended)
# Build the load generator; run the cluster on a fresh benchmark dataset
cargo build --release --locked --features test-support --bin unirust_loadtest
SHARDS=5 ./scripts/cluster.sh start

# Load test (requires running cluster)
./target/release/unirust_loadtest \
--router http://127.0.0.1:50060 \
--count 10000000 \
--streams 16 \
--batch 5000
--batch 5000 --overlap 0.1 --seed 42

# Stop cluster
./scripts/cluster.sh stop
```

## Style Guidelines

- Use `Result<T, UniError>` for fallible operations
- Follow existing error types: `anyhow::Result` for library operations and `tonic::Status` at gRPC boundaries
- Prefer `&str` over `String` for parameters
- Use `#[inline]` for small hot functions
- Avoid `unwrap()` in library code
Expand Down
24 changes: 22 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
All notable changes to this project are documented here. This project follows
Semantic Versioning.

## [Unreleased]

### Fixed

- Authenticate protoc setup requests in CI to avoid unauthenticated GitHub API
rate limits (merged after the v0.2.0 tag).

### Documentation

- Align deployment, architecture, configuration and Rust API examples with the
persistent runtime; describe matching, memory and recovery limits explicitly.
- Correct load-test diagnostic guidance: `UNIRUST_PROFILE` selects a tuning
preset and does not enable a profiler.
- Build API documentation in CI with warnings treated as errors; compile the
crate quick-start example as a doctest.
- Remove unsupported GPU projections and performance guarantees, and distinguish
measured local diagnostics from production capacity claims.

## [0.2.0] - 2026-09-05

### Added
Expand Down Expand Up @@ -92,5 +110,7 @@ Semantic Versioning.
- Distributed integration tests now use temporary persistent shard stores; the
in-memory store remains limited to unit tests.
- Replaced the non-durable historical performance claim with a verified
five-shard, power-loss-durable baseline of 50,598 records/second for the
documented 10-million-record workload.
five-shard measurement reported at release preparation: 50,598 records/second
for a 10-million-record workload using synchronous durable ingestion. This
historical result is not a capacity guarantee or a physical power-cut test;
the later comparative audit used a separate one-million-record workload.
10 changes: 6 additions & 4 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Unirust Container Image
#
# Multi-stage build for efficient, minimal production image
# Multi-stage Rust build with a Debian slim runtime and CLI tools
#
# Usage:
# podman build -t unirust -f Containerfile .
# podman run --rm -p 50061:50061 -v unirust-data:/data -v unirust-backup:/backup unirust shard
# podman run --rm -p 50060:50060 unirust router --shards shard-0:50061
# podman run --rm -p 127.0.0.1:50061:50061 -v unirust-data:/data -v unirust-backup:/backup unirust
# The default command includes --data-dir /data and --backup-dir /backup.
# Supplying an explicit command replaces those defaults; pass both paths to shard.
#
# Or use with compose.yaml for full cluster deployment
# Use compose.yaml or scripts/podman_cluster.sh for a router/shard network.
# Separate named data/backup volumes can still share one host or physical disk.

FROM rust:1.88-bookworm AS builder

Expand Down
Loading
Loading