diff --git a/observability/sessiontimer.go b/observability/sessiontimer.go index 8338839b5..6193bd043 100644 --- a/observability/sessiontimer.go +++ b/observability/sessiontimer.go @@ -1,12 +1,14 @@ package observability import ( + "sync" "time" "github.com/livekit/protocol/utils/options" ) type SessionTimer struct { + mu sync.Mutex lastMilli int64 lastSec int64 lastMin int64 @@ -33,7 +35,23 @@ func NewSessionTimer(startTime time.Time, opts ...SessionTimerOption) *SessionTi return options.Apply(&SessionTimer{lastMilli: ts, lastSec: ts, lastMin: ts}, opts) } +// Reset re-anchors the timer to startTime so that subsequent Advance calls +// measure elapsed time from startTime. It is intended to be called before the +// first Advance (e.g. when a participant actually joins) so that wall-clock +// time accrued before startTime is not counted toward the reported duration. +// The configured min-seconds/min-minutes options are preserved. +func (h *SessionTimer) Reset(startTime time.Time) { + h.mu.Lock() + defer h.mu.Unlock() + ts := startTime.UnixMilli() + h.lastMilli = ts + h.lastSec = ts + h.lastMin = ts +} + func (h *SessionTimer) Advance(now time.Time) (millis, secs, mins int64) { + h.mu.Lock() + defer h.mu.Unlock() ts := now.UnixMilli() if ts > h.lastMilli { millis = ts - h.lastMilli diff --git a/observability/sessiontimer_test.go b/observability/sessiontimer_test.go index 05b318d04..93ac45499 100644 --- a/observability/sessiontimer_test.go +++ b/observability/sessiontimer_test.go @@ -88,4 +88,30 @@ func TestSessionTimer(t *testing.T) { require.EqualValues(t, 45, secs) require.EqualValues(t, 3, mins) }) + + t.Run("Reset re-anchors so pre-reset time is not counted", func(t *testing.T) { + ts := time.Now() + st := NewSessionTimer(ts) + + // 90s elapse before the participant actually joins; that time should not + // be billed once we re-anchor to the join time. + joinedAt := ts.Add(90 * time.Second) + st.Reset(joinedAt) + + millis, secs, mins := st.Advance(joinedAt.Add(30 * time.Second)) + require.EqualValues(t, 30000, millis) + require.EqualValues(t, 30, secs) + require.EqualValues(t, 1, mins) + }) + + t.Run("Reset preserves min-seconds option", func(t *testing.T) { + ts := time.Now() + st := NewSessionTimer(ts, WithMinSeconds(45)) + + joinedAt := ts.Add(10 * time.Second) + st.Reset(joinedAt) + + _, secs, _ := st.Advance(joinedAt.Add(time.Second)) + require.EqualValues(t, 45, secs) + }) }