Feat: UpdateTransaction - #86
Conversation
…he system Related to #59
…saction
* Continued the intrgration of the `UpdateTransaction` with the system,
* Fixed initialization of `HeartbeatTransaction`'s `TransactionId` to use the proper `Replica` method
* Created the `TestUpdateTransaction` class to test the `UpdateTransaction` functionality, this test:
- a basic write operation issued to a non-coordinator replica
- a basic write operation issued to a non-coordinator replica that is crashed, expecting a timeout response
- a basic write operation issued to a non-coordinator replica followed by a crash of the coordinator replica, expecting a timeout response
* Corrected the `transaction_Write.mmd` chart to be compliant to the implementation
Fixes #55, Related to #59, #60, #61
| this.heartbeatTransaction = new HeartbeatTransaction( | ||
| heartbeatTransactionId, | ||
| getNextTransactionId(), | ||
| this, | ||
| getEpochPair() | ||
| ); |
There was a problem hiding this comment.
Using getNextTransactionId() here will fail.
Each replica currently creates its heartbeat transaction using getNextTransactionId(). Since every replica starts its transaction counter at zero, the coordinator creates an ID such as <Replica_0, 0>, while a follower creates <Replica_1, 0>.
However, the coordinator broadcasts HeartbeatMsg using the coordinator's ID. When a follower receives that message, Replica.onMessage() searches its active transactions for the same ID. The follower owns <Replica_1, 0>, so it cannot find a matching transaction and silently ignores the heartbeat. Its watchdog may then expire even though the coordinator is still sending heartbeats.
Could we create the heartbeat transaction using the coordinator's ActorRef and the coordinator-term sequence?
There was a problem hiding this comment.
Yeah, good catch... SO the tests are not correct because the regression is still Green!
I've fixed the destination provided when the `UpdateTransaction` is issued on the replicas that dosn't initiated the transaction. Corrected also the test to ensure this behaviour
Reordered the replica cases to check first if the replica is the coordinator or not.
…RDINATOR_BEAT_INTERVAL` I changed the original function to use a parameter for the heartbeat interval instead the class constant. This is to speedup tests, infact having an interval of 1s could result in really long tests timings
Before, if a `WriteTransaction` is issued on the coord directly would fail due to non complete termination protocol. Now is fixed Fixes #89
- Now each replica contains a `Map<EpochPair, UpdateTransactio>` where the updates are stored. - Each `Replica` epochPair is updated after the each update
alanmasu
left a comment
There was a problem hiding this comment.
I was trying to implement some test too but didn't had time today...
| this.heartbeatTransaction = new HeartbeatTransaction( | ||
| heartbeatTransactionId, | ||
| getNextTransactionId(), | ||
| this, | ||
| getEpochPair() | ||
| ); |
There was a problem hiding this comment.
Yeah, good catch... SO the tests are not correct because the regression is still Green!
| replica.unicast(updateMsg, this.destination.get()); | ||
| this.timeout = replica.scheduleToItself( | ||
| replica.getMaxLatency() * 3, | ||
| new UpdateTimeoutMsg(this.getId(), this.startEpochPair, replica.getSelf())); |
There was a problem hiding this comment.
Could we handle UpdateTimeoutMsg and WriteOkTimeoutMsg before considering the crash handling complete?
Both timeouts are scheduled, but replicaStateMachine() currently has no active branch for either message—the only relevant code is commented out. If the coordinator crashes after a replica forwards a write, or after it receives UPDATE, the timeout is delivered and then ignored; the transaction remains active and no election/recovery path begins.
The specification requires these two timeout cases to detect coordinator failure. When adding the handler, it may also be worth following the heartbeat transaction’s stale-timeout protection, since cancelling an Akka timer does not guarantee an already-queued timeout cannot arrive later.
There was a problem hiding this comment.
yeah, is commented due to no ElectionTransaction is ready! When this will be done, the code is mostly ready
There was no call to `replica.setPosition(int, int)`
…sg and improved test cases for crash scenarios
alanmasu
left a comment
There was a problem hiding this comment.
For me it's ok. We need to complete afte the ElectionTransaction for complete the whole state machine, BTW I think this could be merged and completed after!
| replica.unicast(updateMsg, this.destination.get()); | ||
| this.timeout = replica.scheduleToItself( | ||
| replica.getMaxLatency() * 3, | ||
| new UpdateTimeoutMsg(this.getId(), this.startEpochPair, replica.getSelf())); |
There was a problem hiding this comment.
yeah, is commented due to no ElectionTransaction is ready! When this will be done, the code is mostly ready
An UpdateTransaction has two different identities with different jobs.
TransactionId routes messages to the correct local FSM, while EpochPair
identifies and orders the replicated update across every replica. The update
protocol therefore requires one non-null coordinator-assigned EpochPair to
remain unchanged through UPDATE, ACK, WRITEOK, local application, history,
and completion of the parent WriteTransaction.
Why this was needed
The previous implementation reused startEpochPair as the update identity.
That value only described the initiating replica state when its local
transaction was created; it was not a unique identity allocated by the
coordinator. If two writes reached the coordinator before either committed,
both could start from the same committed pair and therefore broadcast the
same EpochPair for two different updates.
Termination then discarded the pair carried by WRITEOK and independently
incremented each replica local EpochPair. Consequently, the pair used by
UPDATE, ACK, and WRITEOK could differ from the pair stored in updateHistory.
Replicas starting from different local states could also record the same
update under different keys. That breaks the identity needed to order writes,
correlate recovery history, and determine which update was committed.
The coordinator also counted ACK messages rather than distinct replicas and
did not verify that an ACK belonged to the active update pair. A duplicate or
stale ACK could therefore contribute to quorum. Participants similarly
accepted WRITEOK without checking that it matched the update they had
acknowledged.
Timeout handling had two separate defects. A participant already waiting for
WRITEOK scheduled UpdateTimeoutMsg instead of WriteOkTimeoutMsg, and the
state-machine branches for both timeout messages were commented out. A fired
timeout was therefore ignored and the transaction remained in its waiting
state. The old delay was also based only on raw maximum link latency and did
not account for FIFO channel backlog or replica-count tolerance.
Finally, the success regression asserted only the client WriteResult. Client
success alone does not prove that every replica applied the update with the
same index and value or that the replicated identity was recorded correctly.
What changed
Add a coordinator-side update sequence allocator to Replica. The allocator
tracks the current allocation epoch and the next sequence number separately
from the last committed EpochPair. reserveNextUpdateEpochPair is restricted
to the coordinator and advances the reservation immediately, allowing
multiple in-flight updates to receive unique pairs even before an earlier
update commits. setEpochPair keeps the allocator synchronized when commit or
future recovery changes the replica epoch.
Add updateEpochPair to UpdateTransaction. startEpochPair remains the snapshot
from transaction creation, while updateEpochPair represents the mutable,
coordinator-assigned identity of this replicated write. The coordinator now
reserves this pair before broadcasting UPDATE, participants adopt it from
UPDATE, and UPDATE, ACK, WRITEOK, WriteFinish, and history all carry the same
pair.
Validate protocol messages before changing state. UPDATE and WRITEOK must
carry a non-null pair. The coordinator accepts ACKs only while WAITING_ACK,
only for the active update pair, and only once per non-null sender. A
participant ignores WRITEOK for a different pair. These guards prevent stale
or duplicate messages from advancing quorum or committing the wrong update.
Commit the exact pair contained in WRITEOK instead of calculating a new local
pair. Keep the existing setPosition call, set the replica EpochPair to the
coordinator pair, propagate that pair to the parent WriteTransaction, and
store history under an explicit pair argument. This makes the wire identity,
committed replica state, and updateHistory key agree on every replica.
Use phase-specific timeout messages and a shared update timeout bound based on
four times getMaxLatencyPlusTolerance. WAITING_UPDATE handles only a matching
UpdateTimeoutMsg, while WAITING_WRITEOK handles only a matching
WriteOkTimeoutMsg. A valid timeout cancels and clears its timer and moves the
transaction to WAITING_ELECTION. State and pair checks also make already
queued stale timer messages harmless after the FSM changes phase.
How this solves the protocol failure
For a committed state such as <0,4>, concurrent coordinator reservations now
produce <0,5>, <0,6>, and so on instead of reusing <0,4>. For an update
assigned <0,5>, every protocol step now refers to exactly <0,5>:
coordinator reserves <0,5>
coordinator broadcasts UPDATE(<0,5>)
participants return ACK(<0,5>)
coordinator broadcasts WRITEOK(<0,5>) after distinct matching quorum
every replica applies the value and adopts <0,5>
every replica records updateHistory[<0,5>]
Reservation may leave a sequence gap if an in-flight update never commits,
but it never reuses an identity for another update. Preserving uniqueness is
required for ordering and recovery and is safer than forcing contiguous
sequence numbers.
Tests
Add focused protocol tests proving that coordinator reservations are unique,
allocation follows epoch changes, termination uses the WRITEOK pair for both
replica state and history, and both participant timeout phases enter
WAITING_ELECTION.
Strengthen the existing 3-node and 5-node regression cases for coordinator 0
and coordinator 1 by requiring an UpdateApplied callback with the correct
replica ID, index, and value from every replica, rather than relying only on
the final client result.
Verified with:
gradle test --no-daemon --tests it.unitn.ds.TestUpdateTransactionProtocol
--tests it.unitn.ds.regression.TestUpdateTransaction
gradle regression --no-daemon
gradle spotlessJava --no-daemon
The focused run passed all 15 cases, the complete regression task passed, and
the formatting task completed successfully.
Scope limitation
This branch does not yet contain ElectionTransaction or Replica.startElection.
Timeouts now reach the correct WAITING_ELECTION integration state, but this
commit does not start an election or recover and replay the pending update.
That handoff must be connected by the separate election/recovery work before
claiming complete timeout recovery.
Integrate PR #83 read-transaction changes and resolve the Replica overlap by retaining both ReadMsg and UpdateMsg receive registrations and handlers. Focused read and update tests pass together after the resolution.
Remove the explicit null assignment from timeout handling, add diagnostic messages to convergence assertions, and apply the repository Java formatter. Static analysis now reports zero problems and the focused UpdateTransaction suite remains green.
This pull request introduces the new
UpdateTransactionclass to support update operations in the distributed system, along with related protocol messages and integration into theReplicaactor. The changes also include utility methods and minor visibility adjustments to facilitate this new transaction type.Major additions and changes:
Update Transaction Implementation
UpdateTransactionclass inUpdateTransaction.java, which encapsulates the logic for update operations, including state management, protocol messages (UpdateMsg,UpdateAckMsg,WriteOkMsg, etc.), and coordinator/replica state machines.Integration with Replica
UpdateMsghandling in theReplicaactor'screateReceivemethod, and implemented theonUpdateMsghandler to schedule anUpdateTransactionwhen appropriate. [Utility and Refactoring
isCoordinator()method toReplicato allow transactions to check if the current replica is the coordinator.TransactionIdfields inTransaction.javatopublicto enable access from other classes likeUpdateTransaction.Issue linked