Skip to content

refactor(time): put a clock seam in the timestamp cache - #255

Merged
ReneWerner87 merged 2 commits into
masterfrom
claude/amazing-pascal-5kg3k9
Sep 16, 2026
Merged

ReneWerner87 merged 2 commits into
masterfrom
claude/amazing-pascal-5kg3k9

Conversation

@gaby

@gaby gaby commented Sep 16, 2026

Copy link
Copy Markdown
Member

Description

time.go read the wall clock and built its ticker inline, so the only way a test could observe the cache was to wait for real time to pass. time_test.go did exactly that — 3.35 s of sleeps across four tests — and paid for it twice: the waits were the slowest thing in the package's suite, and every assertion had to stay loose enough to survive a loaded CI box. "Timestamp should have updated after 1+ seconds" cannot tell a correct update from one that is a second out.

Both reads now go through two unexported package variables, now and newTicker. Production keeps time.Now and time.NewTicker; the tests install a fake and step it. Nothing is added to the public interface, Timestamp touches neither variable, and the defaults make this a no-op for every caller.

newTicker yields a channel and a stop function rather than a *time.Ticker, because a Ticker the runtime did not build cannot stand in for one — its Stop panics. Two small behaviour fixes came along with that: the tick source is created in StartTimeStampUpdater rather than inside the goroutine, so it exists by the time Start returns; and it is released before close(done) rather than after, so a returning Stop no longer leaves a ticker still live.

Fixes # (issue)

No linked issue — this came out of the same module-boundary review of this repository as #252.

Changes introduced

  • Benchmarks: Benchmark_CalculateTimestamp is unchanged, and this was measured rather than assumed. benchstat, n=10, samples interleaved between compiled test binaries for the two revisions:

    sub-benchmark base head vs base
    fiber 0.2996n ± 5% 0.3084n ± 7% ~ (p=0.529)
    default 53.16n ± 5% 53.05n ± 5% ~ (p=0.631)
    fiber_asserted 175.4n ± 5% 177.2n ± 5% ~ (p=0.404)
    default_asserted 233.2n ± 6% 231.1n ± 6% ~ (p=0.118)

    Every row ~, with B/op and allocs/op identical across all samples. The two stdlib default rows are controls — they are untouched code and came back ~ in both directions, so the run is not noise-dominated. That is the expected outcome: Timestamp's body is unchanged, and the clock is read once per second, never on the measured path.

    Measured on linux/amd64, Go 1.26. No README.md rows change: the catalog at README.md:17-20 is darwin/arm64 (Apple M2 Pro, -12 rows) and this machine is a 4-core linux/amd64 Xeon, so regenerating there would put amd64 numbers under an arm64 header — the defect that still sits at README.md:400-403. The measurement above says there is nothing to record in any case.

  • Documentation Update: doc comments only. The new comments state what the seam is for, why newTicker returns a channel plus a stop function instead of a *time.Ticker, why the tick source is created outside the goroutine, and why it is released before close(done). Benchmark_CalculateTimestamp gains a one-line note that it deliberately stays on the real clock.

  • Changelog/What's New: not applicable — this repository has no changelog file; release notes come from commit subjects.

  • Migration Guide: not needed. The public API is untouched and both new variables are unexported.

  • API Alignment with Express: not applicable — no Express-facing surface is involved.

  • API Longevity: Timestamp, StartTimeStampUpdater and StopTimeStampUpdater keep their exact signatures and semantics. The seam is internal, which is the point: the clock became substitutable without anything new appearing in the interface.

  • Examples: not applicable for a test-observability change.

Speed

base head
the four timestamp tests, in isolation 3.362 s 0.007 s
root package, -race -shuffle=on ~12.5 s ~9.2 s

Three runs each on the same machine. There is no sleep and no polling left: ticks are delivered synchronously. advance sends twice, and since the updater handles ticks one at a time, the second send can only be accepted after the store that followed the first — which is what makes the read after it exact rather than eventual.

Tests

Determinism is the means; exact assertions are the point. Three mutants that master's suite accepts now fail. Each was applied in both worlds and run against that world's own tests:

mutation master's tests these tests
tick path stores now()+1 survived Test_TimeStampUpdater
tick source never released on Stop survived Test_StopTimeStampUpdater
Start's idempotence guard removed survived Test_StartTimeStampUpdater_Idempotent
Start does not publish before returning killed killed

The first is exactly the off-by-one second the ±2 s tolerance was blind to by construction. The other two had no coverage at all — StartTimeStampUpdater's doc comment has always promised that "only one updater runs at a time", and nothing checked it. Those two guards were the coverage gap:

base head
StartTimeStampUpdater 93.3% 100%
StopTimeStampUpdater 87.5% 100%
Timestamp 100% 100%

Benchmark_CalculateTimestamp deliberately keeps the real clock and its ±2 s tolerance: there the tolerance is the cache's own lag against time.Now, not slack in an assertion.

timerTestMu stays. The updater is package-global state regardless of the seam, so these tests still cannot run in parallel with each other — only the waiting went away, not the shared state.

Type of change

  • Code consistency (non-breaking change which improves code reliability and robustness)

The template has no "test quality" or "refactor" option; this is closest. There is no behaviour change for callers and no performance claim — the benchmarks above exist to show the seam costs nothing.

Checklist

  • Followed the inspiration of the Express.js framework for new functionalities — not applicable; no new functionality and no Express-facing API.
  • Conducted a self-review of the code and provided comments for complex or critical parts — specifically the two ordering decisions (ticker created outside the goroutine, released before close(done)) and the two-send synchronization in the test helper, none of which is self-evident from the code.
  • Updated the documentation in the /docs/ directory — not applicable; this repository has no /docs/.
  • Added or updated unit tests to validate the effectiveness of the changes — rewritten deterministically, plus a new test for Start's idempotence, verified by mutation rather than by coverage alone.
  • Ensured that new and existing unit tests pass locally with the changes: make test green (757 tests, -race -shuffle=on), go vet clean, gofumpt clean, golangci-lint 0 issues.
  • Verified that any new dependencies are essential — none added.
  • Aimed for optimal performance with minimal allocations in the new code — allocation counts unchanged everywhere; the fake clock exists only in the test binary.
  • Provided benchmarks for the new code to analyze and improve upon.

One incidental find, not fixed here

README.md:446-447 records 12 B/op / 2 allocs for the two asserted sub-benchmarks, but both master and this branch now measure 0 B/op / 0 allocs — those rows predate the allocation fix that checkTimeStamp's own comment describes. It is pre-existing, it is in the arm64 catalog I cannot regenerate on this machine, and it is out of scope for this diff. Flagging it rather than silently leaving it unmentioned.

Commit formatting

Single commit, conventional-commit style, matching the prevailing convention in this repository's history:

refactor(time): put a clock seam in the timestamp cache


🤖 Generated with Claude Code

https://claude.ai/code/session_01K7Z8sz85kpAwQKxoeFBv7i


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved timestamp updater startup and shutdown reliability.
    • Ensured timestamp updates initialize consistently and stop cleanly.
  • Tests

    • Replaced timing-dependent tests with deterministic clock-based validation.
    • Added coverage for repeated start/stop cycles, timestamp publication, and ticker cleanup.

time.go read the wall clock and built its ticker inline, so the only way a test
could observe the cache was to wait for real time to pass. time_test.go did
exactly that -- 3.35 s of sleeps across four tests -- and paid for it twice: the
waits were the slowest thing in the package's suite, and every assertion had to
stay loose enough to survive a loaded CI box. "Timestamp should have updated
after 1+ seconds" cannot tell a correct update from one that is a second out.

Route both reads through two unexported package variables, now and newTicker.
Production keeps time.Now and time.NewTicker; tests install a fake and step it.
Nothing is added to the public interface, Timestamp touches neither variable,
and the defaults make this a no-op for every caller.

newTicker yields a channel and a stop function rather than a *time.Ticker,
because a Ticker the runtime did not build cannot stand in for one -- its Stop
panics. The tick source is created in StartTimeStampUpdater rather than inside
the goroutine, so it exists by the time Start returns, and it is released before
close(done) rather than after, so a returning Stop leaves no ticker still live.

The tests no longer sleep. In isolation they go from 3.362 s to 0.007 s, and the
root package under -race -shuffle from ~12.5 s to ~9.2 s (three runs each, same
machine). Ticks are delivered synchronously: advance sends twice, and since the
updater takes ticks one at a time, the second send can only be accepted after
the store that followed the first -- which is what makes the read after it exact
rather than eventual.

Exactness is the point. Three mutants that master's suite accepts now fail:

  tick path stores now()+1             Test_TimeStampUpdater
  tick source never released on Stop   Test_StopTimeStampUpdater
  Start's idempotence guard removed    Test_StartTimeStampUpdater_Idempotent

The first is the off-by-one second the ±2 s tolerance was blind to by
construction. The other two were never covered at all: StartTimeStampUpdater's
doc comment has always promised that "only one updater runs at a time", and
nothing checked it. Those two guards were the coverage gap -- Start 93.3% and
Stop 87.5% on master, both 100% here.

Benchmark_CalculateTimestamp keeps the real clock and its ±2 s tolerance, which
is the cache's own lag rather than slack. It is unchanged: benchstat n=10 with
samples interleaved between the revisions puts every row at ~, the stdlib
default rows included. That is the expected result, since Timestamp's body is
untouched and the clock is read once per second, never on the measured path.

No README rows change: the catalog is darwin/arm64, this machine is
linux/amd64, and the measurement says there is nothing to record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7Z8sz85kpAwQKxoeFBv7i
@gaby
gaby requested a review from a team as a code owner September 16, 2026 02:06
@gaby
gaby requested review from ReneWerner87, efectn and sixcolors and removed request for a team September 16, 2026 02:06
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T02:09:47.830625Z cdf54e6 PR opened
🔒 Security Review Completed 2026-09-16T02:09:16.600002Z cdf54e6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.78%. Comparing base (5482477) to head (d23bb8b).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #255      +/-   ##
==========================================
+ Coverage   93.61%   93.78%   +0.17%     
==========================================
  Files          38       38              
  Lines        2426     2429       +3     
==========================================
+ Hits         2271     2278       +7     
+ Misses        135      133       -2     
+ Partials       20       18       -2     
Flag Coverage Δ
unittests 93.78% <100.00%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 55 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d648c567-7de9-4009-93b4-a57bf8d17b30

📥 Commits

Reviewing files that changed from the base of the PR and between cdf54e6 and d23bb8b.

📒 Files selected for processing (1)
  • time_test.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 760ed0d4-6091-4c49-b965-5d8c57f4d5be

📥 Commits

Reviewing files that changed from the base of the PR and between 5482477 and cdf54e6.

📒 Files selected for processing (2)
  • time.go
  • time_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (11)
  • GitHub Check: benchmark / benchmark (2)
  • GitHub Check: benchmark / benchmark (3)
  • GitHub Check: benchmark / benchmark (0)
  • GitHub Check: benchmark / benchmark (4)
  • GitHub Check: benchmark / benchmark (5)
  • GitHub Check: benchmark / benchmark (1)
  • GitHub Check: Build (1.26.x, blacksmith-6vcpu-macos-latest)
  • GitHub Check: Build (1.26.x, blacksmith-4vcpu-windows-2025)
  • GitHub Check: Build (1.27.x, blacksmith-4vcpu-windows-2025)
  • GitHub Check: Build (1.27.x, blacksmith-6vcpu-macos-latest)
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (1)
Do not move exported functions or packages to `internal/` merely because they have no non-test in-module callers; this library intentionally exposes helpers for downstream modules.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • time.go
  • time_test.go
🪛 ast-grep (0.45.3)
time.go

[warning] 64-64: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(now().Unix())
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)

time_test.go

[warning] 93-93: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(testEpoch.Unix())
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 112-112: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(testEpoch.Unix())
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 129-129: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(testEpoch.Unix())
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 142-142: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(testEpoch.Unix())
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)


[warning] 147-147: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(testEpoch.Unix())
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)

🔇 Additional comments (2)
time.go (1)

16-30: LGTM!

Also applies to: 47-47, 51-54, 57-60, 64-65

time_test.go (1)

5-5: LGTM!

Also applies to: 14-75, 89-90, 94-95, 97-102, 104-113, 117-125, 127-130, 133-148, 151-152


📝 Walkthrough

Walkthrough

The timestamp updater now uses replaceable clock functions and creates its ticker before starting the updater goroutine. Tests use a fake clock and atomic counters to verify timestamp updates, idempotent starts, stops, and restarts without sleeping.

Changes

Timestamp updater clock control

Layer / File(s) Summary
Clock seam and updater lifecycle
time.go
Package-private now and newTicker functions provide the current time and ticker source. StartTimeStampUpdater initializes the timestamp and ticker before starting its goroutine, then stops the ticker before closing done.
Deterministic updater tests
time_test.go
A fake clock controls time and ticker events. Tests verify startup updates, idempotent starts, stop behavior, and repeated start-stop cycles with atomic creation and stop counters.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to cdf54

The deterministic timestamp updater preserves lifecycle ownership and test isolation, with no concrete merge-blocking issue identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a clock seam to the timestamp cache.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/amazing-pascal-5kg3k9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit reads each line,
The patch grows clear beneath the moon,
Small changes hop in place,
Tests guard the garden path,
Reviews bloom before the dawn.

Comment @coderabbitai help to get the list of available commands.

Every test in the file swaps newTicker for a fake, so the real one -- the two
statements that build a time.Ticker and hand back its channel and Stop -- ran
nowhere under coverage. codecov/patch caught it on #255 at 62.5% of the diff,
and go tool cover -func had not: it does not list anonymous package-level
closures, so the per-function report read 100% while time.go:26 was dead.

The gap was not cosmetic. Nothing proved the production adapter delivers a live
channel or that the stop it returns works, which is exactly the part of the
seam that a fake cannot vouch for. Test_NewTicker asserts both against the real
clock with a millisecond period, the one real-time wait left in the file, under
a timeout that fails loudly instead of hanging CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7Z8sz85kpAwQKxoeFBv7i

gaby commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

Three notes on the bot feedback, and one correction to the description above.

codecov/patch failed on cdf54e6 (62.50%, 3 lines) — fixed in d23bb8b, and the finding was correct. The uncovered block was the production newTicker closure at time.go:26: every test replaces it with a fake, and benchmarks don't run during a coverage pass, so the two statements that build the real time.Ticker ran nowhere.

Worth saying that my own pre-push check missed this. I used go tool cover -func, which reported 100% on all three named functions in time.go — and does not list anonymous package-level closures at all. The raw profile said time.go:26.63,29.3 2 0 the whole time. Reading the profile rather than the per-function summary is the check that would have caught it.

The fix is not coverage padding. That closure was the one part of the seam a fake cannot vouch for: whether the real tick source delivers a live channel, and whether the Stop it hands back works. Test_NewTicker asserts both against the real clock at a 1 ms period, under a timeout that fails loudly rather than hanging a loaded runner. Every block in time.go is now covered.

Correction to the description: it says there is no sleep or polling left in the file. That is no longer exactly true — Test_NewTicker waits for one real 1 ms tick. It is the only real-time wait left, it is deliberate (it is testing the stdlib adapter, which is the one place a real clock is the point), and it measures 0.00 s. The 3.35 s → 0.007 s figure for the other four tests is unaffected. Test count is 758, not 757.

CodeRabbit's Docstring Coverage warning (44.44% vs 80%): not taking it. The nine functions it scored are four test functions, three one-line fakeClock accessors (now, set, newTicker), and two helpers that already carry doc comments. Reaching 80% means documenting the accessors, and // now returns the fake's current time. above func (fc *fakeClock) now() time.Time is noise, not documentation. The repo does not hold test code to that bar either — bytes/case_test.go is 1 of 9 and strings/case_test.go 2 of 9. The comments in this diff are spent where the code is genuinely non-obvious: the seam's purpose, why newTicker returns a channel plus a stop function, the two ordering decisions in StartTimeStampUpdater, and the two-send synchronization in advance. Happy to add a line to any specific function a reviewer finds unclear.

For the record on the ast-grep uint32(...Unix()) narrowing warnings in that same comment: uint32(time.Now().Unix()) is pre-existing on master and unchanged in intent here — the cache's type is atomic.Uint32, which is the public contract of Timestamp(). Widening it would be an API change, not a fix, and golangci-lint passes clean.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor
2 benchmarks faster (up to 1.97x)
Benchmark Base → Current
Benchmark_FormatInt/large_neg/fiber ⚡ 1.97x 89.59 → 45.53 ns/op
Benchmark_FormatUint/large/strconv ⚡ 1.89x 108.1 → 57.23 ns/op

d23bb8b vs master@5482477 · 2304/2304 results compared · retest: 2/2 improvements reproduced · noise-aware thresholds · full results · github.com/gofiber/utils/v2

@ReneWerner87
ReneWerner87 merged commit bd8bc59 into master Sep 16, 2026
25 checks passed
@ReneWerner87
ReneWerner87 deleted the claude/amazing-pascal-5kg3k9 branch September 16, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants