Skip to content

refactor #326: tighten public API surface across d-engine crates - #363

Merged
JoshuaChi merged 2 commits into
mainfrom
refactor/326-public-api
Apr 14, 2026
Merged

JoshuaChi merged 2 commits into
mainfrom
refactor/326-public-api

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

What Does This PR Do?

Tightens the public API surface across all d-engine crates by hiding
internal types, restricting visibility, and removing methods that leaked
implementation details or created semantic traps for downstream users.

Type:

  • Bug Fix (with test)
  • Feature (issue #___ approved)
  • Documentation
  • Test/Coverage
  • Performance (with benchmark)

Why Is This Needed?

For bugs: See #326 for full issue list. Key problems fixed:

  • GrpcClient had three inherent methods (get_linearizable, get_lease,
    get_eventual) returning Option<ClientResult> that silently shadowed the
    ClientApi trait methods returning Option<Bytes> — callers got the wrong
    type depending on whether ClientApi was in scope
  • EmbeddedEngine::node_id() returned client_id instead of the actual node
    ID (wrong field)
  • NodeBuilder::node_config() setter did not sync self.node_id, causing
    nodes to boot with wrong IDs (reproduced as connection timeout in
    test_distributed_lock_standalone)
  • 15 wildcard re-exports in d-engine-core and internal types visible in
    cargo doc made the public API surface appear ~3x larger than intended

Checklist

Required:

  • make test passes
  • Added tests for new code
  • Commits squashed to 1-2 logical units

If changing APIs:

  • Updated relevant docs
  • Explained why complexity is justified

Testing

How tested:

  • Unit tests: existing unit tests pass; test_ready_notifier_independent
    rewritten as test_rpc_ready_and_leader_election_are_independent after
    ready_notifier() was deleted
  • Integration tests: distributed_lock_standalone, snapshot_recovery_standalone,
    leader_failover_standalone — all updated and passing
  • Manual testing: cargo doc --no-deps reviewed to confirm internal types
    no longer appear in generated docs

For bug fixes:

  • NodeBuilder node_id sync bug is covered by existing distributed lock
    integration test (was failing before fix, passes after)

Does This Follow d-engine's Principles?

  • Solves a real problem for most users (not just my edge case)
  • Keeps implementation simple
  • Doesn't bloat the API surface

Reviewer Notes

Breaking changes — see MIGRATION_GUIDE.md for v0.2.3 → v0.2.4:

  1. GrpcClient::get_linearizable/get_lease/get_eventual removed from inherent
    impl; bring ClientApi into scope to use trait methods (return type is now
    Option<Bytes> not Option<ClientResult>)
  2. NodeBuilder::init() → pub(crate); use NodeBuilder::new().node_config()
  3. RaftNode::node_config field → pub(crate); use node.node_id() accessor
  4. RaftNode::ready_notifier(), from_raft() deleted
  5. RaftNode::set_rpc_ready(), is_rpc_ready() → pub(crate)
  6. d-engine-core wildcard re-exports replaced with explicit list; import
    paths unchanged but #[doc(hidden)] on internal types
  7. d-engine-server re-exports HardState, ProstError, SnapshotError
    now #[doc(hidden)]

Estimated review complexity:

  • Quick (< 100 lines)
  • Medium (< 300 lines)
  • Deep (> 300 lines)

Summary by CodeRabbit

  • Breaking Changes

    • Reduced public API surface: several previously exposed internal details and convenience client APIs are no longer publicly available; some client read responses now return raw bytes wrapped in an option.
    • Node lifecycle and configuration surfaces were restricted or removed; builders and constructors updated to new construction flows.
  • Documentation

    • Added a detailed migration guide for v0.2.3 → v0.2.4 with upgrade steps.
  • Tests

    • Updated tests and examples to match the new API shapes and construction patterns.

- d-engine-core: replace wildcard re-exports with explicit user-facing
  list; add #[doc(hidden)] to 11 internal error types; delete dead
  QuorumStatus enum

- d-engine-server: restrict node_config, set_rpc_ready, is_rpc_ready
  to pub(crate); delete unused ready_notifier() and from_raft(); fix
  EmbeddedEngine::node_id() to use stored field instead of client_id;
  add #[doc(hidden)] to HardState, ProstError, SnapshotError re-exports

- d-engine-client: restrict ClientInner and pool re-export to
  pub(crate); remove utils wildcard re-export; delete three inherent
  get_linearizable/get_lease/get_eventual methods that shadowed
  ClientApi trait and returned Option<ClientResult> instead of
  Option<Bytes>

- NodeBuilder: fix node_config() setter bug (missing node_id sync);
  rename init() to pub(crate); integration tests updated to use
  NodeBuilder::new().node_config()

- examples/docs: update service-discovery watcher and dengine_ctl to
  new API; add MIGRATION_GUIDE.md for v0.2.3->v0.2.4 breaking changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR tightens the public API for v0.2.3 → v0.2.4: it hides internal types and modules, removes several public Node/Client convenience methods and fields, moves node-id ownership to EmbeddedEngine, updates NodeBuilder construction, and adjusts tests/examples to the new public surface.

Changes

Cohort / File(s) Summary
Migration Documentation
MIGRATION_GUIDE.md
Added v0.2.3 → v0.2.4 migration notes listing removed public APIs and recommended replacements.
Grpc Client read methods
d-engine-client/src/grpc_client.rs, d-engine-client/src/grpc_client_test.rs
Removed inherent GrpcClient convenience methods `get_linearizable
Client internals & re-exports
d-engine-client/src/lib.rs, d-engine-client/src/utils_test.rs
Made ClientInner and its fields crate-private, changed pool re-export to pub(crate) and removed utils re-export; tests updated to use targeted imports.
Core crate public surface
d-engine-core/src/lib.rs, d-engine-core/src/errors.rs
Marked many internal modules/errors #[doc(hidden)], reorganized re-exports into user-facing vs hidden, and removed the crate-level QuorumStatus enum.
Embedded engine / client node-id
d-engine-server/src/api/embedded.rs, d-engine-server/src/api/embedded_client.rs
Moved node-id into EmbeddedEngine::Inner (stored at start); removed EmbeddedClient::node_id(); adjusted leader checks and docs accordingly.
Server crate re-exports
d-engine-server/src/lib.rs
Changed public re-exports (HardState, ProstError, SnapshotError) to #[doc(hidden)] and removed Raft re-export.
Node builder & Node API visibility
d-engine-server/src/node/builder.rs, d-engine-server/src/node/mod.rs, d-engine-server/src/node/builder_test.rs
Added NodeBuilder::from_node_config(...), made NodeBuilder::init(...) crate-private, synchronized node_id in .node_config(...); reduced visibility of Node::node_config, set_rpc_ready, is_rpc_ready; removed ready_notifier() and from_raft(); added builder tests.
Node tests & readiness behavior
d-engine-server/src/node/node_test.rs
Refactored tests to remove reliance on ready_notifier(), using direct is_rpc_ready() checks and leader-notifier interaction instead.
Test helpers & expectations
d-engine-server/tests/common/mod.rs, d-engine-server/tests/cas_operations/*, d-engine-server/tests/failover_and_recovery/*, d-engine-server/tests/snapshot_recovery_standalone.rs
Updated test config generation and NodeBuilder usage; adjusted many assertions to expect Option<Bytes> (use .as_ref()/direct compare) instead of mapping inner ClientResult.value.
Examples & tooling adjustments
examples/service-discovery-standalone/watcher.rs, examples/three-nodes-standalone/docker/jepsen/vendor/dengine_ctl/src/main.rs
Imported ClientApi where needed and adapted read-call branches and result handling to new Option<Bytes> shape.
Docs / visibility annotations
multiple files (d-engine-core, d-engine-server, etc.)
Applied #[doc(hidden)] to many internal errors and re-exports; shifted several previously public items to hidden or crate visibility.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through code to trim each seam,

hid the bits that broke the dream,
moved the id where engines dwell,
tests adjusted — all is well,
a tidy v0.2.4 hop, hooray!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: tightening the public API surface across d-engine crates by removing/restricting publicly exposed implementation details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/326-public-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 2

🧹 Nitpick comments (3)
d-engine-server/src/lib.rs (1)

129-135: Keep storage support types discoverable.

HardState is still part of the public custom-storage flow via MetaStore, so hiding the crate-root export makes that path harder to follow from docs.rs. If you want it out of the main API section, a documented storage_types module would be easier to discover than a #[doc(hidden)] re-export.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@d-engine-server/src/lib.rs` around lines 129 - 135, Remove the #[doc(hidden)]
on the HardState re-export so it remains discoverable for the public
custom-storage flow (MetaStore); either keep HardState public at crate root or
move the re-exports into a documented module (e.g., pub mod storage_types) and
re-export HardState (and related types like ProstError and SnapshotError) there
with a module-level doc comment so users can find them on docs.rs.
d-engine-core/src/lib.rs (1)

97-125: Move hidden internals behind a dedicated namespace.

#[doc(hidden)] pub use ...::* still leaves these symbols reachable as d_engine_core::..., so external code can keep binding to the old root paths. If the goal is to make the smaller surface real, consider re-exporting them from an internal module and pointing d-engine-server at that instead.

♻️ Possible shape
-#[doc(hidden)]
-pub use commit_handler::*;
-#[doc(hidden)]
-pub use election::*;
+#[doc(hidden)]
+pub mod internal {
+    pub use super::commit_handler::*;
+    pub use super::election::*;
+    // ...other internal-only re-exports...
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@d-engine-core/src/lib.rs` around lines 97 - 125, The current #[doc(hidden)]
pub use ...::* lines leave internals accessible at the crate root (e.g.,
commit_handler, election, event, maybe_clone_oneshot, membership, network,
purge, raft_context, raft_role, replication, state_machine_handler, type_config,
utils); move them behind a dedicated hidden namespace by creating a module
(e.g., #[doc(hidden)] pub mod internal) and re-exporting those modules inside it
(replace root-level pub use with pub use inside internal), then update
d-engine-server to import from d_engine_core::internal::... so external crates
cannot continue to bind to the old root paths.
MIGRATION_GUIDE.md (1)

369-370: Optional docs polish: clarify crate path for ClientApi import.

Line 369 currently shows d_engine_client::ClientApi; consider noting the equivalent facade import (d_engine::client::ClientApi) if both are supported, to reduce migration ambiguity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MIGRATION_GUIDE.md` around lines 369 - 370, Update the docs to clarify the
crate path for ClientApi by noting both import options: the direct crate import
(d_engine_client::ClientApi) and the facade path (d_engine::client::ClientApi),
and show the example line using either form so readers know they can call
client.get_linearizable("key").await? with the ClientApi trait brought into
scope via either import.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@d-engine-server/src/api/embedded_client.rs`:
- Around line 154-158: The # Errors docblocks for the put and delete methods are
too narrow; update the documentation for both methods (the put and delete
functions in embedded_client.rs) to state that errors may include not only
leader/not-leader, closed channel, or timeout, but also RPC-status failures and
other server-side errors (including non-NotLeader errors); apply the same
broadened wording to both methods' # Errors sections so they accurately reflect
RPC and server error surface.

In `@d-engine-server/src/node/builder.rs`:
- Around line 173-176: The constructor that accepts a ready-made RaftNodeConfig
was made crate-private as init, which forces callers to go through
NodeBuilder::new(...).node_config(...) and triggers new()'s expect path; add a
public constructor pub fn from_node_config(node_config: RaftNodeConfig,
shutdown_signal: watch::Receiver<()>) -> Self that performs the same
initialization as init (or factor the shared logic into a private helper used by
both init and the new public from_node_config) so callers can supply a pre-built
RaftNodeConfig without hitting the panic path; update any docs/examples that
referenced init to use RaftNodeBuilder::from_node_config (or the correct public
function name) and ensure symbol names match RaftNodeConfig, NodeBuilder::new,
node_config, and init.

---

Nitpick comments:
In `@d-engine-core/src/lib.rs`:
- Around line 97-125: The current #[doc(hidden)] pub use ...::* lines leave
internals accessible at the crate root (e.g., commit_handler, election, event,
maybe_clone_oneshot, membership, network, purge, raft_context, raft_role,
replication, state_machine_handler, type_config, utils); move them behind a
dedicated hidden namespace by creating a module (e.g., #[doc(hidden)] pub mod
internal) and re-exporting those modules inside it (replace root-level pub use
with pub use inside internal), then update d-engine-server to import from
d_engine_core::internal::... so external crates cannot continue to bind to the
old root paths.

In `@d-engine-server/src/lib.rs`:
- Around line 129-135: Remove the #[doc(hidden)] on the HardState re-export so
it remains discoverable for the public custom-storage flow (MetaStore); either
keep HardState public at crate root or move the re-exports into a documented
module (e.g., pub mod storage_types) and re-export HardState (and related types
like ProstError and SnapshotError) there with a module-level doc comment so
users can find them on docs.rs.

In `@MIGRATION_GUIDE.md`:
- Around line 369-370: Update the docs to clarify the crate path for ClientApi
by noting both import options: the direct crate import
(d_engine_client::ClientApi) and the facade path (d_engine::client::ClientApi),
and show the example line using either form so readers know they can call
client.get_linearizable("key").await? with the ClientApi trait brought into
scope via either import.
🪄 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: c24c746c-8073-43c8-bc1d-0568bd5b74fd

📥 Commits

Reviewing files that changed from the base of the PR and between 5f5be93 and 14962d6.

📒 Files selected for processing (19)
  • MIGRATION_GUIDE.md
  • 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/utils_test.rs
  • d-engine-core/src/errors.rs
  • d-engine-core/src/lib.rs
  • d-engine-server/src/api/embedded.rs
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/lib.rs
  • d-engine-server/src/node/builder.rs
  • d-engine-server/src/node/mod.rs
  • d-engine-server/src/node/node_test.rs
  • d-engine-server/tests/cas_operations/distributed_lock_standalone.rs
  • d-engine-server/tests/cas_operations/snapshot_recovery_standalone.rs
  • d-engine-server/tests/common/mod.rs
  • d-engine-server/tests/failover_and_recovery/leader_failover_standalone.rs
  • examples/service-discovery-standalone/watcher.rs
  • examples/three-nodes-standalone/docker/jepsen/vendor/dengine_ctl/src/main.rs
💤 Files with no reviewable changes (1)
  • d-engine-client/src/grpc_client.rs

Comment thread d-engine-server/src/api/embedded_client.rs Outdated
Comment thread d-engine-server/src/node/builder.rs
@codecov

codecov Bot commented Apr 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
d-engine-server/src/node/builder_test.rs 88.88% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

…r and election config

- Add NodeBuilder::from_node_config() as a panic-safe public constructor
  accepting a pre-built RaftNodeConfig directly, avoiding the implicit
  RaftNodeConfig::new() detour in new() that can panic in environments
  without a default config file

- Add two unit tests (TDD): test_from_node_config_preserves_caller_config
  verifies caller's config is used intact; test_node_config_setter_syncs_node_id
  covers the node_id sync bug fixed in #326

- Update module-level and start() doc examples to reference from_node_config
  instead of the now-crate-private init()

- Broaden # Errors in EmbeddedClient::put() and delete() to include
  state machine server errors

- Add [raft.election] and [retry.election] to create_node_config() and
  create_node_config_with_role() TOML output so embedded integration tests
  get CI-stable election settings (election_timeout_max 3000ms,
  retry.election.timeout_ms 2000ms) instead of narrow defaults that cause
  flaky leader-failover tests on loaded CI machines

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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: 1

🧹 Nitpick comments (1)
d-engine-server/tests/common/mod.rs (1)

132-140: Centralize duplicated election/retry test tuning values.

These values are now duplicated in generated TOML and in node_config(...); extracting shared constants/helper config will reduce drift risk across tests.

Also applies to: 184-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@d-engine-server/tests/common/mod.rs` around lines 132 - 140, Extract the
duplicated election and retry tuning values into shared test constants or a
helper struct (e.g., ELECTION_TIMEOUT_MIN, ELECTION_TIMEOUT_MAX,
RETRY_MAX_RETRIES, RETRY_TIMEOUT_MS, RETRY_BASE_DELAY_MS, RETRY_MAX_DELAY_MS)
and update both the generated TOML snippets and the node_config(...) call to
reference those constants/helper instead of hard-coding the numbers; ensure the
helper is defined in the tests/common/mod.rs module and used in both places so
future changes stay centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@d-engine-server/src/node/builder_test.rs`:
- Around line 285-299: The test test_node_config_setter_syncs_node_id currently
only asserts builder.node_config.cluster.node_id but the comment claims the
internal node_id is synced too; update the test in
NodeBuilder::<MockStorageEngine, MockStateMachine>::new(...) usage to also
assert the builder's internal node_id field (e.g., builder.node_id) equals 7 so
the setter behavior is actually validated; locate the test function
test_node_config_setter_syncs_node_id and add a second assertion that compares
builder.node_id to 7.

---

Nitpick comments:
In `@d-engine-server/tests/common/mod.rs`:
- Around line 132-140: Extract the duplicated election and retry tuning values
into shared test constants or a helper struct (e.g., ELECTION_TIMEOUT_MIN,
ELECTION_TIMEOUT_MAX, RETRY_MAX_RETRIES, RETRY_TIMEOUT_MS, RETRY_BASE_DELAY_MS,
RETRY_MAX_DELAY_MS) and update both the generated TOML snippets and the
node_config(...) call to reference those constants/helper instead of hard-coding
the numbers; ensure the helper is defined in the tests/common/mod.rs module and
used in both places so future changes stay centralized.
🪄 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: 2ba33432-1c72-40c8-b1ed-c8c0bf68a737

📥 Commits

Reviewing files that changed from the base of the PR and between 14962d6 and 7d22377.

📒 Files selected for processing (4)
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/node/builder.rs
  • d-engine-server/src/node/builder_test.rs
  • d-engine-server/tests/common/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • d-engine-server/src/api/embedded_client.rs
  • d-engine-server/src/node/builder.rs

Comment on lines +285 to +299
#[test]
fn test_node_config_setter_syncs_node_id() {
let (_, shutdown_rx) = watch::channel(());
let mut config = RaftNodeConfig::new().unwrap().validate().unwrap();
config.cluster.node_id = 7;

let builder = NodeBuilder::<MockStorageEngine, MockStateMachine>::new(None, shutdown_rx)
.node_config(config);

// node_config on the builder must reflect the new config after setter
assert_eq!(
builder.node_config.cluster.node_id, 7,
"node_config() setter must sync node_id into both node_config and internal node_id field"
);
}

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 | 🟡 Minor

Test does not assert the internal node_id it claims to validate.

The test message says both node_config and internal node_id are synced, but it only checks builder.node_config.cluster.node_id. Add an assertion for builder.node_id (or an equivalent observable) to actually lock this behavior.

Proposed test fix
 #[test]
 fn test_node_config_setter_syncs_node_id() {
     let (_, shutdown_rx) = watch::channel(());
     let mut config = RaftNodeConfig::new().unwrap().validate().unwrap();
     config.cluster.node_id = 7;

     let builder = NodeBuilder::<MockStorageEngine, MockStateMachine>::new(None, shutdown_rx)
         .node_config(config);

     // node_config on the builder must reflect the new config after setter
     assert_eq!(
         builder.node_config.cluster.node_id, 7,
         "node_config() setter must sync node_id into both node_config and internal node_id field"
     );
+    assert_eq!(
+        builder.node_id, 7,
+        "node_config() setter must sync internal node_id field"
+    );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@d-engine-server/src/node/builder_test.rs` around lines 285 - 299, The test
test_node_config_setter_syncs_node_id currently only asserts
builder.node_config.cluster.node_id but the comment claims the internal node_id
is synced too; update the test in NodeBuilder::<MockStorageEngine,
MockStateMachine>::new(...) usage to also assert the builder's internal node_id
field (e.g., builder.node_id) equals 7 so the setter behavior is actually
validated; locate the test function test_node_config_setter_syncs_node_id and
add a second assertion that compares builder.node_id to 7.

@JoshuaChi
JoshuaChi merged commit 91a13a9 into main Apr 14, 2026
7 of 9 checks passed
@JoshuaChi
JoshuaChi deleted the refactor/326-public-api branch April 14, 2026 14:47
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