Skip to content

Conversation

@romanornr
Copy link
Contributor

PR implement RFC 5905 offset calculation and verify time before warning

Before I got

[DEBUG] | DATABASE | 19/12/2025 22:06:26 | Database manager starting...
[DEBUG] | CONNECTION | 19/12/2025 22:06:26 | Connection manager starting...
[DEBUG] | LOG | 19/12/2025 22:06:26 | Internet connectivity found
[DEBUG] | CONNECTION | 19/12/2025 22:06:26 | Connection manager started.
Your system time is out of sync, this may cause issues with trading
How would you like to show future notifications? (a)lert at startup / (w)arn periodically / (d)isable

No, the issue is not my system/setup

❯ timedatectl timesync-status

       Server: 162.159.200.1 (time.cloudflare.com)
Poll interval: 34min 8s (min: 32s; max 34min 8s)
         Leap: normal
      Version: 4
      Stratum: 3
    Reference: AE20804
    Precision: 1us (-26)
Root distance: 2.852ms (max: 5s)
       Offset: -383us
        Delay: 10.140ms
       Jitter: 1.091ms
 Packet count: 15
    Frequency: -18.014ppm
My PC (perfectly synced)           NTP Server
        |                                       |
   T1 = 10:00:00.000 ─── You send request ───→  |
        |                                       | T2 = 10:00:00.050 (server receives)
        |                                       | T3 = 10:00:00.050 (server sends response)
        |  ←── Response packet arrives ─────────|
   T4 = 10:00:00.100 (you receive it)           |

The 4 timestamps

| Name | When | Which Clock | Where is it? |
|------|------|-------------|--------------|
| **T1** | Client sends request | Client | **Not recorded** in current code! |
| **T2** | Server receives request | Server | `RxTimeSec` / `RxTimeFrac` |
| **T3** | Server sends response | Server | `TxTimeSec` / `TxTimeFrac` |
| **T4** | Client receives response | Client | **Not recorded** in current code! |

Original code

NTPTime := T3              // 10:00:00.050 (server's transmit time)
currentTime := time.Now()  // 10:00:00.100 (this is T4)
diff := NTPTime.Sub(currentTime)  // T3 - T4 = -50ms

The issue
T3 - T4 gives us -(network_delay) + offset
If my clock is perfect (offset = 0), I still get -50ms due to network

The current code asks: "Bonjour, what time did the server say it was?"
But that's stale information (packet took time to travel)

By the time you read T3 = 10:00:00.050, it's already 10:00:00.100 on your clock. You can't just compare them directly.

RFC 5905 compensates for network delay/latency

offset = ((T2 - T1) + (T3 - T4)) / 2

Where:

  • T1 = Origin Timestamp - when client sent the request
  • T2 = Receive Timestamp - when server received the request (RxTimeSec/RxTimeFrac)
  • T3 = Transmit Timestamp - when server sent the response (TxTimeSec/TxTimeFrac)
  • T4 = Destination Timestamp - when client received the response

This formula cancels out the network round-trip time, giving an accurate offset measurement.

Client                          Server
  |                               |
  |-------- Request ------------>| T1 (client records)
  |                               | T2 (server records in RxTime)
  |                               |
  |                               | T3 (server records in TxTime)
  |<------- Response ------------|
  | T4 (client records)           |
  |                               |

Network delay (one way) ≈ ((T4-T1) - (T3-T2)) / 2
Offset = ((T2-T1) + (T3-T4)) / 2

Also worth to mention
Even when the calculation was fixed, I noticed it didn't actually actually check if the time is out of sync. It seemed like it assumed and asked the user how they want to be notified in the future.

Also
Migrated from net.DialTimeout to net.Dialer.DialContext

Anyways after these changes it all worked. Just gonna do the pull request now (can't wait to do 10 more fixes because the automated tests are gonna complain

Sources to confirm:
https://datatracker.ietf.org/doc/html/rfc5905

GuerrillaNtp https://github.com/robertvazan/guerrillantp

  • RFC4330-compliant SNTP client
  • Provides CorrectionOffset and RoundTripTime
  • Uses all four timestamps: OriginTimestamp, ReceiveTimestamp, TransmitTimestamp, DestinationTimestamp

GuerrillaNtp documentation:

{
  "Response": {
    "OriginateTimestamp": "2023-10-27T10:30:00.0000000Z",  // T1
    "ReceiveTimestamp": "2023-10-27T10:30:00.0100000Z",    // T2
    "TransmitTimestamp": "2023-10-27T10:30:00.0150000Z"    // T3
  },
  "CorrectionOffset": "00:00:00.0051234",  // Calculated using RFC formula
  "RoundTripTime": "00:00:00.0258765"      // (T4-T1) - (T3-T2)
}

Type of change

Please delete options that are not relevant and add an x in [] as item is complete.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How has this been tested

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration and
also consider improving test coverage whilst working on a certain feature or package.

  • go test ./... -race
  • golangci-lint run
  • Test X

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation and regenerated documentation via the documentation tool
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally and on Github Actions with my changes
  • Any dependent changes have been merged and published in downstream modules

Copilot AI review requested due to automatic review settings December 19, 2025 21:35
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly implements the RFC 5905 NTP offset calculation, which is a significant improvement over the previous logic that didn't account for network latency. It also adds a check to verify if the system time is actually out of sync before prompting the user, which improves the user experience. The migration to net.Dialer.DialContext is also a good move for context-aware network operations.

I've identified a few areas for improvement, mainly related to code duplication and logging consistency. Please see my detailed comments.

Comment on lines +240 to 300
func (m *ntpManager) getTimeOffset(ctx context.Context) (time.Duration, error) {
dialer := &net.Dialer{
Timeout: ntpDialTimeout,
}

for i := range m.pools {
con, err := net.DialTimeout("udp", m.pools[i], 5*time.Second) //nolint:noctx // TODO: #2006 Use (*net.Dialer).DialContext with (*net.Dialer).Timeout
conn, err := dialer.DialContext(ctx, "udp", m.pools[i])
if err != nil {
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting next", m.pools[i])
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting to next", m.pools[i])
continue
}

if err = con.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
log.Warnf(log.TimeMgr, "Unable to SetDeadline. Error: %s\n", err)
err = con.Close()
if err != nil {
if err = conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {
log.Warnf(log.TimeMgr, "Unable to set deadline on hosts %v. Error %s. attempting to next\n", m.pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

// T1: Record time before sending request (origin timestamp)
t1 := time.Now()

req := &ntpPacket{Settings: 0x1B}
if err = binary.Write(con, binary.BigEndian, req); err != nil {
log.Warnf(log.TimeMgr, "Unable to write. Error: %s\n", err)
err = con.Close()
if err != nil {
if err = binary.Write(conn, binary.BigEndian, req); err != nil {
log.Warnf(log.TimeMgr, "Unable to write to hosts %v. Error %s. Attempting to next\n", m.pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

rsp := &ntpPacket{}
if err = binary.Read(con, binary.BigEndian, rsp); err != nil {
log.Warnf(log.TimeMgr, "Unable to read. Error: %s\n", err)
err = con.Close()
if err != nil {
if err = binary.Read(conn, binary.BigEndian, rsp); err != nil {
log.Warnf(log.TimeMgr, "Unable to read from hosts %v. Error: %s. Attempting to next\n", m.pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

secs := float64(rsp.TxTimeSec) - 2208988800
nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32
// T4L Record time after receiving response (Destination timestamp)
t4 := time.Now()

err = con.Close()
if err != nil {
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
return time.Unix(int64(secs), nanos)

// T2: Server receive timestamp (when server received our request)
t2 := ntpTimestampToTime(rsp.RxTimeSec, rsp.RxTimeFrac)
// T3: Server transmit timestamp (when server sent our response)
t3 := ntpTimestampToTime(rsp.TxTimeSec, rsp.TxTimeFrac)

// RFC 5905 offset calculation: ((T2-T1) + (T3-T4)) / 2
// This formula cancels out the network round-trip time
offset := (t2.Sub(t1) + t3.Sub(t4)) / 2
return offset, nil
}
log.Warnln(log.TimeMgr, "No valid NTP servers found, using current system time")
return time.Now().UTC()
log.Warnln(log.TimeMgr, "No valid NTP servers found")
return 0, errNoValidNTPServer
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is significant code duplication between getTimeOffset and the new CheckNTPOffset function. The logic is almost identical. To improve maintainability and reduce redundancy, you could refactor getTimeOffset to call CheckNTPOffset. This would centralize the NTP offset calculation logic. Note that this would change some log messages to be consistent with CheckNTPOffset.

func (m *ntpManager) getTimeOffset(ctx context.Context) (time.Duration, error) {
	return CheckNTPOffset(ctx, m.pools)
}

}

if err = conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {
log.Warnf(log.TimeMgr, "NTP check: Unable to set deadline on %v. Error %s. Attempting next\n", pools[i], err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Several log.Warnf calls in this file have a trailing newline character (\n) in the format string (here and on lines 57, 66, 232, 253, 265, 274). This is inconsistent with other log.Warnf calls that don't have it. log.Warnf does not add a newline by default, so this leads to inconsistent formatting in logs. Please remove the explicit \n for consistency. If a newline is always intended, consider using log.Warnln.

Suggested change
log.Warnf(log.TimeMgr, "NTP check: Unable to set deadline on %v. Error %s. Attempting next\n", pools[i], err)
log.Warnf(log.TimeMgr, "NTP check: Unable to set deadline on %v. Error %s. Attempting next", pools[i], err)


secs := float64(rsp.TxTimeSec) - 2208988800
nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32
// T4L Record time after receiving response (Destination timestamp)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a typo in the comment. T4L should be T4.

// T4: Record time after receiving response (Destination timestamp)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical bug in the NTP time synchronization implementation by properly implementing the RFC 5905 offset calculation formula. Previously, the code incorrectly calculated time difference using T3 - T4 (server transmit time minus client receive time), which included network latency and produced false positives. The fix implements the correct formula ((T2-T1) + (T3-T4)) / 2 which cancels out network round-trip time. Additionally, the PR adds proper time verification before prompting users, ensuring warnings only appear when time is actually out of sync.

Key Changes:

  • Implemented RFC 5905 compliant offset calculation using all four NTP timestamps (T1, T2, T3, T4)
  • Added CheckNTPOffset function to verify time sync before warning users at startup
  • Migrated from deprecated net.DialTimeout to context-aware net.Dialer.DialContext

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.

File Description
engine/ntp_manager.go Core fix implementing RFC 5905 offset calculation, added CheckNTPOffset function, refactored time query logic to use proper timestamps and context
engine/ntp_manager_test.go Updated test signatures to use context.Context, removed redundant test code, added Pool configuration to tests
engine/engine.go Added time verification logic before prompting user, only shows warning when time is actually out of sync


secs := float64(rsp.TxTimeSec) - 2208988800
nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32
// T4L Record time after receiving response (Destination timestamp)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "T4L" should be "T4:"

Suggested change
// T4L Record time after receiving response (Destination timestamp)
// T4: Record time after receiving response (Destination timestamp)

Copilot uses AI. Check for mistakes.
conn, err := dialer.DialContext(ctx, "udp", m.pools[i])
if err != nil {
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting next", m.pools[i])
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting to next", m.pools[i])
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error message formatting: "attempting to next" should be "attempting next" to match the pattern used in CheckNTPOffset function (line 40).

Suggested change
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting to next", m.pools[i])
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v. Attempting next\n", m.pools[i])

Copilot uses AI. Check for mistakes.
err = con.Close()
if err != nil {
if err = conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {
log.Warnf(log.TimeMgr, "Unable to set deadline on hosts %v. Error %s. attempting to next\n", m.pools[i], err)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error message formatting: "attempting to next" should be "attempting next" to match the pattern used in CheckNTPOffset function.

Copilot uses AI. Check for mistakes.
err = con.Close()
if err != nil {
if err = binary.Write(conn, binary.BigEndian, req); err != nil {
log.Warnf(log.TimeMgr, "Unable to write to hosts %v. Error %s. Attempting to next\n", m.pools[i], err)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error message formatting: "Attempting to next" should be "Attempting next" to match the pattern used in CheckNTPOffset function.

Copilot uses AI. Check for mistakes.
err = con.Close()
if err != nil {
if err = binary.Read(conn, binary.BigEndian, rsp); err != nil {
log.Warnf(log.TimeMgr, "Unable to read from hosts %v. Error: %s. Attempting to next\n", m.pools[i], err)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error message formatting: "Attempting to next" should be "Attempting next" to match the pattern used in CheckNTPOffset function.

Copilot uses AI. Check for mistakes.
Comment on lines +345 to +355
allowedDiff := *bot.Config.NTPClient.AllowedDifference
allowedNegDiff := -*bot.Config.NTPClient.AllowedNegativeDifference
if offset > allowedDiff || offset < allowedNegDiff {
gctlog.Warnf(gctlog.TimeMgr, "System time offset detected: %v (allowed: +%v / %v)", offset, allowedDiff, allowedNegDiff)
responseMessage, err := bot.Config.SetNTPCheck(os.Stdin)
if err != nil {
return fmt.Errorf("unable to set NTP check: %w", err)
}
gctlog.Infoln(gctlog.TimeMgr, responseMessage)
} else {
gctlog.Debugf(gctlog.TimeMgr, "System time is in sync (offset: %v)", offset)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential nil pointer dereference: If CheckNTPConfig() fails to set the default values for AllowedDifference or AllowedNegativeDifference (which could happen in unusual circumstances), dereferencing these pointers on lines 345-346 would cause a panic. Consider adding nil checks before dereferencing, or ensure CheckNTPConfig is called earlier in the config initialization flow to guarantee these values are set.

Suggested change
allowedDiff := *bot.Config.NTPClient.AllowedDifference
allowedNegDiff := -*bot.Config.NTPClient.AllowedNegativeDifference
if offset > allowedDiff || offset < allowedNegDiff {
gctlog.Warnf(gctlog.TimeMgr, "System time offset detected: %v (allowed: +%v / %v)", offset, allowedDiff, allowedNegDiff)
responseMessage, err := bot.Config.SetNTPCheck(os.Stdin)
if err != nil {
return fmt.Errorf("unable to set NTP check: %w", err)
}
gctlog.Infoln(gctlog.TimeMgr, responseMessage)
} else {
gctlog.Debugf(gctlog.TimeMgr, "System time is in sync (offset: %v)", offset)
if bot.Config.NTPClient.AllowedDifference == nil || bot.Config.NTPClient.AllowedNegativeDifference == nil {
gctlog.Warnf(gctlog.TimeMgr, "NTP client allowed time differences are not configured; skipping offset validation")
} else {
allowedDiff := *bot.Config.NTPClient.AllowedDifference
allowedNegDiff := -*bot.Config.NTPClient.AllowedNegativeDifference
if offset > allowedDiff || offset < allowedNegDiff {
gctlog.Warnf(gctlog.TimeMgr, "System time offset detected: %v (allowed: +%v / %v)", offset, allowedDiff, allowedNegDiff)
responseMessage, err := bot.Config.SetNTPCheck(os.Stdin)
if err != nil {
return fmt.Errorf("unable to set NTP check: %w", err)
}
gctlog.Infoln(gctlog.TimeMgr, responseMessage)
} else {
gctlog.Debugf(gctlog.TimeMgr, "System time is in sync (offset: %v)", offset)
}

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +91
func CheckNTPOffset(ctx context.Context, pools []string) (time.Duration, error) {
if len(pools) == 0 {
return 0, errors.New("no NTP pools configured")
}

dialer := &net.Dialer{
Timeout: ntpDialTimeout,
}

for i := range pools {
conn, err := dialer.DialContext(ctx, "udp", pools[i])
if err != nil {
log.Warnf(log.TimeMgr, "NTP check: Unable to connect to %v, attempting next", pools[i])
continue
}

if err = conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {
log.Warnf(log.TimeMgr, "NTP check: Unable to set deadline on %v. Error %s. Attempting next\n", pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

// T1: Record time before sending request (origin timestamp)
t1 := time.Now()

req := &ntpPacket{Settings: 0x1B}
if err = binary.Write(conn, binary.BigEndian, req); err != nil {
log.Warnf(log.TimeMgr, "NTP check: Unable to write to %v. Error %s. Attempting next\n", pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

rsp := &ntpPacket{}
if err = binary.Read(conn, binary.BigEndian, rsp); err != nil {
log.Warnf(log.TimeMgr, "NTP check: Unable to read from %v. Error: %s. Attempting next\n", pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

// T4: Record time after receiving response (Destination timestamp)
t4 := time.Now()

if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}

// T2: Server receive timestamp (when server received our request)
t2 := ntpTimestampToTime(rsp.RxTimeSec, rsp.RxTimeFrac)
// T3: Server transmit timestamp (when server sent our response)
t3 := ntpTimestampToTime(rsp.TxTimeSec, rsp.TxTimeFrac)

// RFC 5905 offset calculation: ((T2-T1) + (T3-T4)) / 2
// This formula cancels out the network round-trip time
offset := (t2.Sub(t1) + t3.Sub(t4)) / 2
return offset, nil
}
return 0, errNoValidNTPServer
}
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new CheckNTPOffset function (lines 28-91) is a public API function but lacks test coverage. Given that this is a critical function that validates system time synchronization and is called during engine startup, it should have unit tests covering both success and failure scenarios (e.g., no pools configured, connection failures, valid offset calculation).

Copilot uses AI. Check for mistakes.
Comment on lines +240 to 300
func (m *ntpManager) getTimeOffset(ctx context.Context) (time.Duration, error) {
dialer := &net.Dialer{
Timeout: ntpDialTimeout,
}

for i := range m.pools {
con, err := net.DialTimeout("udp", m.pools[i], 5*time.Second) //nolint:noctx // TODO: #2006 Use (*net.Dialer).DialContext with (*net.Dialer).Timeout
conn, err := dialer.DialContext(ctx, "udp", m.pools[i])
if err != nil {
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting next", m.pools[i])
log.Warnf(log.TimeMgr, "Unable to connect to hosts %v attempting to next", m.pools[i])
continue
}

if err = con.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
log.Warnf(log.TimeMgr, "Unable to SetDeadline. Error: %s\n", err)
err = con.Close()
if err != nil {
if err = conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {
log.Warnf(log.TimeMgr, "Unable to set deadline on hosts %v. Error %s. attempting to next\n", m.pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

// T1: Record time before sending request (origin timestamp)
t1 := time.Now()

req := &ntpPacket{Settings: 0x1B}
if err = binary.Write(con, binary.BigEndian, req); err != nil {
log.Warnf(log.TimeMgr, "Unable to write. Error: %s\n", err)
err = con.Close()
if err != nil {
if err = binary.Write(conn, binary.BigEndian, req); err != nil {
log.Warnf(log.TimeMgr, "Unable to write to hosts %v. Error %s. Attempting to next\n", m.pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

rsp := &ntpPacket{}
if err = binary.Read(con, binary.BigEndian, rsp); err != nil {
log.Warnf(log.TimeMgr, "Unable to read. Error: %s\n", err)
err = con.Close()
if err != nil {
if err = binary.Read(conn, binary.BigEndian, rsp); err != nil {
log.Warnf(log.TimeMgr, "Unable to read from hosts %v. Error: %s. Attempting to next\n", m.pools[i], err)
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
continue
}

secs := float64(rsp.TxTimeSec) - 2208988800
nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32
// T4L Record time after receiving response (Destination timestamp)
t4 := time.Now()

err = con.Close()
if err != nil {
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
return time.Unix(int64(secs), nanos)

// T2: Server receive timestamp (when server received our request)
t2 := ntpTimestampToTime(rsp.RxTimeSec, rsp.RxTimeFrac)
// T3: Server transmit timestamp (when server sent our response)
t3 := ntpTimestampToTime(rsp.TxTimeSec, rsp.TxTimeFrac)

// RFC 5905 offset calculation: ((T2-T1) + (T3-T4)) / 2
// This formula cancels out the network round-trip time
offset := (t2.Sub(t1) + t3.Sub(t4)) / 2
return offset, nil
}
log.Warnln(log.TimeMgr, "No valid NTP servers found, using current system time")
return time.Now().UTC()
log.Warnln(log.TimeMgr, "No valid NTP servers found")
return 0, errNoValidNTPServer
}
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is significant code duplication between CheckNTPOffset (lines 28-91) and getTimeOffset (lines 240-300). Both functions perform essentially the same NTP query logic with only minor differences in logging messages. Consider extracting the common NTP query logic into a shared helper function to improve maintainability and reduce duplication.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +98
func ntpTimestampToTime(seconds, fractional uint32) time.Time {
unixSeconds := int64(seconds) - ntpEpochOffset
nanos := (int64(fractional) * 1.e9) >> 32
return time.Unix(unixSeconds, nanos)
}
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ntpTimestampToTime helper function (lines 94-98) lacks test coverage. Given that this function performs time conversion calculations that are critical for accurate NTP offset computation, it should have unit tests to verify correctness of the conversion from NTP timestamp format (seconds + fractional seconds) to time.Time.

Copilot uses AI. Check for mistakes.
@codecov
Copy link

codecov bot commented Dec 19, 2025

Codecov Report

❌ Patch coverage is 27.58621% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.05%. Comparing base (7bc0a06) to head (2d85737).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
engine/ntp_manager.go 32.87% 44 Missing and 5 partials ⚠️
engine/engine.go 0.00% 14 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2137      +/-   ##
==========================================
- Coverage   42.09%   42.05%   -0.05%     
==========================================
  Files         444      444              
  Lines      143153   143201      +48     
==========================================
- Hits        60267    60227      -40     
- Misses      75758    75841      +83     
- Partials     7128     7133       +5     
Files with missing lines Coverage Δ
engine/engine.go 49.10% <0.00%> (-0.99%) ⬇️
engine/ntp_manager.go 61.07% <32.87%> (-15.85%) ⬇️

... and 14 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@romanornr
Copy link
Contributor Author

Nice man, getting e-jumped by AI these days

@romanornr
Copy link
Contributor Author

@copilot open a new pull request to apply changes based on the comments in this thread

@thrasher- thrasher- added the review me This pull request is ready for review label Dec 29, 2025
@romanornr
Copy link
Contributor Author

please let me know if its ok, will do the things gemini suggested

Copy link
Collaborator

@shazbert shazbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just did a really brief check of this, tests are failing and some minor critiques I will need to go into the weeds fully to understand what is going on.

}

// T1: Record time before sending request (origin timestamp)
t1 := time.Now()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change variable names so we can remove commentary. t1 to origin

Comment on lines +18 to +19
ntpDialTimeout = 5 * time.Second
ntpReadWriteTimeout = 5 * time.Second
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: You can combine these as ntpDefaultTimeout

Comment on lines +33 to +36
dialer := &net.Dialer{
Timeout: ntpDialTimeout,
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dialer := &net.Dialer{
Timeout: ntpDialTimeout,
}
dialer := &net.Dialer{Timeout: ntpDialTimeout}

Squishy squashy

for i := range pools {
conn, err := dialer.DialContext(ctx, "udp", pools[i])
if err != nil {
log.Warnf(log.TimeMgr, "NTP check: Unable to connect to %v, attempting next", pools[i])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Warnf(log.TimeMgr, "NTP check: Unable to connect to %v, attempting next", pools[i])
log.Warnf(log.TimeMgr, "NTP check: Unable to connect to %q, attempting next", pools[i])

continue
}

if err = conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if err = conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {
if err := conn.SetDeadline(time.Now().Add(ntpReadWriteTimeout)); err != nil {

// CheckNTPOffset performs a one-time NTP check and returns the time offset.
// This can be called before the NTP manager is started to verify time sync.
// It uses the RFC 5905 formula: offset = ((T2-T1) + (T3-T4)) / 2
func CheckNTPOffset(ctx context.Context, pools []string) (time.Duration, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a dedicated test

continue
}

rsp := &ntpPacket{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might need some packet validation to make sure its correct. Stratum not zero, the mode is correct, and the TxTime txFractions are not zero.

Comment on lines +76 to +78
if err = conn.Close(); err != nil {
log.Errorln(log.TimeMgr, err)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: You can always have a sub func then just defer this close which saves on the error pathways, then in the loop just have an error check and continue and/or return value, might be useful for getTimeOffset as well to consolidate some code. Up to you.

}

// T4: Record time after receiving response (Destination timestamp)
t4 := time.Now()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destinationTs?

}

// ntpTimestampToTime converts timestamp (seconds and fractional) to time.Time
func ntpTimestampToTime(seconds, fractional uint32) time.Time {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a dedicated test

@thrasher- thrasher- self-requested a review January 22, 2026 05:06
@thrasher-
Copy link
Collaborator

@codex please review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@romanornr
Copy link
Contributor Author

Just did a really brief check of this, tests are failing and some minor critiques I will need to go into the weeds fully to understand what is going on.

Critiques are good! exactly what I was looking for. Yeah, I remember writing this PR when I was sick but had to know if the concept idea was in the right direction.

Will take another attempt by trying to refine this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review me This pull request is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants