karawaci.kode

← Semua snippet

Go Menengah Utility

Go Fiber graceful shutdown dengan context

Graceful shutdown Fiber: tunggu request inflight selesai, close DB pool, drain message queue. Tanpa interrupt user di tengah transaksi.

Dipublikasikan 22 Juni 2026

Deploy Fiber service tanpa graceful shutdown = request inflight kepotong, transaksi tergantung setengah jalan. Pattern shutdown yang bener: tangkap SIGTERM, stop terima request baru, tunggu yang lagi diproses selesai, close resource. Snippet ini full stack — HTTP, DB, NATS consumer.

Kode

package main

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/gofiber/fiber/v3"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/nats-io/nats.go"
)

// Server bungkus semua dependency
type Server struct {
	app    *fiber.App
	db     *pgxpool.Pool
	nc     *nats.Conn
	logger *slog.Logger
}

func NewServer(logger *slog.Logger) (*Server, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	db, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
	if err != nil {
		return nil, fmt.Errorf("db: %w", err)
	}

	nc, err := nats.Connect(os.Getenv("NATS_URL"),
		nats.MaxReconnects(-1),
		nats.ReconnectWait(2*time.Second),
	)
	if err != nil {
		db.Close()
		return nil, fmt.Errorf("nats: %w", err)
	}

	app := fiber.New(fiber.Config{
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	})

	s := &Server{app: app, db: db, nc: nc, logger: logger}
	s.routes()
	return s, nil
}

func (s *Server) routes() {
	s.app.Get("/healthz", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{"status": "ok"})
	})

	s.app.Post("/order", func(c fiber.Ctx) error {
		// Pakai context dari request — kalau client cancel, propagate
		ctx := c.Context()

		var order struct {
			ProdukID int `json:"produk_id"`
			Qty      int `json:"qty"`
		}
		if err := c.Bind().JSON(&order); err != nil {
			return c.Status(400).JSON(fiber.Map{"error": err.Error()})
		}

		var orderID int
		err := s.db.QueryRow(ctx,
			"INSERT INTO orders (produk_id, qty) VALUES ($1, $2) RETURNING id",
			order.ProdukID, order.Qty,
		).Scan(&orderID)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}

		return c.JSON(fiber.Map{"order_id": orderID})
	})
}

// startConsumer subscribe NATS dan return cancel function
func (s *Server) startConsumer(ctx context.Context) (func(), error) {
	sub, err := s.nc.Subscribe("order.created", func(m *nats.Msg) {
		s.logger.Info("processing order event", "data", string(m.Data))
		// proses event (idempotent)
		time.Sleep(500 * time.Millisecond)
		_ = m.Ack()
	})
	if err != nil {
		return nil, err
	}

	cancel := func() {
		if err := sub.Drain(); err != nil {
			s.logger.Error("drain subscription gagal", "err", err)
		}
	}
	return cancel, nil
}

func (s *Server) Run(ctx context.Context, addr string) error {
	cancelConsumer, err := s.startConsumer(ctx)
	if err != nil {
		return err
	}

	// Start HTTP server di goroutine
	serverErr := make(chan error, 1)
	go func() {
		s.logger.Info("HTTP listen", "addr", addr)
		if err := s.app.Listen(addr); err != nil && !errors.Is(err, fiber.ErrGracefulShutdown) {
			serverErr <- err
		}
		close(serverErr)
	}()

	// Tunggu signal atau ctx canceled
	select {
	case <-ctx.Done():
		s.logger.Info("shutdown signal diterima, mulai graceful shutdown")
	case err := <-serverErr:
		return fmt.Errorf("server crash: %w", err)
	}

	// Shutdown sequence
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()

	var wg sync.WaitGroup
	wg.Add(3)

	go func() {
		defer wg.Done()
		s.logger.Info("stop terima request baru, tunggu inflight selesai")
		if err := s.app.ShutdownWithContext(shutdownCtx); err != nil {
			s.logger.Error("HTTP shutdown gagal", "err", err)
		}
	}()

	go func() {
		defer wg.Done()
		s.logger.Info("drain NATS subscriber")
		cancelConsumer()
		s.nc.Close()
	}()

	go func() {
		defer wg.Done()
		s.logger.Info("close DB pool")
		s.db.Close()
	}()

	wg.Wait()
	s.logger.Info("graceful shutdown selesai")
	return nil
}

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	server, err := NewServer(logger)
	if err != nil {
		logger.Error("init server gagal", "err", err)
		os.Exit(1)
	}

	// Listen untuk SIGTERM (k8s) dan SIGINT (Ctrl+C)
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
	defer stop()

	if err := server.Run(ctx, ":8080"); err != nil {
		logger.Error("server run gagal", "err", err)
		os.Exit(1)
	}
}

Pemakaian

# Build & run
go run main.go

# Trigger graceful shutdown (Ctrl+C atau Kubernetes scale-down)
# Output:
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"shutdown signal diterima"}
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"stop terima request baru"}
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"drain NATS subscriber"}
# {"time":"2026-06-22T10:00:00Z","level":"INFO","msg":"close DB pool"}
# {"time":"2026-06-22T10:00:05Z","level":"INFO","msg":"graceful shutdown selesai"}
# k8s/deployment.yaml — penting set gracePeriod
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 30  # > shutdown timeout di code
      containers:
        - name: api
          image: tokopedia-api:v1.0
          lifecycle:
            preStop:
              exec:
                # Delay 5s sebelum SIGTERM supaya Service endpoint terupdate dulu
                command: ["/bin/sh", "-c", "sleep 5"]
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080

Kapan dipakai

  • HTTP API production di Kubernetes / Cloud Run.
  • Background worker yang consume message queue.
  • Service yang punya DB transaction in-progress.
  • Cron / scheduled job yang panjang.

Catatan

  • signal.NotifyContext lebih clean dari channel manual — Go 1.16+.
  • NATS Drain vs Close — Drain selesaikan pending message, Close paksa putus. Pakai Drain dulu, baru Close.
  • DB pool Close — pgx pool tunggu inflight query selesai sebelum tutup. Aman dipanggil terakhir.
  • preStop sleep 5s di k8s — beri waktu Service endpoint controller propagate “pod not ready” sebelum container start SIGTERM. Tanpa ini, ada window k8s tetap routing ke pod yang shutdown.
  • terminationGracePeriodSeconds harus > shutdown timeout di code. Default k8s 30s, code di sini timeout 20s — aman.
  • errors.Is fiber.ErrGracefulShutdown — Fiber return error khusus saat shutdown. Bukan error sebenarnya, exclude dari log error.

Test graceful shutdown locally: jalankan, kirim request long-running ke /order, terus Ctrl+C. Request harus selesai sukses, bukan EOF.

# tags

gofibergraceful-shutdownsignalcontext

Ditulis oleh Asti Larasati · 22 Juni 2026