Skip to content

Fix commands that modifies the objects into Deleted state#256

Merged
MrGuin merged 6 commits into
mainfrom
fix_cmd_delete
Dec 5, 2025
Merged

Fix commands that modifies the objects into Deleted state#256
MrGuin merged 6 commits into
mainfrom
fix_cmd_delete

Conversation

@MrGuin

@MrGuin MrGuin commented Dec 2, 2025

Copy link
Copy Markdown
Collaborator
  • Add a DEL command to log and forward a DEL command to standby nodes when a command deletes the object.
  • Update CcEntry and CcPage's last_dirty_ts with KeyObjectStandbyForwardCc.commit_ts so that CcPages with buffered commands won't be skipped by SnapshotSync.

Related PR: eloqdata/eloqkv#319
Closes eloqdata/eloqkv#318, https://github.com/eloqdata/project_tracker/issues/72 and https://github.com/eloqdata/project_tracker/issues/82.

Summary by CodeRabbit

  • New Features

    • System now tracks and propagates object deletions so deletes (including TTL-expired items) are forwarded and retired as overwrites when appropriate.
  • Bug Fixes

    • Modification flow now clearly distinguishes deletes vs writes and consistently advances commit timestamps across execution and forwarding paths.
    • TTL-expiry handling now reliably triggers delete semantics during forwarding and retirement.
  • Chores

    • Adjusted forward-path behavior for consistent handling of delete transitions.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 2, 2025

Copy link
Copy Markdown

Walkthrough

Adds explicit delete tracking via a new ObjectCommandResult::object_deleted_ flag and ExecResult::Delete; changes object post-processing to treat deleted or TTL‑expired objects as retire cases, gate writes for deleted objects, forward delete transitions as overwrite‑retire commands via AddOverWriteCommand, and propagate commit_ts using max() in multiple paths.

Changes

Cohort / File(s) Summary
Object result & exec enum
tx_service/include/tx_operation_result.h, tx_service/include/tx_command.h
Add bool object_deleted_ to ObjectCommandResult (initialized in Reset). Add Delete member to ExecResult enum and fix comment typo.
Object command processing & forwarding
tx_service/include/cc/object_cc_map.h, tx_service/src/tx_execution.cpp
Propagate object_deleted_ from command results; treat TTL‑expired OR deleted as retire cases; skip adding normal write commands when object_deleted; when forwarding, convert delete/ttl‑expire retire commands to overwrite‑retire; update commit_ts := max(current, underlying.CommitTs()) at relevant points.
Standby forwarding API & behavior
tx_service/include/standby.h, tx_service/src/standby.cpp
Replace AddTxCommand with AddOverWriteCommand. New method asserts cmd->IsOverwrite(), sets has_overwrite, clears prior cmd list, serializes and appends the single overwrite command (single‑command overwrite contract).
Command-set typing
tx_service/include/command_set.h
Replace auto with explicit reference type std::unordered_map<CcEntryAddr, CmdSetEntry> &table_cmd_set in AddObjectCommand local binding.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant TxExec as TxExecution
    participant CcMap as ObjectCcMap
    participant Standby as StandbyForwardEntry
    participant Store as StorageLayer

    Client->>TxExec: Execute object op (Write/Delete or TTL event)
    TxExec->>CcMap: Apply ObjectCommandOp -> ObjectCommandResult (includes object_deleted_)
    CcMap->>CcMap: Determine ttl_expired_ or object_deleted_
    alt ttl_expired_ or object_deleted_
        CcMap->>Standby: Create retire overwrite command
        Standby->>Standby: AddOverWriteCommand(cmd) — assert overwrite, set has_overwrite, clear list, append single serialized cmd
        Standby->>Store: Forward overwrite-retire to standby nodes
    else modified (not deleted)
        CcMap->>TxExec: AddObjectCommand to command set (write logged)
        TxExec->>Standby: Forward normal tx command(s)
    end
    Note right of CcMap: commit_ts := max(commit_ts, underlying.CommitTs())
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Verify initialization and uses of object_deleted_ across single- and multi-object paths in tx_execution.cpp.
  • Confirm all call sites updated for AddOverWriteCommand and overwrite semantics (clearing prior cmd_list, single serialized cmd).
  • Check correctness where commit_ts is merged via max() with underlying CCE CommitTs(), and ensure no regressions in timestamp ordering.
  • Audit switch/case and serialization uses of the new ExecResult::Delete.

Possibly related PRs

Poem

🐇 I hop through flags and tidy trace,

object_deleted clears a space,
Overwrite hops, old commands fall,
Standbys learn to heed the call,
Commit timestamps climb, keeping pace.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The description covers key objectives but is missing required checklist items from the template (tests, documentation confirmation, test suite pass confirmation). Complete the pre-submission checklist by confirming test coverage, documentation updates, and passing the required test suite (./mtr --suite=mono_main,mono_multi,mono_basic).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix commands that modifies the objects into Deleted state' clearly describes the main change: handling and fixing the behavior of commands that delete objects.
Linked Issues check ✅ Passed The changes implement object deletion tracking and proper forwarding of delete commands to standby nodes, which addresses the core issue where SETRANGE with empty values incorrectly deletes keys [#318].
Out of Scope Changes check ✅ Passed All changes relate to tracking object deletions, forwarding delete commands, and updating commit timestamps for standby synchronization—all directly supporting the fix for the SETRANGE behavior issue [#318].
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix_cmd_delete

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 (2)
src/tx_execution.cpp (2)

6729-6821: Single-object path: deletion handling and logging behavior look consistent with intent

The introduction of object_deleted and the new conditions:

  • Using ttl_expired || object_deleted before enqueuing RetireExpiredTTLObjectCommand, and
  • Skipping the main AddObjectCommand when object_deleted is true,

ensure that a full-object delete results in only the retire/delete command being logged/forwarded, and not an additional “normal” object-modifying command for the same key. This matches the PR goal of emitting a dedicated DEL/retire command to standbys instead of replaying the original command, while still preserving the existing TTL-expiry path for non-deletion commands.

Two minor follow-ups:

  • The comment above the if still only mentions TTL expiry; consider updating it so it also covers the object_deleted case for future readers.
  • Please double‑check that RetireExpiredTTLObjectCommand() indeed generates the correct command for both TTL-expired and explicit-delete cases, and that tests cover: (a) TTL expiry without delete, (b) explicit delete without TTL expiry, and (c) both combined.
-        if (lock_acquired == LockType::WriteLock)
-        {
-            // If the cce is expired before the command is applyed
-            // Add a retire command before the write command
-            if (ttl_expired || object_deleted)
+        if (lock_acquired == LockType::WriteLock)
+        {
+            // If the cce is expired before the command is applied or the
+            // command deletes the object, add a retire/delete command
+            // before any normal write command.
+            if (ttl_expired || object_deleted)

7215-7251: Multi-object path: per-key delete semantics aligned with single-object behavior

For MultiObjectCommandOp::PostProcess, the new logic:

  • Emitting a retire/delete command when cmd_res.ttl_expired_ || cmd_res.object_deleted_, and
  • Guarding the subsequent AddObjectCommand with !cmd_res.object_deleted_,

brings the multi-key flow in line with the single-key flow and prevents logging a normal modifying command for keys whose objects are fully deleted. That should avoid replaying non-applicable commands on standbys while still logging TTL-expiry retire commands where appropriate.

Same minor cleanup and verification notes as in the single-object path:

  • The comment above the if (cmd_res.ttl_expired_ || cmd_res.object_deleted_) block still only talks about TTL expiry; updating it to explicitly mention deletion would reduce confusion.
  • Please confirm via tests that multi-key operations spanning a mix of (a) normal updates, (b) TTL-expiry, and (c) deletes produce the expected sequence of logged/forwarded commands per key.
-                // If the cce is expired before the command is applyed
-                // Add a retire command before the write command.
-                // If the cmd will delete the object, just add a delete
-                // command.
-                if (cmd_res.ttl_expired_ || cmd_res.object_deleted_)
+                // If the cce is expired before the command is applied, or the
+                // command deletes the object, add a retire/delete command for
+                // this key instead of a normal write command.
+                if (cmd_res.ttl_expired_ || cmd_res.object_deleted_)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83622e6 and e664942.

📒 Files selected for processing (7)
  • include/cc/object_cc_map.h (6 hunks)
  • include/command_set.h (1 hunks)
  • include/standby.h (1 hunks)
  • include/tx_command.h (1 hunks)
  • include/tx_operation_result.h (2 hunks)
  • src/standby.cpp (1 hunks)
  • src/tx_execution.cpp (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
include/standby.h (1)
src/standby.cpp (2)
  • AddOverWriteCommand (53-62)
  • AddOverWriteCommand (53-53)
🔇 Additional comments (11)
include/command_set.h (1)

71-72: Explicit type specification is a minor clarity improvement.

The change from auto& to explicit std::unordered_map<CcEntryAddr, CmdSetEntry>& makes the type immediately visible without needing to trace table_it->second. While functionally equivalent, this improves code readability.

include/tx_command.h (2)

55-55: Typo fix: "Succes" → "Success".

Good catch on the typo.


56-56: All ExecResult values, including Delete, are already properly handled throughout the codebase.

The ExecResult enum has six values (Fail, Read, Write, Delete, Block, Unlock), and all are accounted for in the command execution handler at include/cc/object_cc_map.h:1013-1165:

  • Delete and Write are evaluated as modification flags (lines 1024-1025, 1065-1066)
  • Block is explicitly checked and returns early (line 1142)
  • Unlock is explicitly checked (line 1161)
  • Fail and Read are handled implicitly via the default control flow

No changes are needed.

include/tx_operation_result.h (2)

1007-1007: Proper initialization of object_deleted_ in Reset().

The new object_deleted_ flag is correctly initialized to false in the Reset() method, ensuring clean state between operations.


1027-1033: New object_deleted_ field complements object_modified_ for deletion tracking.

The addition of object_deleted_ alongside object_modified_ provides clear semantics:

  • object_modified_: Command updates the object (write set/log)
  • object_deleted_: Command deletes the object

This separation enables proper handling of deletion scenarios in standby forwarding and retire command logic.

include/standby.h (1)

48-48: New AddOverWriteCommand method provides clear semantics for overwrite/delete scenarios.

The addition of AddOverWriteCommand(TxCommand *cmd) alongside the existing AddTxCommand(ApplyCc &cc_req) creates a dedicated entry point for overwrite/delete commands. The naming clearly communicates the method's purpose for forwarding retire/overwrite commands to standby nodes.

src/standby.cpp (1)

53-62: AddOverWriteCommand implementation correctly enforces overwrite-only semantics.

The implementation properly:

  1. Validates input with assert(cmd->IsOverwrite()) - appropriate for internal API
  2. Sets the has_overwrite protocol flag
  3. Clears existing commands via req.clear_cmd_list() - correct for overwrite semantics where the new command replaces all previous commands
  4. Serializes and appends the overwrite command

The clear_cmd_list() call is intentional and correct: overwrite commands (including deletions) should replace any accumulated commands rather than append to them.

include/cc/object_cc_map.h (4)

132-133: LGTM!

Clean addition of object_deleted tracking alongside the existing object_modified pattern. The binding follows the established convention in this method.


921-926: LGTM!

Using AddOverWriteCommand for the retire command correctly signals object deletion semantics to standby nodes during TTL expiration handling.


1024-1043: LGTM!

The deletion tracking logic correctly identifies when a command results in object deletion and routes the appropriate retire command to standby nodes for cleanup.


1069-1094: LGTM!

The logic correctly handles the sequence where TTL reset recovery command may be followed by a deletion retire command, with normal write forwarding as the fallback. The conditional structure properly ensures deleted objects get retire commands forwarded to standby nodes.

Comment thread tx_service/include/cc/object_cc_map.h Outdated
Comment thread tx_service/include/cc/object_cc_map.h Outdated
// skipped by checkpointer.
commit_ts = cce->CommitTs();
// Update dirty_commit_ts with the req.CommitTs().
commit_ts = std::max(commit_ts, cce->CommitTs());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this should be applied to ReplayLogCc as well

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It does already.

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

Caution

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

⚠️ Outside diff range comments (1)
tx_service/src/tx_execution.cpp (1)

1086-1095: Fix TX_TRACE callsites with wrong identifiers / missing semicolon (won’t compile as-is)

There are a few obvious TX_TRACE issues that will break compilation or logging:

  • Line 1086–1095, ProcessTxRequest(ReloadCacheTxRequest &req): the trace macro passes &fi_req, which is not in scope; this should be &req.
  • Line 7768–7774, PostProcess(BatchReadOperation &batch_read_op): the trace macro passes &read, which is not in scope; this should be &batch_read_op.
  • Line 7530–7541, PostProcess(KickoutDataAllOp &kickout_data_all_op): the lambda’s return ...append(std::to_string(this->tx_term_)) line is missing the terminating semicolon, unlike other TX_TRACE lambdas.

Suggested fix:

void TransactionExecution::ProcessTxRequest(ReloadCacheTxRequest &req)
{
-    TX_TRACE_ACTION_WITH_CONTEXT(
-        this,
-        &fi_req,
+    TX_TRACE_ACTION_WITH_CONTEXT(
+        this,
+        &req,
         [this]() -> std::string
         {
             return std::string("\"tx_number\":")
                 .append(std::to_string(this->TxNumber()))
                 .append("\"tx_term\":")
                 .append(std::to_string(this->tx_term_));
         });
    ...
}
void TransactionExecution::PostProcess(BatchReadOperation &batch_read_op)
{
-    TX_TRACE_ACTION_WITH_CONTEXT(
-        this,
-        &read,
+    TX_TRACE_ACTION_WITH_CONTEXT(
+        this,
+        &batch_read_op,
         [this]() -> std::string
         {
             return std::string("\"tx_number\":")
                 .append(std::to_string(this->TxNumber()))
                 .append("\"tx_term\":")
                 .append(std::to_string(this->tx_term_));
         });
    ...
}
void TransactionExecution::PostProcess(KickoutDataAllOp &kickout_data_all_op)
{
     TX_TRACE_ACTION_WITH_CONTEXT(
         this,
         &kickout_data_all_op,
         [this]() -> std::string
         {
             return std::string("\"tx_number\":")
                 .append(std::to_string(this->TxNumber()))
-                .append("\"tx_term\":")
-                .append(std::to_string(this->tx_term_))
-        });
+                .append("\"tx_term\":")
+                .append(std::to_string(this->tx_term_));
+        });
     state_stack_.pop_back();
}

These are critical to fix even if they predate this PR, otherwise the translation unit will not compile.

Also applies to: 7530-7541, 7768-7774

♻️ Duplicate comments (1)
tx_service/include/cc/object_cc_map.h (1)

1065-1067: Convert LOG(INFO) to DLOG(INFO) to avoid production log spam.

This logging statement executes for every object modification and appears to be debug logging.

Apply this diff:

             object_deleted = exec_rst == ExecResult::Delete;
             object_modified = object_deleted || exec_rst == ExecResult::Write;
-            LOG(INFO) << "object_deleted: " << object_deleted;
+            DLOG(INFO) << "object_deleted: " << object_deleted;
🧹 Nitpick comments (2)
tx_service/include/tx_command.h (1)

51-59: Consider pinning explicit ExecResult numeric values to avoid ABI/wire changes

ExecResult::Delete is inserted between Write and Block without explicit underlying values, which shifts the numeric codes for Block and Unlock. If ExecResult is ever serialized by value (RPC, logs that are parsed, persisted state, metrics labels by cast, etc.), this becomes a silent protocol/ABI change.

To make future additions safe and keep current behavior predictable, consider assigning explicit values and, if needed, preserving historical codes for existing states, e.g.:

enum class ExecResult : uint8_t {
    Fail   = 0,  // Failed to execute command
    Read   = 1,  // Success to execute readonly command
    Write  = 2,  // Success to execute the command and modified object.
    Block  = 3,  // The command is blocked
    Unlock = 4,  // There has not expected result and release ccentry lock
    Delete = 5,  // The object will be deleted by the command.
};

(Or any explicit mapping that matches your existing deployments’ expectations.)

tx_service/include/standby.h (1)

46-50: Minor naming consistency: AddOverWriteCommand vs IsOverwrite

NIT: to align with TxCommand::IsOverwrite(), consider renaming this to AddOverwriteCommand (single word) for consistency and searchability across the codebase.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77d1ffb and 9601b27.

📒 Files selected for processing (7)
  • tx_service/include/cc/object_cc_map.h (6 hunks)
  • tx_service/include/command_set.h (1 hunks)
  • tx_service/include/standby.h (1 hunks)
  • tx_service/include/tx_command.h (1 hunks)
  • tx_service/include/tx_operation_result.h (2 hunks)
  • tx_service/src/standby.cpp (1 hunks)
  • tx_service/src/tx_execution.cpp (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tx_service/include/command_set.h
🧰 Additional context used
🧬 Code graph analysis (1)
tx_service/include/standby.h (1)
tx_service/src/standby.cpp (2)
  • AddOverWriteCommand (53-62)
  • AddOverWriteCommand (53-53)
🔇 Additional comments (9)
tx_service/include/tx_operation_result.h (1)

998-1033: Delete-tracking flag integration in ObjectCommandResult looks consistent

Resetting object_deleted_ alongside object_modified_ and documenting both flags keeps the result object internally coherent and makes delete-aware flows easier to reason about. As you wire this through execution paths, just ensure callers maintain a clear invariant (e.g., whether delete operations should also mark object_modified_ or rely solely on object_deleted_ for logging/forwarding decisions).

tx_service/src/standby.cpp (1)

53-61: AddOverWriteCommand enforces a single-command overwrite; confirm this matches all callsites’ expectations

AddOverWriteCommand asserts cmd->IsOverwrite(), sets has_overwrite, and clears cmd_list before serializing the command. That effectively discards any commands previously queued on this StandbyForwardEntry and makes the overwrite the sole forwarded command.

If the intent is “this overwrite/delete supersedes any earlier buffered commands for the object,” this is perfect. If there are code paths that may call AddTxCommand() first (e.g., buffer non-overwrite ops) and then add an overwrite/delete that should logically coexist in the same forward batch, those earlier commands will be silently dropped.

Might be worth double-checking the new callsites to ensure this “clear then append” semantics are always desired.

tx_service/src/tx_execution.cpp (2)

6767-6846: Object deletion handling in PostProcess(ObjectCommandOp) looks consistent

Using object_deleted to (a) include deletes in the ttl_expired || object_deleted retire path and (b) skip adding the primary command to cmd_set_ when the object is deleted ensures that WAL/forwarding only sees the canonical delete/retire command, which aligns with the new delete semantics. No changes needed here.


7261-7301: Multi-object delete semantics mirror single-object path correctly

Extending the retire path to cmd_res.ttl_expired_ || cmd_res.object_deleted_ and guarding the main AddObjectCommand with !cmd_res.object_deleted_ provides the same “log only the canonical delete/retire command” behavior for multi-object commands as for single-object, while still preserving TTL-expired handling and OCC checks. This looks correct.

tx_service/include/cc/object_cc_map.h (5)

130-134: LGTM!

The addition of object_deleted reference follows the existing pattern for object_modified and properly exposes the new field for tracking delete transitions.


921-926: LGTM!

Using AddOverWriteCommand for TTL-expired cleanup ensures standby nodes properly clear the object state via the overwrite flow.


1024-1043: LGTM!

The delete detection and conditional forwarding logic correctly routes delete operations through AddOverWriteCommand while continuing to use AddTxCommand for non-delete modifications.


1074-1094: Verify: Can ttl_reset and object_deleted both be true?

If a command both triggers TTL reset (line 1074) and deletes the object (line 1083), both AddOverWriteCommand(recover_command) and AddOverWriteCommand(retire_command.get()) will execute. This would forward a recover command followed immediately by a delete command, which seems redundant.

Consider whether object_deleted should skip the TTL reset recovery path, or if this combination is intentionally allowed for correctness on standby nodes.


2005-2017: LGTM!

Taking the maximum of commit_ts and cce->CommitTs() ensures that when buffered commands advance the entry beyond the incoming request's timestamp, the dirty tracking correctly reflects the actual committed version. This prevents CcPages with buffered commands from being skipped during SnapshotSync.

@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

Caution

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

⚠️ Outside diff range comments (2)
tx_service/src/tx_execution.cpp (1)

7246-7305: Assert guarding needs refinement to match single-object path behavior

In the multi-object PostProcess path, the assert at line 7269 is guarded by ttl_expired_ || object_deleted_:

if (cmd_res.ttl_expired_ || cmd_res.object_deleted_)
{
    assert(cmd_res.ttl_ == UINT64_MAX);
    // ...
}

However, the single-object PostProcess path (line 6795+) does not include this assert in the equivalent block—it only asserts ttl == UINT64_MAX within the ttl_reset condition (line 6816). This inconsistency suggests the assert in the multi-object path may be overly strict:

  1. If object_deleted_ can be true with ttl_ values other than UINT64_MAX, this assert will break debug builds. The execution layer should either:

    • Restrict the assert to TTL-expiry only:
      if (cmd_res.ttl_expired_) {
          assert(cmd_res.ttl_ == UINT64_MAX);
      }
      or
    • Ensure deletes always carry ttl_ == UINT64_MAX before reaching this point.
  2. The comment "add a retire/delete command for this key instead of a normal write command" is now misleading. When ttl_expired_ == true && object_deleted_ == false, a normal command is still added. Clarify that TTL expiry adds both retire and normal commands, while explicit deletes add only the retire command.

tx_service/include/cc/object_cc_map.h (1)

1073-1093: Both ttl_reset_ and object_deleted can execute simultaneously, sending contradictory commands to standby nodes.

The current structure allows both the ttl_reset_ block (line 1073) and the object_deleted block (line 1082) to execute in the same code path because the second is an independent if-else rather than else-if. When both conditions are true, standby nodes receive both RecoverTTLObjectCommand (line 1079) and RetireExpiredTTLObjectCommand (line 1088)—operations that semantically contradict each other.

Restructure to enforce mutual exclusivity:

if (obj_result.ttl_reset_) {
    auto recover_command = cmd->RecoverTTLObjectCommand();
    forward_entry->AddOverWriteCommand(recover_command);
} else if (object_deleted) {
    auto retire_command = cmd->RetireExpiredTTLObjectCommand();
    forward_entry->AddOverWriteCommand(retire_command.get());
} else {
    forward_entry->AddTxCommand(req);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9601b27 and 173a8aa.

📒 Files selected for processing (2)
  • tx_service/include/cc/object_cc_map.h (6 hunks)
  • tx_service/src/tx_execution.cpp (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tx_service/include/cc/object_cc_map.h (1)
tx_service/include/tx_command.h (1)
  • ExecResult (51-260)
🔇 Additional comments (3)
tx_service/src/tx_execution.cpp (1)

6753-6850: Delete-aware single-object command handling looks correct; please verify RetireExpiredTTLObjectCommand() semantics

The new object_deleted flag and the updated logic in the write-lock branch do what you want:

  • For ttl_expired || object_deleted, you emit a retire/delete command via RetireExpiredTTLObjectCommand() before any “normal” write.
  • For deleted objects, you now skip the normal AddObjectCommand entirely (if (!object_deleted)), so the log/forward path won’t see a non-DEL command for a deleted object.
  • For non-deleting writes with TTL expiry, you still log both the retire command and the real command, which preserves the old TTL semantics.

This matches the PR’s goal of making deletes explicit and avoiding writing normal object commands for already-deleted objects.

One thing to double-check: RetireExpiredTTLObjectCommand() is now used for both TTL expiry and logical deletes. That helper must generate the appropriate DEL/retire representation for delete-causing commands (e.g., empty SETRANGE) so that standbys and snapshot sync see a proper delete, not just a TTL-retire pattern. If it still assumes “TTL expired” only, you may need to branch inside that helper based on the new ExecResult::Delete / object_deleted_ semantics.

tx_service/include/cc/object_cc_map.h (2)

133-133: LGTM!

The object_deleted reference extraction follows the same pattern as object_modified and enables clean tracking of deletion state throughout the Execute method.


1078-1079: No action needed — the calling patterns are correct and necessary.

Line 1079 passes recover_command directly to AddOverWriteCommand, while lines 925, 1036, and 1088 use .get(). This is not an inconsistency: RecoverTTLObjectCommand() returns TxCommand* (raw pointer), while RetireExpiredTTLObjectCommand() returns std::unique_ptr<TxCommand>. Since AddOverWriteCommand expects TxCommand*, line 1079 correctly passes the raw pointer directly, and the other lines correctly extract the raw pointer from the unique_ptr using .get(). The different calling patterns are appropriate given the different return types.

Likely an incorrect or invalid review comment.

Comment thread tx_service/include/cc/object_cc_map.h

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

♻️ Duplicate comments (1)
tx_service/include/cc/object_cc_map.h (1)

2410-2418: Verify timestamp consistency with KeyObjectStandbyForwardCc.

This is a follow-up to the past review comment. KeyObjectStandbyForwardCc (line 2007) now uses std::max(commit_ts, cce->CommitTs()) before updating last_dirty_commit_ts_ to account for buffered commands that may have advanced the entry's timestamp.

However, ReplayLogCc at lines 2412-2418 still uses the original commit_ts directly. After EmplaceAndCommitBufferedTxnCommand (line 2379), cce->CommitTs() may exceed commit_ts, potentially causing last_dirty_commit_ts_ to be set to a stale value.

If the behavior is intentionally different for replay scenarios, a brief comment explaining why would be helpful.

🧹 Nitpick comments (1)
tx_service/include/standby.h (1)

46-49: Clarify AddOverWriteCommand’s destructive contract in the API.

From the implementation, this method asserts cmd->IsOverwrite(), sets has_overwrite, clears any existing cmd_list, and then appends a single serialized command. That “single overwrite that discards prior commands” contract isn’t obvious from the declaration alone.

Consider:

  • Adding a brief comment here documenting that this replaces any previously queued commands for the entry.
  • Optionally normalizing the spelling to AddOverwriteCommand for consistency with IsOverwrite().

Based on relevant code snippet in tx_service/src/standby.cpp.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 173a8aa and a9319bc.

📒 Files selected for processing (7)
  • tx_service/include/cc/object_cc_map.h (6 hunks)
  • tx_service/include/command_set.h (1 hunks)
  • tx_service/include/standby.h (1 hunks)
  • tx_service/include/tx_command.h (1 hunks)
  • tx_service/include/tx_operation_result.h (2 hunks)
  • tx_service/src/standby.cpp (1 hunks)
  • tx_service/src/tx_execution.cpp (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tx_service/include/tx_operation_result.h
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T03:56:58.811Z
Learnt from: thweetkomputer
Repo: eloqdata/tx_service PR: 150
File: include/cc/local_cc_shards.h:626-631
Timestamp: 2025-10-09T03:56:58.811Z
Learning: For the LocalCcShards class in include/cc/local_cc_shards.h: Writer locks (unique_lock) should continue using the original meta_data_mux_ (std::shared_mutex) rather than fast_meta_data_mux_ (FastMetaDataMutex) at this stage. Only reader locks may use the FastMetaDataMutex wrapper.

Applied to files:

  • tx_service/include/cc/object_cc_map.h
🧬 Code graph analysis (1)
tx_service/include/standby.h (1)
tx_service/src/standby.cpp (2)
  • AddOverWriteCommand (53-62)
  • AddOverWriteCommand (53-53)
🔇 Additional comments (10)
tx_service/include/command_set.h (1)

70-72: Explicit map type binding in AddObjectCommand looks good.

Using an explicit std::unordered_map<CcEntryAddr, CmdSetEntry> & instead of auto & is a no-op behaviorally and improves readability/tooling; no issues here.

tx_service/include/tx_command.h (1)

51-59: No action required—ExecResult::Delete is already properly integrated.

The new Delete enum member is already fully handled throughout the codebase. All existing references to ExecResult use safe enum comparisons (not numeric ordinals), and the Delete case is correctly handled in object_cc_map.h (lines 1024–1025, 1065–1066), where deletion is detected and triggers appropriate retirement command logic. No serialization dependencies or switch statements over ExecResult were found.

tx_service/src/standby.cpp (1)

53-62: AddOverWriteCommand correctly enforces overwrite semantics for TTL-expiry and recovery operations.

The implementation is appropriate and well-guarded:

  • All four call sites pass commands from RetireExpiredTTLObjectCommand() or RecoverTTLObjectCommand() methods—factory methods specifically designed for overwrite scenarios.
  • Call contexts confirm usage only during TTL expiry handling ("Forward retire command to standby node to clear the object").
  • assert(cmd->IsOverwrite()) provides development-time validation.
  • clear_cmd_list() correctly discards prior buffered commands, contrasting with AddTxCommand which appends without clearing.

The destructive semantics align with intended usage: retire and recover operations should supersede any prior commands for the object in standby forwarding.

tx_service/src/tx_execution.cpp (2)

6753-6847: Delete-aware logging in PostProcess(ObjectCommandOp) looks correct

Using object_deleted to (a) emit a retire/delete command when the command deletes the object, and (b) suppress the normal AddObjectCommand for that key ensures that WAL/forwarding only replays a delete for logically deleted objects, while still handling TTL-expiry via ttl_expired. This matches the PR intent and avoids replaying a stale “normal” command on a key that no longer exists.


7246-7300: Confirm ttl_ invariant now also holds for deleted objects in multi-object path

The condition was broadened from cmd_res.ttl_expired_ to cmd_res.ttl_expired_ || cmd_res.object_deleted_, but the existing assertion assert(cmd_res.ttl_ == UINT64_MAX); remains. This now assumes that for any explicit delete (object_deleted_ == true), ttl_ is also set to UINT64_MAX.

If the execution layer ever sets object_deleted_ == true while leaving ttl_ at some other value (e.g., the prior TTL), this assertion will start firing for valid deletes. Please double‑check that the ObjectCommandResult producer sets ttl_ == UINT64_MAX for all delete cases, or narrow the assertion to only the ttl_expired_ case if that invariant is not guaranteed.

tx_service/include/cc/object_cc_map.h (5)

130-133: LGTM!

Clean addition of the object_deleted reference binding following the established pattern for object_modified.


921-926: LGTM!

Using AddOverWriteCommand for TTL-expired retire commands correctly implements the delete-forward semantics required by the PR objectives.


1024-1043: LGTM!

The delete detection via ExecResult::Delete and conditional forwarding logic correctly implements the PR objective of forwarding delete commands to standby nodes using overwrite semantics.


2004-2016: LGTM!

Using std::max(commit_ts, cce->CommitTs()) correctly handles the case where buffered commands may have advanced the entry's commit timestamp beyond the request's original commit_ts, ensuring dirty pages are not skipped by SnapshotSync.


1078-1093: No issue found. RecoverTTLObjectCommand() returns a raw pointer (TxCommand*) while RetireExpiredTTLObjectCommand() returns std::unique_ptr<TxCommand>. The usage at line 1079 (direct pass) and line 1088 (.get() call) are both correct for their respective return types.

Likely an incorrect or invalid review comment.

@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

Caution

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

⚠️ Outside diff range comments (1)
tx_service/src/standby.cpp (1)

53-62: Clarify the implementation of factory methods that feed into AddOverWriteCommand.

The factory methods RetireExpiredTTLObjectCommand() and RecoverTTLObjectCommand() called at all callsites have only base class implementations that assert(false) and return nullptr. No overrides exist in derived command classes. This means:

  1. The assert(cmd->IsOverwrite()) at line 55 is unreachable in current form—calls would crash with the factory method's assert(false) first.
  2. The destructive behavior (line 58 clears the command list) is intentional, as confirmed by the if/else logic at callsites (callsites distinguish between delete vs. non-delete paths).

Either provide implementations of these factory methods that return properly-typed overwrite commands, or clarify if these code paths are incomplete/not yet exercised.

🧹 Nitpick comments (1)
tx_service/include/cc/object_cc_map.h (1)

1798-1807: Duplicate last_dirty_commit_ts_ update in UploadTxCommandsCc.

This block updates last_dirty_commit_ts_ twice with the same condition and value:

if (commit_ts > last_dirty_commit_ts_) {
    last_dirty_commit_ts_ = commit_ts;
}
if (commit_ts > last_dirty_commit_ts_) {
    last_dirty_commit_ts_ = commit_ts;
}

The second if is redundant and can be safely removed to reduce noise.

-            if (commit_ts > last_dirty_commit_ts_)
-            {
-                last_dirty_commit_ts_ = commit_ts;
-            }
             if (commit_ts > last_dirty_commit_ts_)
             {
                 last_dirty_commit_ts_ = commit_ts;
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9319bc and aa28e2c.

📒 Files selected for processing (7)
  • tx_service/include/cc/object_cc_map.h (6 hunks)
  • tx_service/include/command_set.h (1 hunks)
  • tx_service/include/standby.h (1 hunks)
  • tx_service/include/tx_command.h (1 hunks)
  • tx_service/include/tx_operation_result.h (2 hunks)
  • tx_service/src/standby.cpp (1 hunks)
  • tx_service/src/tx_execution.cpp (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tx_service/include/command_set.h
  • tx_service/include/tx_operation_result.h
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T03:56:58.811Z
Learnt from: thweetkomputer
Repo: eloqdata/tx_service PR: 150
File: include/cc/local_cc_shards.h:626-631
Timestamp: 2025-10-09T03:56:58.811Z
Learning: For the LocalCcShards class in include/cc/local_cc_shards.h: Writer locks (unique_lock) should continue using the original meta_data_mux_ (std::shared_mutex) rather than fast_meta_data_mux_ (FastMetaDataMutex) at this stage. Only reader locks may use the FastMetaDataMutex wrapper.

Applied to files:

  • tx_service/include/cc/object_cc_map.h
🧬 Code graph analysis (2)
tx_service/include/cc/object_cc_map.h (1)
tx_service/include/tx_command.h (1)
  • ExecResult (51-260)
tx_service/include/standby.h (1)
tx_service/src/standby.cpp (2)
  • AddOverWriteCommand (53-62)
  • AddOverWriteCommand (53-53)
🔇 Additional comments (6)
tx_service/include/tx_command.h (1)

53-58: No switch statements on ExecResult exist in the codebase—only if/comparison statements.

The enum adds Delete as expected, and it's already properly handled in all existing ExecResult usage locations (object_cc_map.h lines 1024–1025 and 1066–1067). The verification request in the original comment cannot be applied as stated since there are no switch statements to verify.

tx_service/include/standby.h (1)

48-48: All callsites of the renamed method have been properly updated.

The breaking API change is correctly implemented. The method signature changed from AddTxCommand(TxCommand *cmd) to AddOverWriteCommand(TxCommand *cmd), and all callsites using the TxCommand* version have been updated. The remaining AddTxCommand calls in the codebase reference a different overload—AddTxCommand(ApplyCc &cc_req)—which was not renamed and correctly remains unchanged.

tx_service/src/tx_execution.cpp (2)

6754-6847: Deleted-object handling in single-object post-processing looks correct

Using object_deleted to (a) emit a retire/delete command when ttl_expired || object_deleted and (b) gate the normal AddObjectCommand when !object_deleted matches the intended semantics: deleted objects no longer generate a regular overwrite entry but still get a DEL/retire log entry and forward path. This keeps TTL-only behavior unchanged while covering explicit deletes as well.


7263-7300: Multi-object delete/TTL retire behavior is consistent and safe

The multi-object path now mirrors the single-object post-processing: for each key, a retire/delete command is added when ttl_expired_ || object_deleted_, and the normal AddObjectCommand is skipped when object_deleted_ is true. This avoids logging regular overwrites for deleted objects while still capturing delete transitions for logging and standby forwarding, without changing the existing TTL-expiration behavior.

tx_service/include/cc/object_cc_map.h (2)

914-927: TTL‑expired forwarding now uses overwrite semantics – behavior looks correct.

The change to forward TTL‑retire operations via AddOverWriteCommand(retire_command.get()) in the ttl‑expired branch aligns with the overwrite‑first semantics used elsewhere and ensures standbys see a clear delete transition when the primary treats the object as expired. This matches the PR goal of explicitly propagating delete‑state transitions.


2006-2018: Using std::max(commit_ts, cce->CommitTs()) for dirty timestamps is appropriate.

Updating commit_ts with std::max(commit_ts, cce->CommitTs()) before writing last_dirty_commit_ts_ and ccp->last_dirty_commit_ts_ ensures the dirty watermark never lags behind the CCEntry’s actual version, even when buffered commands advance cce->CommitTs() beyond the incoming forward message’s CommitTs(). This is consistent with the goal of preventing SnapshotSync/checkpointer from skipping pages that still have buffered or newly applied commands.

Comment on lines 130 to 134
ObjectCommandResult &obj_result = hd_res->Value();
CcEntryAddr &cce_addr = obj_result.cce_addr_;
bool &object_modified = obj_result.object_modified_;
bool &object_deleted = obj_result.object_deleted_;
CcEntry<KeyT, ValueT, false, false> *cce = nullptr;

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 | 🔴 Critical

Fix AddOverWriteCommand call with std::unique_ptr and confirm delete tracking semantics.

The new object_deleted tracking and use of ExecResult::Delete look consistent with the intent (treat delete-state transitions as modifications and drive overwrite/delete forwarding). However, there is a correctness bug in the TTL‑reset forward path:

  • Line 1079 creates a std::unique_ptr<TxCommand>:
    • auto recover_command = cmd->RecoverTTLObjectCommand();
  • Line 1080 calls:
    • forward_entry->AddOverWriteCommand(recover_command);

Given the other call sites use a raw pointer (retire_command.get()), AddOverWriteCommand is very likely declared as taking a TxCommand*. Passing a std::unique_ptr<TxCommand> here will not compile (no implicit conversion), or, if there is an overload taking a std::unique_ptr, it should be passed with std::move to transfer ownership.

Update this call to match the other site:

-        auto recover_command = cmd->RecoverTTLObjectCommand();
-        forward_entry->AddOverWriteCommand(recover_command);
+        auto recover_command = cmd->RecoverTTLObjectCommand();
+        if (recover_command) {
+            forward_entry->AddOverWriteCommand(recover_command.get());
+        }

This both fixes the type mismatch and guards against a possible null recover command.

Separately, the revised object_deleted / object_modified logic around ExecResult::Delete looks correct and should ensure deletes are treated as modifications for logging, forwarding, and lock release callbacks.

Also applies to: 1014-1095

🤖 Prompt for AI Agents
In tx_service/include/cc/object_cc_map.h around lines 1014 to 1095 (specifically
1079–1080 and similar sites), the call to AddOverWriteCommand passes a
std::unique_ptr<TxCommand> (recover_command) directly which mismatches the other
call sites that pass a raw pointer and may not compile or transfer ownership
correctly; change the call to pass the raw pointer (recover_command.get()) to
match existing usage and add a null check before calling AddOverWriteCommand to
guard against a null recover command; alternatively, if AddOverWriteCommand is
intended to take ownership, call it with std::move(recover_command) instead—pick
the raw-pointer form to match retire_command.get() unless you intentionally want
ownership transfer, and apply the same fix to all similar occurrences in the
1014–1095 range.

@MrGuin MrGuin merged commit 53fb45a into main Dec 5, 2025
4 checks passed
@MrGuin MrGuin deleted the fix_cmd_delete branch December 5, 2025 08:41
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.

[Bug]: SETRANGE with empty value behaves differently from Redis

2 participants