mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-14 09:30:34 +00:00
494 lines
10 KiB
Markdown
494 lines
10 KiB
Markdown
# Server Package
|
|
|
|
Graceful HTTP server with request draining and shutdown coordination.
|
|
|
|
## Quick Start
|
|
|
|
```go
|
|
import "github.com/bitechdev/ResolveSpec/pkg/server"
|
|
|
|
// Create server
|
|
srv := server.NewGracefulServer(server.Config{
|
|
Addr: ":8080",
|
|
Handler: router,
|
|
})
|
|
|
|
// Start server (blocks until shutdown signal)
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
## Features
|
|
|
|
✅ Graceful shutdown on SIGINT/SIGTERM
|
|
✅ Request draining (waits for in-flight requests)
|
|
✅ Automatic request rejection during shutdown
|
|
✅ Health and readiness endpoints
|
|
✅ Shutdown callbacks for cleanup
|
|
✅ Configurable timeouts
|
|
|
|
## Configuration
|
|
|
|
```go
|
|
config := server.Config{
|
|
// Server address
|
|
Addr: ":8080",
|
|
|
|
// HTTP handler
|
|
Handler: myRouter,
|
|
|
|
// Maximum time for graceful shutdown (default: 30s)
|
|
ShutdownTimeout: 30 * time.Second,
|
|
|
|
// Time to wait for in-flight requests (default: 25s)
|
|
DrainTimeout: 25 * time.Second,
|
|
|
|
// Request read timeout (default: 10s)
|
|
ReadTimeout: 10 * time.Second,
|
|
|
|
// Response write timeout (default: 10s)
|
|
WriteTimeout: 10 * time.Second,
|
|
|
|
// Idle connection timeout (default: 120s)
|
|
IdleTimeout: 120 * time.Second,
|
|
}
|
|
|
|
srv := server.NewGracefulServer(config)
|
|
```
|
|
|
|
## Shutdown Behavior
|
|
|
|
**Signal received (SIGINT/SIGTERM):**
|
|
|
|
1. **Mark as shutting down** - New requests get 503
|
|
2. **Drain requests** - Wait up to `DrainTimeout` for in-flight requests
|
|
3. **Shutdown server** - Close listeners and connections
|
|
4. **Execute callbacks** - Run registered cleanup functions
|
|
|
|
```
|
|
Time Event
|
|
─────────────────────────────────────────
|
|
0s Signal received: SIGTERM
|
|
├─ Mark as shutting down
|
|
├─ Reject new requests (503)
|
|
└─ Start draining...
|
|
|
|
1s In-flight: 50 requests
|
|
2s In-flight: 32 requests
|
|
3s In-flight: 12 requests
|
|
4s In-flight: 3 requests
|
|
5s In-flight: 0 requests ✓
|
|
└─ All requests drained
|
|
|
|
5s Execute shutdown callbacks
|
|
6s Shutdown complete
|
|
```
|
|
|
|
## Health Checks
|
|
|
|
### Health Endpoint
|
|
|
|
Returns 200 when healthy, 503 when shutting down:
|
|
|
|
```go
|
|
router.HandleFunc("/health", srv.HealthCheckHandler())
|
|
```
|
|
|
|
**Response (healthy):**
|
|
```json
|
|
{"status":"healthy"}
|
|
```
|
|
|
|
**Response (shutting down):**
|
|
```json
|
|
{"status":"shutting_down"}
|
|
```
|
|
|
|
### Readiness Endpoint
|
|
|
|
Includes in-flight request count:
|
|
|
|
```go
|
|
router.HandleFunc("/ready", srv.ReadinessHandler())
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{"ready":true,"in_flight_requests":12}
|
|
```
|
|
|
|
**During shutdown:**
|
|
```json
|
|
{"ready":false,"reason":"shutting_down"}
|
|
```
|
|
|
|
## Shutdown Callbacks
|
|
|
|
Register cleanup functions to run during shutdown:
|
|
|
|
```go
|
|
// Close database
|
|
server.RegisterShutdownCallback(func(ctx context.Context) error {
|
|
logger.Info("Closing database connection...")
|
|
return db.Close()
|
|
})
|
|
|
|
// Flush metrics
|
|
server.RegisterShutdownCallback(func(ctx context.Context) error {
|
|
logger.Info("Flushing metrics...")
|
|
return metricsProvider.Flush(ctx)
|
|
})
|
|
|
|
// Close cache
|
|
server.RegisterShutdownCallback(func(ctx context.Context) error {
|
|
logger.Info("Closing cache...")
|
|
return cache.Close()
|
|
})
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/middleware"
|
|
"github.com/bitechdev/ResolveSpec/pkg/metrics"
|
|
"github.com/bitechdev/ResolveSpec/pkg/server"
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
func main() {
|
|
// Initialize metrics
|
|
metricsProvider := metrics.NewPrometheusProvider()
|
|
metrics.SetProvider(metricsProvider)
|
|
|
|
// Create router
|
|
router := mux.NewRouter()
|
|
|
|
// Apply middleware
|
|
rateLimiter := middleware.NewRateLimiter(100, 20)
|
|
sizeLimiter := middleware.NewRequestSizeLimiter(middleware.Size10MB)
|
|
sanitizer := middleware.DefaultSanitizer()
|
|
|
|
router.Use(rateLimiter.Middleware)
|
|
router.Use(sizeLimiter.Middleware)
|
|
router.Use(sanitizer.Middleware)
|
|
router.Use(metricsProvider.Middleware)
|
|
|
|
// API routes
|
|
router.HandleFunc("/api/data", dataHandler)
|
|
|
|
// Create graceful server
|
|
srv := server.NewGracefulServer(server.Config{
|
|
Addr: ":8080",
|
|
Handler: router,
|
|
ShutdownTimeout: 30 * time.Second,
|
|
DrainTimeout: 25 * time.Second,
|
|
})
|
|
|
|
// Health checks
|
|
router.HandleFunc("/health", srv.HealthCheckHandler())
|
|
router.HandleFunc("/ready", srv.ReadinessHandler())
|
|
|
|
// Metrics endpoint
|
|
router.Handle("/metrics", metricsProvider.Handler())
|
|
|
|
// Register shutdown callbacks
|
|
server.RegisterShutdownCallback(func(ctx context.Context) error {
|
|
log.Println("Cleanup: Flushing metrics...")
|
|
return nil
|
|
})
|
|
|
|
server.RegisterShutdownCallback(func(ctx context.Context) error {
|
|
log.Println("Cleanup: Closing database...")
|
|
// return db.Close()
|
|
return nil
|
|
})
|
|
|
|
// Start server (blocks until shutdown)
|
|
log.Printf("Starting server on :8080")
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Wait for shutdown to complete
|
|
srv.Wait()
|
|
log.Println("Server stopped")
|
|
}
|
|
|
|
func dataHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Your handler logic
|
|
time.Sleep(100 * time.Millisecond) // Simulate work
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"message":"success"}`))
|
|
}
|
|
```
|
|
|
|
## Kubernetes Integration
|
|
|
|
### Deployment with Probes
|
|
|
|
```yaml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: myapp
|
|
spec:
|
|
replicas: 3
|
|
template:
|
|
spec:
|
|
containers:
|
|
- name: app
|
|
image: myapp:latest
|
|
ports:
|
|
- containerPort: 8080
|
|
|
|
# Liveness probe - is app running?
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: 8080
|
|
initialDelaySeconds: 10
|
|
periodSeconds: 10
|
|
timeoutSeconds: 5
|
|
|
|
# Readiness probe - can app handle traffic?
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /ready
|
|
port: 8080
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
|
|
# Graceful shutdown
|
|
lifecycle:
|
|
preStop:
|
|
exec:
|
|
command: ["/bin/sh", "-c", "sleep 5"]
|
|
|
|
# Environment
|
|
env:
|
|
- name: SHUTDOWN_TIMEOUT
|
|
value: "30"
|
|
```
|
|
|
|
### Service
|
|
|
|
```yaml
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: myapp
|
|
spec:
|
|
selector:
|
|
app: myapp
|
|
ports:
|
|
- port: 80
|
|
targetPort: 8080
|
|
type: LoadBalancer
|
|
```
|
|
|
|
## Docker Compose
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "8080:8080"
|
|
environment:
|
|
- SHUTDOWN_TIMEOUT=30
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
stop_grace_period: 35s # Slightly longer than shutdown timeout
|
|
```
|
|
|
|
## Testing Graceful Shutdown
|
|
|
|
### Test Script
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
# Start server in background
|
|
./myapp &
|
|
SERVER_PID=$!
|
|
|
|
# Wait for server to start
|
|
sleep 2
|
|
|
|
# Send some requests
|
|
for i in {1..10}; do
|
|
curl http://localhost:8080/api/data &
|
|
done
|
|
|
|
# Wait a bit
|
|
sleep 1
|
|
|
|
# Send shutdown signal
|
|
kill -TERM $SERVER_PID
|
|
|
|
# Try to send more requests (should get 503)
|
|
curl -v http://localhost:8080/api/data
|
|
|
|
# Wait for server to stop
|
|
wait $SERVER_PID
|
|
echo "Server stopped gracefully"
|
|
```
|
|
|
|
### Expected Output
|
|
|
|
```
|
|
Starting server on :8080
|
|
Received signal: terminated, initiating graceful shutdown
|
|
Starting graceful shutdown...
|
|
Waiting for 8 in-flight requests to complete...
|
|
Waiting for 4 in-flight requests to complete...
|
|
Waiting for 1 in-flight requests to complete...
|
|
All requests drained in 2.3s
|
|
Cleanup: Flushing metrics...
|
|
Cleanup: Closing database...
|
|
Shutting down HTTP server...
|
|
Graceful shutdown complete
|
|
Server stopped
|
|
```
|
|
|
|
## Monitoring In-Flight Requests
|
|
|
|
```go
|
|
// Get current in-flight count
|
|
count := srv.InFlightRequests()
|
|
fmt.Printf("In-flight requests: %d\n", count)
|
|
|
|
// Check if shutting down
|
|
if srv.IsShuttingDown() {
|
|
fmt.Println("Server is shutting down")
|
|
}
|
|
```
|
|
|
|
## Advanced Usage
|
|
|
|
### Custom Shutdown Logic
|
|
|
|
```go
|
|
// Implement custom shutdown
|
|
go func() {
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
|
|
<-sigChan
|
|
log.Println("Shutdown signal received")
|
|
|
|
// Custom pre-shutdown logic
|
|
log.Println("Running custom cleanup...")
|
|
|
|
// Shutdown with callbacks
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := srv.ShutdownWithCallbacks(ctx); err != nil {
|
|
log.Printf("Shutdown error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Start server
|
|
srv.server.ListenAndServe()
|
|
```
|
|
|
|
### Multiple Servers
|
|
|
|
```go
|
|
// HTTP server
|
|
httpSrv := server.NewGracefulServer(server.Config{
|
|
Addr: ":8080",
|
|
Handler: httpRouter,
|
|
})
|
|
|
|
// HTTPS server
|
|
httpsSrv := server.NewGracefulServer(server.Config{
|
|
Addr: ":8443",
|
|
Handler: httpsRouter,
|
|
})
|
|
|
|
// Start both
|
|
go httpSrv.ListenAndServe()
|
|
go httpsSrv.ListenAndServe()
|
|
|
|
// Shutdown both on signal
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt)
|
|
<-sigChan
|
|
|
|
ctx := context.Background()
|
|
httpSrv.Shutdown(ctx)
|
|
httpsSrv.Shutdown(ctx)
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Set appropriate timeouts**
|
|
- `DrainTimeout` < `ShutdownTimeout`
|
|
- `ShutdownTimeout` < Kubernetes `terminationGracePeriodSeconds`
|
|
|
|
2. **Register cleanup callbacks** for:
|
|
- Database connections
|
|
- Message queues
|
|
- Metrics flushing
|
|
- Cache shutdown
|
|
- Background workers
|
|
|
|
3. **Health checks**
|
|
- Use `/health` for liveness (is app alive?)
|
|
- Use `/ready` for readiness (can app serve traffic?)
|
|
|
|
4. **Load balancer considerations**
|
|
- Set `preStop` hook in Kubernetes (5-10s delay)
|
|
- Allows load balancer to deregister before shutdown
|
|
|
|
5. **Monitoring**
|
|
- Track in-flight requests in metrics
|
|
- Alert on slow drains
|
|
- Monitor shutdown duration
|
|
|
|
## Troubleshooting
|
|
|
|
### Shutdown Takes Too Long
|
|
|
|
```go
|
|
// Increase drain timeout
|
|
config.DrainTimeout = 60 * time.Second
|
|
```
|
|
|
|
### Requests Still Timing Out
|
|
|
|
```go
|
|
// Increase write timeout
|
|
config.WriteTimeout = 30 * time.Second
|
|
```
|
|
|
|
### Force Shutdown Not Working
|
|
|
|
The server will force shutdown after `ShutdownTimeout` even if requests are still in-flight. Adjust timeouts as needed.
|
|
|
|
### Debugging Shutdown
|
|
|
|
```go
|
|
// Enable debug logging
|
|
import "github.com/bitechdev/ResolveSpec/pkg/logger"
|
|
|
|
logger.SetLevel("debug")
|
|
```
|