Skip to content

feat: add ExtendHold to reservation-core#139

Open
skel84 wants to merge 1 commit intomainfrom
feat/reservation-extend-hold
Open

feat: add ExtendHold to reservation-core#139
skel84 wants to merge 1 commit intomainfrom
feat/reservation-extend-hold

Conversation

@skel84
Copy link
Owner

@skel84 skel84 commented Mar 26, 2026

Summary

  • add ExtendHold to reservation-core commands, codecs, snapshot encoding, and live state transitions
  • preserve correct expiry behavior by ignoring stale queued expiry entries after a hold deadline is extended
  • add recovery coverage proving an extended hold survives restart and later logical-slot progress

Verification

  • cargo test -p reservation-core
  • cargo clippy -p reservation-core --all-targets --all-features -- -D warnings
  • cargo fmt --all --check
  • cargo test
  • scripts/check_repo.sh

Refs #128
Closes #129

@coderabbitai
Copy link

coderabbitai bot commented Mar 26, 2026

Summary by CodeRabbit

  • New Features
    • Added functionality to extend hold deadlines with validation ensuring deadlines only move forward in time.
    • Invalid extension requests and expired holds are properly rejected.
    • Extended deadlines are preserved during system recovery and snapshots.

Walkthrough

This PR introduces support for a new ExtendHold command to the reservation system, enabling deadline extensions on existing holds. The implementation includes command definition, binary codec support, state machine logic with validation, snapshot/recovery support, and comprehensive test coverage across the reservation-core crate.

Changes

Cohort / File(s) Summary
Command Definition and Codec
crates/reservation-core/src/command.rs, crates/reservation-core/src/command_codec.rs
Added TAG_EXTEND_HOLD constant and Command::ExtendHold { hold_id, deadline_slot } variant. Implemented binary encoding/decoding with tag ID 6, followed by hold_id (u128 LE) and deadline_slot (u64 LE). Updated codec tests for round-trip validation.
Snapshot Serialization
crates/reservation-core/src/snapshot.rs
Extended snapshot encoding/decoding to handle Command::ExtendHold variant. Updated test snapshot data to include the new command type with proper field serialization.
State Machine Logic
crates/reservation-core/src/state_machine.rs
Implemented apply_extend_hold with comprehensive validation: rejects missing holds, non-Held states, non-increasing deadlines. Auto-expires holds when request slot exceeds deadline. Updated retire_state to check deadline before auto-expiring held items. Added multiple test cases covering success, validation failures, and expiry preservation.
Recovery Tests
crates/reservation-core/src/recovery.rs
Added helper functions (create_pool_request, place_hold_request, extend_hold_request) and new test recovery_preserves_extended_deadline_before_later_request validating WAL recovery correctly preserves extended deadlines across replay scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add ExtendHold to reservation-core' directly and clearly describes the primary change: introducing an ExtendHold command to the reservation-core system.
Description check ✅ Passed The description adequately covers the summary of changes and verification steps, though the template requires Linked Issue, Changes, Validation checklist items, and Docs sections.
Linked Issues check ✅ Passed The PR successfully implements ExtendHold command across codecs, snapshots, and state machine with recovery tests, directly fulfilling #129's requirement to use extracted substrate and maintain correctness.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the ExtendHold command and its supporting infrastructure in reservation-core, with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 feat/reservation-extend-hold

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

@skel84
Copy link
Owner Author

skel84 commented Mar 26, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
crates/reservation-core/src/snapshot.rs (1)

546-558: ⚠️ Potential issue | 🟡 Minor

Make the fixture reflect the post-extension state.

Line 550 still leaves the hold at Slot(5), while Lines 556-558 record a successful ExtendHold to Slot(7). That fixture cannot come from the live state machine, so the restore test no longer validates the extended-deadline state this PR is adding.

🧪 Minimal fix
             holds: vec![HoldRecord {
                 hold_id: HoldId(21),
                 pool_id: PoolId(11),
                 quantity: 2,
-                deadline_slot: Slot(5),
+                deadline_slot: Slot(7),
                 state: HoldState::Held,
             }],
As per coding guidelines, "Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/reservation-core/src/snapshot.rs` around lines 546 - 558, The
fixture's held record does not reflect the successful ExtendHold operation:
update the HoldRecord used in the snapshot (the holds vec containing HoldRecord
with HoldId(21), PoolId(11), etc.) so its deadline_slot is Slot(7) instead of
Slot(5) to match the OperationRecord that records Command::ExtendHold { hold_id:
HoldId(21), deadline_slot: Slot(7) }; ensure HoldState::Held remains unchanged
and no other fields are altered.
🧹 Nitpick comments (1)
crates/reservation-core/src/command_codec.rs (1)

198-205: Keep the old variant coverage when adding the new round-trip case.

Swapping the single test input to ExtendHold drops direct regression coverage for CreatePool. A small table-driven test over all Command variants would keep both existing tags and the new tag pinned.

🧪 Possible expansion
 #[test]
-fn internal_command_round_trips() {
-    let command = Command::ExtendHold {
-        hold_id: HoldId(7),
-        deadline_slot: Slot(9),
-    };
-
-    let decoded = decode_internal_command(&encode_internal_command(command)).unwrap();
-    assert_eq!(decoded, command);
+fn internal_commands_round_trip() {
+    let commands = [
+        Command::CreatePool {
+            pool_id: PoolId(5),
+            total_capacity: 9,
+        },
+        Command::PlaceHold {
+            pool_id: PoolId(5),
+            hold_id: HoldId(6),
+            quantity: 2,
+            deadline_slot: Slot(7),
+        },
+        Command::ConfirmHold { hold_id: HoldId(6) },
+        Command::ReleaseHold { hold_id: HoldId(6) },
+        Command::ExtendHold {
+            hold_id: HoldId(6),
+            deadline_slot: Slot(8),
+        },
+        Command::ExpireHold { hold_id: HoldId(6) },
+    ];
+
+    for command in commands {
+        let decoded = decode_internal_command(&encode_internal_command(command)).unwrap();
+        assert_eq!(decoded, command);
+    }
 }
As per coding guidelines, "Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/reservation-core/src/command_codec.rs` around lines 198 - 205, Replace
the single-case test in internal_command_round_trips with a table-driven loop
that iterates over all Command variants (including CreatePool and the new
ExtendHold), calling encode_internal_command and decode_internal_command for
each and asserting equality to preserve regression coverage; locate the test in
internal_command_round_trips and build a Vec or array of Command instances, then
for each element call
decode_internal_command(&encode_internal_command(cmd)).unwrap() and
assert_eq!(decoded, cmd).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/reservation-core/src/state_machine.rs`:
- Around line 477-479: The code currently always calls
self.push_hold_expiry(hold_id, deadline_slot) after updating hold.deadline_slot
and self.replace_hold(hold), which enqueues a duplicate entry into
hold_retire_queue and can hit RetireQueueError::Full (e.g., max_holds = 1) when
PlaceHold → ExtendHold occurs; change push_hold_expiry to first check for an
existing queued entry for hold_id and either update its deadline in-place or
skip adding a new entry (i.e., deduplicate keyed by hold_id), or provide an API
on the retire queue to reschedule an existing entry instead of pushing; update
the implementation around self.push_hold_expiry/self.replace_hold to reschedule
rather than append, and add a regression test (max_holds = 1) that PlaceHold
followed by ExtendHold does not panic and results in only a single retire entry
for the hold_id.

---

Outside diff comments:
In `@crates/reservation-core/src/snapshot.rs`:
- Around line 546-558: The fixture's held record does not reflect the successful
ExtendHold operation: update the HoldRecord used in the snapshot (the holds vec
containing HoldRecord with HoldId(21), PoolId(11), etc.) so its deadline_slot is
Slot(7) instead of Slot(5) to match the OperationRecord that records
Command::ExtendHold { hold_id: HoldId(21), deadline_slot: Slot(7) }; ensure
HoldState::Held remains unchanged and no other fields are altered.

---

Nitpick comments:
In `@crates/reservation-core/src/command_codec.rs`:
- Around line 198-205: Replace the single-case test in
internal_command_round_trips with a table-driven loop that iterates over all
Command variants (including CreatePool and the new ExtendHold), calling
encode_internal_command and decode_internal_command for each and asserting
equality to preserve regression coverage; locate the test in
internal_command_round_trips and build a Vec or array of Command instances, then
for each element call
decode_internal_command(&encode_internal_command(cmd)).unwrap() and
assert_eq!(decoded, cmd).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 912c9dea-3d58-45b9-be0d-ea96c35137e2

📥 Commits

Reviewing files that changed from the base of the PR and between 86ed8da and 480b4cb.

📒 Files selected for processing (5)
  • crates/reservation-core/src/command.rs
  • crates/reservation-core/src/command_codec.rs
  • crates/reservation-core/src/recovery.rs
  • crates/reservation-core/src/snapshot.rs
  • crates/reservation-core/src/state_machine.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: semgrep-cloud-platform/scan
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage.
Add extensive logging where it materially improves debuggability or operational clarity. Use the right log level: error for invariant breaks, corruption, and failed operations that require intervention; warn for degraded but expected conditions such as overload, lag, or rejected requests; info for meaningful lifecycle and state-transition events; debug for detailed execution traces useful in development; trace only for very high-volume diagnostic detail.
Logging must be structured and purposeful. Do not add noisy logs that obscure signal or hide bugs.

Files:

  • crates/reservation-core/src/command_codec.rs
  • crates/reservation-core/src/command.rs
  • crates/reservation-core/src/snapshot.rs
  • crates/reservation-core/src/recovery.rs
  • crates/reservation-core/src/state_machine.rs
🔇 Additional comments (5)
crates/reservation-core/src/command.rs (1)

8-8: LGTM.

The new tag and explicit Command::ExtendHold variant keep the command surface and wire tags aligned.

Also applies to: 47-50

crates/reservation-core/src/snapshot.rs (1)

369-376: LGTM.

ExtendHold is encoded and decoded with the same field order, so snapshot round-tripping stays symmetric.

Also applies to: 402-405

crates/reservation-core/src/command_codec.rs (1)

82-89: LGTM.

The new wire tag is serialized and parsed consistently, so ExtendHold round-trips cleanly through the codec.

Also applies to: 115-118

crates/reservation-core/src/state_machine.rs (1)

732-734: Good stale-expiry guard.

This check prevents an old queued deadline from expiring a hold before its current deadline_slot.

crates/reservation-core/src/recovery.rs (1)

306-339: Nice recovery regression.

The helpers keep the scenario deterministic, and the new test proves an extended hold survives replay plus later logical-slot advancement.

Also applies to: 521-620

Comment on lines +477 to +479
hold.deadline_slot = deadline_slot;
self.replace_hold(hold);
self.push_hold_expiry(hold_id, deadline_slot);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not enqueue a second expiry entry for the same hold.

Line 479 appends another retire entry even though the old deadline entry is still in hold_retire_queue. Because that queue is allocated with max_holds, the sequence PlaceHoldExtendHold already hits the RetireQueueError::Full panic path when max_holds = 1, and the same thing happens any time the queue is otherwise full. This needs an in-place reschedule or dedup keyed by hold_id, not an additional queue entry. Please also pin it with a max_holds = 1 regression.

As per coding guidelines, "Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage."

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

In `@crates/reservation-core/src/state_machine.rs` around lines 477 - 479, The
code currently always calls self.push_hold_expiry(hold_id, deadline_slot) after
updating hold.deadline_slot and self.replace_hold(hold), which enqueues a
duplicate entry into hold_retire_queue and can hit RetireQueueError::Full (e.g.,
max_holds = 1) when PlaceHold → ExtendHold occurs; change push_hold_expiry to
first check for an existing queued entry for hold_id and either update its
deadline in-place or skip adding a new entry (i.e., deduplicate keyed by
hold_id), or provide an API on the retire queue to reschedule an existing entry
instead of pushing; update the implementation around
self.push_hold_expiry/self.replace_hold to reschedule rather than append, and
add a regression test (max_holds = 1) that PlaceHold followed by ExtendHold does
not panic and results in only a single retire entry for the hold_id.

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.

M14-T01: Build one new engine or engine slice against the extracted substrate

1 participant