Post

Icon 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

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:

  1. The UI would call nodeManager.update() with a new timestamp T1 and state USER_EDIT
  2. The NodeObserver would collect the StateFlow emission and call node.type.emit(node)
  3. This would trigger ClientProcessor.post(node)BaseNodeProcessor.post(node)
  4. BaseNodeProcessor.post() would see USER_EDIT state and call nodeManager.update(node.copy(state = NodeState.NONE))
  5. The copy() call did not update the timestamp, so it kept timestamp T1
  6. 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

  1. BaseNodeProcessor.kt - Use complete() instead of manual copy()
  2. ClientScreen.kt - Use edited() helper method

Testing

Verified the fix by:

  1. Opening Client screen
  2. Clicking “Mark FTUE Complete” button multiple times
  3. No duplicate timestamp errors
  4. 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 NONE
  • execute(node) - Sets state to EXECUTED
  • edited(node) - Sets state to USER_EDIT
  • error(node) - Sets state to ERROR
  • running(node) - Sets state to RUNNING
  • idle(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.

This post is licensed under CC BY 4.0 by the author.