Frontend Labs
⋯

Day 47 — Connection Draining

Learning Objectives

  • Implement graceful backend removal without dropping active connections
  • Build the drain state machine: active → draining → removed
  • Test with 50 concurrent active connections

Exercise: Backend with Drain Support

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
    "time"
)

type DrainState int32

const (
    StateActive   DrainState = iota
    StateDraining
    StateRemoved
)

type DrainableBackend struct {
    Addr     string
    state    atomic.Int32
    inFlight sync.WaitGroup
}

func (b *DrainableBackend) State() DrainState {
    return DrainState(b.state.Load())
}

// AcquireSlot returns true if a new connection can be sent to this backend.
// Returns false if draining or removed.
func (b *DrainableBackend) AcquireSlot() bool {
    if b.state.Load() != int32(StateActive) {
        return false
    }
    b.inFlight.Add(1)

    // Re-check state after Add (race: state may have changed between check and Add)
    if b.state.Load() != int32(StateActive) {
        b.inFlight.Done()
        return false
    }
    return true
}

func (b *DrainableBackend) ReleaseSlot() {
    b.inFlight.Done()
}

// Drain marks the backend as draining and waits for in-flight to complete
func (b *DrainableBackend) Drain(timeout time.Duration) bool {
    b.state.Store(int32(StateDraining))
    fmt.Printf("[DRAIN] %s: draining (waiting for in-flight connections)\n", b.Addr)

    done := make(chan struct{})
    go func() {
        b.inFlight.Wait()
        close(done)
    }()

    select {
    case <-done:
        b.state.Store(int32(StateRemoved))
        fmt.Printf("[DRAIN] %s: drained cleanly\n", b.Addr)
        return true
    case <-time.After(timeout):
        b.state.Store(int32(StateRemoved))
        fmt.Printf("[DRAIN] %s: drain timeout, force-removed\n", b.Addr)
        return false
    }
}

func main() {
    backend := &DrainableBackend{Addr: "10.0.0.1:8080"}

    // Simulate 50 concurrent connections
    var wg sync.WaitGroup
    for i := 0; i < 50; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            if !backend.AcquireSlot() {
                fmt.Printf("[conn %d] rejected (draining)\n", id)
                return
            }
            defer backend.ReleaseSlot()
            // Simulate request processing (variable duration)
            time.Sleep(time.Duration(id%500) * time.Millisecond)
        }(i)
    }

    // Drain after 100ms (while connections are still active)
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Active connections at drain time: state=ACTIVE\n")

    cleaned := backend.Drain(30 * time.Second)
    fmt.Printf("Drain result: clean=%v\n", cleaned)

    wg.Wait()
    fmt.Printf("Final state: %v\n", backend.State())
}

Real-World Context

Kubernetes deployment rollovers use drain: when a new version deploys, old pods receive SIGTERM, stop accepting new requests, and wait for in-flight to complete. Without drain: users get connection reset errors during deploys. With drain: deploys are invisible to users. terminationGracePeriodSeconds is your drain timeout.


Generated from week7.md · 8 Oct 2026 16:36