logdb 0.6.0

Embedded, append-only, crash-recoverable, optionally tamper-proof local log database
Documentation
# Changelog

All notable changes to this project are documented in this file. The format
is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [0.6.0] — 2026-07-09

### Added

- **Key-based shard routing** (cr-037): `ShardMap::select_shard_by_key(key)` routes
  records by deterministic CRC32C hash — same key ⇒ same shard. `LogDb::append_with_key(content, shard_key)`. `ShardMap::claim_on_shard`.
- **`LogDb::scan_shard(shard, from_id, to_id)`** (cr-037 B): per-shard scan
  iterator for incremental seq-map checkpoint replay.

## [0.5.0] — 2026-07-08

### Added

- **`KeyRing`: rotation-capable encryption keys** (cr-032). Replaces the single
  `Config.encryption_key: Option<[u8; 32]>` with
  `Config.encryption_keys: Option<Arc<KeyRing>>`. A `KeyRing` carries an `active`
  key (encrypts new writes) plus a decrypt window of prior keys; reads try each
  until one authenticates (AES-256-GCM AEAD), so rotating the active key keeps
  older records readable with **no on-disk format change**. Construct with
  `KeyRing::single`, `KeyRing::new`, or `KeyRing::with_ids`.
- **Segment-header `encryption_key_id`** (cr-032 Phase 3): each segment records
  the 128-bit id of the key that encrypted it (in previously-reserved header
  bytes; no `FORMAT_VERSION` bump). Recovery uses it as an O(1) hint to unmask
  the hash-chain key, and operators can see which key a segment depends on
  (retirement metadata, backup manifests).

### Changed

- **Hash-chain key under encryption is now a stable secret, masked on disk**
  (cr-032 Phase 3). Previously (0.3.0) the chain key was derived from the active
  encryption key and the header stored zeros; rotating the key therefore severed
  the chain and made recovery truncate pre-rotation records (silent history loss).
  The chain key is now a random per-shard secret independent of the active key,
  stored masked (`header.hash_init = chain_key ⊕ derive(active)`) so it stays
  off-disk-plaintext, and recovery unmasks+verifies it. Rotating the active key
  no longer breaks the hash chain. (Encrypted + hash-chain was unreleased, so
  there is no migration story.)
- **BREAKING: `Config.encryption_key``Config.encryption_keys`** — callers must
  pass `KeyRing::single(..)` (or `KeyRing::new` / `with_ids`) instead of a raw
  `[u8; 32]`.

## [0.3.0] — 2026-07-01

### Changed (BREAKING)

- **`hash-chain` with an encryption key is now a real MAC.** When both
  `hash-chain` and `encryption` are enabled and an encryption key is set, the
  chain key is **derived from the encryption key** (domain-separated BLAKE3 KDF)
  instead of seeded from wall-clock time, and is **never written to disk** (the
  segment header stores zeros). An attacker who reads the segment file but lacks
  the key can no longer recompute the chain and forge a tail. Without an
  encryption key, behavior is unchanged (clock-seeded tamper-evidence). This
  changes the stored chain hashes for `hash-chain` + `encryption` segments (no
  migration; pre-release data only).
- **Migrated to Rust edition 2024** (`logdb` and `logdbd`). **MSRV is now
  `1.85`** (was `1.74` — the previous value was unachievable under
  `--all-features` because `blake3` 1.8 uses edition 2024). The migration is
  behavior-preserving (mechanical: `unsafe fn` bodies gain explicit `unsafe {}`
  blocks, `if let … else``match` to keep drop order, the recovery macro's
  fragment specifier pinned to `:expr_2021`).
- **`LogDb::drop` now performs a best-effort bounded drain** (≤5 s) of
  already-published records before aborting background threads, and emits a
  `tracing` warning if it cannot reach a clean state. Previously, dropping an
  unflushed `LogDb` silently lost in-flight records. Drop is a safety net, not a
  guarantee — call `shutdown()` / `drain()` for assured durability. Skipped
  during panic unwinding.
- **License is now `Apache-2.0`** (was `MIT OR Apache-2.0`). The `LICENSE` file
  already contained only the Apache-2.0 text; the crate `license` field and the
  README statements are updated to match. Downstream license-check tooling that
  keyed on the dual SPDX expression should be updated.
- `Tailer::next_batch` now returns `Result<Option<Vec<Record>>, TailerError>`
  instead of `Result<_, String>`. `TailerError` wraps the underlying `ReadError`
  (`#[from]`), so callers can forward it via `?`. Migration: callers that
  stringified the error (`.unwrap()` / `format!(..)`) are unaffected —
  `TailerError` implements `Display`.
- **Narrowed public API surface.** Implementation modules (`config`, `error`,
  `ring`, `storage`, `pipeline`, `health`, `platform`, `reader`, `record`,
  `shard`, `recovery`, `tailer`) are now `pub(crate)` — they are no longer part
  of the supported public API / semver surface. The stable types are re-exported
  at the crate root: use `logdb::Config`, `logdb::Record`, `logdb::ScanIter`,
  `logdb::Tailer`, `logdb::AppendError`, … instead of `logdb::config::Config`,
  `logdb::ring::Ring`, etc. Migration: change `logdb::<module>::<Type>` imports
  to the crate-root re-export. (Internal types like `Ring`, `SegmentManager`,
  `ShardMap` are no longer reachable from downstream crates.)
- `LogDb::open` now returns `Result<Self, OpenError>` instead of
  `Result<Self, String>`. `OpenError` is a structured enum
  (`InvalidConfig` / `Recovery` / `SegmentCreate` / `ThreadSpawn`) so callers
  can match on the failure category and forward it via `?`. Migration: replace
  any `String`-matching on `open`'s error with `OpenError` (`.unwrap()` /
  `.map_err(|e| format!(..))` callers are unaffected — `OpenError` implements
  `Display`).
- `Config::validate` now returns `Result<(), ConfigError>` instead of
  `Result<(), String>`, with one variant per constraint
  (`InvalidRingSize` / `InvalidShardCount` / `SegmentTooSmall` /
  `ContentTooLarge` / `ZeroIndexStride` / `HashChainRequiresSingleShard`).
- `append_batch(&[])` now returns `AppendError::EmptyBatch` instead of the
  misleading `AppendError::ContentTooLarge { size: 0, max: 0 }`.

### Fixed

- **Stale-manifest read miss after segment deletion.** `SegmentManifest` caches
  the segment directory listing and refreshes it only when the directory mtime
  changes. On filesystems with coarse mtime granularity (or under mtime
  propagation lag), a segment deleted by checkpoint truncation / retention
  could remain in the cache, so `read()` returned an entry pointing at a
  now-missing file — a transient read miss. This was the root cause of the
  intermittently-failing `checkpoint_truncation` test (which retried around it).
  `find()` now detects a cache-served entry whose file is gone, force-rescans,
  and re-looks-up. The fast path adds no overhead (freshly-scanned entries are
  trusted).

### Added

- `LogDb::read_batch(&[u64]) -> Result<Vec<Option<Record>>, ReadError>` — a
  multi-get. For clustered ids it is dramatically faster than N individual
  `read()`s: a single forward pass reads each record once (with OS read-ahead)
  instead of re-seeking and re-scanning to each id. Measured ~190× on a 20k-record
  batch (~3 µs/record vs ~600 µs). Result order matches `ids`; missing /
  not-yet-durable ids yield `None`.
- `OpenError`, `ConfigError`, and `AppendError::EmptyBatch` error types.
- `tracing` feature: an off-by-default feature that emits structured events
  (segment rolls, retention, recovery warnings, flush/drain timeouts, the
  best-effort drain on drop) via the `tracing` crate. Off by default, logdb
  stays zero-extra-dependency.
- `LogDb::health_code() -> Option<u8>` — exposes the internal health state
  for readiness probes. `None` when healthy; `Some(code)` when degraded
  (e.g. disk-full). Self-heals once the fs recovers.
- `metrics` feature: an off-by-default feature that emits quantitative metrics
  via the `metrics` facade crate — install a recorder (e.g. a Prometheus
  exporter) in the host to collect them. Emits `logdb.appends` (counter),
  `logdb.segment.rolls` (counter), `logdb.flush.duration` &
  `logdb.recovery.duration` (histograms), and `LogDb::record_gauges()` samples
  `logdb.durable_lag` / `logdb.queue_depth` / `logdb.wal_bytes` (gauges).
- **CI coverage job** (`cargo llvm-cov`, non-blocking) and new "Coverage"
  section in the Dev Guide testing docs. Qualification scripts and long-soak /
  long-fuzz documentation already exist in `scripts/run-all.sh`.
- **Async integration guide** ([`docs/en/usage/async.md`]docs/en/usage/async.md):
  documents that `LogDb` is `Send + Sync`, common hot-path calls (`append`,
  `read`, `flush`) are microseconds and safe to call inline from an async task,
  and `spawn_blocking` is only needed for `shutdown` / very large range scans.
  `logdbd` is the reference async example.
- `testing` feature: an off-by-default feature that re-exposes the internal
  modules as `#[doc(hidden)] pub`, for the deployed test binary
  (`examples/testsuite`) and the `tests/fuzz` integration target. Not a
  supported public API.
- Crate-root re-exports: `RecordId`, `ScanIter`, `Tailer`, and the shard-id
  helpers (`encode_record_id` / `decode_record_id` / `shard_bits`).
- **Security deliverables:** root [`SECURITY.md`]https://github.com/lv-agent/lv-logdb/blob/main/SECURITY.md
  (private vulnerability disclosure + SLA), a documented
  [threat model]docs/en/security/threat-model.md (what `encryption` /
  `hash-chain` protect against — and what they do **not**, including the
  caveat that the hash-chain key is derived from non-secret entropy and
  persisted in cleartext, so the chain is tamper-evidence not authenticity),
  and [key-management]docs/en/security/key-management.md guidance (CSPRNG
  generation, envelope encryption, rotation).
- **License compliance:** `cargo-deny` config (`deny.toml`) enforcing a
  permissive-only allow-list (no copyleft anywhere in the graph, enforced in
  CI) and a [`THIRDPARTY.md`]THIRDPARTY.md inventory of the 29 runtime
  dependencies.
- Cargo manifest metadata for crates.io publishing: `repository`, `homepage`,
  `documentation`, `readme`, `keywords`, `categories`, and `rust-version`
  (MSRV = 1.74, dictated by `thiserror` 2).
- `LICENSE` is now packaged inside the crate directory (it previously lived
  only at the workspace root, so `cargo package` omitted it).

### Known limitations (unchanged)

- `recovery::recover_shard` still returns `Result<_, String>` (18 internal
  error sites). It is captured by `OpenError::Recovery { shard, reason }`.
  Full structuring is deferred to a follow-up that also narrows the `recovery`
  module's visibility.

## [0.2.0] — 2026-06-30

First tagged release. logdb is an embedded, append-only, crash-recoverable,
optionally tamper-proof / compressed / encrypted / remotely-pushable local
log database.

### Added

- **Sharding (`Config.shards`, range `[1, 256]`)** — multiple independent
  rings for high-core-count write scalability. Fully production-ready as of
  0.2.0: append, point read, range scan / `replay_from`, named tailers,
  crash recovery, checkpoint truncation, and retention all work across
  shards (including non-power-of-two shard counts and segment rolls). Record
  ids are bit-packed: `global = (local_seq << shard_bits) | shard_id`,
  `shard_bits = ceil(log2(num_shards))`.
- **Features**: `hash-chain` (BLAKE3 keyed tamper-evident chain; requires
  `shards == 1`), `compression` (streaming zstd, per-frame), `encryption`
  (AES-256-GCM, per-frame), `remote-push` (async durable push; the `Pusher`
  itself is a private daemon-level building block — no public push API yet).
- **Tailers** — named consumers with independent, persisted, per-shard
  progress (`new_tailer`, `next_batch`, `commit`, `seek`, `reset`,
  `position`, `positions`). `next_batch` merges every shard's newly-durable
  records into one ascending-global-id batch (lossless; cross-batch ordering
  is best-effort, like Kafka per-partition offsets).
- **WAL checkpoint** — persistent, survives crash; old segments fully covered
  by the checkpoint are truncated on the next roll, per shard.
- **Crash recovery** — automatic on `open`: per-shard torn-write detection
  and truncation, optional hash-chain verification, sparse-index rebuild.
- **Diagnostics**`wal_usage()`, `ring_size()`, `recovery_report()`,
  `producer_cursor()` / `committed_cursor()` / `durable_cursor()`.
- **Scan throughput optimization** — raw-mode range scan reads in 64 KB
  windows instead of one syscall per record (~12× faster on small records:
  ~1100 ns/rec → ~90 ns/rec).
- Bilingual documentation (English + Chinese): 23-page Usage Guide and
  7-page Dev Guide, mirrored.

### On-disk layout

- `shards == 1`: segment files (`segment-NNNNNNNN.log`) live flat in
  `data_dir/`.
- `shards > 1`: each shard is an independent log under `data_dir/s<shard>/`.
- Sparse index sidecars (`segment-NNNNNNNN.idx`) for raw segments; frame
  layout (length-prefixed, optionally compressed+encrypted) for
  compressed/encrypted segments. Checkpoint and tailer progress are
  CRC-protected atomic-write files.

### Fixed (during release prep)

- Rolled segment headers under encryption were missing
  `FLAG_ENCRYPTED_AES256GCM`, so every segment after the first was read as
  raw — scan/replay/tailer ground through encrypted bytes byte-by-byte
  (pathologically slow) and dropped all post-roll records. Now set on both
  roll paths.
- `wal_usage()` and `recovery_report()` were broken under `shards > 1`
  (missed `s<shard>/` subdirs; mixed per-shard local cursors with global ids).
- Pre-allocated segment headers stored the local ring seq as `base_sequence`
  instead of the global id, breaking point reads / truncation / recovery
  after a roll under `shards > 1`.

### Known limitations

- `hash-chain` requires `shards == 1` (a global chain needs single-shard
  order); multi-shard hashing is deferred.
- The `Pusher` / remote push is single-shard and not wired to a public API;
  `shards > 1` + remote push is not supported yet (a public push API will
  make it cross-shard).
- `replicate` (standby write-in) requires `shards == 1`.

[0.3.0]: https://github.com/lv-agent/lv-logdb/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/lv-agent/lv-logdb/releases/tag/v0.2.0