Skip to content

Commit af66371

Browse files
committed
MinError calculation fixes
MinError was being calculated incorrectly. Restored correct "causality violation" calculation and added better tests.
1 parent a9d79f5 commit af66371

3 files changed

Lines changed: 106 additions & 41 deletions

File tree

ntp.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,8 @@ type Response struct {
341341
// the current month's last minute.
342342
Leap LeapIndicator
343343

344-
// MinError is a lower bound on the error between the client and server
345-
// clocks. When the client and server are not synchronized to the same
346-
// clock, the reported timestamps may appear to violate the principle of
347-
// causality. In other words, the NTP server's response may indicate
348-
// that a message was received before it was sent. In such cases, the
349-
// minimum error may be useful.
344+
// MinError is a lower bound on the "causality violation" between the
345+
// client and server clocks when they are not yet synchronized.
350346
MinError time.Duration
351347

352348
// KissCode is a 4-character string describing the reason for a "kiss of
@@ -754,11 +750,32 @@ func rtt(t1, t2, t3, t4 time.Time) time.Duration {
754750
return max(t4.Sub(t1)-t3.Sub(t2), 0)
755751
}
756752

757-
// minError calculates a lower bound on the error between the client and
758-
// server clocks using the four NTP timestamps.
753+
// minError calculates a lower bound on the "causality violation" detected
754+
// between the client and server clocks when they are not yet synchronized.
759755
func minError(t1, t2, t3, t4 time.Time) time.Duration {
760-
if t2.Before(t1) || t4.Before(t3) {
761-
return max(t1.Sub(t2), t3.Sub(t4))
756+
// rtt = (t4 - t1) - (t3 - t2)
757+
// offset = ((t2 - t1) + (t3 - t4)) / 2
758+
//
759+
// t2 - t1 = rtt/2 + offset
760+
// t4 - t3 = rtt/2 - offset
761+
//
762+
// If the client and server clocks are synchronized, then the offset
763+
// should be roughly zero and the timestamp differences should be roughly
764+
// equal to the one-way trip time (rtt/2). If the clocks are not
765+
// synchronized, then the magnitude of the offset relative to the one-way
766+
// trip time provides a lower bound on the "causality violation" between
767+
// the two clocks.
768+
769+
offset := offset(t1, t2, t3, t4)
770+
if offset < 0 {
771+
offset = -offset
772+
}
773+
774+
rtt := rtt(t1, t2, t3, t4)
775+
ott := rtt / 2
776+
777+
if offset > ott {
778+
return offset - ott
762779
}
763780
return 0
764781
}

ntp4_test.go

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -226,37 +226,6 @@ func TestOfflineV4CustomDialerDeprecated(t *testing.T) {
226226
assert.True(t, dialerCalled)
227227
}
228228

229-
func TestOfflineV4MinError(t *testing.T) {
230-
start := time.Now()
231-
232-
for org := 1 * time.Second; org <= 10*time.Second; org += time.Second {
233-
for rec := 1 * time.Second; rec <= 10*time.Second; rec += time.Second {
234-
for xmt := rec; xmt <= 10*time.Second; xmt += time.Second {
235-
for dst := org; dst <= 10*time.Second; dst += time.Second {
236-
t1 := start.Add(org)
237-
t2 := start.Add(rec)
238-
t3 := start.Add(xmt)
239-
t4 := start.Add(dst)
240-
241-
var error0, error1 time.Duration
242-
if org >= rec {
243-
error0 = org - rec
244-
}
245-
if xmt >= dst {
246-
error1 = xmt - dst
247-
}
248-
249-
var mex time.Duration
250-
mex = max(error0, error1)
251-
252-
m := minError(t1, t2, t3, t4)
253-
assert.Equal(t, mex, m)
254-
}
255-
}
256-
}
257-
}
258-
}
259-
260229
func TestOfflineV4OffsetCalculation(t *testing.T) {
261230
now := time.Now()
262231
t1 := toTimestamp(now)

ntp_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,85 @@ func TestOfflineFixHostPort(t *testing.T) {
175175
}
176176
}
177177

178+
func TestOfflineMinError(t *testing.T) {
179+
const format = "5.000000"
180+
base := time.Date(2026, 0, 0, 0, 0, 0, 0, time.UTC)
181+
182+
cases := []struct {
183+
t1, t2, t3, t4 time.Time
184+
minError time.Duration
185+
}{
186+
{
187+
base,
188+
base.Add(1 * time.Millisecond),
189+
base.Add(1 * time.Millisecond),
190+
base.Add(1 * time.Millisecond),
191+
0,
192+
},
193+
{
194+
base,
195+
base.Add(1 * time.Millisecond),
196+
base.Add(1 * time.Millisecond),
197+
base.Add(10 * time.Millisecond),
198+
0,
199+
},
200+
{
201+
base,
202+
base.Add(1 * time.Millisecond),
203+
base.Add(1 * time.Millisecond),
204+
base.Add(25 * time.Microsecond),
205+
975 * time.Microsecond,
206+
},
207+
{
208+
base,
209+
base.Add(1 * time.Millisecond),
210+
base.Add(1 * time.Millisecond),
211+
base.Add(2 * time.Microsecond),
212+
998 * time.Microsecond,
213+
},
214+
{
215+
base,
216+
base.Add(25 * time.Millisecond),
217+
base.Add(25 * time.Millisecond),
218+
base.Add(1 * time.Millisecond),
219+
24 * time.Millisecond,
220+
},
221+
{
222+
base.Add(78 * time.Millisecond),
223+
base.Add(38 * time.Millisecond),
224+
base.Add(38 * time.Millisecond),
225+
base.Add(94 * time.Millisecond),
226+
40 * time.Millisecond,
227+
},
228+
{
229+
base.Add(78 * time.Millisecond),
230+
base.Add(38 * time.Millisecond),
231+
base.Add(39 * time.Millisecond),
232+
base.Add(94 * time.Millisecond),
233+
40 * time.Millisecond,
234+
},
235+
{
236+
base.Add(78 * time.Millisecond),
237+
base.Add(38 * time.Millisecond),
238+
base.Add(39 * time.Millisecond),
239+
base.Add(95 * time.Millisecond),
240+
40 * time.Millisecond,
241+
},
242+
}
243+
244+
for _, c := range cases {
245+
m := minError(c.t1, c.t2, c.t3, c.t4)
246+
if m != c.minError {
247+
t.Errorf("minError(%s, %s, %s, %s) = %s; want %s",
248+
c.t1.Format(format),
249+
c.t2.Format(format),
250+
c.t3.Format(format),
251+
c.t4.Format(format),
252+
m, c.minError)
253+
}
254+
}
255+
}
256+
178257
func TestOfflineKissCode(t *testing.T) {
179258
codes := []struct {
180259
id uint32

0 commit comments

Comments
 (0)