🐛 bug: remove unbounded background goroutines from logger, cache, and internal storage memory#4378
🐛 bug: remove unbounded background goroutines from logger, cache, and internal storage memory#4378Copilot wants to merge 17 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4378 +/- ##
==========================================
- Coverage 91.40% 91.34% -0.06%
==========================================
Files 132 132
Lines 13120 13125 +5
==========================================
- Hits 11992 11989 -3
- Misses 711 717 +6
- Partials 417 419 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@copilot Thanks, this does remove the goroutine leak, but please reconsider the mechanism per case instead of removing all three goroutines uniformly. Logger + cache timestamp: the original bug was N goroutines for N middleware instances. The cleanest fix is a single process-wide coarse clock: one background updater started once via internal/storage/memory GC: this case is different. That Storage already has a real lifecycle ( Summary: coarse-clock singleton (or lazy) for logger/cache, stoppable goroutine (buffered done + idempotent Close) for the memory storage GC. |
Addressed in b7a14fa (plus 8cef4d3 for the follow-up field alignment). Logger/cache stay on the lazy request-time path, |
There was a problem hiding this comment.
Pull request overview
This PR removes per-middleware timestamp updater goroutines from logger and cache paths, while making the internal memory storage GC lifecycle safer through idempotent close behavior.
Changes:
- Logger timestamps now refresh lazily during request handling after the configured interval.
- Cache expiration/Age logic now reads current Unix seconds on demand instead of using a ticker-maintained timestamp.
- Internal memory storage
Close()is now idempotent and non-blocking, with focused regression tests added.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
middleware/logger/logger.go |
Replaces background timestamp updater with lazy atomic refresh logic. |
middleware/logger/logger_test.go |
Adds coverage for ${time} refreshing after TimeInterval. |
middleware/cache/cache.go |
Removes cache timestamp ticker and computes timestamps at request/store time. |
internal/storage/memory/memory.go |
Adds sync.Once and buffered stop signaling for safe repeated Close() calls. |
internal/storage/memory/memory_test.go |
Adds regression coverage for non-blocking repeated Close(). |
|
@copilot unit tests are failing. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@middleware/logger/logger.go`:
- Around line 52-59: The refresh logic advances nextTimestampUpdate before
storing the new formatted time, which can let a concurrent caller skip refresh
and read the stale value; fix by reordering so the timestamp is published first
and then the deadline is advanced: call
timestamp.Store(now.In(cfg.timeZoneLocation).Format(cfg.TimeFormat)) before
performing nextTimestampUpdate.CompareAndSwap(...). Apply the same reorder to
the other refresh block (the section around lines 123-126) where
nextTimestampUpdate is advanced prior to storing the timestamp.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 2543a7bb-413a-4767-b5f0-808f375359d6
📒 Files selected for processing (5)
internal/storage/memory/memory.gointernal/storage/memory/memory_test.gomiddleware/cache/cache.gomiddleware/logger/logger.gomiddleware/logger/logger_test.go
|
@copilot check the review hints and solve the comments, after this you mark the comment as resolved |
Addressed in 4bacbcb: logger now uses a monotonic |
|
@copilot follow up on the cache/logger timestamp choice. The lazy The codebase already has the coarse-clock-singleton primitive in // gofiber/utils/time.go
func Timestamp() uint32 // ~1 ns atomic load
func StartTimeStampUpdater() // single process-wide goroutine via sync.Once, idempotent
func StopTimeStampUpdater() // safe to call repeatedlyExisting call sites in this repo that already use it:
That means the cache change in this PR is the only middleware that diverges from the established pattern. Please align it:
Replace the per-request // in New(), once per constructor:
utils.StartTimeStampUpdater()
// on the hot path (before taking the lock, as already done):
ts := uint64(utils.Timestamp())Reason:
This one is a bit different because the logger needs the formatted timestamp string (TimeFormat + Location), not just unix seconds, and formatting is more expensive than the load. Two acceptable options:
Option 1 is the better long-term answer because logger is genuinely a hot path. Option 2 is acceptable as a stopgap. General guidance going forward When you have a choice between equivalent correctness options on a hot path, please pick the cheapest one and look around the repo first to see if there is already an established primitive for it. In this codebase that means: check After the change, please re-run |
|
@copilot small follow up on the logger change, easy to fold into the Option 1 refactor: The per-request line data.Timestamp.Store(timestamp.Load())adds an atomic Load + atomic Store on every request, where the old code did a plain field copy ( When you restructure to the
Goal: per-request timestamp cost should end up at exactly one |
Addressed in 05dffa8 (with c9c6e6c). Cache now starts the shared Logger stays on the monotonic lazy refresh path because its default Re-ran |
Addressed in aa60782. Logger now uses a shared timestamp scheduler keyed by format/timezone/interval, request data carries a plain string so the hot path is a single atomic load, and |
|
@copilot follow-up on the logger timestamp design. The current Suggested minimal design// timestamp.go
package logger
import (
"sync"
"sync/atomic"
"time"
)
type tsKey struct {
format, zone string
interval time.Duration
}
var (
tsMu sync.Mutex
tsCache = map[tsKey]*atomic.Value{}
)
func sharedTimestamp(format string, loc *time.Location, interval time.Duration) *atomic.Value {
key := tsKey{format, loc.String(), interval}
tsMu.Lock()
defer tsMu.Unlock()
if v, ok := tsCache[key]; ok {
return v
}
v := &atomic.Value{}
v.Store(time.Now().In(loc).Format(format))
tsCache[key] = v
go func() {
for {
time.Sleep(interval)
v.Store(time.Now().In(loc).Format(format))
}
}()
return v
}And in var timestamp *atomic.Value
if strings.Contains(cfg.Format, "${"+TagTime+"}") {
timestamp = sharedTimestamp(cfg.TimeFormat, cfg.timeZoneLocation, cfg.TimeInterval)
}
// hot path:
data.Timestamp = timestamp.Load().(string)(Or keep Why this is enough
What this does not do (and that is OK)Issue #4330 also asks for an explicit What to delete
General principle: prefer the smallest change that fixes the reported symptom. A 143-line scheduler with dynamic timer juggling to manage at most 1 - 3 goroutines is the wrong shape of solution. |
Addressed in a763a54. Logger timestamp sharing is now reduced to a keyed |
Description
This change removes per-instance background goroutine patterns from logger and cache, while preserving the background GC in
internal/storage/memoryand making it cleanly stoppable through the storage’s existing lifecycle.Logger
${time}updater with shared timestamp state keyed byTimeFormat, timezone, andTimeInterval.Data.Timestampas a plainstring.Cache
gofiber/utilsby starting the shared timestamp updater inNew()and usingutils.Timestamp()for request-time second reads.time.Now()read only near expiration andAgeboundary cases so cache TTL andAgebehavior stay unchanged while avoiding the per-request wall-clock call in the common case.Internal storage memory
internal/storage/memory.donechannel and an idempotentClose()so storage instances can stop their GC goroutine without blocking or double-close issues.Tests
require.Eventuallyinstead of a fixed sleep for more reliable CI behavior.internal/storage/memory.Close()calls do not block.Changes introduced
List the new features or adjustments introduced in this pull request. Provide details on benchmarks, documentation updates, changelog entries, and if applicable, the migration guide.
${time}request path at a single atomic load without regressing behavior-sensitive paths.${time}to shared keyed timestamp state instead of a central scheduler, makeinternal/storage/memoryGC shutdown safe with a buffered stop signal and idempotentClose(), and align cache with the repo-wide shared timestamp updater while preserving expiration andAgesemantics with exact-time fallback guards.${time}now uses shared keyed timestamp state with one atomic load per request, cache usesutils.Timestamp()on the hot path with exact-time fallback near expiration boundaries, andinternal/storage/memorycontinues to reclaim expired entries in the background and now stops cleanly viaClose().Type of change
Please delete options that are not relevant.
Checklist
Before you submit your pull request, please make sure you meet these requirements:
/docs/directory for Fiber's documentation.Commit formatting
Please use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: CONTRIBUTING.md