-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(#2182): mitigate cache stampede with single-flight #4146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,21 +11,33 @@ | |||||||||||||
|
|
||||||||||||||
| "github.com/gofiber/fiber/v2" | ||||||||||||||
| "github.com/gofiber/fiber/v2/utils" | ||||||||||||||
| "golang.org/x/sync/singleflight" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| // timestampUpdatePeriod is the period which is used to check the cache expiration. | ||||||||||||||
| // It should not be too long to provide more or less acceptable expiration error, and in the same | ||||||||||||||
| // time it should not be too short to avoid overwhelming of the system | ||||||||||||||
| // timestampUpdatePeriod is the period that is used to check the cache expiration. | ||||||||||||||
| // It should not be too long to provide more or less acceptable expiration error, and, | ||||||||||||||
| // at the same time, it should not be too short to avoid overwhelming the system. | ||||||||||||||
| const timestampUpdatePeriod = 300 * time.Millisecond | ||||||||||||||
|
|
||||||||||||||
| // loadResult holds the response data returned from a singleflight load so waiters | ||||||||||||||
| // can apply it to their context without running the handler. | ||||||||||||||
| type loadResult struct { | ||||||||||||||
| Body []byte | ||||||||||||||
| Status int | ||||||||||||||
| Ctype []byte | ||||||||||||||
| Cencoding []byte | ||||||||||||||
| Headers map[string][]byte | ||||||||||||||
| Exp uint64 | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // cache status | ||||||||||||||
| // unreachable: when cache is bypass, or invalid | ||||||||||||||
| // hit: cache is served | ||||||||||||||
| // miss: do not have cache record | ||||||||||||||
| const ( | ||||||||||||||
| // cacheUnreachable: when cache was bypassed or is invalid | ||||||||||||||
| cacheUnreachable = "unreachable" | ||||||||||||||
| cacheHit = "hit" | ||||||||||||||
| cacheMiss = "miss" | ||||||||||||||
| // cacheHit: cache served | ||||||||||||||
| cacheHit = "hit" | ||||||||||||||
| // cacheMiss: no cache record for the given key | ||||||||||||||
| cacheMiss = "miss" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| // directives | ||||||||||||||
|
|
@@ -43,11 +55,14 @@ | |||||||||||||
| "Trailers": nil, | ||||||||||||||
| "Transfer-Encoding": nil, | ||||||||||||||
| "Upgrade": nil, | ||||||||||||||
| "Content-Type": nil, // already stored explicitely by the cache manager | ||||||||||||||
| "Content-Encoding": nil, // already stored explicitely by the cache manager | ||||||||||||||
| "Content-Type": nil, // already stored explicitly by the cache manager | ||||||||||||||
| "Content-Encoding": nil, // already stored explicitly by the cache manager | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // New creates a new middleware handler | ||||||||||||||
| // New creates a new middleware handler. When Config.SingleFlight is true, concurrent | ||||||||||||||
| // cache misses for the same key are coalesced (single-flight): only one request runs | ||||||||||||||
| // the handler and populates the cache; others wait and share the result, preventing | ||||||||||||||
| // cache stampede. Recommend SingleFlight: true for high-concurrency deployments. | ||||||||||||||
| func New(config ...Config) fiber.Handler { | ||||||||||||||
| // Set default config | ||||||||||||||
| cfg := configDefault(config...) | ||||||||||||||
|
|
@@ -63,12 +78,13 @@ | |||||||||||||
| // Cache settings | ||||||||||||||
| mux = &sync.RWMutex{} | ||||||||||||||
| timestamp = uint64(time.Now().Unix()) | ||||||||||||||
| sf singleflight.Group | ||||||||||||||
| ) | ||||||||||||||
| // Create manager to simplify storage operations ( see manager.go ) | ||||||||||||||
| // Create a manager to simplify storage operations ( see manager.go ) | ||||||||||||||
| manager := newManager(cfg.Storage) | ||||||||||||||
| // Create indexed heap for tracking expirations ( see heap.go ) | ||||||||||||||
| // Create an indexed heap to track expirations ( see heap.go ) | ||||||||||||||
| heap := &indexedHeap{} | ||||||||||||||
| // count stored bytes (sizes of response bodies) | ||||||||||||||
| // Count bytes stored (sizes of response bodies) | ||||||||||||||
| var storedBytes uint = 0 | ||||||||||||||
|
|
||||||||||||||
| // Update timestamp in the configured interval | ||||||||||||||
|
|
@@ -79,22 +95,24 @@ | |||||||||||||
| } | ||||||||||||||
| }() | ||||||||||||||
|
|
||||||||||||||
| // Delete key from both manager and storage | ||||||||||||||
| // Delete a key from both manager and storage | ||||||||||||||
| deleteKey := func(dkey string) { | ||||||||||||||
| manager.delete(dkey) | ||||||||||||||
| // External storage saves body data with different key | ||||||||||||||
| // External storage saves body data with a different key | ||||||||||||||
| if cfg.Storage != nil { | ||||||||||||||
| manager.delete(dkey + "_body") | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Return new handler | ||||||||||||||
| // Return a new handler | ||||||||||||||
| return func(c *fiber.Ctx) error { | ||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||
| // Refrain from caching | ||||||||||||||
| if hasRequestDirective(c, noStore) { | ||||||||||||||
| return c.Next() | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||
| // Only cache selected methods | ||||||||||||||
| var isExists bool | ||||||||||||||
| for _, method := range cfg.Methods { | ||||||||||||||
|
|
@@ -108,6 +126,7 @@ | |||||||||||||
| return c.Next() | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||
| // Get key from request | ||||||||||||||
| // TODO(allocation optimization): try to minimize the allocation from 2 to 1 | ||||||||||||||
| key := cfg.KeyGenerator(c) + "_" + c.Method() | ||||||||||||||
|
|
@@ -121,7 +140,7 @@ | |||||||||||||
| // Get timestamp | ||||||||||||||
| ts := atomic.LoadUint64(×tamp) | ||||||||||||||
|
|
||||||||||||||
| // Check if entry is expired | ||||||||||||||
| // Check if entry has expired | ||||||||||||||
| if e.exp != 0 && ts >= e.exp { | ||||||||||||||
| deleteKey(key) | ||||||||||||||
| if cfg.MaxBytes > 0 { | ||||||||||||||
|
|
@@ -134,6 +153,7 @@ | |||||||||||||
| if cfg.Storage != nil { | ||||||||||||||
| e.body = manager.getRaw(key + "_body") | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Set response headers from cache | ||||||||||||||
| c.Response().SetBodyRaw(e.body) | ||||||||||||||
| c.Response().SetStatusCode(e.status) | ||||||||||||||
|
|
@@ -146,6 +166,7 @@ | |||||||||||||
| c.Response().Header.SetBytesV(k, v) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Set Cache-Control header if enabled | ||||||||||||||
| if cfg.CacheControl { | ||||||||||||||
| maxAge := strconv.FormatUint(e.exp-ts, 10) | ||||||||||||||
|
|
@@ -163,7 +184,133 @@ | |||||||||||||
| // make sure we're not blocking concurrent requests - do unlock | ||||||||||||||
| mux.Unlock() | ||||||||||||||
|
|
||||||||||||||
| // Continue stack, return err to Fiber if exist | ||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||
| // Single-flight path (optional) | ||||||||||||||
| // Handle concurrent cache misses (single-flight) -> mitigate cache stampede | ||||||||||||||
| if cfg.SingleFlight { | ||||||||||||||
| // Single-flight: one request runs the handler and populates cache; others wait and share the result. | ||||||||||||||
| v, err, shared := sf.Do(key, func() (any, error) { | ||||||||||||||
|
Check failure on line 192 in middleware/cache/cache.go
|
||||||||||||||
| if err := c.Next(); err != nil { | ||||||||||||||
| return nil, err | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Begin critical section: lock entry and timestamp | ||||||||||||||
| mux.Lock() | ||||||||||||||
| defer mux.Unlock() | ||||||||||||||
| ts := atomic.LoadUint64(×tamp) | ||||||||||||||
| e := manager.get(key) | ||||||||||||||
| bodySize := uint(len(c.Response().Body())) | ||||||||||||||
|
|
||||||||||||||
| expiration := cfg.Expiration | ||||||||||||||
| if cfg.ExpirationGenerator != nil { | ||||||||||||||
| expiration = cfg.ExpirationGenerator(c, &cfg) | ||||||||||||||
| } | ||||||||||||||
| exp := ts + uint64(expiration.Seconds()) | ||||||||||||||
| res := loadResult{ | ||||||||||||||
| Body: utils.CopyBytes(c.Response().Body()), | ||||||||||||||
| Status: c.Response().StatusCode(), | ||||||||||||||
| Ctype: utils.CopyBytes(c.Response().Header.ContentType()), | ||||||||||||||
| Cencoding: utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding)), | ||||||||||||||
|
Comment on lines
+209
to
+213
|
||||||||||||||
| Exp: exp, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Store response headers if enabled | ||||||||||||||
| if cfg.StoreResponseHeaders { | ||||||||||||||
| res.Headers = make(map[string][]byte) | ||||||||||||||
| c.Response().Header.VisitAll( | ||||||||||||||
| func(k []byte, v []byte) { | ||||||||||||||
| keyS := string(k) | ||||||||||||||
| if _, ok := ignoreHeaders[keyS]; !ok { | ||||||||||||||
| res.Headers[keyS] = utils.CopyBytes(v) | ||||||||||||||
| } | ||||||||||||||
| }, | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // If middleware marks request for bypass, return result without caching. | ||||||||||||||
| if cfg.Next != nil && cfg.Next(c) { | ||||||||||||||
|
||||||||||||||
| if cfg.Next != nil && cfg.Next(c) { | |
| if cfg.Next != nil && cfg.Next(c) { | |
| if cfg.CacheHeader != "" { | |
| c.Set(cfg.CacheHeader, "unreachable") | |
| } |
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the Next case, when bodySize > cfg.MaxBytes the SingleFlight closure returns without caching, but the outer code still sets the cache status to miss. The non-singleflight path marks this as unreachable. Please align SingleFlight behavior with the existing path for oversized responses.
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SingleFlight path always sets cfg.CacheHeader to miss after sf.Do(...), even when the request was intentionally bypassed (Next) or not cached due to MaxBytes. To match current behavior, the status header should reflect unreachable for those cases (and only be miss when the response is actually cached/populated).
| // Set cache status header. | |
| c.Set(cfg.CacheHeader, cacheMiss) | |
| // Set cache status header only for waiters that received a shared result. | |
| if shared { | |
| c.Set(cfg.CacheHeader, cacheMiss) | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,8 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "net/http/httptest" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sync/atomic" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -108,6 +110,77 @@ func Test_Cache(t *testing.T) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utils.AssertEqual(t, cachedBody, body) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Test_Cache_SingleFlight verifies that with SingleFlight enabled, concurrent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // misses for the same key result in exactly one handler invocation and all | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // requesters receive the same response (stampede prevention). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func Test_Cache_SingleFlight(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Parallel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var handlerCalls int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app := fiber.New() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.Use(New(Config{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expiration: 10 * time.Second, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SingleFlight: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| KeyGenerator: func(c *fiber.Ctx) string { return "/singleflight" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.Get("/singleflight", func(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| n := atomic.AddInt64(&handlerCalls, 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.SendString(fmt.Sprintf("ok-%d", n)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cold cache: fire many concurrent requests for the same key. Only one | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // handler run should occur; all requesters get the same body. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const concurrency = 50 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var wg sync.WaitGroup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bodies := make([][]byte, concurrency) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i := 0; i < concurrency; i++ { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wg.Add(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| go func(idx int) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer wg.Done() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| req := httptest.NewRequest("GET", "/singleflight", nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp, err := app.Test(req) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("request %d: %v", idx, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body, _ := io.ReadAll(resp.Body) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bodies[idx] = body | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }(i) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wg.Wait() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utils.AssertEqual(t, int64(1), atomic.LoadInt64(&handlerCalls), "handler should be invoked exactly once") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expectedBody := []byte("ok-1") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i := 0; i < concurrency; i++ { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utils.AssertEqual(t, expectedBody, bodies[i], fmt.Sprintf("request %d body", i)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Test_Cache_DefaultConfig_BackwardsCompatible ensures default config (SingleFlight false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // keeps existing behavior: no coalescing; existing tests pass unchanged. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func Test_Cache_DefaultConfig_BackwardsCompatible(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Parallel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app := fiber.New() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.Use(New()) // SingleFlight defaults to false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.Get("/", func(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.SendString("default") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+161
to
+171
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // keeps existing behavior: no coalescing; existing tests pass unchanged. | |
| func Test_Cache_DefaultConfig_BackwardsCompatible(t *testing.T) { | |
| t.Parallel() | |
| app := fiber.New() | |
| app.Use(New()) // SingleFlight defaults to false | |
| app.Get("/", func(c *fiber.Ctx) error { | |
| return c.SendString("default") | |
| }) | |
| // keeps existing behavior: no coalescing for concurrent misses; existing tests pass unchanged. | |
| func Test_Cache_DefaultConfig_BackwardsCompatible(t *testing.T) { | |
| t.Parallel() | |
| var handlerCalls int64 | |
| app := fiber.New() | |
| app.Use(New()) // SingleFlight defaults to false | |
| app.Get("/", func(c *fiber.Ctx) error { | |
| atomic.AddInt64(&handlerCalls, 1) | |
| return c.SendString("default") | |
| }) | |
| // Cold cache: many concurrent requests for the same key should not be fully | |
| // coalesced when SingleFlight is false; the handler should run more than once. | |
| const concurrency = 50 | |
| var wg sync.WaitGroup | |
| for i := 0; i < concurrency; i++ { | |
| wg.Add(1) | |
| go func(idx int) { | |
| defer wg.Done() | |
| req := httptest.NewRequest("GET", "/", nil) | |
| resp, err := app.Test(req) | |
| if err != nil { | |
| t.Errorf("request %d: %v", idx, err) | |
| return | |
| } | |
| // Drain body to ensure handler completes. | |
| _, _ = io.ReadAll(resp.Body) | |
| }(i) | |
| } | |
| wg.Wait() | |
| calls := atomic.LoadInt64(&handlerCalls) | |
| utils.AssertEqual(t, true, calls > 1, "handler should be invoked more than once when SingleFlight is false") | |
| // Sequential requests still exhibit normal caching behavior. |
Uh oh!
There was an error while loading. Please reload this page.