Skip to content

Latest commit

 

History

History
100 lines (68 loc) · 5.13 KB

non-unique.md

File metadata and controls

100 lines (68 loc) · 5.13 KB

How HLC's are not unique?

Let's say we have three HLC nodes with an artificial wallTime that is frozen in time, currently at 1n.

Node2 is 1n ahead of node1 and node3 is another 4n ahead of node1. In a real system the difference between the nodes may be much larger. Also of note is that the nodes are progressing in parallel, meaning that one node likely stays ahead of the others.

  • node1: Timestamp { wallTime: 1n, logical: 1 }
  • node2: Timestamp { wallTime: 2n, logical: 1 }
  • node3: Timestamp { wallTime: 6n, logical: 1 }

Without syncing or progresses in the wallTime, naturally node1 and node2 produce the same, non-unique timestamps consistently.

  • node1: Timestamp { wallTime: 1n, logical: 2 }
  • node2: Timestamp { wallTime: 2n, logical: 2 }
  • node3: Timestamp { wallTime: 6n, logical: 2 }

As you can see all of the node's logical counters increased even though the wallTime didn't making the timestamps unique only per node. In practice, time advances which means that two timestamps would need to be created within < 1 nanosecond of time, which is very unlikely. Let's advance time by 1n (wallTime=2) and we shall see that the logical clock is reset.

  • node1: Timestamp { wallTime: 2n, logical: 0 }
  • node2: Timestamp { wallTime: 3n, logical: 0 }
  • node3: Timestamp { wallTime: 7n, logical: 0 }

The strength of HLC's is revealed when a sync happens. If we sync all nodes, we will notice that all timestamps are beyond the wallTime=2n and also beyond the previous largest timestamp of node3.

  • node1: Timestamp { wallTime: 7n, logical: 3 }
  • node2: Timestamp { wallTime: 7n, logical: 3 }
  • node3: Timestamp { wallTime: 7n, logical: 4 }

If we further advance time by 1n (wallTime=3), it will not make a difference for node1 and node2. Their wallTime is still behind the last wallTime they used, but their logical component will increase.

  • node1: Timestamp { wallTime: 7n, logical: 4 }
  • node2: Timestamp { wallTime: 7n, logical: 4 }
  • node3: Timestamp { wallTime: 8n, logical: 0 }

Just to illustrate this point, we can advance time by 1n (wallTime=4) once more and the wallTime will still not have increased.

  • node1: Timestamp { wallTime: 7n, logical: 5 }
  • node2: Timestamp { wallTime: 7n, logical: 5 }
  • node3: Timestamp { wallTime: 9n, logical: 0 }

Only if we advance time by 4n (wallTime=8) to a point where it caught up with the 5n advance of node3 during the last sync, the other nodes use the wallTime again.

  • node1: Timestamp { wallTime: 8n, logical: 0 }
  • node2: Timestamp { wallTime: 9n, logical: 0 }
  • node3: Timestamp { wallTime: 13n, logical: 0 }

Unlike the case of multiple operations per nanosecond, this case is actually rather common. Clocks are never 100% in sync and after a sync process it is very likely that all but the most advanced clock will use the logical component.

The clocks using the logical component will increment independently. This will create equivalent, non-unique timestamps in the process.

Bonus Question: Why don't we sync the wallTime?

There is a reasonably argument to be made that the wallTime should be increased for the nodes lagging behind. You can add an offset to the largest wallTime that you received.

It is a good idea to do that, particularly to reduce the chance of duplicates and it may make a little more sense for users. However, you need to be careful when offsets to the wallTime don't compound.

In the previous examples, the time for all nodes advanced at the same pace. But in reality it does not. Each node increments at slightly different paces. Let's say we adjust _node2's? wallTime to add 4n => 5n offset to its wallClock. When we next advance time by 1n (wallTime=9) we will see that node2 and node3 both advanced.

  • node2: Timestamp { wallTime: 14n, logical: 0 }
  • node3: Timestamp { wallTime: 14n, logical: 0 }

What could happen though is that node2 advances sometimes at a pace of 6n while node3 advances sometimes advances at the pace of 1n.

  • node2: Timestamp { wallTime: 16n, logical: 0 }
  • node3: Timestamp { wallTime: 15n, logical: 0 }

Suddenly node2 is ahead and node3 needs to compensate by increasing its offset by +1, even though it is physically the still 3n ahead of node2! From that point on every unevenness in advance causes all nodes to adjust for the biggest entry. This will lead to clocks automaticaly drifting little by little into the future.

Due to this difficulty, using the wallTime synching is not (yet?) generalized and you need to be careful to make sure that you don't exceed the time of the actually "furthest ahead" node.

Important to note

Synching wallTime will reduce the probability for a non-unique timestamp drastically, but even so it is still possible to have non-unique timestamps: Two nodes that happen to have the same wallTime and logical time.

The probability depends on the numbers of nodes, the amount of average drift and number of timestamps created. Unless you feel confident that your probabilities are in your favor, it is a good idea to assume timestamps as non-unique.


(Generated from non-unique.js)