Class TwoPhaseParticipant

java.lang.Object
com.loomcache.server.transaction.twopc.TwoPhaseParticipant

public final class TwoPhaseParticipant extends Object
Participant side of the cross-group 2-phase commit protocol introduced in BLK-2026-04-22-001 Phase C.

Each participant represents a single Raft group that holds a slice of the transaction's keys. The coordinator (running on the raft-0 leader) sends TwoPhaseCommands.PrepareGroup and TwoPhaseCommands.DecideGroup commands scoped to this group; this class ensures both commands are durably appended to the participant group's OWN Raft log BEFORE being applied to local state. Owner-shard linearizable reads on the same group are therefore fenced against committed cross-group transactions — which is the correctness gap BLK-001 targets.

Flow

  1. Inbound PREPARE_GROUP (submitPrepareGroup(TwoPhaseCommands.PrepareGroup)): replicate the command into the group's Raft log via RaftNodeApi.submitCommand(byte[]). The apply path (below) does the actual work.
  2. Apply PREPARE_GROUP (applyPrepareGroup(TwoPhaseCommands.PrepareGroup)): decode the per-group ReplicatedTransactionCommand slice, register a pending intent keyed by txId, schedule the orphaned-intent timeout, and reply PrepareAck(COMMIT) to the coordinator.
  3. Inbound DECIDE_GROUP (submitDecideGroup(TwoPhaseCommands.DecideGroup, int)): replicate into the group's Raft log.
  4. Apply DECIDE_GROUP (applyDecideGroup(TwoPhaseCommands.DecideGroup, int)): on COMMIT, apply the per-group operations atomically via CrossGroupTransactionExecutor.applyCommittedTransaction(Transaction, Function); on ABORT, just drop the intent. Cancel the orphaned-intent timers and send DecideAck back.

Orphaned intents

If a PREPARE applied but no DECIDE arrives within orphanedIntentTimeout the participant repeatedly sends a DecideQuery to the coordinator (best-effort). It does not unilaterally decide ABORT: in a CP/bank production posture, blocking a prepared key is safer than allowing one group to abort while another eventually commits.

Thread-safety

All public methods are safe for concurrent invocation. Per-txId state is protected with a monitor held briefly across transitions.
  • Field Details

    • DEFAULT_ORPHANED_INTENT_TIMEOUT

      public static final Duration DEFAULT_ORPHANED_INTENT_TIMEOUT
      Default orphaned-intent timeout before the participant starts chasing the coordinator.
    • DEFAULT_UNILATERAL_ABORT_GRACE

      public static final Duration DEFAULT_UNILATERAL_ABORT_GRACE
      Retry interval between orphan DECIDE_QUERY attempts.
    • DEFAULT_ACK_RETRY_DELAY

      public static final Duration DEFAULT_ACK_RETRY_DELAY
      Delay between reply-dispatch retry attempts when the raft-0 leader is unknown (transient post-election propagation gap) or when the transport rejects the send. Symmetric to TwoPhaseCoordinator.DEFAULT_DISPATCH_RETRY_DELAY.
    • DEFAULT_ACK_MAX_ATTEMPTS

      public static final int DEFAULT_ACK_MAX_ATTEMPTS
      Maximum reply-dispatch attempts per (txId, type). At the default delay this bounds total wall time at 5s — well under the 30s coord phase timeout, so retry exhaustion still lets the phase timeout fire with the same semantics as before.
      See Also:
    • DEFAULT_COMPLETED_DECISION_RETENTION

      public static final Duration DEFAULT_COMPLETED_DECISION_RETENTION
      How long a participant retains completed decisions for duplicate DECIDE idempotency and snapshot replay. After this safety window expires, a duplicate COMMIT for an unknown transaction fails closed instead of being acknowledged from unbounded memory.
  • Constructor Details

  • Method Details

    • setNearCacheInvalidationManager

      public void setNearCacheInvalidationManager(@Nullable NearCacheInvalidationManager manager)
    • submitPrepareGroup

      public boolean submitPrepareGroup(@NonNull TwoPhaseCommands.PrepareGroup cmd)
      Replicate a received TwoPhaseCommands.PrepareGroup into the participant group's Raft log. The actual intent registration + reply happens on the apply thread via applyPrepareGroup(TwoPhaseCommands.PrepareGroup).
      Returns:
      true iff the participant's raft-N node accepted the submit. A false return typically means this node is no longer the raft-N leader; the caller should let the coordinator retry after a NOT_LEADER bounce.
    • submitDecideGroup

      public boolean submitDecideGroup(@NonNull TwoPhaseCommands.DecideGroup cmd, int groupId)
      Replicate a received TwoPhaseCommands.DecideGroup into the participant group's Raft log. The participant must supply the groupId because the TwoPhaseCommands.DecideGroup record itself does not carry one — the inbound dispatcher extracts it from Message.mapName() which the coordinator always stamps with "raft-<groupId>".
    • applyPrepareGroup

      public void applyPrepareGroup(@NonNull TwoPhaseCommands.PrepareGroup cmd)
      Apply a committed TwoPhaseCommands.PrepareGroup log entry. This runs on the Raft apply thread of the participant's group.

      Registers a pending intent, schedules the orphaned-intent timeout, and dispatches TwoPhaseCommands.PrepareAck back to the coordinator.

    • applyDecideGroup

      public @NonNull TransactionResult applyDecideGroup(@NonNull TwoPhaseCommands.DecideGroup cmd, int groupId)
      Apply a committed TwoPhaseCommands.DecideGroup log entry. On COMMIT, runs the per-group operations atomically via CrossGroupTransactionExecutor.applyCommittedTransaction(Transaction, Function). On ABORT, simply drops the intent. COMMIT apply failures are fail-closed: the participant keeps the intent pending and does not acknowledge, so coordinator retry or local recovery can re-drive the decision.

      Returns the TransactionResult for the apply so the caller (state machine applier in CacheNode) can include the per-group outputs in any follow-up response.

    • pendingCount

      public int pendingCount()
      Returns:
      the number of in-flight prepared-but-not-decided transactions on this node. Primarily for metrics and tests.
    • hasPreparedIntents

      public boolean hasPreparedIntents()
    • completedCountForTesting

      public int completedCountForTesting()
    • snapshotCompletedDecisionsForGroup

      public @NonNull List<Map<String,Object>> snapshotCompletedDecisionsForGroup(int groupId)
    • restoreCompletedDecisions

      public void restoreCompletedDecisions(@Nullable Object rawRecords)
    • restoreCompletedDecisionsForGroup

      public void restoreCompletedDecisionsForGroup(int groupId, @Nullable Object rawRecords)