← Semua pembelajaran / Go Nol → Enterprise
Fase 3 · HTTP — Request sampai Response

Graceful shutdown

Setiap deploy mematikan container lama. Tanpa penutupan yang rapi, setiap deploy juga memotong setiap permintaan yang sedang berjalan — dan di ECS itu terjadi puluhan kali sehari.

Sumber asli pkg.go.dev Resmi Rangkuman ~6 menit baca

Intisari

  • srv.Shutdown(ctx) berhenti menerima koneksi baru, lalu menunggu yang sedang berjalan selesai.
  • signal.NotifyContext mengubah SIGTERM jadi pembatalan context — satu baris, tanpa channel manual.
  • ECS mengirim SIGTERM, lalu SIGKILL setelah stopTimeout (default 30 detik).
  • Tidur beberapa detik sebelum mulai menutup — ALB butuh waktu untuk berhenti mengirim permintaan baru.
  • Urutan matinya penting: server HTTP dulu, lalu worker, baru koneksi database.

Bentuk lengkapnya

func main() {
	// SIGINT (Ctrl-C) dan SIGTERM (ECS, Kubernetes) → pembatalan context.
	ctx, hentikanSinyal := signal.NotifyContext(context.Background(),
		os.Interrupt, syscall.SIGTERM)
	defer hentikanSinyal()

	srv := &http.Server{Addr: ":8080", Handler: handler, /* timeout ... */}

	go func() {
		if err := srv.ListenAndServe(); err != nil &&
			!errors.Is(err, http.ErrServerClosed) {
			log.Error("server berhenti tak terduga", "err", err)
			hentikanSinyal()
		}
	}()
	log.Info("server siap", "addr", srv.Addr)

	<-ctx.Done()                       // menunggu sinyal
	hentikanSinyal()                    // sinyal kedua = mati paksa
	log.Info("sinyal berhenti diterima, menutup rapi")

	// 1. Beri load balancer waktu menyadari kita mau berhenti.
	siap.Store(false)                   // /siap mulai membalas 503
	time.Sleep(5 * time.Second)

	// 2. Tutup server HTTP: tolak koneksi baru, tunggu yang berjalan.
	ctxMati, batal := context.WithTimeout(context.Background(), 20*time.Second)
	defer batal()

	if err := srv.Shutdown(ctxMati); err != nil {
		log.Error("shutdown melebihi tenggat, memutus paksa", "err", err)
		_ = srv.Close()
	}

	// 3. Hentikan pekerja latar, lalu tunggu mereka.
	pekerja.Berhenti()
	pekerja.Tunggu(ctxMati)

	// 4. Terakhir: tutup koneksi keluar.
	db.Close()
	rdb.Close()
	_ = penyedia.Shutdown(ctxMati)      // OpenTelemetry: kirim sisa trace

	log.Info("selesai")
}

Kenapa harus tidur dulu

t=0.0  ECS mengirim SIGTERM
t=0.0  ❌ Kalau langsung Shutdown():
       ALB masih menganggap task ini sehat dan MASIH mengirim
       permintaan baru → koneksi ditolak → pengguna melihat 502.

t=0.0  ✅ Dengan jeda:
       /siap mulai membalas 503
t=2.0  ALB gagal health check, menandai target unhealthy
t=5.0  ALB berhenti mengirim permintaan baru → baru Shutdown()
t=5–25 Permintaan yang sedang berjalan diselesaikan
t=25   Proses keluar dengan kode 0

Ini penyebab paling umum "beberapa 502 setiap deploy". Aplikasi menutup diri dengan benar, tapi terlalu cepat — sebelum load balancer sempat tahu. Jedanya harus lebih besar dari (interval health check × ambang unhealthy). Untuk ALB dengan interval 5 detik dan ambang 2, jeda 5–10 detik sudah memadai.

Selaras dengan ECS

Setelan ECS / ALBNilaiHarus
stopTimeout container60 dtk> jeda + Shutdown + worker
Deregistration delay target group30 dtk≥ durasi permintaan terpanjang
Health check interval5 dtk—
Unhealthy threshold2interval × ambang < jeda tidur

Kalau proses belum keluar saat stopTimeout habis, ECS mengirim SIGKILL — dan SIGKILL tidak bisa ditangkap. Semua yang sedang berjalan mati di tengah: transaksi yang belum di-commit, pesan antrean yang belum di-ack, berkas yang belum ditutup. Anggaran waktumu harus muat di dalamnya.

Dua endpoint kesehatan, bukan satu

// Liveness: "prosesnya masih hidup?" — JANGAN menyentuh database.
mux.HandleFunc("GET /sehat", func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status":"ok"}`))
})

// Readiness: "boleh dikirimi lalu lintas?" — boleh memeriksa dependensi,
// dan inilah yang berubah saat shutdown dimulai.
mux.HandleFunc("GET /siap", func(w http.ResponseWriter, r *http.Request) {
	if !siap.Load() {
		http.Error(w, "sedang berhenti", http.StatusServiceUnavailable)
		return
	}
	ctx, batal := context.WithTimeout(r.Context(), time.Second)
	defer batal()
	if err := db.PingContext(ctx); err != nil {
		http.Error(w, "database tidak siap", http.StatusServiceUnavailable)
		return
	}
	w.WriteHeader(http.StatusOK)
})

Health check ALB harus menunjuk ke endpoint yang tidak menyentuh database. Kalau database sempat lambat, health check yang memeriksanya akan menyatakan semua task tidak sehat — ECS lalu mematikan semuanya, dan gangguan sebagian berubah jadi pemadaman total. Ini kesalahan konfigurasi yang mahal, dan muncul lagi di Fase 10.

Yang membuat shutdown menggantung

PenyebabPerbaikan
Koneksi hijacked (WebSocket)Shutdown tidak menunggunya — tutup sendiri lewat RegisterOnShutdown
Handler yang mengabaikan ctxTeruskan context sampai lapisan terdalam
Worker tanpa sinyal berhentiselect dengan <-ctx.Done() (Fase 2)
Koneksi keep-alive menganggurDitangani Shutdown; pastikan IdleTimeout terset
SSE / long pollingHandler harus keluar saat r.Context() selesai

Latihan: jalankan server dengan endpoint yang tidur 10 detik. Panggil endpoint itu, lalu tekan Ctrl-C di terminal server saat permintaan masih berjalan. Buktikan permintaannya tetap selesai dan servernya keluar setelahnya. Lalu ganti srv.Shutdown(ctx) jadi srv.Close() dan ulangi — perhatikan koneksinya terputus di tengah.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.