Duplicate Timestamp Bug Fix
Analysis and fix for duplicate timestamp error when updating Client node metadata, addressing improper state transitions that reused timestamps
Duplicate Timestamp Bug Fix
Problem Description
When updating a Client node’s metadata (specifically the ftue flag in ClientScreen.kt), the update would fail with a duplicate timestamp error. This happened because:
- The UI would call
nodeManager.update()with a new timestamp T1 and stateUSER_EDIT - The
NodeObserverwould collect the StateFlow emission and callnode.type.emit(node) - This would trigger
ClientProcessor.post(node)→BaseNodeProcessor.post(node) BaseNodeProcessor.post()would seeUSER_EDITstate and callnodeManager.update(node.copy(state = NodeState.NONE))- The
copy()call did not update the timestamp, so it kept timestamp T1 - When clicking the button again, the existing node already had the latest timestamp, causing the duplicate timestamp check to fail
Root Cause
In BaseNodeProcessor.post(), the state transition logic was:
1
2
3
NodeState.CREATED, NodeState.USER_EDIT, NodeState.SNAPSHOT_UPDATE -> {
nodeManager.update(node.copy(state = NodeState.NONE)) // ❌ Same timestamp!
}
This created a node with state NONE but kept the same timestamp, which violated the assumption that each state change should have a unique, increasing timestamp.
Solution
Fix 1: Use nodeManager.complete() in BaseNodeProcessor
Changed BaseNodeProcessor.post() to use nodeManager.complete() which properly updates both state and timestamp:
1
2
3
NodeState.CREATED, NodeState.USER_EDIT, NodeState.SNAPSHOT_UPDATE -> {
nodeManager.complete(node) // ✅ Updates state AND timestamp
}
The complete() method (from BaseNodeManager.kt) does:
1
2
3
override fun complete(node: Node) {
update(node.copy(state = NodeState.NONE, timestamp = Clock.System.now().toEpochMilliseconds()))
}
Fix 2: Use nodeManager.edited() in UI code
Changed ClientScreen.kt to use the proper helper method instead of manually managing state and timestamp:
Before:
1
2
3
4
5
nodeManager.update(clientNode.value.copy(
state = NodeState.USER_EDIT,
timestamp = Clock.System.now().toEpochMilliseconds(),
meta = meta
))
After:
1
nodeManager.edited(clientNode.value.copy(meta = meta))
The edited() method properly sets both state and timestamp:
1
2
3
override fun edited(node: Node) {
update(node.copy(state = NodeState.USER_EDIT, timestamp = Clock.System.now().toEpochMilliseconds()))
}
Why This Matters
Timestamp Deduplication
The NodeManager uses timestamps to deduplicate rapid updates. The deduplication logic:
1
2
3
4
5
// In ServerNodeManager actor loop
if (existingTimestamp >= node.timestamp) {
logger.w("Duplicate timestamp detected for node ${node.id}: $existingTimestamp >= ${node.timestamp}")
return@processActorOperation
}
State Machine Integrity
Each state transition should be a distinct event with:
- Unique timestamp
- Clear state change
- Proper event propagation
By reusing timestamps, we were creating “phantom” state transitions that broke the state machine integrity.
Impact
This bug affected:
- ✅ Client node metadata updates (FTUE flag)
- ✅ Any node with rapid USER_EDIT → NONE transitions
- ✅ UI responsiveness when clicking buttons multiple times
After the fix:
- ✅ All state transitions properly update timestamps
- ✅ Duplicate timestamp errors eliminated
- ✅ UI updates work correctly on repeated clicks
Files Changed
- BaseNodeProcessor.kt - Use
complete()instead of manualcopy() - ClientScreen.kt - Use
edited()helper method
Testing
Verified the fix by:
- Opening Client screen
- Clicking “Mark FTUE Complete” button multiple times
- No duplicate timestamp errors
- State properly transitions USER_EDIT → NONE → USER_EDIT → NONE
Best Practices
✅ DO Use Helper Methods
NodeManager provides helper methods that properly manage state AND timestamp:
complete(node)- Sets state to NONEexecute(node)- Sets state to EXECUTEDedited(node)- Sets state to USER_EDITerror(node)- Sets state to ERRORrunning(node)- Sets state to RUNNINGidle(node)- Sets state to IDLE
❌ DON’T Manually Copy State
1
2
3
4
5
// ❌ BAD - Reuses timestamp
nodeManager.update(node.copy(state = NodeState.NONE))
// ✅ GOOD - Updates both state and timestamp
nodeManager.complete(node)
Conclusion
This bug highlighted the importance of using NodeManager’s helper methods for state transitions. These methods encapsulate the proper timestamp management, ensuring state machine integrity and preventing duplicate timestamp errors.