-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
base: main
Are you sure you want to change the base?
Changes from all commits
337f77a
4af52fb
3b38506
98f76cf
c8703bb
9f64a7b
b9b4251
bd4dc0c
6d7fbc2
9196f2e
5be40f1
6287ea1
d0607b5
f4682bd
a476945
beb5896
0fd8df7
a192bd1
4c8a17e
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 |
---|---|---|
@@ -0,0 +1,81 @@ | ||
--- | ||
id: loadshedding | ||
--- | ||
|
||
# Load Shedding | ||
|
||
The **Load Shedding** middleware for [Fiber](https://github.com/gofiber/fiber) helps maintain server stability by applying request-processing timeouts. It prevents resource exhaustion by gracefully rejecting requests that exceed a specified time limit. This is particularly beneficial in high-traffic scenarios, where preventing overload is crucial to sustaining service availability and performance. | ||
|
||
## Features | ||
|
||
- **Request Timeout Enforcement**: Automatically terminates any request that exceeds the configured processing time. | ||
- **Customizable Response**: Enables you to define a specialized response for timed-out requests. | ||
- **Exclusion Logic**: Lets you skip load-shedding for specific requests, such as health checks or other critical endpoints. | ||
- **Enhanced Stability**: Helps avoid server crashes or sluggish performance under heavy load by shedding excess requests. | ||
|
||
## Use Cases | ||
|
||
- **High-Traffic Scenarios**: Safeguard critical resources by rejecting overly long or resource-intensive requests. | ||
- **Health Check Protection**: Exclude monitoring endpoints (e.g., `/health`) to ensure uninterrupted external checks. | ||
- **Dynamic Load Management**: Utilize exclusion logic to adjust load-shedding behavior for specific routes or request types. | ||
|
||
--- | ||
|
||
## Signature | ||
|
||
```go | ||
func New(timeout time.Duration, loadSheddingHandler fiber.Handler, exclude func(fiber.Ctx) bool) fiber.Handler | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in order to ensure expandability in the future and not have to make breaking changes, i would like to stick with the configuration concept, which we use in all other middlewares and which has proven to be very easy to expand |
||
``` | ||
|
||
## Config | ||
|
||
| Property | Type | Description | Default | | ||
|-----------------------|--------------------|----------------------------------------------------------------------------------|-----------| | ||
| `timeout` | `time.Duration` | The maximum allowed processing time for a request. | Required | | ||
| `loadSheddingHandler`| `fiber.Handler` | The handler invoked for requests that exceed the `timeout`. | Required | | ||
| `exclude` | `func(fiber.Ctx) bool` | Optional function to exclude certain requests from load-shedding logic. | `nil` | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have similar behavior in all other middlewares in the core repo and also in the contrib repository the users know the parameter as |
||
|
||
gaby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
## Example Usage | ||
|
||
Import the middleware and configure it within your Fiber application: | ||
|
||
```go | ||
import ( | ||
"time" | ||
"github.com/gofiber/fiber/v3" | ||
"github.com/gofiber/fiber/v3/middleware/loadshedding" | ||
) | ||
|
||
func main() { | ||
app := fiber.New() | ||
|
||
// Basic usage with a 5-second timeout | ||
app.Use(loadshedding.New( | ||
5*time.Second, | ||
func(c fiber.Ctx) error { | ||
return c.Status(fiber.StatusServiceUnavailable).SendString("Service unavailable due to high load") | ||
}, | ||
nil, | ||
)) | ||
|
||
// Advanced usage with an exclusion function for specific endpoints | ||
app.Use(loadshedding.New( | ||
3*time.Second, | ||
func(c fiber.Ctx) error { | ||
return c.Status(fiber.StatusServiceUnavailable).SendString("Request timed out") | ||
}, | ||
func(c fiber.Ctx) bool { | ||
// Exclude /health from load-shedding | ||
return c.Path() == "/health" | ||
}, | ||
)) | ||
|
||
app.Get("/", func(c fiber.Ctx) error { | ||
// Simulating a long-running request | ||
time.Sleep(4 * time.Second) | ||
return c.SendString("Hello, world!") | ||
}) | ||
|
||
app.Listen(":3000") | ||
} | ||
``` | ||
gaby marked this conversation as resolved.
Show resolved
Hide resolved
|
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 | ||
} | ||
} | ||
} |
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) | ||
} |
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.
what is the difference to the timeout middleware? could you also store a handler in the config that handles these cases?