clock

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

Documentation

Overview

Package clock holds one session's (or process's) estimate of the Korbit server clock — the single piece of shared state behind every signed request — and the one operation that measures and installs it. State is the estimate; Syncer is the measurement home shared by every signing surface (see below).

Why one estimate is shared

Korbit verifies each signed `timestamp` against an asymmetric window: serverTime − recvWindow ≤ ts < serverTime + 1000 (recvWindow default 5s, max 60s; the +1s future bound is fixed). So a corrected timestamp must track the SERVER's clock, not UTC, and must lean into the past by the measurement uncertainty so it can never cross the +1s bound.

A single State is meant to be shared by every surface that signs against that window within a session or process:

  • WebSocket upgrade signing (the private endpoint's signed query),
  • REST request signing (backfill reads, and any L1 client wired to it),
  • the corrective EXCEED_TIME_WINDOW resync (re-measure → Install),
  • delivery-delay measurement (translating server frame timestamps into local terms via Offset).

Sharing one State means a correction discovered on any surface (typically a REST EXCEED_TIME_WINDOW resync) is instantly visible to all the others: the skew is measured once per session, not re-learned per call.

The surface

State — the estimate (mutex-guarded, safe for concurrent use):

  • New(now) — build over a local-clock reader (test seam).
  • Install(offset,lean)— record a measured estimate (plain int64s).
  • SignNow() — the timestamp to sign with (estimate, leaned past).
  • ServerNowMs() — the server-now estimate (local + offset, UN-leaned), for defaulting server-relative lookback windows sent on the wire (the history walk's start). NOT for stamping local records — the action journal uses the caller's own system clock.
  • Offset() — the raw un-leaned offset (delay measurement).
  • Measured() — whether an estimate has been installed yet.
  • RecvWindowMs() — the one widened recvWindow every signer includes (REST, the WS upgrade, the corrective resync); 0 below the 5s default (omit-below-5s — the server then applies its own 5s window).

Syncer — the measurement operation over a State (safe for concurrent use):

  • NewSyncer(state,measure,now,coolDownMs) — build it; measure is a MeasureFunc.
  • Sync() — measure and Install, subject to single-flight + cooldown.
  • State() — the shared State it installs into.
  • DefaultCoolDownMs — the production cooldown spacing.

The 3×RTTmin (== 6×lean) widening rule lives here in one place, so every signing surface — REST, the WS upgrade, and the corrective resync — enforces the identical validity window (RecvWindowMs, omit-below-5s).

Requirements for callers

  • Build ONE Syncer per process and share it (with its State) across every signing surface, so the offset is measured once and a correction on any surface is seen by all. Do NOT make a second Syncer/State for the same process — that defeats the shared-estimate property.
  • The estimate is injected, not a package global, so the test suite can run many in-process commands without cross-test skew leakage.
  • Proactive measurement is the caller's choice (the CLI gates it on --time-sync on); the reactive resync calls Sync — on a correctable EXCEED_TIME_WINDOW rejection, or the stream's delivery-delay kick against an unmeasured clock — and is wired for every signed call (and a public streaming session that opts into delay detection) except under --time-sync off (which opts out of all correction). Loop/storm safety comes from three layers: the retry layer's per-call once-flag, the Syncer's single-flight, and its cooldown.

The Syncer (measurement + install)

State holds the estimate; the Syncer owns the one operation that MEASURES and installs it. A process builds ONE Syncer over its State and shares it with every signing surface, so the offset is measured once and a correction is visible everywhere. The Syncer adds single-flight (concurrent triggers share one probe) and a cooldown (a fresh estimate is reused for DefaultCoolDownMs before re-probing), which together with the retry layer's per-call once-flag keep an EXCEED_TIME_WINDOW condition from looping or storming /v2/time. See syncer.go. The measurement primitive itself (MeasureClockOffset) stays in internal/korbit; the Syncer takes a plain MeasureFunc the caller wires to it, preserving the no-import-cycle rule below.

Dependency rule (no cycle with package korbit)

This package MUST NOT import internal/korbit, and internal/korbit MUST NOT import this package. The measurement primitive (MeasureClockOffset, the ClockOffset type) stays in korbit; the L1 korbit.Client takes a plain `func() int64` (wired from State.SignNow) rather than a *State. Install therefore takes plain int64s — the caller converts from korbit.ClockOffset (offsetMs = off.OffsetMs, leanMs = off.UncertaintyMs()) at the wiring seam.

Index

Constants

View Source
const DefaultCoolDownMs = 2000

DefaultCoolDownMs is the minimum spacing between server-clock measurements a Syncer enforces (see Syncer). A successful estimate is reused for this long before another trigger re-probes, so a persistently rejected signer cannot hammer /v2/time. It is short enough that a genuinely needed correction after the window is barely delayed, and the shared estimate means the first successful resync already fixes signing for every surface.

Variables

This section is empty.

Functions

This section is empty.

Types

type MeasureFunc

type MeasureFunc func() (offsetMs, leanMs int64, err error)

MeasureFunc probes the server clock and returns the offset (serverClock − localClock) and the lean (the measurement uncertainty, RTTmin/2) to install. It performs network I/O and is called by Sync WITHOUT any Syncer lock held. Returning an error leaves the previous estimate untouched. It is the seam that keeps this package free of any korbit import: the caller wraps korbit.MeasureClockOffset and converts the result to plain int64s (offsetMs = off.OffsetMs, leanMs = off.UncertaintyMs()).

type State

type State struct {
	// contains filtered or unexported fields
}

State is one session's (or one process's) estimate of the Korbit server clock, shared by everything that signs against it: the WebSocket upgrade query, REST request signing, the EXCEED_TIME_WINDOW corrective resync, and delivery-delay measurement. It is mutex-guarded and safe for concurrent use.

One State == one estimate. Sharing a single State across a session's WS upgrade signing, REST signing, and corrective resync is the whole point: a clock correction triggered by any one of them (e.g. a REST EXCEED_TIME_WINDOW resync) is immediately visible to the others, so the skew is measured once per session rather than re-discovered on every surface.

func New

func New(now func() int64) *State

New builds a State over the given local clock. now must be non-nil (the unix ms reader); callers wire it to time.Now().UnixMilli or a test seam. Until Install is called the estimate is the bare local clock (offset 0, no lean).

func (*State) Install

func (s *State) Install(offsetMs, leanMs int64)

Install records a measured server-clock estimate. offsetMs is (serverClock - localClock); leanMs is the measurement uncertainty that SignNow leans the signed timestamp into the past by (so it can never cross the server's fixed +1s future bound). Callers convert these from a korbit.ClockOffset (offsetMs = off.OffsetMs, leanMs = off.UncertaintyMs()); the conversion lives at the call site so this package stays free of any korbit import (no dependency cycle — see doc.go).

func (*State) Measured

func (s *State) Measured() bool

Measured reports whether an estimate has been installed yet.

func (*State) Offset

func (s *State) Offset() int64

Offset is the raw (un-leaned) offset estimate, 0 until measured. Used to translate server-stamped frame timestamps into local terms (delivery-delay measurement) and to stamp backfill events with the server's notion of now.

func (*State) RecvWindowMs

func (s *State) RecvWindowMs() int

RecvWindowMs returns the single signed-request validity window every signer includes — REST signing, the WebSocket upgrade, and the EXCEED_TIME_WINDOW corrective resync all read it, so the enforced window is identical on every surface. It is the widened window to include when the measured uncertainty is large enough that the past-lean could push a signed timestamp below the server's default `serverTime - 5000` lower bound, else 0 — the omit-below-5s convention: at or under the 5s default the param is omitted and the server applies its own 5s window (sending an explicit 5000 would enforce the same window). The widened value is 3×RTTmin (== 6×lean, since lean is RTTmin/2), capped at the server maximum of 60s. 0 until measured.

func (*State) ServerNowMs

func (s *State) ServerNowMs() int64

ServerNowMs is the best estimate of the server's current unix-ms time: the local clock plus the raw offset, UN-leaned (unlike SignNow, which leans into the past for the signing bound). It reads the same local-clock seam the estimate was measured against, so it honors a test clock. Before Install it is the bare local clock. Used to default server-relative lookback windows sent on the wire (the history walk's start).

func (*State) SignNow

func (s *State) SignNow() int64

SignNow is the clock to sign with: the server-clock estimate leaned into the past by the measurement uncertainty, so a signed timestamp can never cross the server's fixed +1s future bound. Before Install it is the bare local clock.

type Syncer

type Syncer struct {

	// Log is the optional operational logger for sync decisions: single-flight
	// share and cooldown reuse at Debug, a new estimate installed at Info, a
	// measurement failure at Warn. nil = silent. The probe-level RTT/offset
	// telemetry lives in the MeasureFunc (the caller threads a logger into
	// korbit.MeasureClockOffset). It carries no secret.
	Log *slog.Logger
	// contains filtered or unexported fields
}

Syncer is the single home for server-clock measurement in a process: one shared State plus the one measurement operation that installs into it. It is the central timekeeping instance — every signing surface (REST signing, the WebSocket upgrade, the place protocol's resync) shares ONE Syncer, so a correction discovered on any of them is instantly visible to all.

Two guards keep an EXCEED_TIME_WINDOW condition from looping or storming /v2/time:

  • Single-flight: concurrent Sync calls collapse onto ONE in-flight probe; the late callers wait for it and share its result. A burst of simultaneously rejected signed calls (e.g. a Promise.all of placements) therefore triggers a single measurement, not one per call.
  • Cooldown: a successful estimate is reused (no re-probe) for coolDownMs. Rapid sequential rejections re-measure at most once per window. The estimate is not given up — a trigger after the window re-probes — so a clock that legitimately drifts over a long session is still corrected.

These complement the per-call once-flag in the retry layer (which caps each logical call to a single resync). Together: at most one resync per call, and at most one measurement per cooldown window across the whole process.

A Syncer is safe for concurrent use.

func NewSyncer

func NewSyncer(state *State, measure MeasureFunc, now func() int64, coolDownMs int64) *Syncer

NewSyncer builds a Syncer over state, measuring with measure. now is the local clock (unix ms) used only for the cooldown spacing; pass the same clock the rest of the process uses (injectable for tests). coolDownMs <= 0 disables the cooldown (every non-concurrent trigger re-probes); production wires DefaultCoolDownMs.

func (*Syncer) State

func (s *Syncer) State() *State

State returns the shared estimate the Syncer installs into — the clock to sign against (State.SignNow) and read the offset/recvWindow from.

func (*Syncer) Sync

func (s *Syncer) Sync() error

Sync measures the server clock and installs the estimate, subject to single-flight and the cooldown. It returns nil when it reused a recent estimate (no probe) or after a successful measurement, and the measurement error otherwise. A failed measurement leaves the previous estimate in place and does NOT start the cooldown, so the next trigger re-probes immediately rather than reusing a stale (or never-measured) estimate.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL