Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
89c90f5
docs: add durable log queue design plan
iamd3vil Jul 21, 2026
bb0ee61
feat(smtp): add transient SmtpError variant answered as 452
iamd3vil Jul 21, 2026
d13de8f
feat(metrics): register log-queue metrics
iamd3vil Jul 21, 2026
82c0ffc
feat(logqueue): durable segmented append-log queue engine
iamd3vil Jul 21, 2026
feb11c6
feat(worker): claim-based delivery path for the log queue
iamd3vil Jul 21, 2026
0aadb63
feat(config): add [queue] section for the log-queue backend
iamd3vil Jul 21, 2026
1b13aec
feat(server): log-queue backend selection and SMTP ack cutover
iamd3vil Jul 21, 2026
966dcea
feat(cli): queue inspection and legacy-spool migration commands
iamd3vil Jul 21, 2026
9dab82f
docs(storage): document the log-queue backend
iamd3vil Jul 21, 2026
b31fd24
fix(logqueue): sweep fully dead segments the deletion event missed
iamd3vil Jul 21, 2026
9fabd6f
docs: add ARCHITECTURE.md for the log-queue design
iamd3vil Jul 21, 2026
113652e
docs(site): update architecture page for the log queue
iamd3vil Jul 21, 2026
2cfb07a
feat(storage)!: remove the SQLite storage backend
iamd3vil Jul 21, 2026
e11cf90
feat(config): default storage_type to "log" and warn on unmigrated sp…
iamd3vil Jul 21, 2026
237fdb5
test(metrics): tolerate concurrent global-counter mutation
iamd3vil Jul 21, 2026
45e7492
docs: move PLAN.md into docs/plans/
iamd3vil Jul 21, 2026
0b21059
chore: retire stale scaffolding allows and dead code
iamd3vil Jul 21, 2026
580086c
chore: eliminate every dead_code allow
iamd3vil Jul 21, 2026
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
495 changes: 495 additions & 0 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

437 changes: 4 additions & 433 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion config.example.huml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ storage::
bounced_retention: "7d"
deferred_retention: "2d"
interval: "1h"
storage_type: "fs"
storage_type: "log"

filters::
- ::
Expand All @@ -66,3 +66,13 @@ filters::
log::
format: "fmt"
level: "info"

# Optional durable append-log mail queue (not yet wired into the serving path)
# queue::
# append_writers: 1 # Number of shards / concurrent append writers (default: 1)
# pending_append_bytes: 134217728 # Pending (not-yet-durable) append bytes before backpressure (default: 128 MiB)
# segment_target_bytes: 67108864 # Target size of each active segment file, per shard (default: 64 MiB)
# compaction_dead_ratio: 0.50 # Dead-byte fraction in a sealed segment that makes it compaction-eligible (default: 0.50)
# compaction_min_age: "60s" # Minimum age of a sealed segment before compaction eligibility (default: 60s)
# disk_reserve_bytes: 1073741824 # Minimum free disk space required to accept new mail (default: 1 GiB)
# checkpoint_interval_bytes: 8388608 # Bytes of appended data between durability checkpoints (default: 8 MiB)
26 changes: 11 additions & 15 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,29 +78,25 @@ key_type = "rsa" # "rsa" or "ed25519"

# Storage configuration
[storage]
storage_type = "fs" # "fs" for filesystem, "sqlite" for SQLite-backed storage
storage_type = "log" # "log" (durable log queue, default) or "fs" (legacy one file per message)
base_path = "/var/lib/hedwig/mail"

# SQLite storage example (num_shards/batch_* live under [storage], tuning under [storage.sqlite]):
# [storage]
# storage_type = "sqlite"
# base_path = "/var/lib/hedwig/mail"
# num_shards = 16 # Number of database shards (default: 16)
# batch_size = 100 # Write-batch size (default: 100)
# batch_timeout_ms = 5 # Max time (ms) to wait before flushing a batch (default: 5)
#
# [storage.sqlite]
# synchronous = "NORMAL" # SQLite synchronous setting: OFF | NORMAL | FULL (default: NORMAL)
# cache_size_mb = 1600 # Page-cache size per shard in MiB (default: 1600)
# busy_timeout_ms = 5000 # SQLite busy-timeout in ms (default: 5000)
# pool_max_connections = 10 # sqlx pool max connections per shard (default: 10)

# Optional retention policy for local spool cleanup
[storage.cleanup]
bounced_retention = "7d"
deferred_retention = "2d"
interval = "1h"

# Optional durable append-log mail queue (not yet wired into the serving path)
# [queue]
# append_writers = 1 # Number of shards / concurrent append writers (default: 1)
# pending_append_bytes = 134217728 # Pending (not-yet-durable) append bytes before backpressure (default: 128 MiB)
# segment_target_bytes = 67108864 # Target size of each active segment file, per shard (default: 64 MiB)
# compaction_dead_ratio = 0.50 # Dead-byte fraction in a sealed segment that makes it compaction-eligible (default: 0.50)
# compaction_min_age = "60s" # Minimum age of a sealed segment before compaction eligibility (default: 60s)
# disk_reserve_bytes = 1073741824 # Minimum free disk space required to accept new mail (default: 1 GiB)
# checkpoint_interval_bytes = 8388608 # Bytes of appended data between durability checkpoints (default: 8 MiB)

# Optional email filters
[[filters]]
type = "from_domain_filter"
Expand Down
32 changes: 0 additions & 32 deletions dev/config.sqlite.toml

This file was deleted.

8 changes: 4 additions & 4 deletions docs/PRODUCTION_HARDENING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ Production deployments should set `server.helo_hostname` to the public FQDN for

## 🟡 Important

### 6. ~~Filesystem storage lacks durability guarantees~~ → Addressed (SQLite backend)
### 6. ~~Filesystem storage lacks durability guarantees~~ → Addressed (log-queue backend)

**Status:** Addressed via `SqliteStorage` backend (`storage_type = "sqlite"`). SQLite transactions provide atomic writes — no partial writes, no fsync gaps. See `docs/specs/2026-03-29-sqlite-storage-design.md`.
**Status:** Addressed via the durable log queue (`storage_type = "log"`): checksummed append-only records with torn-tail recovery, and fsync barriers at every destructive boundary. (Previously addressed by the SQLite backend, since removed.) See `ARCHITECTURE.md`.

**Problem:** `fs_storage.rs` uses `tokio::fs::write()` directly — no temp-file + rename, no fsync. On crash or power loss:
- Partially written files can corrupt the queue
Expand All @@ -70,9 +70,9 @@ Production deployments should set `server.helo_hostname` to the public FQDN for

---

### 7. ~~Filesystem storage doesn't scale to millions of files~~ → Addressed (SQLite backend)
### 7. ~~Filesystem storage doesn't scale to millions of files~~ → Addressed (log-queue backend)

**Status:** Addressed via `SqliteStorage` backend. Sharded SQLite databases with indexed queries replace flat directory walks. See `docs/specs/2026-03-29-sqlite-storage-design.md`.
**Status:** Addressed via the log queue: segmented append-only storage keeps file count proportional to live backlog, with no per-message files and no directory scans. (Previously addressed by the SQLite backend, since removed.)

**Problem:** Flat directories (`queued/`, `deferred/`, `bounced/`) with millions of files means very slow `readdir()` calls. Startup replay and cleanup become directory-walk bound. ext4 performance degrades significantly past ~100K files per directory.

Expand Down
Loading
Loading