Skip to content
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

🚀 feat: Add Load-Shedding Middleware #3264

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Here's a quick overview of the changes in Fiber `v3`:
- [Filesystem](#filesystem)
- [Monitor](#monitor)
- [Healthcheck](#healthcheck)
- [Load shedding](#loadshedding)
- [📋 Migration guide](#-migration-guide)

## Drop for old Go versions
Expand Down Expand Up @@ -810,6 +811,15 @@ The Healthcheck middleware has been enhanced to support more than two routes, wi

Refer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes.

### Load shedding
We've added **Load Shedding Middleware**.It ensures system stability under high load by enforcing timeouts on request processing. This mechanism allows the application to shed excessive load gracefully and maintain responsiveness.

**Functionality**
- **Timeout Enforcement**: Automatically terminates requests exceeding a specified processing time.
- **Custom Response**: Uses a configurable load-shedding handler to define the response for shed requests.
- **Request Exclusion**: Allows certain requests to bypass load-shedding logic through an exclusion filter.

This middleware is designed to enhance server resilience and improve the user experience during periods of high traffic.
## 📋 Migration guide

- [🚀 App](#-app-1)
Expand Down Expand Up @@ -1361,6 +1371,34 @@ app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthc
app.Get("/live", healthcheck.NewHealthChecker())
```

#### Load shedding
This middleware uses `context.WithTimeout` to manage the lifecycle of requests. If a request exceeds the specified timeout, the custom load-shedding handler is triggered, ensuring the system remains stable under stress.

**Key Parameters**

`timeout` (`time.Duration`): The maximum time a request is allowed to process. Requests exceeding this time are terminated.

`loadSheddingHandler` (`fiber.Handler`): A custom handler that executes when a request exceeds the timeout. Typically used to return a `503 Service Unavailable` response or a custom message.

`exclude` (`func(fiber.Ctx) bool`): A filter function to exclude specific requests from being subjected to the load-shedding logic (optional).

**Usage Example**

```go
import "github.com/gofiber/fiber/v3/middleware/loadshedding

app.Use(loadshedding.New(
10*time.Second, // Timeout duration
func(c fiber.Ctx) error { // Load shedding response
return c.Status(fiber.StatusServiceUnavailable).
SendString("Service overloaded, try again later.")
},
func(c fiber.Ctx) bool { // Exclude health checks
return c.Path() == "/health"
},
))
```

#### Monitor

Since v3 the Monitor middleware has been moved to the [Contrib package](https://github.com/gofiber/contrib/tree/main/monitor)
Expand Down
45 changes: 45 additions & 0 deletions middleware/loadshedding/loadshedding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package loadshedding

import (
"context"
"time"

"github.com/gofiber/fiber/v3"
)

// New creates a middleware handler enforces a timeout on request processing to manage server load.
// If a request exceeds the specified timeout, a custom load-shedding handler is executed.
func New(timeout time.Duration, loadSheddingHandler fiber.Handler, exclude func(fiber.Ctx) bool) fiber.Handler {
return func(c fiber.Ctx) error {
// Skip load-shedding logic for requests matching the exclusion criteria
if exclude != nil && exclude(c) {
return c.Next()
}

// Create a context with a timeout for the current request
ctx, cancel := context.WithTimeout(c.Context(), timeout)
defer cancel()

// Set the new context with a timeout
c.SetContext(ctx)

// Process the request and capture any error
err := c.Next()

// Create a channel to signal when request processing completes
done := make(chan error, 1)

// Send the result of the request processing to the channel
go func() {
done <- err
}()

// Handle either request completion or timeout
select {
case <-ctx.Done(): // Triggered if the timeout expires
return loadSheddingHandler(c)
case err := <-done: // Triggered if request processing completes
return err
}
}
}
92 changes: 92 additions & 0 deletions middleware/loadshedding/loadshedding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package loadshedding_test

import (
"net/http/httptest"
"testing"
"time"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/loadshedding"
"github.com/stretchr/testify/require"
)

// Helper handlers
func successHandler(c fiber.Ctx) error {
return c.SendString("Request processed successfully!")
}

func timeoutHandler(c fiber.Ctx) error {
time.Sleep(2 * time.Second) // Simulate a long-running request
return c.SendString("This should not appear")
}

func loadSheddingHandler(c fiber.Ctx) error {
return c.Status(fiber.StatusServiceUnavailable).SendString("Service Overloaded")
}

func excludedHandler(c fiber.Ctx) error {
return c.SendString("Excluded route")
}

// go test -run Test_LoadSheddingExcluded
func Test_LoadSheddingExcluded(t *testing.T) {
t.Parallel()
app := fiber.New()

// Middleware with exclusion
app.Use(loadshedding.New(
1*time.Second,
loadSheddingHandler,
func(c fiber.Ctx) bool { return c.Path() == "/excluded" },
))
app.Get("/", successHandler)
app.Get("/excluded", excludedHandler)

// Test excluded route
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/excluded", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
}

// go test -run Test_LoadSheddingTimeout
func Test_LoadSheddingTimeout(t *testing.T) {
t.Parallel()
app := fiber.New()

// Middleware with a 1-second timeout
app.Use(loadshedding.New(
1*time.Second, // Middleware timeout
loadSheddingHandler,
nil,
))
app.Get("/", timeoutHandler)

// Create a custom request
req := httptest.NewRequest(fiber.MethodGet, "/", nil)

// Test timeout behavior
resp, err := app.Test(req, fiber.TestConfig{
Timeout: 3 * time.Second, // Ensure the test timeout exceeds middleware timeout
})
require.NoError(t, err)
require.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode)
}

// go test -run Test_LoadSheddingSuccessfulRequest
func Test_LoadSheddingSuccessfulRequest(t *testing.T) {
t.Parallel()
app := fiber.New()

// Middleware with sufficient time for request to complete
app.Use(loadshedding.New(
2*time.Second,
loadSheddingHandler,
nil,
))
app.Get("/", successHandler)

// Test successful request
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
}