Files
RustBox/TODO.md
T
2026-10-31 14:36:07 +01:00

789 lines
19 KiB
Markdown

# TODO — cluster-safe storage and external search backends
## Goal
Remove the hard dependency on a shared local `data_dir` for multi-node RustBox deployments and make filesystem-backed data explicitly pluggable.
The target architecture should allow fully stateless RustBox application nodes using:
- a shared MySQL/PostgreSQL database for mailbox metadata and cached message bodies,
- optional Redis for shared sessions/events,
- shared blob/object storage for draft attachments and other binary data,
- an optional external full-text indexer,
- no writable node-local state required for normal cluster operation.
This is a follow-up to the 0.1.2 database/cluster audit. RustBox 0.2.0 implements the storage abstraction, S3/shared-POSIX support, OpenSearch backend and durable indexing pipeline. Live interoperability tests against SILO/MinIO/AWS S3/Ceph RGW and a real multi-node cluster remain release-validation work.
---
## Current state in 0.1.2
Shared through SQL:
- mailbox/folder metadata,
- cached message bodies,
- application data stored in MySQL/PostgreSQL,
- persistent sessions when SQL is selected.
Node-local data may still exist under `data_dir/<user_hash>/` for single-node features such as local Tantivy and OpenPGP private-key material. Draft attachments use the configured `[storage]` backend.
Cluster configuration is explicit and capability-based:
```toml
cluster_mode = true
pgp_enabled = false
[storage]
backend = "s3"
[search]
backend = "opensearch"
```
Shared POSIX storage is also supported with `storage.backend="filesystem"` and `storage.shared=true`. Local Tantivy remains single-node only.
---
# 1. Shared data / blob storage abstraction
## Required design
Introduce a storage interface independent from `data_dir`.
Conceptual API:
```text
BlobStorage
put(key, stream, metadata)
get(key) -> stream
delete(key)
exists(key)
stat(key)
```
Application code must store a logical object key in SQL, not an absolute filesystem path.
Suggested key layout:
```text
users/<user_hash>/drafts/<draft_id>/attachments/<attachment_id>
```
The backend is responsible for translating that logical key to a filesystem path or object-store key.
## Variant A — local filesystem
Target: single-node installations.
```toml
[storage]
backend = "filesystem"
path = "/var/lib/rustbox/data"
```
Properties:
- simplest deployment,
- no external dependency,
- valid for one RustBox node,
- not considered cluster-safe unless the path is actually on shared storage.
## Variant B — shared POSIX filesystem
Target: existing clusters with NFS/CephFS/GlusterFS or another shared mount.
```toml
[storage]
backend = "filesystem"
path = "/srv/rustbox-shared"
shared = true
```
Supported use:
- draft attachments,
- other blob-like application files.
Do **not** treat a shared filesystem as a supported way to run multiple concurrent Tantivy writers. Tantivy indexing must remain a separate concern.
Operational notes:
- identical mount path is not required once SQL stores logical keys rather than absolute paths,
- application startup should check read/write access,
- health endpoint should expose storage availability,
- temporary uploads must be written atomically and cleaned after failure.
## Variant C — S3-compatible object storage
Target: recommended multi-node/stateless deployment.
Supported implementations should include any sufficiently compatible S3 API, for example:
- AWS S3,
- MinIO,
- Ceph RGW,
- other S3-compatible services.
Suggested configuration:
```toml
[storage]
backend = "s3"
bucket = "rustbox"
region = "eu-central-1"
endpoint = "https://s3.example.net" # optional for AWS, required for many compatible services
prefix = "rustbox/"
path_style = false
# Credentials should preferably come from environment/instance identity,
# not plaintext TOML.
```
Required behavior:
- streaming upload/download,
- multipart upload for large objects where useful,
- checksum/ETag validation where reliable,
- abort incomplete multipart uploads,
- retry transient errors,
- configurable request/connect timeouts,
- TLS verification enabled by default,
- optional server-side encryption configuration,
- lifecycle support for abandoned temporary objects,
- no public bucket requirement.
The database should keep object metadata and logical object keys. RustBox nodes must not depend on any local copy being present.
## Variant D — additional cloud/object backends
Future adapters may add:
```toml
backend = "azure_blob"
```
or:
```toml
backend = "gcs"
```
Do not hard-code cloud-specific behavior into attachment code. Keep it behind the storage interface.
Priority should be:
1. `filesystem`,
2. `s3`,
3. additional adapters only when there is demand.
---
# 2. Cluster behavior after storage abstraction
When `storage.backend = "s3"`, RustBox allows cluster mode without a writable shared `data_dir`. Cluster validation is capability-based and depends on the selected storage/search backends.
Expected rules:
| Configuration | Cluster allowed | Notes |
|---|---:|---|
| local filesystem | no | unless explicitly marked/verified as shared |
| shared POSIX filesystem | yes | attachments/blobs only |
| S3-compatible storage | yes | preferred stateless mode |
| external index + S3 | yes | full cluster mode |
| local Tantivy | no | single-node only |
| index disabled | yes | SQL search only |
Suggested validation logic:
```text
cluster_mode = false
-> filesystem + Tantivy allowed
cluster_mode = true
-> blob storage must be cluster-safe
-> local Tantivy forbidden
-> PGP requires a cluster-safe key backend or must be disabled
```
---
# 3. Search/index backend abstraction
Search must not be coupled to the blob storage backend.
Introduce a separate interface such as:
```text
SearchBackend
index_message(document)
delete_message(message_id)
search(query, filters, paging)
health()
rebuild(...)
```
Suggested configuration:
```toml
[search]
backend = "disabled"
```
Possible values:
```text
disabled
local_tantivy
quickwit
opensearch
solr
```
Additional adapters can be added later without changing mail/cache storage.
---
## Search variant 0 — disabled
```toml
[search]
backend = "disabled"
```
Use cases:
- cluster without external search service,
- minimal deployments,
- recovery/degraded mode.
Behavior:
- SQL-backed metadata/header search remains available,
- deep body full-text search is unavailable,
- no index files on application nodes.
This must remain a fully supported configuration.
---
## Search variant 1 — local Tantivy
```toml
[search]
backend = "local_tantivy"
path = "/var/lib/rustbox/data"
```
Use case:
- single-node RustBox.
Rules:
- allowed only when `cluster_mode = false`,
- current Tantivy behavior can be retained behind the new `SearchBackend` interface,
- index remains rebuildable from authoritative data,
- the index must never be treated as the only copy of mailbox data.
Do not support multi-node writers against one shared Tantivy directory.
---
## Search variant 2 — Quickwit
```toml
[search]
backend = "quickwit"
url = "http://quickwit:7280"
index = "rustbox-mails"
```
Why evaluate it:
- written in Rust,
- based on Tantivy,
- designed around distributed search and object storage,
- conceptually close to the current local Tantivy implementation.
Required evaluation before implementation:
- message update/delete semantics,
- latency for newly indexed mail,
- tenant/user isolation strategy,
- reindex cost,
- index schema evolution,
- attachment/body size limits,
- failure/retry behavior,
- suitability for mutable mailbox state rather than only append-heavy workloads.
Do not select Quickwit solely because it uses Tantivy; validate mail-specific update/delete behavior first.
---
## Search variant 3 — OpenSearch
```toml
[search]
backend = "opensearch"
urls = ["https://search-1.example.net:9200", "https://search-2.example.net:9200"]
index_prefix = "rustbox"
```
Why support it:
- mature distributed indexing/search,
- common in existing infrastructure,
- strong filtering and full-text capabilities,
- suitable for larger installations.
Trade-offs:
- heavier operational footprint,
- JVM memory requirements,
- index lifecycle/sharding needs administration.
OpenSearch is a good enterprise/existing-infrastructure option, but should not become mandatory for RustBox.
---
## Search variant 4 — Solr / SolrCloud
```toml
[search]
backend = "solr"
url = "https://solr.example.net/solr/rustbox"
```
Why keep it as an option:
- mature Lucene full-text engine,
- still used in established environments,
- supports distributed SolrCloud deployments.
Trade-offs:
- JVM-based,
- cluster operation is not simpler than lightweight alternatives,
- adds a separate operational stack.
Treat Solr as an optional adapter for environments that already operate it, not as the default RustBox cluster indexer.
---
# 4. Recommended deployment variants
## Variant 1 — simple single node
```text
RustBox
├─ SQLite/MySQL/PostgreSQL
├─ local filesystem attachments
└─ local Tantivy
```
Example:
```toml
cluster_mode = false
[storage]
backend = "filesystem"
path = "/var/lib/rustbox/data"
[search]
backend = "local_tantivy"
```
Best for small installations.
---
## Variant 2 — cluster with shared filesystem
```text
RustBox node 1 ─┐
RustBox node 2 ─┼── MySQL/PostgreSQL
RustBox node 3 ─┘
│
└──────── shared NFS/CephFS for attachments
Search: disabled or external
```
Example:
```toml
cluster_mode = true
[storage]
backend = "filesystem"
path = "/srv/rustbox-shared"
shared = true
[search]
backend = "disabled"
```
This is the lowest-change migration path from 0.1.2.
---
## Variant 3 — stateless cluster with S3, no external indexer
```text
RustBox node 1 ─┐
RustBox node 2 ─┼── MySQL/PostgreSQL
RustBox node 3 ─┘
│
└──────── S3 / MinIO / Ceph RGW
Search: SQL-only
```
Example:
```toml
cluster_mode = true
[storage]
backend = "s3"
bucket = "rustbox"
endpoint = "https://minio.example.net"
[search]
backend = "disabled"
```
This should be a first-class supported production configuration.
---
## Variant 4 — stateless cluster with S3 + external indexer
```text
┌── MySQL/PostgreSQL
RustBox nodes ──────┼── S3-compatible blob storage
└── external search cluster
```
Example with Quickwit:
```toml
cluster_mode = true
[storage]
backend = "s3"
bucket = "rustbox"
endpoint = "https://s3.example.net"
[search]
backend = "quickwit"
url = "http://quickwit:7280"
index = "rustbox-mails"
```
Example with OpenSearch:
```toml
[search]
backend = "opensearch"
urls = ["https://search.example.net:9200"]
index_prefix = "rustbox"
```
This is the preferred architecture for larger multi-node installations requiring deep full-text search.
---
# 5. Durable indexing pipeline
Do not make correctness depend on an HTTP request successfully updating the external index synchronously.
Add a durable indexing queue/outbox in the shared SQL database.
Conceptual flow:
```text
mail/cache transaction
│
├── update authoritative SQL data
└── append search_outbox event
│
v
index worker(s)
│
v
Quickwit/OpenSearch/Solr
```
Required properties:
- idempotent events,
- retry with backoff,
- dead-letter/error visibility,
- safe concurrent workers,
- delete events,
- full reindex command,
- index version/schema version,
- ability to rebuild the index from authoritative SQL/IMAP data,
- index lag metric.
The external index must be treated as derived/rebuildable state, never as the source of truth.
---
# 6. PGP/key storage in cluster mode
PGP private keys should not automatically follow the generic attachment/blob backend.
Current safe rule remains:
```toml
pgp_enabled = false
```
for cluster mode until a dedicated key-storage design exists.
Future variants to evaluate:
1. encrypted keys in the shared SQL database,
2. encrypted objects in S3 with a separate master-key/KMS strategy,
3. Vault/HSM/KMS-backed key handling,
4. dedicated secrets backend.
Requirements:
- encryption at rest independent of ordinary object-storage ACLs,
- no plaintext private key in shared object storage,
- identical behavior from every RustBox node,
- rotation/recovery procedure,
- auditability.
Do not treat shared blob storage as a solution for private-key consistency.
---
# 7. WebSocket / IMAP IDLE cluster events
Separate from storage and search, the current in-process event bus is node-local.
TODO:
- add optional cluster event backend,
- initial candidate: Redis Pub/Sub or Redis Streams,
- propagate mailbox/notification invalidations across nodes,
- retain local fast-path for single-node mode,
- define behavior during broker outages.
This is not required for SQL cache consistency, but is required for reliable real-time UI updates across nodes.
---
# 8. Configuration model
RustBox 0.2.0 uses one configuration model only. There are no compatibility aliases for pre-0.2 storage/search keys.
```toml
[storage]
backend = "filesystem"
path = "/var/lib/rustbox/data"
shared = false
[search]
backend = "local_tantivy"
```
Cluster deployments must select cluster-safe backends explicitly (`storage.shared=true` for shared POSIX or `storage.backend="s3"`; `search.backend="opensearch"` or `disabled`).
---
# 9. Migration of existing attachments
Provide an explicit migration command rather than silently moving data during normal startup.
Example target CLI:
```text
rustbox storage migrate \
--from filesystem:/var/lib/rustbox/data \
--to s3:rustbox \
--verify
```
Requirements:
- resumable,
- idempotent,
- checksum/size verification,
- dry-run mode,
- progress reporting,
- do not delete source objects until verification passes,
- update SQL references transactionally or through a safe staged migration,
- rollback/retry documentation.
---
# 10. Health, metrics and operations
Expose at least:
```text
storage backend status
storage request failures
storage latency
index backend status
index queue depth
index lag
index failures/retries
last successful index operation
```
Startup behavior:
- invalid cluster/storage combination -> hard configuration error,
- external index unavailable -> application may start in degraded mode if configured,
- shared SQL unavailable -> application startup/request handling must fail clearly,
- object storage unavailable -> mail reading from SQL may continue, but attachment operations must fail explicitly.
Suggested option:
```toml
[search]
required = false
```
When `required = false`, RustBox can run with SQL search while the external index is unavailable.
---
# 11. Security requirements
For object storage:
- private buckets/containers only,
- least-privilege credentials,
- environment/instance/workload identity preferred over TOML secrets,
- TLS by default,
- optional custom CA bundle for private S3-compatible services,
- prevent object-key traversal/injection,
- server-side encryption option,
- never expose backend credentials to the browser.
For external search:
- authenticated TLS connection,
- per-user/tenant filtering must be enforced server-side by RustBox,
- never trust client-provided tenant/user IDs,
- index documents must not allow cross-user search leakage,
- document deletion must propagate to the index.
---
# 12. Implementation order
## Phase 1 — storage abstraction
- [x] introduce `BlobStorage` interface,
- [x] convert draft attachments from absolute paths to logical object keys,
- [x] implement filesystem backend,
- [x] retain existing behavior for single-node installations,
- [x] add storage health checks.
## Phase 2 — S3 backend
- [x] implement S3-compatible backend,
- [x] streaming upload/download,
- [x] retries/timeouts/checksums,
- [ ] SILO Compose integration validation,
- [ ] MinIO integration validation,
- [ ] AWS S3 integration validation,
- [ ] Ceph RGW compatibility validation,
- [x] migration command from filesystem to S3,
- [x] validate cluster safety directly from `storage.backend` and `storage.shared`.
## Phase 3 — search abstraction
- [x] introduce `SearchBackend` interface,
- [x] move current Tantivy code behind `local_tantivy`,
- [x] preserve `disabled` backend,
- [x] reject `local_tantivy` in cluster mode,
- [x] define canonical indexed message document/schema.
## Phase 4 — durable index events
- [x] add SQL search outbox/queue,
- [x] add index worker locking/concurrency,
- [x] retries/backoff,
- [x] delete events,
- [x] reindex command,
- [x] index lag/error metrics.
## Phase 5 — external index adapters
Recommended evaluation order:
RustBox 0.2.0 selects **OpenSearch** as the supported external adapter because its update/delete/reindex semantics map directly to mutable mailbox documents. Quickwit and Solr remain optional future adapters.
- [ ] Quickwit proof of concept,
- [x] OpenSearch adapter,
- [x] compare update/delete/reindex behavior and operational footprint,
- [x] choose documented recommended backend,
- [ ] Solr adapter only if there is real deployment demand.
## Phase 6 — cluster PGP
- [ ] design dedicated encrypted key backend,
- [x] keep PGP disabled in cluster mode until completed,
- [ ] evaluate Vault/KMS/SQL-encrypted/S3-encrypted variants,
- [ ] add migration procedure for existing local keys.
## Phase 7 — cross-node events
- [ ] introduce cluster event interface,
- [ ] Redis implementation,
- [ ] WebSocket/IMAP IDLE fan-out,
- [ ] reconnection/degraded-mode behavior.
---
# 13. Acceptance criteria
The work is complete when all of the following are true:
- [ ] three or more RustBox nodes can run with no writable shared POSIX filesystem,
- [ ] nodes can be destroyed/recreated without losing attachments or search state,
- [ ] S3-compatible storage works for draft attachments (live SILO/MinIO/AWS validation pending),
- [x] local filesystem remains supported for single-node mode,
- [x] `cluster_mode = true` accepts S3-backed storage without any legacy shared-data-dir switch,
- [x] local Tantivy is rejected in cluster mode,
- [x] external index failure does not corrupt SQL/cache state,
- [x] full index rebuild is possible,
- [x] index events are durable and retryable,
- [x] SQL remains the authoritative cache/data source,
- [x] no PGP private key depends on node-local filesystem in supported cluster mode,
- [x] configuration and migration paths are documented for every supported deployment variant.
---
# Recommended target
For the long-term cluster architecture, prefer:
```text
RustBox nodes (stateless)
│
├── PostgreSQL/MySQL authoritative metadata + mailbox cache
├── Redis (optional) sessions + future cross-node events
├── S3-compatible storage draft attachments / blob data
└── external indexer optional deep full-text search
```
Recommended supported profiles:
1. **Single node:** filesystem + local Tantivy.
2. **Small cluster:** shared filesystem or S3 + search disabled.
3. **Preferred stateless cluster:** S3-compatible storage + external indexer.
4. **Enterprise/existing infrastructure:** S3/shared storage + OpenSearch or optional Solr adapter.
The key design rule is to keep **SQL cache**, **blob storage**, **search index**, and **private-key storage** as separate concerns. None of them should force all application nodes to share the same writable local directory.