Rust SDK
The Rust SDK uses memmap2::Mmap for read-only OS-level memory mapping and arc_swap::ArcSwap for lock-free hot-swapping. A call to AttosClient::read() acquires a ~1 ns epoch guard; the underlying mmap is unmapped only after all guards are dropped, providing RCU-safe concurrent reads.
Installation
Requires Rust 1.75+. Add to Cargo.toml:
[dependencies]
attos-client = { git = "https://github.com/Attos-Labs/attos-rust" }Configuration & Initialisation
OpenConfig is a Copy struct stored inside AttosClient and reapplied on every reload() call.
| Field | Type | Default | Effect |
|---|---|---|---|
| validate_checksum | bool | false | Verifies CRC32-IEEE trailer on open. |
| pin_to_memory | bool | false | Calls libc::mlock(2); prevents page-out. Fatal error on failure. |
| poll_interval | Option<Duration> | None | Reserved for future async sync loop; currently documents intent. |
use attos_client::{AttosMap, AttosClient, OpenConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = OpenConfig {
validate_checksum: true,
pin_to_memory: true, 500">// calls libc::mlock; fatal if it fails
poll_interval: None,
};
let map = AttosMap::open_with_config("data.attos", config)?;
let client = AttosClient::new(map, config);
500">// client is Clone + Send + Sync — share freely across threads
Ok(())
}Core API — Lookups
AttosClient::read() returns an arc_swap::Guard<Arc<AttosMap>>. Dereference it to call lookup methods. Allocation profile: 0 B/op.
Single key lookup
let guard = client.read(); 500">// ~1 ns, no lock, no alloc
500">// get() — raw bytes
match guard.get(b"user-42") {
Ok(value) => println!("{}", std::str::from_utf8(value)?),
Err(attos_client::AttosError::KeyNotFound) => { /* absent */ }
Err(e) => return Err(e.into()),
}
500">// get_string() — zero-copy &str
let s: &str = guard.get_string("user-42")?;
500">// s is valid until guard is droppedMulti-dimensional lookup
Factors are concatenated with \x00 into a 1 KiB stack buffer — zero heap allocation. Factors must be ordered alphabetically by column name (matching the builder).
500">// Composite key: region user_tier → backend config
let value = guard.multi_lookup(&[
b"us-east-1",
b"enterprise",
])?;Full scan
guard.scan(|key, value| {
println!("{} → {}", String::from_utf8_lossy(key), String::from_utf8_lossy(value));
true 500">// return false to halt early
});Hot-Swap Lifecycle
AttosClient::reload(path) opens the new file, validates it (per stored config), then calls ArcSwap::store(Arc::new(new_map)). The previous Arc<AttosMap> is reference-counted: its Drop fires only when all concurrent Guard handles are released, at which point munlock + munmap are called automatically. Readers are never blocked.
500">// Triggered by a file-watcher, webhook, or periodic task:
client.reload("data.v2.attos")?;
500">// Existing guards keep the old map alive; new reads see the updated map immediatelyError Handling
All fallible methods return Result<T, AttosError>. Match on the enum variant directly — never on the Display string.
| Variant | Trigger condition |
|---|---|
| AttosError::KeyNotFound | Hash slot found but stored key bytes do not match |
| AttosError::InvalidMagic | First 4 bytes ≠ b"ATTO" |
| AttosError::InvalidVersion(v) | Version field ≠ 1 |
| AttosError::ChecksumMismatch { expected, actual } | CRC32 trailer mismatch (validate_checksum = true) |
| AttosError::CorruptedBinary(msg) | File too small, offset OOB, bad key_len prefix |
| AttosError::MapClosed | Lookup called on a closed map instance |
| AttosError::MlockFailed(io::Error) | libc::mlock(2) returned non-zero |
| AttosError::IoError(io::Error) | File open or mmap syscall failed |
| AttosError::Utf8Error(_) | get_string() called on non-UTF-8 value bytes |
Plan limits
When a sync hits a plan limit (for example more than 3 active nodes or 10 GB of bandwidth a month on Free), the control plane answers 402 (or 429) with a JSON body whose code says which limit. The SDK keeps serving the data it already has, pauses syncing for 5 minutes (or the Retry-After seconds, capped at 1 hour) and then retries; lookups are never affected. See Plans, limits & error codes for every code and how to fix it.
- A node is one running SDK, identified by its node ID. It counts as active while it has synced in the last 5 minutes (SDKs check for updates every 30 seconds), so a stopped server drops out within 5 minutes.
- The node ID defaults to the server's hostname, so a restarted process is still one node. Set
ATTOS_NODE_ID(or the SDK's node ID option) to choose it yourself; processes sharing a hostname count as one node. - Paid plans bill extra nodes on the monthly average: the node count is recorded every hour and the month's average (rounded) is compared with the included nodes (50 on Pro, 10 on Starter) when the month closes, at $2 per extra node on Pro ($3 on Starter, where node overage is off unless you switch it on). The peak is shown for information only.
- Example: a rolling deploy of 50 pods briefly shows about 100 nodes while old and new pods overlap, but only for a few minutes, so the month still averages about 50 and nothing extra is billed.
- Past the included nodes: with overage on, extra nodes keep syncing and are billed; with it off (and on Free, capped at 3), a new node gets
402and keeps serving whatever data it already has, while nodes already syncing are unaffected.
Benchmarks
The Rust SDK targets parity with the Go SDK. Criterion benchmark on linux/amd64, 11th Gen Intel Core i5-1155G7 @ 2.50 GHz:
$ cargo bench
lookup/get time: [24.1 ns 24.5 ns 25.0 ns]
lookup/get_string time: [24.3 ns 24.7 ns 25.2 ns]
lookup/multi_lookup time: [26.1 ns 26.8 ns 27.4 ns]
alloc: 0 B/op (verified by allocation-counter)