Files
2026-09-16 18:16:30 +08:00

203 lines
5.5 KiB
Go

package cfprobe
import (
"math"
"net/http"
"strings"
"sync"
"time"
)
const (
maxCalibrationAge = 24 * time.Hour
dateCalibrationThreshold = 20 * time.Second
responseDateHeader = "Date"
)
// ReportTime describes the local wall clock and the latest independent clock
// calibration. Pointer fields intentionally serialize as null before the first
// successful calibration.
type ReportTime struct {
LocalTS int64 `json:"local_ts"`
AccurateTS *int64 `json:"accurate_ts"`
OffsetMS *int64 `json:"offset_ms"`
Source *string `json:"source"`
RoundTripMS *uint64 `json:"round_trip_ms"`
SampleAgeMS *uint64 `json:"sample_age_ms"`
}
type calibratedClock struct {
mu sync.Mutex
calibration *clockCalibration
}
type clockCalibration struct {
source string
anchor time.Time
accurateAtAnchor int64
roundTripMS uint64
}
func newDateCalibration(dateTime int64, roundTrip time.Duration, anchor time.Time) clockCalibration {
roundTripMS := durationMilliseconds(roundTrip)
// The HTTP Date header is generated close to response transmission. With
// one server timestamp, half of the request RTT is the best available
// estimate of the response's remaining travel time.
halfRoundTrip := roundTripMS/2 + roundTripMS%2
return clockCalibration{
source: "date",
anchor: anchor,
accurateAtAnchor: addMilliseconds(dateTime, halfRoundTrip),
roundTripMS: roundTripMS,
}
}
func (c clockCalibration) snapshot(now time.Time) ReportTime {
age := now.Sub(c.anchor)
if age < 0 {
age = 0
}
ageMS := durationMilliseconds(age)
accurateTS := c.timestamp(now)
localTS := now.UnixMilli()
offsetMS := saturatingSubtract(accurateTS, localTS)
source := c.source
roundTripMS := c.roundTripMS
return ReportTime{
LocalTS: localTS,
AccurateTS: &accurateTS,
OffsetMS: &offsetMS,
Source: &source,
RoundTripMS: &roundTripMS,
SampleAgeMS: &ageMS,
}
}
func (c clockCalibration) timestamp(at time.Time) int64 {
deltaMilliseconds := int64(at.Sub(c.anchor) / time.Millisecond)
return saturatingAdd(c.accurateAtAnchor, deltaMilliseconds)
}
func (c *calibratedClock) snapshot(now time.Time) ReportTime {
c.mu.Lock()
defer c.mu.Unlock()
calibration := c.calibrationAt(now, false)
if calibration == nil {
return ReportTime{LocalTS: now.UnixMilli()}
}
return calibration.snapshot(now)
}
// timestamp maps an observation time onto the calibrated Unix timeline. A
// fresh calibration may be used retroactively so samples collected shortly
// before the Date response can still be corrected before they are reported.
func (c *calibratedClock) timestamp(at time.Time) (int64, bool) {
c.mu.Lock()
defer c.mu.Unlock()
calibration := c.calibrationAt(at, true)
if calibration == nil {
return at.UnixMilli(), false
}
return calibration.timestamp(at), true
}
// correctLocalTimestamp applies the calibrated offset at observedAt to an
// absolute timestamp supplied by the local OS, such as boot_time.
func (c *calibratedClock) correctLocalTimestamp(localTimestamp int64, observedAt time.Time) int64 {
accurateAt, ok := c.timestamp(observedAt)
if !ok {
return localTimestamp
}
offset := saturatingSubtract(accurateAt, observedAt.UnixMilli())
return saturatingAdd(localTimestamp, offset)
}
func (c *calibratedClock) calibrationAt(at time.Time, allowRetroactive bool) *clockCalibration {
if calibrationUsable(c.calibration, at, allowRetroactive) {
return c.calibration
}
return nil
}
func calibrationUsable(calibration *clockCalibration, at time.Time, allowRetroactive bool) bool {
if calibration == nil {
return false
}
age := at.Sub(calibration.anchor)
if age < 0 {
return allowRetroactive && age >= -maxCalibrationAge
}
return age <= maxCalibrationAge
}
func (c *calibratedClock) updateDate(dateTime int64, roundTrip time.Duration, anchor time.Time) (ReportTime, bool) {
calibration := newDateCalibration(dateTime, roundTrip, anchor)
now := time.Now()
c.mu.Lock()
defer c.mu.Unlock()
current := c.calibrationAt(anchor, false)
if current != nil {
currentTimestamp := current.timestamp(anchor)
if absoluteDifference(currentTimestamp, calibration.accurateAtAnchor) <= uint64(dateCalibrationThreshold/time.Millisecond) {
return current.snapshot(now), false
}
}
c.calibration = &calibration
return calibration.snapshot(now), true
}
func durationMilliseconds(duration time.Duration) uint64 {
if duration <= 0 {
return 0
}
return uint64(duration / time.Millisecond)
}
func addMilliseconds(timestamp int64, milliseconds uint64) int64 {
if milliseconds > math.MaxInt64 || timestamp > math.MaxInt64-int64(milliseconds) {
return math.MaxInt64
}
return timestamp + int64(milliseconds)
}
func saturatingSubtract(left, right int64) int64 {
if right > 0 && left < math.MinInt64+right {
return math.MinInt64
}
if right < 0 && left > math.MaxInt64+right {
return math.MaxInt64
}
return left - right
}
func saturatingAdd(left, right int64) int64 {
if right > 0 && left > math.MaxInt64-right {
return math.MaxInt64
}
if right < 0 && left < math.MinInt64-right {
return math.MinInt64
}
return left + right
}
func absoluteDifference(left, right int64) uint64 {
if left >= right {
return uint64(left) - uint64(right)
}
return uint64(right) - uint64(left)
}
func responseDateTime(headers http.Header) (int64, bool) {
raw := strings.TrimSpace(headers.Get(responseDateHeader))
if raw == "" {
return 0, false
}
parsed, err := http.ParseTime(raw)
if err != nil {
return 0, false
}
return parsed.UnixMilli(), true
}