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