Skip to content

fix #320, #323: saturating_duration_since in learner promotion + expose ClientApi in embedded mode - #355

Merged
JoshuaChi merged 3 commits into
mainfrom
fix/320-323-quick-fixes
Apr 12, 2026
Merged

JoshuaChi merged 3 commits into
mainfrom
fix/320-323-quick-fixes

Conversation

@JoshuaChi

@JoshuaChi JoshuaChi commented Apr 12, 2026 •

Copy link
Copy Markdown
Contributor

What Does This PR Do?

Two independent small fixes: prevents a potential panic in learner promotion
logic under clock anomalies, and exposes ClientApi directly from
d-engine-server so embedded-mode users don't need a separate dependency.

Type:

  • Bug Fix (with test)

Why Is This Needed?

#320 — saturating_duration_since in learner promotion

Instant::duration_since panics in debug mode if the argument is later
than self (can occur under VM snapshots or certain NTP adjustments).
Replaced with saturating_duration_since at all three call sites in the
stale-learner cleanup path, returning Duration::ZERO instead of panicking.

#323 — expose ClientApi in embedded mode

EmbeddedClient implements ClientApi, but ClientApi itself was not
re-exported from d-engine-server. Embedded-mode users who wanted to use
ClientApi as a trait bound in their own code had to add d-engine-client
as a separate dependency — pulling in the full gRPC client crate just to
access a trait definition. Fixed by re-exporting ClientApi, ClientApiError,
and ClientApiResult directly from d-engine-server.


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: 74 learner-related tests pass (-p d-engine-core learner)
  • Build: d-engine-server compiles cleanly with new re-exports

For bug fixes:


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

Two unrelated fixes bundled to avoid two separate CI runs. Each commit
is self-contained and can be reviewed independently.

Estimated review complexity:

  • Quick (< 100 lines)

Summary by CodeRabbit

  • Bug Fixes

    • Improved time calculations used during learner promotion maintenance to prevent negative durations from affecting cleanup and logging, reducing spurious stale-entry handling and improving stability under clock skew.
  • New Features

    • Exposed a unified client operations API (put/get/delete/CAS/watch) from the server library, simplifying embedded-mode client usage and error handling.

@coderabbitai

coderabbitai Bot commented Apr 12, 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: 08907511-51c1-4948-a41b-f3dc01249ab2

📥 Commits

Reviewing files that changed from the base of the PR and between 65a0f03 and 71a6d32.

📒 Files selected for processing (2)
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-server/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
  • d-engine-core/src/raft_role/leader_state.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • d-engine-server/src/lib.rs

📝 Walkthrough

Walkthrough

Replaces duration_since with saturating_duration_since in stale-learner promotion logic to avoid negative durations, and adds public re-exports (ClientApi, ClientApiError, ClientApiResult) from d_engine_core::client in the server library.

Changes

Cohort / File(s) Summary
Raft Role Duration Handling
d-engine-core/src/raft_role/leader_state.rs
Replaced duration_since with saturating_duration_since for elapsed-time computation in pending-promotion cleanup and stale-entry detection to prevent underflow/negative durations.
Server Public API Exports
d-engine-server/src/lib.rs
Renamed re-export section heading to "Client API" and added pub use d_engine_core::client::{ClientApi, ClientApiError, ClientApiResult}; to expose unified client operation types.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related issues

Poem

🐰 I nibble code and hop with glee,

Clamping time so it stays debris-free,
Client APIs now shown in view,
Small fixes, steady and true.

🚥 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 directly references the two bug fixes (#320 and #323) and accurately summarizes both main changes: using saturating_duration_since in learner promotion and exposing ClientApi in embedded mode.
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 fix/320-323-quick-fixes

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.

🧹 Nitpick comments (1)
d-engine-core/src/raft_role/leader_state.rs (1)

3664-3668: Nice fix; consider reusing the computed duration once.

This is correct as-is. As a small cleanup, compute elapsed once and use it for both logging and the stale-threshold condition.

♻️ Small refactor
-                trace!(
-                    "Inspecting entry: {:?} - {:?} - {:?}",
-                    entry,
-                    now.saturating_duration_since(entry.ready_since),
-                    &config.stale_learner_threshold
-                );
-                if now.saturating_duration_since(entry.ready_since) > config.stale_learner_threshold
-                {
+                let elapsed = now.saturating_duration_since(entry.ready_since);
+                trace!(
+                    "Inspecting entry: {:?} - {:?} - {:?}",
+                    entry,
+                    elapsed,
+                    &config.stale_learner_threshold
+                );
+                if elapsed > config.stale_learner_threshold {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@d-engine-core/src/raft_role/leader_state.rs` around lines 3664 - 3668,
Compute the elapsed duration once into a local variable and reuse it for both
the log and the stale check: replace repeated calls to
now.saturating_duration_since(entry.ready_since) with a single let elapsed =
now.saturating_duration_since(entry.ready_since) and then use elapsed when
comparing to config.stale_learner_threshold and when passing to the logger;
update the references around entry.ready_since, now, and
config.stale_learner_threshold accordingly (e.g., in the block containing the
stale learner condition).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@d-engine-core/src/raft_role/leader_state.rs`:
- Around line 3664-3668: Compute the elapsed duration once into a local variable
and reuse it for both the log and the stale check: replace repeated calls to
now.saturating_duration_since(entry.ready_since) with a single let elapsed =
now.saturating_duration_since(entry.ready_since) and then use elapsed when
comparing to config.stale_learner_threshold and when passing to the logger;
update the references around entry.ready_since, now, and
config.stale_learner_threshold accordingly (e.g., in the block containing the
stale learner condition).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 01b4672e-78a1-47b4-aba6-3216c0612d7d

📥 Commits

Reviewing files that changed from the base of the PR and between 79ef2f9 and 65a0f03.

📒 Files selected for processing (2)
  • d-engine-core/src/raft_role/leader_state.rs
  • d-engine-server/src/lib.rs

@codecov

codecov Bot commented Apr 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
d-engine-core/src/raft_role/leader_state.rs 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

…arner promotion

Instant::duration_since panics if the argument is later than self (e.g.
under VM snapshots or certain NTP adjustments). saturating_duration_since
returns Duration::ZERO instead, making the stale-entry cleanup safe under
clock anomalies.

Three call sites in leader_state.rs: stale retain filter and
stale-detection loop (both trace log and threshold check).
…-engine-server

Embedded-mode users had no way to use ClientApi as a trait bound without
adding d-engine-client as a separate dependency. Since EmbeddedClient
already implements ClientApi, the trait itself should be accessible from
the same crate.
@JoshuaChi
JoshuaChi force-pushed the fix/320-323-quick-fixes branch from 65a0f03 to 71a6d32 Compare April 12, 2026 13:25
@JoshuaChi
JoshuaChi merged commit 8db9bb1 into main Apr 12, 2026
8 of 9 checks passed
@JoshuaChi
JoshuaChi deleted the fix/320-323-quick-fixes branch April 12, 2026 13:52
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