Skip to content

refactor #260: expunge proto::ClientResult from user-facing API - #389

Merged
JoshuaChi merged 3 commits into
mainfrom
refactor/260-d-engine-core-dep
May 23, 2026
Merged

JoshuaChi merged 3 commits into
mainfrom
refactor/260-d-engine-core-dep

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

What Does This PR Do?

Decouples d-engine-core's public API from d-engine-proto by defining
native Rust types (ErrorCode, ClientResponse, KvEntry, LeaderHint,
ReadConsistencyPolicy) in core and confining all proto↔core conversions
to a single layer (d-engine-server/src/proto_convert.rs). Users now
program against stable, semantically meaningful Rust types rather than
protobuf-generated code.

Type:


Why Is This Needed?

Issue: #260

Previously, ErrorCode, ReadConsistencyPolicy, and ClientResult were
re-exported directly from d-engine-proto into the public API. This
meant users had to import auto-generated protobuf types, and any
wire-format change (field rename, enum reorder) could silently break
application code. It also made it impossible to add an HTTP/QUIC
transport without touching d-engine-core.

This PR draws a hard boundary:

d-engine-proto  ←  wire format only (Raft internals)
d-engine-core   ←  native client API types (ErrorCode, KvEntry, …)
d-engine-server ←  proto_convert.rs: the single conversion layer
d-engine-client ←  consumes core types; proto visible only at gRPC boundary
d-engine        ←  facade; re-exports everything cleanly

Checklist

Required:

  • make test passes (formatting + Clippy -D warnings + 1 375 tests)
  • Added tests for new code
  • Commits squashed to 1 logical unit

If changing APIs:

  • Updated relevant docs (lib.rs module-level rustdoc, protocol module
    comments)
  • Explained why complexity is justified (proto_convert.rs module
    header documents the orphan-rule rationale and performance notes)

Testing

How tested:

  • Unit tests: d-engine-core/src/client/types_test.rs — 18 new
    tests covering ClientResponse constructors and predicates
    (is_write_success, is_term_outdated, is_propose_failure,
    not_leader with/without hint, TryFrom<i32> for ErrorCode — all 23
    discriminants)
  • Unit tests: d-engine-server/src/proto_convert_test.rs — round-trip
    tests for every conversion function in the new conversion layer
  • Unit tests (TDD): client_ext_test.rs — into_read_results now
    explicitly type-annotated Vec<Option<KvEntry>>; added
    test_into_read_results_multiple_entries_are_kv_entries
  • Unit tests (TDD): grpc_client_test.rs — added
    test_get_with_policy_returns_native_kv_entry and
    test_get_multi_with_policy_returns_native_kv_entries with explicit
    Option<KvEntry> / Vec<Option<KvEntry>> type annotations (these
    fail to compile until the return type is changed — true red/green cycle)
  • Integration tests: all existing integration tests in
    d-engine-server/tests/ pass unchanged

For bug fixes: N/A


Does This Follow d-engine's Principles?

  • Solves a real problem for most users (not just my edge case) —
    any user importing ErrorCode or ReadConsistencyPolicy was
    depending on a generated type; this removes that coupling for everyone
  • Keeps implementation simple — conversions are #[inline] free
    functions in one file; no new traits, no new crates
  • Doesn't bloat the API surface — net removal: ClientResult and
    proto ErrorCode / ReadConsistencyPolicy removed from public
    exports; replaced by equivalents already in d-engine-core

Reviewer Notes

Focus areas:

  1. d-engine-server/src/proto_convert.rs — the new conversion layer.
    Verify core_error_to_proto / proto_error_to_core are symmetric
    and that the TryFrom<i32> discriminant list in
    d-engine-core/src/client/types.rs stays in sync with error.proto.

  2. proto_convert.rs has three #[allow(dead_code)] functions
    (proto_error_to_core, to_core_response, parse_leader_hint).
    These are the reverse path (proto → core for client-side parsing) and
    are intentionally kept for future use; ticket perf(read): linearizable read lease fast path + fix Raft lease clock (SystemTime → Instant) #390 tracks whether to
    promote or remove them.

  3. GrpcClient::get_with_policy and get_multi_with_policy are the only
    public methods that previously returned a proto type directly. They
    now return Option<KvEntry> / Vec<Option<KvEntry>>. This is a
    breaking change for any caller that pattern-matched on
    proto::ClientResult — the example in
    examples/client-usage-standalone shows the migration.

Estimated review complexity:

  • Quick (< 100 lines)
  • Medium (< 300 lines)
  • Deep (> 300 lines) — large diff due to import-path updates across
    the entire workspace; the logical change is narrow (type substitution
    • one new file)

Summary by CodeRabbit

  • New Features

    • Native client API types introduced, including a native key/value entry model for read results.
    • Centralized proto↔core conversion helpers added to translate between wire and core representations.
  • API Changes

    • Read APIs now return native key/value entries and accept strongly typed read-consistency enums.
    • Public exports updated to surface core client error/types instead of proto-layer types.
  • Tests

    • Tests migrated to native types and expanded with conversion and type-boundary validations.

Review Change Stack

Replace proto::ClientResult with core::KvEntry as the return type of
GrpcClient::get_with_policy() and get_multi_with_policy().  Previously
these power-user methods leaked a protobuf-generated type into the public
API surface despite the rest of the read API (ClientApi::get,
get_linearizable, etc.) already returning plain Bytes.

Changes:
- client_ext.rs: into_read_results() now returns Vec<Option<KvEntry>>
- grpc_client.rs: get_with_policy / get_multi_with_policy return KvEntry
- lib.rs: protocol module replaces ClientResult export with KvEntry
- examples/client-usage-standalone: updated to KvEntry pattern match

Tests (TDD -- red before green):
- test_into_read_results_success: explicit Vec<Option<KvEntry>> annotation
- test_into_read_results_multiple_entries_are_kv_entries (new)
- test_get_with_policy_returns_native_kv_entry (new)
- test_get_multi_with_policy_returns_native_kv_entries (new)

proto::ClientResult survives only inside d-engine-proto's wire layer.
All user-visible read results are now proto-free native Rust types.
@coderabbitai

coderabbitai Bot commented May 22, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0dc5c486-8f8e-4c9b-8822-a7f0e0863b72

📥 Commits

Reviewing files that changed from the base of the PR and between f508297 and b41dfa1.

📒 Files selected for processing (2)
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs

📝 Walkthrough

Walkthrough

Adds core, transport-agnostic client types; migrates client/server code, leader/state paths, state-machine reads, mocks, tests, examples, and public re-exports to use core types; introduces proto↔core conversion helpers and accompanying tests.

Changes

Client Type Migration to Core

Layer / File(s) Summary
Core client types and tests
d-engine-core/src/client/types.rs, d-engine-core/src/client/types_test.rs
Defines native client request/response types (WriteOperation, ClientWriteRequest, ClientReadRequest, ClientResponse, KvEntry, ErrorCode) and unit tests validating constructors, predicates, and discriminants.
Proto ↔ Core conversion and tests
d-engine-server/src/proto_convert.rs, d-engine-server/src/proto_convert_test.rs
Bi-directional conversions between proto wire types and core client types, including error-metadata/leader-hint parsing and round-trip tests.
Client libraries, proto ext, mocks, tests
d-engine-client/src/grpc_client.rs, d-engine-client/src/proto/client_ext.rs, d-engine-client/src/mock_rpc.rs, d-engine-client/src/grpc_client_test.rs, d-engine-client/src/proto/client_ext_test.rs, d-engine-client/src/lib.rs
GrpcClient now returns KvEntry/Vec<Option<KvEntry>>; ClientResponseExt converts read results into KvEntry; mocks store core ClientResponse and convert to proto on reply; tests updated to assert KvEntry shapes and add return-type boundary tests; re-exports switched to core ErrorCode/KvEntry.
Server gRPC handlers & embedded client
d-engine-server/src/network/grpc/grpc_raft_service.rs, d-engine-server/src/api/embedded_client.rs
gRPC handlers use proto_convert for proto↔core translation; EmbeddedClient builds core ClientWriteRequest/ClientReadRequest, uses extract_read_payload, and maps errors with LeaderHint/retry_after_ms.
Leader/role state write conversion
d-engine-core/src/raft_role/leader_state.rs
Adds write_op_to_proto to serialize WriteOperation into protobuf WriteCommand, updates write proposal path, and refactors read-policy selection to use Option<ReadConsistencyPolicy>.
Role-state/process changes
d-engine-core/src/raft_role/role_state.rs
Switches to core client types for reads/writes, refactors NOT_LEADER response construction to use LeaderHint, and updates eventual-read/local-processing signatures.
State-machine handler
d-engine-core/src/state_machine_handler/mod.rs, .../default_state_machine_handler.rs
read_from_state_machine now returns Option<Vec<KvEntry>> and constructs KvEntry items.
Event/buffer/import updates
d-engine-core/src/event.rs, d-engine-core/src/raft_role/..., d-engine-core/src/replication/mod.rs
Replace proto client imports with crate::client re-exports across events, buffers, replication, and tests.
Tests, examples, public exports
many test files, d-engine-server/src/lib.rs, d-engine-client/src/lib.rs, examples/client-usage-standalone/src/main.rs
Update imports/re-exports to use core ErrorCode, KvEntry, ReadConsistencyPolicy, WriteOperation; adjust tests to use strongly typed policies and core shapes; update example read handling to destructure KvEntry.
sequenceDiagram
  participant Client
  participant grpc_raft_service
  participant proto_convert
  participant leader_state
  participant state_machine
  Client->>grpc_raft_service: proto ClientRead/Write request
  grpc_raft_service->>proto_convert: to_core_read_req / to_core_write_req
  proto_convert->>leader_state: core ClientReadRequest/ClientWriteRequest
  leader_state->>state_machine: apply/read
  state_machine-->>leader_state: ClientResponse (core)
  leader_state->>proto_convert: to_proto_response
  proto_convert-->>grpc_raft_service: proto ClientResponse
  grpc_raft_service-->>Client: gRPC response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

"I hopped from proto fields to core,
KvEntry seeds on a brighter floor.
Requests now travel tidy and neat —
Leader hints and errors all meet.
🐇✨"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/260-d-engine-core-dep

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
d-engine-core/src/state_machine_handler/mod.rs (1)

103-108: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve miss positions in multi-key reads.

Option<Vec<KvEntry>> cannot encode mixed hit/miss results in request order, so a read like [missing_key, present_key] gets collapsed and cannot satisfy the new Vec<Option<KvEntry>> client contract. The read-path contract here needs one slot per requested key, e.g. Vec<Option<KvEntry>>, and implementations should fill it in request order instead of dropping misses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@d-engine-core/src/state_machine_handler/mod.rs` around lines 103 - 108,
Change the read_from_state_machine signature and behavior to return a
per-request-slot result so misses are preserved: update the fn
read_from_state_machine(&self, keys: Vec<bytes::Bytes>) -> Vec<Option<KvEntry>>
(replace the current Option<Vec<KvEntry>> return) and update all implementations
and callers to construct and return a Vec<Option<KvEntry>> with one element per
input key in the same request order (fill Some(KvEntry) for hits and None for
misses) rather than dropping misses; ensure any code that consumed the old
Option<Vec<KvEntry>> is adjusted to handle the new Vec<Option<KvEntry>>
contract.
d-engine-client/src/grpc_client_test.rs (1)

441-446: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Model missing keys by omission, not empty values.

Line 445 currently injects an entry for a “missing” key using empty bytes, which tests empty-value semantics instead of missing-key semantics (None).

Suggested test fix
-    for (i, key) in keys.iter().enumerate() {
-        client_results.push(KvEntry {
-            key: key.clone(),
-            value: match &values[i] {
-                Some(value) => value.clone(),
-                None => Bytes::copy_from_slice(&[]), // empty value for not found
-            },
-        });
-    }
+    for (i, key) in keys.iter().enumerate() {
+        if let Some(value) = &values[i] {
+            client_results.push(KvEntry {
+                key: key.clone(),
+                value: value.clone(),
+            });
+        }
+    }
@@
-        assert_eq!(
-            results[i].as_ref().map(|r| r.value.clone()),
-            values[i].clone().or(Some(Bytes::new()))
-        );
+        assert_eq!(results[i].as_ref().map(|r| r.value.clone()), values[i].clone());

Also applies to: 488-490

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@d-engine-client/src/grpc_client_test.rs` around lines 441 - 446, The test
currently models missing keys by inserting a KvEntry with an empty Bytes value;
instead, when values[i] is None you should omit creating/pushing a KvEntry to
represent a missing key. Update the match around client_results.push(KvEntry {
key: key.clone(), value: ... }) so that Some(value) pushes a KvEntry with
value.clone(), and None does not push anything (skip/continue). Apply the same
change to the other occurrence that mirrors this logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@d-engine-core/src/client/types_test.rs`:
- Around line 7-39: The test test_error_code_all_variants_are_exhaustive
currently only asserts the array length which is not compile-time exhaustive;
replace the length-based check with a wildcard-free match over ErrorCode so the
compiler forces you to list every variant. Concretely, in
test_error_code_all_variants_are_exhaustive remove the assert_eq!(codes.len(),
23) and instead implement a function or expression that performs a match without
a `_` arm (e.g., fn assert_exhaustive(code: ErrorCode) { match code {
ErrorCode::Success => (), ErrorCode::ConnectionTimeout => (), ... } } ) and
invoke it in the test (listing every ErrorCode variant as explicit arms) so
adding a new variant will cause a compile error until the test is updated.

In `@d-engine-server/src/api/embedded_client.rs`:
- Around line 135-149: map_error_response currently discards server-provided
backoff guidance (response.retry_after_ms) and instead uses a fixed 100ms in
not_leader_error; update map_error_response so it extracts and forwards the
retry_after_ms from the server response (when present) into the returned
ClientApiError (pass it into not_leader_error or include it in server_error),
ensuring ErrorCode::NotLeader and the default branch preserve retry_after_ms;
search for map_error_response, not_leader_error, ClientApiError, ErrorCode,
LeaderHint and update their callsites listed in the comment to propagate
retry_after_ms instead of dropping it.
- Around line 297-302: The match on response.result currently treats any
non-Read ClientResponsePayload as a benign "not found" by using `_ => Ok(None)`;
instead, detect and surface protocol violations by returning an Err when
response.result is Some(...) but not ClientResponsePayload::Read. Update the
match handling around response.result (the branch that currently matches
ClientResponsePayload::Read and the `_ => Ok(None)` fallback) to return a
descriptive error (using the function's Result error type) for unexpected
payload variants rather than coercing them to None; apply the same change to the
other analogous block handling response.result (the block in the 373-384 region)
so malformed success payloads are surfaced consistently.

In `@d-engine-server/src/network/grpc/grpc_raft_service.rs`:
- Around line 415-418: The request validation only checks
proto_req.command.is_none() but doesn't verify that the nested
WriteCommand.operation is present, which can lead to a panic inside
proto_convert::to_core_write_req; update the validation before calling
to_core_write_req to return Err(Status::invalid_argument(...)) when either
proto_req.command is None or proto_req.command.as_ref().and_then(|c|
c.operation.as_ref()) is None (i.e., when a WriteCommand has operation == None),
so the method validates both proto_req and the nested WriteCommand.operation and
only then calls proto_convert::to_core_write_req.
- Around line 457-460: The conversion silently defaults invalid
consistency_policy values to None; modify the handling in the Read RPC path by
first validating the raw enum value from the incoming request (use the proto
enum's from_i32 / TryFrom) before calling proto_convert::to_core_read_req, and
if the value is invalid return a gRPC invalid-argument error instead of
proceeding; update the flow around proto_convert::to_core_read_req and the
self.cmd_tx.send(d_engine_core::ClientCmd::Read(core_req, resp_tx)) invocation
so only validated requests are forwarded (use an explicit mapping/validation
function for consistency_policy and return an error response when mapping
fails).

---

Outside diff comments:
In `@d-engine-client/src/grpc_client_test.rs`:
- Around line 441-446: The test currently models missing keys by inserting a
KvEntry with an empty Bytes value; instead, when values[i] is None you should
omit creating/pushing a KvEntry to represent a missing key. Update the match
around client_results.push(KvEntry { key: key.clone(), value: ... }) so that
Some(value) pushes a KvEntry with value.clone(), and None does not push anything
(skip/continue). Apply the same change to the other occurrence that mirrors this
logic.

In `@d-engine-core/src/state_machine_handler/mod.rs`:
- Around line 103-108: Change the read_from_state_machine signature and behavior
to return a per-request-slot result so misses are preserved: update the fn
read_from_state_machine(&self, keys: Vec<bytes::Bytes>) -> Vec<Option<KvEntry>>
(replace the current Option<Vec<KvEntry>> return) and update all implementations
and callers to construct and return a Vec<Option<KvEntry>> with one element per
input key in the same request order (fill Some(KvEntry) for hits and None for
misses) rather than dropping misses; ensure any code that consumed the old
Option<Vec<KvEntry>> is adjusted to handle the new Vec<Option<KvEntry>>
contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0da8f9f5-4e93-42a9-af44-c2c2b747dfea

📥 Commits

Reviewing files that changed from the base of the PR and between 8929703 and e50bf49.

📒 Files selected for processing (50)
  • d-engine-client/src/error_test.rs
  • d-engine-client/src/grpc_client.rs
  • d-engine-client/src/grpc_client_test.rs
  • d-engine-client/src/lib.rs
  • d-engine-client/src/mock_rpc.rs
  • d-engine-client/src/mock_rpc_service.rs
  • d-engine-client/src/pool.rs
  • d-engine-client/src/pool_test.rs
  • d-engine-client/src/proto/client_ext.rs
  • d-engine-client/src/proto/client_ext_test.rs
  • d-engine-core/src/client/client_api.rs
  • d-engine-core/src/client/client_api_error.rs
  • d-engine-core/src/client/client_api_error_test.rs
  • d-engine-core/src/client/mod.rs
  • d-engine-core/src/client/types.rs
  • d-engine-core/src/client/types_test.rs
  • d-engine-core/src/event.rs
  • d-engine-core/src/raft_role/buffers/propose_batch_buffer.rs
  • d-engine-core/src/raft_role/buffers/propose_batch_buffer_test.rs
  • d-engine-core/src/raft_role/candidate_state_test.rs
  • d-engine-core/src/raft_role/follower_state_test.rs
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-core/src/raft_role/leader_state_test/backpressure_test.rs
  • d-engine-core/src/raft_role/leader_state_test/buffer_cleanup_test.rs
  • d-engine-core/src/raft_role/leader_state_test/client_read_test.rs
  • d-engine-core/src/raft_role/leader_state_test/client_write_test.rs
  • d-engine-core/src/raft_role/leader_state_test/commit_index_test.rs
  • d-engine-core/src/raft_role/leader_state_test/deadline_test.rs
  • d-engine-core/src/raft_role/leader_state_test/event_handling_test.rs
  • d-engine-core/src/raft_role/leader_state_test/fatal_error_test.rs
  • d-engine-core/src/raft_role/leader_state_test/pending_lease_reads_test.rs
  • d-engine-core/src/raft_role/leader_state_test/pending_reads_test.rs
  • d-engine-core/src/raft_role/leader_state_test/replication_test.rs
  • d-engine-core/src/raft_role/learner_state_test.rs
  • d-engine-core/src/raft_role/role_state.rs
  • d-engine-core/src/replication/mod.rs
  • d-engine-core/src/state_machine_handler/default_state_machine_handler.rs
  • d-engine-core/src/state_machine_handler/mod.rs
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/lib.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs
  • d-engine-server/src/proto_convert.rs
  • d-engine-server/src/proto_convert_test.rs
  • d-engine-server/tests/cas_operations/leader_failover_cas_standalone.rs
  • d-engine-server/tests/client_manager/mod.rs
  • d-engine-server/tests/common/mod.rs
  • d-engine-server/tests/embedded_client/embedded_client_operations.rs
  • d-engine-server/tests/readonly_and_learner_mode/learner_readonly_sync_standalone.rs
  • d-engine-server/tests/watch_and_subscriptions/watch_events_grpc_standalone.rs
  • examples/client-usage-standalone/src/main.rs

Comment thread d-engine-core/src/client/types_test.rs Outdated
Comment thread d-engine-server/src/api/embedded_client.rs
Comment thread d-engine-server/src/api/embedded_client.rs Outdated
Comment thread d-engine-server/src/network/grpc/grpc_raft_service.rs Outdated
Comment on lines +457 to +460
let core_req = proto_convert::to_core_read_req(request.into_inner());
let (resp_tx, resp_rx) = MaybeCloneOneshot::new();
self.cmd_tx
.send(d_engine_core::ClientCmd::Read(
request.into_inner(),
resp_tx,
))
.send(d_engine_core::ClientCmd::Read(core_req, resp_tx))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject invalid consistency_policy values instead of silently defaulting.

Line 457 converts the request without validating raw enum values first. Invalid integers currently degrade to None policy, which changes behavior silently.

Proposed fix
-        let core_req = proto_convert::to_core_read_req(request.into_inner());
+        let proto_req = request.into_inner();
+        if let Some(raw) = proto_req.consistency_policy {
+            if d_engine_proto::client::ReadConsistencyPolicy::try_from(raw).is_err() {
+                return Err(Status::invalid_argument("Invalid consistency_policy"));
+            }
+        }
+        let core_req = proto_convert::to_core_read_req(proto_req);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@d-engine-server/src/network/grpc/grpc_raft_service.rs` around lines 457 -
460, The conversion silently defaults invalid consistency_policy values to None;
modify the handling in the Read RPC path by first validating the raw enum value
from the incoming request (use the proto enum's from_i32 / TryFrom) before
calling proto_convert::to_core_read_req, and if the value is invalid return a
gRPC invalid-argument error instead of proceeding; update the flow around
proto_convert::to_core_read_req and the
self.cmd_tx.send(d_engine_core::ClientCmd::Read(core_req, resp_tx)) invocation
so only validated requests are forwarded (use an explicit mapping/validation
function for consistency_policy and return an error response when mapping
fails).

Issue 1 (Minor) — types_test.rs:
Add compile-time exhaustiveness guard for ErrorCode variants.
The previous length-based assertion could be bypassed by adding a
variant without updating the array; a wildcard-free match function
now guarantees a compile error if any variant is unhandled.

Issue 4 (Critical) — grpc_raft_service.rs:
Validate the nested WriteCommand.operation field at the gRPC boundary.
Previously only command.is_some() was checked; a request with
WriteCommand { operation: None } passed validation and triggered
an unreachable!() panic in proto_convert -- externally exploitable.
Now a single check covers both the outer command and inner operation.

Not addressed (pre-existing, separate tickets):
- embedded_client.rs: retry_after_ms dropped in map_error_response
- embedded_client.rs: non-read payloads silently coerced to None
- grpc_raft_service.rs: invalid consistency_policy int -- intentional
  proto3 degradation to server default, not a bug
JoshuaChi added a commit that referenced this pull request May 22, 2026
…pected read payloads; warn on unknown consistency_policy

embedded_client: thread `retry_after_ms` from server responses through
`map_error_response` and `not_leader_error` (all 6 callsites). Falls
back to 100ms only when server provides None. Previously all NotLeader
errors silently discarded server-side backoff guidance.

embedded_client: extract `extract_read_payload` helper and replace
`_ => Ok(None)` / `_ => Ok(vec![None; n])` wildcard arms in
`get_with_consistency` and `get_multi_with_consistency`. A WriteResult
or missing payload in a read response now surfaces as
`Protocol { InvalidResponse }` instead of masking the violation as
"key not found".

grpc_raft_service: emit `warn!` when an unrecognised
`consistency_policy` integer is received and degraded to cluster
default. Preserves proto3 forward-compatibility while making
silent degradation observable in logs.

Tests: 9 unit tests in `error_helper_tests` —
  - not_leader_error: server value forwarding, None fallback, Some(0) edge case

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
d-engine-server/src/api/embedded_client.rs (1)

289-293: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the example to import the new core enum.

The example above this method still uses d_engine_proto::client::ReadConsistencyPolicy, but this API now takes d_engine_core::config::ReadConsistencyPolicy. Following the docs will fail for callers migrating to the new surface.

📝 Proposed fix
-    /// use d_engine_proto::client::ReadConsistencyPolicy;
+    /// use d_engine_core::config::ReadConsistencyPolicy;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@d-engine-server/src/api/embedded_client.rs` around lines 289 - 293, The
example above the get_with_consistency method references the old enum path;
update any imports and example usage to use
d_engine_core::config::ReadConsistencyPolicy instead of
d_engine_proto::client::ReadConsistencyPolicy so callers of get_with_consistency
(the method signature taking ReadConsistencyPolicy) compile against the new core
enum; locate examples around the get_with_consistency function and replace the
import and any fully-qualified references to the old module with
d_engine_core::config::ReadConsistencyPolicy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@d-engine-server/src/api/embedded_client.rs`:
- Around line 289-293: The example above the get_with_consistency method
references the old enum path; update any imports and example usage to use
d_engine_core::config::ReadConsistencyPolicy instead of
d_engine_proto::client::ReadConsistencyPolicy so callers of get_with_consistency
(the method signature taking ReadConsistencyPolicy) compile against the new core
enum; locate examples around the get_with_consistency function and replace the
import and any fully-qualified references to the old module with
d_engine_core::config::ReadConsistencyPolicy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73dfe28d-8682-4e52-9323-056f1da9be2b

📥 Commits

Reviewing files that changed from the base of the PR and between fe1c3d2 and f508297.

📒 Files selected for processing (2)
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/network/grpc/grpc_raft_service.rs

…pected read payloads; warn on unknown consistency_policy
@JoshuaChi
JoshuaChi force-pushed the refactor/260-d-engine-core-dep branch from f508297 to b41dfa1 Compare May 22, 2026 15:20
@JoshuaChi
JoshuaChi merged commit 0e0c697 into main May 23, 2026
9 checks passed
@JoshuaChi
JoshuaChi deleted the refactor/260-d-engine-core-dep branch May 23, 2026 02:07
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