Skip to content

Add periodic keep-alive timesync (opt-in)#28

Open
stefanrammo wants to merge 1 commit into
mainfrom
task/add-timesync
Open

Add periodic keep-alive timesync (opt-in)#28
stefanrammo wants to merge 1 commit into
mainfrom
task/add-timesync

Conversation

@stefanrammo

Copy link
Copy Markdown
Collaborator

Periodic eCurrentTimeRequest keeps an otherwise-idle connection alive and estimates the client/server clock offset. Off by default; opt in via new studio.api.Client(url, listener, autoConnect, { enableTimeSync: true }), period via options.timeSyncPeriodSec (default 10s, minimum 1, capped at the runtime timer maximum).

  • Three-sample cycle, smallest-RTT sample selected; delta is packetReceived - (remoteTime + RTT/2), signed nanoseconds, measured on an epoch-anchored monotonic clock so wall-clock steps cannot corrupt the offset or stall the cycle
  • Applied to value timestamps (int64 Long); event timestamps stay in server-clock terms as the subscribeToEvents resume token
  • A delta refresh fans out to live same-host connections, and once the system is connected also seeds later ones; changes of 20 ms or less are treated as jitter and ignored
  • A request unanswered for one period is discarded and re-emitted so keep-alive resumes; lacking a request id, a late uncorrelated response is kept transient (min-RTT-of-3 and the next cycle overwriting the offset), not frozen
  • An error-only disconnect (no close event follows) runs the same cleanup as close; frames from a torn-down or replaced transport are then discarded, so a late or old-socket frame cannot re-arm timesync or send on a dead connection

Also fixes two connection-teardown bugs:

  • emitCurrentTimeRequest guards socketTransport.readyState() == OPEN before the raw send; without it the periodic send throws on a CLOSING/CLOSED socket when the interval fires before stopTimesync (introduced with this feature's raw send)
  • onError fires onDisconnected once, via a shared notifyDisconnectedOnce() now used by onClosed too; an error-only disconnect previously skipped the onDisconnected lifecycle notification that a close performs (pre-existing)

CDP-3259

Periodic eCurrentTimeRequest keeps an otherwise-idle connection alive and
estimates the client/server clock offset. Off by default; opt in via
new studio.api.Client(url, listener, autoConnect, { enableTimeSync: true }),
period via options.timeSyncPeriodSec (default 10s, minimum 1, capped at
the runtime timer maximum).

- Three-sample cycle, smallest-RTT sample selected; delta is
  packetReceived - (remoteTime + RTT/2), signed nanoseconds, measured on
  an epoch-anchored monotonic clock so wall-clock steps cannot corrupt
  the offset or stall the cycle
- Applied to value timestamps (delivered as signed int64 Longs; an absent
  timestamp passes through unchanged); event timestamps stay in
  server-clock terms as the subscribeToEvents resume token
- A delta refresh fans out to live same-host connections, and once the
  system is connected also seeds later ones; changes of 20 ms or less are
  treated as jitter and ignored
- A request unanswered for one period is discarded and re-emitted so
  keep-alive resumes. Lacking a request id, a response that outlives its
  watchdog lands in the re-emitted request's sample with a near-zero RTT
  (which would win the selection) and a period-stale remoteTime; a cycle
  whose sample offsets disagree by more than the samples' round-trip
  times is therefore dropped and re-measured at the next period
- An error-only disconnect (no close event follows) notifies once, stops
  the stall and timesync timers, and schedules the reconnect; frames from
  a torn-down or replaced transport are then discarded, so a late or
  old-socket frame cannot re-arm timesync or send on a dead connection
- _initTimesync, _timestampDeltaNs and _timesyncTimerActive are test
  observability hooks with no production consumers

Also fixes a pre-existing lifecycle gap: onError fires onDisconnected once, via
a shared notifyDisconnectedOnce() now used by onClosed too; an error-only
disconnect previously skipped the onDisconnected notification that a close
performs.

CDP-3259
@stefanrammo stefanrammo requested review from Karmo7 and nuubik July 2, 2026 05:22
@Karmo7

Karmo7 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

"event timestamps stay in server-clock terms as the subscribeToEvents resume token" - I think this should be configurable. I know this is how it is currently in CDP Studio but it is not always good. E.g. in Analyze mode when plotting, currently events and values do not match when added to same graph.

@Karmo7 Karmo7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Time sync part looks good and the options map to constructor is a good idea. I think though event timestamps syncing should be configurable as well.

And as for the timestamp publishing, maybe should discuss it for a moment. In C++ client, we have found issues with the current time adjusting, namely there are use cases where one would want to know both the original and adjusted timestamp, so maybe we could figure out how to give both to the user of the client.

One example of such use cases is plotting signals from multiple applications. If the controller clocks are out of sync, we would want to enable StudioAPI synchronizations but it would still be good to see the original timestamp in a tooltip to better match the graphed values with some other log files.

Comment thread index.js
}
}
// Event timestamps stay in server-clock domain: they are the
// subscribeToEvents(startingFrom) resume token; shifting breaks resume.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As we discovered in CDP Studio Alarms pane, not shifting makes it also hard to subscribe in some cases, e.g. if one want all alarm events starting from last 15 seconds (to catch periodically repeated alarm reprise events and immediately know which alarms are active). So as I wrote in another comment, I think it should be configurable whether to shift event timestamps. Or alternatively, the time diff should be provided with the original timestamp (so client code has access to both timestamps).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I can add an adjustEventTimestamps option to the constructor, default off. When enabled, event timestamps are shifted the same way as values and subscribeToEvents(startingFrom) takes client-clock times, converted back to server clock internally.
For original+adjusted: value callbacks get the original timestamp as an optional third argument, shifted events keep an originalTimestamp field, and a getter exposes the current offset so either timestamp can be derived.

@Karmo7 Karmo7 Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Then the next question is that if both adjusted and original timestamps are always available, then I guess the timesync could be by default enabled? Or what does @nuubik think?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants