Skip to content

Commit

Permalink
🔥 Add Cache Invalidation Option to Cache Middleware (#3036)
Browse files Browse the repository at this point in the history
* Add an option to invalidate cache

* Add a summary about the cache middleware update

* Rename the option to make it clearer

* Rename hard tab

* Fix markdown formatting

* Revert unnecessary change

* Clarify the description of cache invalidator

* Add empty line

---------

Co-authored-by: RW <[email protected]>
  • Loading branch information
hcancelik and ReneWerner87 authored Jun 26, 2024
1 parent b9936a3 commit c9b7b1a
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 19 deletions.
44 changes: 29 additions & 15 deletions docs/middleware/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,31 +62,45 @@ app.Get("/", func(c fiber.Ctx) error {
})
```

You can also invalidate the cache by using the `CacheInvalidator` function as shown below:

```go
app.Use(cache.New(cache.Config{
CacheInvalidator: func(c fiber.Ctx) bool {
return fiber.Query[bool](c, "invalidateCache")
},
}))
```

The `CacheInvalidator` function allows you to define custom conditions for cache invalidation. Return true if conditions such as specific query parameters or headers are met, which require the cache to be invalidated. For example, in this code, the cache is invalidated when the query parameter invalidateCache is set to true.

## Config

| Property | Type | Description | Default |
|:---------------------|:------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------|
| Next | `func(fiber.Ctx) bool` | Next defines a function that is executed before creating the cache entry and can be used to execute the request without cache creation. If an entry already exists, it will be used. If you want to completely bypass the cache functionality in certain cases, you should use the [skip middleware](skip.md). | `nil` |
| Expiration | `time.Duration` | Expiration is the time that a cached response will live. | `1 * time.Minute` |
| CacheHeader | `string` | CacheHeader is the header on the response header that indicates the cache status, with the possible return values "hit," "miss," or "unreachable." | `X-Cache` |
| CacheControl | `bool` | CacheControl enables client-side caching if set to true. | `false` |
| KeyGenerator | `func(fiber.Ctx) string` | Key allows you to generate custom keys. | `func(c fiber.Ctx) string { return utils.CopyString(c.Path()) }` |
| ExpirationGenerator | `func(fiber.Ctx, *cache.Config) time.Duration` | ExpirationGenerator allows you to generate custom expiration keys based on the request. | `nil` |
| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | In-memory store |
| Store (Deprecated) | `fiber.Storage` | Deprecated: Use Storage instead. | In-memory store |
| Key (Deprecated) | `func(fiber.Ctx) string` | Deprecated: Use KeyGenerator instead. | `nil` |
| StoreResponseHeaders | `bool` | StoreResponseHeaders allows you to store additional headers generated by next middlewares & handler. | `false` |
| MaxBytes | `uint` | MaxBytes is the maximum number of bytes of response bodies simultaneously stored in cache. | `0` (No limit) |
| Methods | `[]string` | Methods specifies the HTTP methods to cache. | `[]string{fiber.MethodGet, fiber.MethodHead}` |
| Property | Type | Description | Default |
| :------------------- | :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
| Next | `func(fiber.Ctx) bool` | Next defines a function that is executed before creating the cache entry and can be used to execute the request without cache creation. If an entry already exists, it will be used. If you want to completely bypass the cache functionality in certain cases, you should use the [skip middleware](skip.md). | `nil` |
| Expiration | `time.Duration` | Expiration is the time that a cached response will live. | `1 * time.Minute` |
| CacheHeader | `string` | CacheHeader is the header on the response header that indicates the cache status, with the possible return values "hit," "miss," or "unreachable." | `X-Cache` |
| CacheControl | `bool` | CacheControl enables client-side caching if set to true. | `false` |
| CacheInvalidator | `func(fiber.Ctx) bool` | CacheInvalidator defines a function that is executed before checking the cache entry. It can be used to invalidate the existing cache manually by returning true. | `nil` |
| KeyGenerator | `func(fiber.Ctx) string` | Key allows you to generate custom keys. | `func(c fiber.Ctx) string { return utils.CopyString(c.Path()) }` |
| ExpirationGenerator | `func(fiber.Ctx, *cache.Config) time.Duration` | ExpirationGenerator allows you to generate custom expiration keys based on the request. | `nil` |
| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | In-memory store |
| Store (Deprecated) | `fiber.Storage` | Deprecated: Use Storage instead. | In-memory store |
| Key (Deprecated) | `func(fiber.Ctx) string` | Deprecated: Use KeyGenerator instead. | `nil` |
| StoreResponseHeaders | `bool` | StoreResponseHeaders allows you to store additional headers generated by next middlewares & handler. | `false` |
| MaxBytes | `uint` | MaxBytes is the maximum number of bytes of response bodies simultaneously stored in cache. | `0` (No limit) |
| Methods | `[]string` | Methods specifies the HTTP methods to cache. | `[]string{fiber.MethodGet, fiber.MethodHead}` |

## Default Config

```go
var ConfigDefault = Config{
Next: nil,
Expiration: 1 * time.Minute,
CacheHeader: "X-Cache",
CacheHeader: "X-Cache",
CacheControl: false,
CacheInvalidator: nil,
KeyGenerator: func(c fiber.Ctx) string {
return utils.CopyString(c.Path())
},
Expand Down
4 changes: 4 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ DRAFT section

## 🧬 Middlewares

### Cache

We are excited to introduce a new option in our caching middleware: Cache Invalidator. This feature provides greater control over cache management, allowing you to define a custom conditions for invalidating cache entries.

### CORS

We've made some changes to the CORS middleware to improve its functionality and flexibility. Here's what's new:
Expand Down
5 changes: 5 additions & 0 deletions middleware/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ func New(config ...Config) fiber.Handler {
// Get timestamp
ts := atomic.LoadUint64(&timestamp)

// Invalidate cache if requested
if cfg.CacheInvalidator != nil && cfg.CacheInvalidator(c) && e != nil {
e.exp = ts - 1
}

// Check if entry is expired
if e.exp != 0 && ts >= e.exp {
deleteKey(key)
Expand Down
34 changes: 34 additions & 0 deletions middleware/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,40 @@ func Test_CustomCacheHeader(t *testing.T) {
require.Equal(t, cacheMiss, resp.Header.Get("Cache-Status"))
}

func Test_CacheInvalidation(t *testing.T) {
t.Parallel()

app := fiber.New()
app.Use(New(Config{
CacheControl: true,
CacheInvalidator: func(c fiber.Ctx) bool {
return fiber.Query[bool](c, "invalidate")
},
}))

app.Get("/", func(c fiber.Ctx) error {
return c.SendString(time.Now().String())
})

resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
bodyCached, err := io.ReadAll(respCached.Body)
require.NoError(t, err)
require.True(t, bytes.Equal(body, bodyCached))
require.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl))

respInvalidate, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?invalidate=true", nil))
require.NoError(t, err)
bodyInvalidate, err := io.ReadAll(respInvalidate.Body)
require.NoError(t, err)
require.NotEqual(t, body, bodyInvalidate)
}

// Because time points are updated once every X milliseconds, entries in tests can often have
// equal expiration times and thus be in an random order. This closure hands out increasing
// time intervals to maintain strong ascending order of expiration
Expand Down
14 changes: 10 additions & 4 deletions middleware/cache/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ type Config struct {
// Optional. Default: false
CacheControl bool

// CacheInvalidator defines a function to invalidate the cache when returned true
//
// Optional. Default: nil
CacheInvalidator func(fiber.Ctx) bool

// Key allows you to generate custom keys, by default c.Path() is used
//
// Default: func(c fiber.Ctx) string {
Expand Down Expand Up @@ -69,10 +74,11 @@ type Config struct {

// ConfigDefault is the default config
var ConfigDefault = Config{
Next: nil,
Expiration: 1 * time.Minute,
CacheHeader: "X-Cache",
CacheControl: false,
Next: nil,
Expiration: 1 * time.Minute,
CacheHeader: "X-Cache",
CacheControl: false,
CacheInvalidator: nil,
KeyGenerator: func(c fiber.Ctx) string {
return utils.CopyString(c.Path())
},
Expand Down

0 comments on commit c9b7b1a

Please sign in to comment.