Skip to content

Commit 2c6386e

Browse files
authored
Merge pull request #22 from elixir-toniq/hlclock-relax-constraints
relax `drift?` to match the HLC paper, enforce counter boundaries 1. Relax recv max drift to check only l - pt and not abs(l - pt). In the paper, maximum drift is limited to the absolute value of the difference between the logical and physical time as a corollary of the algorithm, which only requires checking when the logical clock is in the future of physical time. This allows us to recv old timestamps, which simplifies clients by allowing them to recv the timestamps of all events before acting and allowing this library to ensure that the causal order is preserved. 2. We were checking max drift on send, but that only catches errors caused by a system administrator setting the system clock to jump or, more commonly, causing an error whenever the computer running the VM sleeps.
2 parents 5859ff4 + ad2eba1 commit 2c6386e

3 files changed

Lines changed: 45 additions & 23 deletions

File tree

lib/hlclock/server.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ defmodule HLClock.Server do
3434
end
3535

3636
def handle_call(:send_timestamp, _from, data) do
37-
case Timestamp.send(data.timestamp, physical_time(), data.max_drift) do
37+
case Timestamp.send(data.timestamp, physical_time()) do
3838
{:ok, timestamp} ->
3939
{:reply, {:ok, timestamp}, %{data | timestamp: timestamp}}
4040

@@ -60,7 +60,7 @@ defmodule HLClock.Server do
6060
def handle_info(:periodic_send, data) do
6161
Process.send_after(self(), :periodic_send, interval(data))
6262

63-
case Timestamp.send(data.timestamp, physical_time(), data.max_drift) do
63+
case Timestamp.send(data.timestamp, physical_time()) do
6464
{:ok, ts} ->
6565
{:noreply, %{data | timestamp: ts}}
6666

lib/hlclock/timestamp.ex

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ defmodule HLClock.Timestamp do
1010
languages/representations.
1111
"""
1212

13+
import Kernel, except: [send: 2]
14+
1315
defstruct [:time, :counter, :node_id]
1416

1517
alias __MODULE__, as: T
@@ -41,15 +43,25 @@ defmodule HLClock.Timestamp do
4143
Generate a single HLC Timestamp for sending to other nodes or
4244
local causality tracking
4345
"""
44-
def send(%{time: old_time, counter: counter, node_id: node_id}, pt, max_drift) do
46+
def send(%{time: old_time, counter: counter, node_id: node_id}, pt) do
4547
new_time = max(old_time, pt)
4648
new_counter = advance_counter(old_time, counter, new_time)
4749

48-
with :ok <- handle_drift(old_time, new_time, max_drift) do
49-
{:ok, new(new_time, new_counter, node_id)}
50+
case handle_counter(new_counter) do
51+
:ok -> {:ok, new(new_time, new_counter, node_id)}
52+
err -> err
5053
end
5154
end
5255

56+
# Compatibility for older users of Timestamp that may be providing the max_drift.
57+
def send(
58+
%{time: old_time, counter: counter, node_id: node_id},
59+
pt,
60+
_max_drift
61+
) do
62+
send(%{time: old_time, counter: counter, node_id: node_id}, pt)
63+
end
64+
5365
@doc """
5466
Given the current timestamp for this node and a provided remote timestamp,
5567
perform the merge of both logical time and logical counters. Returns the new
@@ -59,15 +71,9 @@ defmodule HLClock.Timestamp do
5971
new_time = Enum.max([physical_time, local.time, remote.time])
6072

6173
with {:ok, node_id} <- compare_node_ids(local.node_id, remote.node_id),
62-
:ok <-
63-
handle_drift(
64-
remote.time,
65-
physical_time,
66-
max_drift,
67-
:remote_drift_violation
68-
),
69-
:ok <- handle_drift(new_time, physical_time, max_drift),
70-
new_counter <- merge_logical(new_time, local, remote) do
74+
:ok <- handle_drift(remote.time, physical_time, max_drift),
75+
new_counter <- merge_logical(new_time, local, remote),
76+
:ok <- handle_counter(new_counter) do
7177
{:ok, new(new_time, new_counter, node_id)}
7278
end
7379
end
@@ -175,18 +181,18 @@ defmodule HLClock.Timestamp do
175181
end
176182
end
177183

178-
defp handle_drift(l, pt, max_drift, err \\ :clock_drift_violation) do
184+
defp handle_drift(l, pt, max_drift) do
179185
cond do
180186
drift?(l, pt, max_drift) ->
181-
{:error, err}
187+
{:error, :remote_drift_violation}
182188

183189
true ->
184190
:ok
185191
end
186192
end
187193

188194
defp drift?(l, pt, max_drift) do
189-
abs(l - pt) > max_drift
195+
l - pt > max_drift
190196
end
191197

192198
defp advance_counter(old_time, counter, new_time) do
@@ -199,6 +205,14 @@ defmodule HLClock.Timestamp do
199205
end
200206
end
201207

208+
defp handle_counter(counter) do
209+
if counter > 0xFFFF do
210+
{:error, :max_counter_violation}
211+
else
212+
:ok
213+
end
214+
end
215+
202216
defimpl String.Chars do
203217
def to_string(ts) do
204218
logical_time =

test/hlclock/timestamp_test.exs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ defmodule HLClock.TimestampTest do
9191
property "for a fixed physical time, logical counter incremented" do
9292
check all(time <- ntp_millis()) do
9393
t0 = Timestamp.new(time, 0)
94-
{:ok, t1} = Timestamp.send(t0, 0, @max_drift)
94+
{:ok, t1} = Timestamp.send(t0, 0)
9595
assert t0.counter == 0
9696
assert t1.counter == 1
9797
refute Timestamp.before?(t1, t0)
@@ -100,15 +100,14 @@ defmodule HLClock.TimestampTest do
100100

101101
test "physical time can move backwards" do
102102
t0 = Timestamp.new(10, 0, 0)
103-
{:ok, t1} = Timestamp.send(t0, 9, @max_drift)
103+
{:ok, t1} = Timestamp.send(t0, 9)
104104
assert t0.time == t1.time
105105
assert t1.counter == 1
106106
end
107107

108-
test "send can fail due to excessive drift" do
109-
t0 = Timestamp.new(0, 0, 0)
110-
{:error, err} = Timestamp.send(t0, 5 + 1, @max_drift)
111-
assert err == :clock_drift_violation
108+
test "max counter exception" do
109+
t0 = Timestamp.new(10, 0xFFFF, 0)
110+
assert {:error, :max_counter_violation} == Timestamp.send(t0, 9)
112111
end
113112
end
114113

@@ -122,6 +121,15 @@ defmodule HLClock.TimestampTest do
122121
assert t2.node_id == 0
123122
end
124123

124+
test "max counter exception" do
125+
t0 = Timestamp.new(10, 0xFFFF, 0)
126+
remote = Timestamp.new(9, 0xFFFF, 1)
127+
pt = 9
128+
129+
assert {:error, :max_counter_violation} ==
130+
Timestamp.recv(t0, remote, pt, @max_drift)
131+
end
132+
125133
test "events test" do
126134
events = [
127135
# valid steps

0 commit comments

Comments
 (0)