go-micro.dev/v5@v5.12.0/internal/website/docs/examples/realworld/graceful-shutdown.md (about) 1 --- 2 layout: default 3 --- 4 5 # Graceful Shutdown 6 7 Properly shutting down services to avoid dropped requests and data loss. 8 9 ## The Problem 10 11 Without graceful shutdown: 12 - In-flight requests are dropped 13 - Database connections leak 14 - Resources aren't cleaned up 15 - Load balancers don't know service is down 16 17 ## Solution 18 19 Go Micro handles SIGTERM/SIGINT by default, but you need to implement cleanup logic. 20 21 ## Basic Pattern 22 23 ```go 24 package main 25 26 import ( 27 "context" 28 "os" 29 "os/signal" 30 "syscall" 31 "time" 32 "go-micro.dev/v5" 33 "go-micro.dev/v5/logger" 34 ) 35 36 func main() { 37 svc := micro.NewService( 38 micro.Name("myservice"), 39 micro.BeforeStop(func() error { 40 logger.Info("Service stopping, running cleanup...") 41 return cleanup() 42 }), 43 ) 44 45 svc.Init() 46 47 // Your service logic 48 if err := svc.Handle(new(Handler)); err != nil { 49 logger.Fatal(err) 50 } 51 52 // Run with graceful shutdown 53 if err := svc.Run(); err != nil { 54 logger.Fatal(err) 55 } 56 57 logger.Info("Service stopped gracefully") 58 } 59 60 func cleanup() error { 61 // Close database connections 62 // Flush logs 63 // Stop background workers 64 // etc. 65 return nil 66 } 67 ``` 68 69 ## Database Cleanup 70 71 ```go 72 type Service struct { 73 db *sql.DB 74 } 75 76 func (s *Service) Shutdown(ctx context.Context) error { 77 logger.Info("Closing database connections...") 78 79 // Stop accepting new requests 80 s.db.SetMaxOpenConns(0) 81 82 // Wait for existing connections to finish (with timeout) 83 done := make(chan struct{}) 84 go func() { 85 s.db.Close() 86 close(done) 87 }() 88 89 select { 90 case <-done: 91 logger.Info("Database closed gracefully") 92 return nil 93 case <-ctx.Done(): 94 logger.Warn("Database close timeout, forcing") 95 return ctx.Err() 96 } 97 } 98 ``` 99 100 ## Background Workers 101 102 ```go 103 type Worker struct { 104 quit chan struct{} 105 done chan struct{} 106 } 107 108 func (w *Worker) Start() { 109 w.quit = make(chan struct{}) 110 w.done = make(chan struct{}) 111 112 go func() { 113 defer close(w.done) 114 ticker := time.NewTicker(5 * time.Second) 115 defer ticker.Stop() 116 117 for { 118 select { 119 case <-ticker.C: 120 w.doWork() 121 case <-w.quit: 122 logger.Info("Worker stopping...") 123 return 124 } 125 } 126 }() 127 } 128 129 func (w *Worker) Stop(timeout time.Duration) error { 130 close(w.quit) 131 132 select { 133 case <-w.done: 134 logger.Info("Worker stopped gracefully") 135 return nil 136 case <-time.After(timeout): 137 return fmt.Errorf("worker shutdown timeout") 138 } 139 } 140 ``` 141 142 ## Complete Example 143 144 ```go 145 package main 146 147 import ( 148 "context" 149 "database/sql" 150 "fmt" 151 "os" 152 "os/signal" 153 "sync" 154 "syscall" 155 "time" 156 157 "go-micro.dev/v5" 158 "go-micro.dev/v5/logger" 159 ) 160 161 type Application struct { 162 db *sql.DB 163 workers []*Worker 164 wg sync.WaitGroup 165 mu sync.RWMutex 166 closing bool 167 } 168 169 func NewApplication(db *sql.DB) *Application { 170 return &Application{ 171 db: db, 172 workers: make([]*Worker, 0), 173 } 174 } 175 176 func (app *Application) AddWorker(w *Worker) { 177 app.workers = append(app.workers, w) 178 w.Start() 179 } 180 181 func (app *Application) Shutdown(ctx context.Context) error { 182 app.mu.Lock() 183 if app.closing { 184 app.mu.Unlock() 185 return nil 186 } 187 app.closing = true 188 app.mu.Unlock() 189 190 logger.Info("Starting graceful shutdown...") 191 192 // Stop accepting new work 193 logger.Info("Stopping workers...") 194 for _, w := range app.workers { 195 if err := w.Stop(5 * time.Second); err != nil { 196 logger.Warnf("Worker failed to stop: %v", err) 197 } 198 } 199 200 // Wait for in-flight requests (with timeout) 201 shutdownComplete := make(chan struct{}) 202 go func() { 203 app.wg.Wait() 204 close(shutdownComplete) 205 }() 206 207 select { 208 case <-shutdownComplete: 209 logger.Info("All requests completed") 210 case <-ctx.Done(): 211 logger.Warn("Shutdown timeout, forcing...") 212 } 213 214 // Close resources 215 logger.Info("Closing database...") 216 if err := app.db.Close(); err != nil { 217 logger.Errorf("Database close error: %v", err) 218 } 219 220 logger.Info("Shutdown complete") 221 return nil 222 } 223 224 func main() { 225 db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) 226 if err != nil { 227 logger.Fatal(err) 228 } 229 230 app := NewApplication(db) 231 232 // Add background workers 233 app.AddWorker(&Worker{name: "cleanup"}) 234 app.AddWorker(&Worker{name: "metrics"}) 235 236 svc := micro.NewService( 237 micro.Name("myservice"), 238 micro.BeforeStop(func() error { 239 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 240 defer cancel() 241 return app.Shutdown(ctx) 242 }), 243 ) 244 245 svc.Init() 246 247 handler := &Handler{app: app} 248 if err := svc.Handle(handler); err != nil { 249 logger.Fatal(err) 250 } 251 252 // Run service 253 if err := svc.Run(); err != nil { 254 logger.Fatal(err) 255 } 256 } 257 ``` 258 259 ## Kubernetes Integration 260 261 ### Liveness and Readiness Probes 262 263 ```go 264 func (h *Handler) Health(ctx context.Context, req *struct{}, rsp *HealthResponse) error { 265 // Liveness: is the service alive? 266 rsp.Status = "ok" 267 return nil 268 } 269 270 func (h *Handler) Ready(ctx context.Context, req *struct{}, rsp *ReadyResponse) error { 271 h.app.mu.RLock() 272 closing := h.app.closing 273 h.app.mu.RUnlock() 274 275 if closing { 276 // Stop receiving traffic during shutdown 277 return fmt.Errorf("shutting down") 278 } 279 280 // Check dependencies 281 if err := h.app.db.Ping(); err != nil { 282 return fmt.Errorf("database unhealthy: %w", err) 283 } 284 285 rsp.Status = "ready" 286 return nil 287 } 288 ``` 289 290 ### Kubernetes Manifest 291 292 ```yaml 293 apiVersion: apps/v1 294 kind: Deployment 295 metadata: 296 name: myservice 297 spec: 298 replicas: 3 299 template: 300 spec: 301 containers: 302 - name: myservice 303 image: myservice:latest 304 ports: 305 - containerPort: 8080 306 livenessProbe: 307 httpGet: 308 path: /health 309 port: 8080 310 initialDelaySeconds: 10 311 periodSeconds: 10 312 readinessProbe: 313 httpGet: 314 path: /ready 315 port: 8080 316 initialDelaySeconds: 5 317 periodSeconds: 5 318 lifecycle: 319 preStop: 320 exec: 321 # Give service time to drain before SIGTERM 322 command: ["/bin/sh", "-c", "sleep 10"] 323 terminationGracePeriodSeconds: 40 324 ``` 325 326 ## Best Practices 327 328 1. **Set timeouts**: Don't wait forever for shutdown 329 2. **Stop accepting work early**: Set readiness to false 330 3. **Drain in-flight requests**: Let current work finish 331 4. **Close resources properly**: Databases, file handles, etc. 332 5. **Log shutdown progress**: Help debugging 333 6. **Handle SIGTERM and SIGINT**: Kubernetes sends SIGTERM 334 7. **Coordinate with load balancer**: Use readiness probes 335 8. **Test shutdown**: Regularly test graceful shutdown works 336 337 ## Testing Shutdown 338 339 ```bash 340 # Start service 341 go run main.go & 342 PID=$! 343 344 # Send some requests 345 for i in {1..10}; do 346 curl http://localhost:8080/endpoint & 347 done 348 349 # Trigger graceful shutdown 350 kill -TERM $PID 351 352 # Verify all requests completed 353 wait 354 ``` 355 356 ## Common Pitfalls 357 358 - **No timeout**: Service hangs during shutdown 359 - **Not stopping workers**: Background jobs continue 360 - **Database leaks**: Connections not closed 361 - **Ignored signals**: Service killed forcefully 362 - **No readiness probe**: Traffic during shutdown 363 364 ## Related 365 366 - [API Gateway Example](api-gateway.md) - Multi-service architecture 367 - [Getting Started Guide](../../getting-started.md) - Basic service setup