Skip to content

harden proto storage — recovery, atomicity, persistence - #90

Merged
kacy merged 6 commits into
mainfrom
harden/proto-storage-v2
Feb 11, 2026
Merged

harden proto storage — recovery, atomicity, persistence#90
kacy merged 6 commits into
mainfrom
harden/proto-storage-v2

Conversation

@kacy

@kacy kacy commented Feb 11, 2026

Copy link
Copy Markdown
Owner

Summary

second phase of proto storage hardening, addressing 9 issues found in a deep audit:

  • schema recovery: ProtoRegister records were skipped during AOF replay — schemas are now collected during recovery and restored into the shared registry on shard startup
  • atomic field mutations: SETFIELD/DELFIELD used a 3-step read-modify-write across two shard roundtrips. moved the mutation into the shard's single-threaded dispatch for inherent atomicity
  • AOF rewrite durability: handle_rewrite() truncated the AOF (which contained ProtoRegister records) without re-persisting schemas. schemas are now written after truncation and synced before the response is sent
  • broadcast error logging: let _ = engine.broadcast(...) in ProtoRegister now logs a warning on failure
  • concurrent mode warning: startup warning when both --concurrent and --protobuf are set, since DEL/TTL/EXPIRE don't affect proto keys in concurrent mode
  • proto value size limit: 64MB max per value, checked before decode in validate()
  • schema count cap: 1024 schemas max to prevent unbounded registration
  • memory sizing fix: proto value overhead corrected from 24 to 48 bytes (String + Bytes structs)

What was tested

  • cargo test -p emberkv-core --features protobuf — 335 unit tests
  • cargo test -p ember-protocol — 294 tests
  • cargo test -p ember-integration-tests — 113 integration tests (44 proto-specific)
  • cargo clippy --workspace --features protobuf -- -D warnings — clean
  • cargo check without protobuf feature — no regressions

new tests added:

  • unit: value_too_large_rejected, value_at_limit_allowed, schema_count_limit
  • integration: schema_recovery_after_restart, schema_survives_aof_rewrite, del_removes_proto_key, concurrent_schema_recovery_after_restart

Design considerations

  • schemas are re-persisted by every shard after AOF rewrite. this is redundant but harmless — restore() is idempotent, and it guarantees recovery works regardless of which shard's AOF is read first
  • the AOF sync after rewrite (writer.sync()) ensures schema records are durable before the rewrite response is sent. without this, a kill between rewrite and next fsync cycle would lose schemas
  • field ops return ProtoFieldUpdated with the full serialized data, which maps cleanly to AofRecord::ProtoSet for persistence — no new AOF record type needed

kacy added 6 commits February 10, 2026 21:29
schemas were lost on restart because ProtoRegister records were
skipped during AOF replay. now recovery collects them (last-wins
dedup) and the shard restores them into the shared registry on
startup.

changes:
- RecoveryResult gains a `schemas` vec (behind protobuf feature)
- replay_aof collects ProtoRegister records into a schema map
- spawn_shard/run_shard accept an optional SharedSchemaRegistry
- after recovery, schemas are restored via registry.restore()
- engine passes its schema_registry clone to each shard
the previous read-modify-write pattern for PROTO.SETFIELD and
PROTO.DELFIELD spanned two shard roundtrips, leaving a window
where another client could overwrite the key between the read
and write. moving the mutation into the shard's single-threaded
dispatch makes it inherently atomic.

changes:
- add ProtoSetField/ProtoDelField ShardRequest variants
- add ProtoFieldUpdated ShardResponse variant
- dispatch_proto_field_op helper reads, mutates, and writes back
  within a single dispatch call
- to_aof_record maps field ops to full ProtoSet records
- connection.rs and concurrent_handler.rs simplified to just
  route the request and match on the response
after a BGREWRITEAOF, the snapshot captures proto values but the
AOF is truncated — losing any ProtoRegister records. on the next
restart, recovery loads the values but has no schemas, breaking
all field-level operations.

now handle_rewrite writes ProtoRegister records for all registered
schemas into the AOF immediately after truncation.
- log a warning when broadcasting ProtoRegisterAof fails instead
  of silently discarding the error with `let _ =`
- add a startup warning when both concurrent mode and protobuf
  are enabled, since generic commands (DEL, TTL, EXPIRE) don't
  affect proto keys in concurrent mode
- add MAX_PROTO_VALUE_BYTES (64MB) check in validate() to prevent
  a single PROTO.SET from exhausting shard memory
- add MAX_SCHEMAS (1024) check in register() to cap the number of
  registered schemas
- fix proto memory overhead from 24 to 48 bytes to properly
  account for both String (24 bytes) and Bytes (24 bytes) structs
add unit tests for value size limit rejection, at-limit acceptance,
and schema count cap. add integration tests for schema recovery after
restart, schema survival through AOF rewrite, DEL on proto keys, and
concurrent mode schema recovery.

fix: sync AOF after re-persisting schemas during rewrite — without
this, schema records could remain in the BufWriter buffer and be lost
if the server is killed before the next fsync cycle.
@kacy
kacy merged commit d199f8a into main Feb 11, 2026
5 of 7 checks passed
@kacy
kacy deleted the harden/proto-storage-v2 branch February 11, 2026 02:49
kacy added a commit that referenced this pull request Feb 11, 2026
* thread schema registry into shards and recover schemas from AOF

schemas were lost on restart because ProtoRegister records were
skipped during AOF replay. now recovery collects them (last-wins
dedup) and the shard restores them into the shared registry on
startup.

changes:
- RecoveryResult gains a `schemas` vec (behind protobuf feature)
- replay_aof collects ProtoRegister records into a schema map
- spawn_shard/run_shard accept an optional SharedSchemaRegistry
- after recovery, schemas are restored via registry.restore()
- engine passes its schema_registry clone to each shard

* atomic SETFIELD/DELFIELD via shard-local mutation

the previous read-modify-write pattern for PROTO.SETFIELD and
PROTO.DELFIELD spanned two shard roundtrips, leaving a window
where another client could overwrite the key between the read
and write. moving the mutation into the shard's single-threaded
dispatch makes it inherently atomic.

changes:
- add ProtoSetField/ProtoDelField ShardRequest variants
- add ProtoFieldUpdated ShardResponse variant
- dispatch_proto_field_op helper reads, mutates, and writes back
  within a single dispatch call
- to_aof_record maps field ops to full ProtoSet records
- connection.rs and concurrent_handler.rs simplified to just
  route the request and match on the response

* persist schemas through AOF rewrite

after a BGREWRITEAOF, the snapshot captures proto values but the
AOF is truncated — losing any ProtoRegister records. on the next
restart, recovery loads the values but has no schemas, breaking
all field-level operations.

now handle_rewrite writes ProtoRegister records for all registered
schemas into the AOF immediately after truncation.

* broadcast error logging and concurrent mode warning

- log a warning when broadcasting ProtoRegisterAof fails instead
  of silently discarding the error with `let _ =`
- add a startup warning when both concurrent mode and protobuf
  are enabled, since generic commands (DEL, TTL, EXPIRE) don't
  affect proto keys in concurrent mode

* proto value size limit, memory correction, schema count cap

- add MAX_PROTO_VALUE_BYTES (64MB) check in validate() to prevent
  a single PROTO.SET from exhausting shard memory
- add MAX_SCHEMAS (1024) check in register() to cap the number of
  registered schemas
- fix proto memory overhead from 24 to 48 bytes to properly
  account for both String (24 bytes) and Bytes (24 bytes) structs

* tests and sync fix for recovery, rewrite, and limits

add unit tests for value size limit rejection, at-limit acceptance,
and schema count cap. add integration tests for schema recovery after
restart, schema survival through AOF rewrite, DEL on proto keys, and
concurrent mode schema recovery.

fix: sync AOF after re-persisting schemas during rewrite — without
this, schema records could remain in the BufWriter buffer and be lost
if the server is killed before the next fsync cycle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant