From 8c7fa05d6b77c890e01950acca4516bf8e18f326 Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Mon, 6 Oct 2025 20:38:59 +0300 Subject: [PATCH] Document mono package, support Unix and Parse. --- .changeset/mono-docs-and-api.md | 5 +++ utils/mono/mono.go | 54 ++++++++++++++++++++++++++++++++- utils/mono/mono_test.go | 17 ++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 .changeset/mono-docs-and-api.md diff --git a/.changeset/mono-docs-and-api.md b/.changeset/mono-docs-and-api.md new file mode 100644 index 000000000..a0082953b --- /dev/null +++ b/.changeset/mono-docs-and-api.md @@ -0,0 +1,5 @@ +--- +"@livekit/protocol": patch +--- + +Document mono package, support Unix and Parse. diff --git a/utils/mono/mono.go b/utils/mono/mono.go index eab94df71..cfba51650 100644 --- a/utils/mono/mono.go +++ b/utils/mono/mono.go @@ -12,6 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package mono enforces use of monotonic time when creating/parsing time.Time from external sources. +// +// Using time.Now produces monotonic time values that correctly measure time difference in the presence of clock resets. +// +// On the other hand, time produce by time.Unix or time.Parse doesn't have this property. Clock reset may lead to incorrect +// durations computed from these timestamps. To fix this, prefer using Unix and Parse provided by this package. +// +// Monotonic time could also be erased when using functions like Truncate, Round, In, UTC. Be careful when using these. +// +// More details: https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md package mono import "time" @@ -21,21 +31,63 @@ var ( epochNano = epoch.UnixNano() ) +// resetClock resets the reference timestamp. +// Used in tests only. +func resetClock() { + epoch = time.Now() + epochNano = epoch.UnixNano() +} + +// jumpClock adjusts reference timestamp by a given duration emulating a clock reset/jump. +// Used in tests only. +func jumpClock(dt time.Duration) { + epoch = epoch.Add(-dt) // we pretend time.Now() jumps, not the reference + epochNano = epoch.UnixNano() +} + +// FromTime ensures that time.Time value uses monotonic clock. +// +// Deprecated: You should probably use Unix or Parse instead. func FromTime(t time.Time) time.Time { + return fromTime(t) +} + +func fromTime(t time.Time) time.Time { if t.IsZero() { return time.Time{} } return epoch.Add(t.Sub(epoch)) } +// Now is a wrapper for time.Time. +// +// Deprecated: time.Now always uses monotonic clock. func Now() time.Time { - return epoch.Add(time.Since(epoch)) + return time.Now() +} + +// Unix is an analog of time.Unix that produces monotonic time. +func Unix(sec, nsec int64) time.Time { + return fromTime(time.Unix(sec, nsec)) +} + +// Parse is an analog of time.Parse that produces monotonic time. +func Parse(layout, value string) (time.Time, error) { + t, err := time.Parse(layout, value) + if err != nil { + return time.Time{}, err + } + return fromTime(t), nil } +// UnixNano returns the number of nanoseconds elapsed, based on the application start time. +// This value may be different from time.Now().UnixNano() in the presence of time resets. func UnixNano() int64 { return epochNano + int64(time.Since(epoch)) } +// UnixMicro returns the number of microseconds elapsed, based on the application start time. +// This value may be different from time.Now().UnixMicro() in the presence of time resets. func UnixMicro() int64 { return UnixNano() / 1000 } diff --git a/utils/mono/mono_test.go b/utils/mono/mono_test.go index a3a3c4b6e..2e89ce94a 100644 --- a/utils/mono/mono_test.go +++ b/utils/mono/mono_test.go @@ -9,9 +9,24 @@ import ( func TestMonoZero(t *testing.T) { ts := time.Time{} - ts2 := FromTime(ts) + ts2 := fromTime(ts) require.True(t, ts.IsZero()) require.True(t, ts2.IsZero()) require.True(t, ts.Equal(ts2)) require.Equal(t, ts.String(), ts2.String()) } + +func TestMono(t *testing.T) { + t.Cleanup(resetClock) // restore + + ts1 := time.Now() + ts2 := ts1.Add(time.Second) + + ts1m := fromTime(ts1) + // emulate a clock reset, +1h jump + // TODO: use synctest when we switch to Go 1.25 + jumpClock(time.Hour) + ts2m := fromTime(ts2) + + require.Equal(t, ts2.Sub(ts1), ts2m.Sub(ts1m)) +}