Signal Handling

Dewy uses Unix signals to control processes, enabling proper application restarts and clean shutdown procedures. Signal handling is a critical feature for zero-downtime deployments and safe system operations in production environments.

Supported Signals

Dewy supports the following Unix signals, each triggering different behaviors:

SIGHUP

SIGHUP signals are received but ignored by dewy itself. This design assumes that SIGHUP will be sent to child processes (managed applications). Applications are expected to handle SIGHUP signals to perform graceful restarts or configuration reloads.

SIGUSR1

When dewy receives a SIGUSR1 signal, it gracefully restarts the currently running server application. This signal can be used to manually restart the server when a new version of the application has been deployed.

SIGINT

SIGINT signals (typically sent by Ctrl+C) cause dewy to terminate gracefully. This includes stopping the scheduler and sending termination messages through the notification system as part of the cleanup process.

SIGTERM

SIGTERM signals are treated as termination requests from process management systems (such as systemd). Like SIGINT, they trigger proper cleanup procedures before process termination.

SIGQUIT

SIGQUIT signals are also treated as termination signals, executing the same shutdown procedures as SIGINT and SIGTERM.

Signal Handling Implementation

Dewy's signal handling is implemented in the waitSigs() function (dewy.go:125-153). This function runs in a goroutine that continuously monitors specified signals.

func (d *Dewy) waitSigs(ctx context.Context) {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

    for sig := range sigCh {
        d.logger.Debug("PID received signal", slog.Int("pid", os.Getpid()), slog.String("signal", sig.String()))
        switch sig {
        case syscall.SIGHUP:
            continue
        case syscall.SIGUSR1:
            // Server restart processing
        case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
            // Termination processing
        }
    }
}

When signals are received, the process ID and signal name are logged, and appropriate actions are executed based on the signal type.

Server Restart Mechanism

Server restarts triggered by SIGUSR1 signals are handled by the restartServer() function. This feature enables switching to new application versions without downtime.

The restart process sends a SIGHUP signal to the current process, which triggers a graceful restart through the server-starter library integration.

# Manual server restart example
kill -USR1 <dewy_process_id>

Upon successful restart, completion notifications are sent through the notification system.

Shutdown Process

When termination signals (SIGINT, SIGTERM, SIGQUIT) are received, dewy performs cleanup in the following sequence:

First, the periodic job scheduler is stopped to prevent new deployment processes from starting. Next, termination messages are sent through the notification system to alert administrators that dewy is shutting down. Finally, the main processing loop is terminated and the process is completely stopped.

Application Implementation Examples

Applications managed by dewy can implement proper signal handling to enable safer and more reliable deployments.

HTTP Server Applications

HTTP server applications should implement graceful shutdown to properly terminate connections during request processing.

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{
        Addr:    ":8080",
        Handler: http.DefaultServeMux,
    }

    // Create signal channel
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)

    go func() {
        sig := <-sigCh
        log.Printf("Received signal: %s", sig)

        // Graceful shutdown
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()

        if err := server.Shutdown(ctx); err != nil {
            log.Printf("Server shutdown error: %v", err)
        }
    }()

    log.Println("Starting server on :8080")
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Printf("Server error: %v", err)
    }
}

This example implements graceful shutdown when receiving SIGHUP signals, allowing existing requests to complete before stopping the server.

Applications with Database Connections

Applications using database connection pools need to properly close connections during termination.

package main

import (
    "database/sql"
    "log"
    "os"
    "os/signal"
    "syscall"
    _ "github.com/lib/pq"
)

type App struct {
    db *sql.DB
}

func (a *App) shutdown() {
    log.Println("Closing database connections...")
    if err := a.db.Close(); err != nil {
        log.Printf("Database close error: %v", err)
    }
    log.Println("Application shutdown complete")
}

func main() {
    db, err := sql.Open("postgres", "postgresql://user:pass@localhost/db")
    if err != nil {
        log.Fatal(err)
    }

    app := &App{db: db}

    // Signal handling
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)

    go func() {
        sig := <-sigCh
        log.Printf("Received signal: %s", sig)
        app.shutdown()
        os.Exit(0)
    }()

    // Application main processing
    log.Println("Application started")
    select {} // Block indefinitely
}

Proper database connection pool closure prevents connection leaks and reduces load on the database server.

Background Workers

Worker applications that perform long-running processes need to handle interruption and resumption properly.

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"
)

type Worker struct {
    ctx    context.Context
    cancel context.CancelFunc
}

func (w *Worker) start() {
    w.ctx, w.cancel = context.WithCancel(context.Background())

    for {
        select {
        case <-w.ctx.Done():
            log.Println("Worker stopped")
            return
        default:
            // Simulate long-running processing
            log.Println("Processing...")
            time.Sleep(5 * time.Second)
        }
    }
}

func (w *Worker) stop() {
    log.Println("Stopping worker...")
    w.cancel()
}

func main() {
    worker := &Worker{}

    // Signal handling
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)

    go func() {
        sig := <-sigCh
        log.Printf("Received signal: %s", sig)
        worker.stop()
    }()

    log.Println("Worker started")
    worker.start()
}

This pattern uses context.Context to control process interruption, enabling safe process termination upon signal reception.

WebSocket Servers

Applications managing WebSocket connections need proper connection termination procedures.

package main

import (
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"

    "github.com/gorilla/websocket"
)

type WebSocketServer struct {
    clients map[*websocket.Conn]bool
    mu      sync.RWMutex
}

func (ws *WebSocketServer) addClient(conn *websocket.Conn) {
    ws.mu.Lock()
    defer ws.mu.Unlock()
    ws.clients[conn] = true
}

func (ws *WebSocketServer) removeClient(conn *websocket.Conn) {
    ws.mu.Lock()
    defer ws.mu.Unlock()
    delete(ws.clients, conn)
    conn.Close()
}

func (ws *WebSocketServer) closeAllConnections() {
    ws.mu.Lock()
    defer ws.mu.Unlock()

    log.Printf("Closing %d WebSocket connections", len(ws.clients))
    for conn := range ws.clients {
        conn.WriteMessage(websocket.CloseMessage, []byte("Server shutting down"))
        conn.Close()
    }
    ws.clients = make(map[*websocket.Conn]bool)
}

func main() {
    server := &WebSocketServer{
        clients: make(map[*websocket.Conn]bool),
    }

    upgrader := websocket.Upgrader{}

    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        conn, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            return
        }
        server.addClient(conn)
        defer server.removeClient(conn)

        // WebSocket processing
        for {
            _, _, err := conn.ReadMessage()
            if err != nil {
                break
            }
        }
    })

    // Signal handling
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)

    go func() {
        sig := <-sigCh
        log.Printf("Received signal: %s", sig)
        server.closeAllConnections()
        os.Exit(0)
    }()

    log.Println("WebSocket server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Proper WebSocket connection termination prevents timeout errors on the client side and improves user experience.

Practical Usage Examples

In production environments, signal transmission is typically performed through process management systems or scripts.

systemd Integration

When managing dewy with systemd, proper service file configuration enables system-level signal transmission.

[Unit]
Description=Dewy Deployment Service
After=network.target

[Service]
Type=simple
User=dewy
WorkingDirectory=/opt/dewy
ExecStart=/usr/local/bin/dewy server --registry ghr://myorg/myapp --port 8080 -- /opt/myapp/current/myapp
ExecReload=/bin/kill -USR1 $MAINPID
KillSignal=SIGTERM
TimeoutStopSec=30
Restart=always

[Install]
WantedBy=multi-user.target

This configuration enables application restart via the systemctl reload dewy command.

Monitoring and Log Output

Log output during signal reception provides important information for system operation monitoring.

# Monitor dewy logs
journalctl -u dewy -f

# Display only signal reception logs
journalctl -u dewy | grep "received signal"

Logs record received signal types, process IDs, and execution results, which can be utilized for troubleshooting and operational monitoring.

Troubleshooting

This section covers common signal handling issues and their solutions.

When Signals Are Not Processed Correctly

If signals are not being handled as expected, first check that the process is running normally.

# Check dewy process status
ps aux | grep dewy

# Send signal to process
kill -USR1 <process_id>

# Check signal reception in logs
tail -f /var/log/dewy.log

Issues may arise if the process is in a zombie state or if there are insufficient permissions to send signals.

When Restart Fails

If restart via SIGUSR1 fails, there may be issues with the application's signal handling implementation. Check application logs to verify that SIGHUP signals are being processed correctly.

Also verify that server-starter library configuration is correct. Issues may stem from port binding problems or incorrect process startup path configuration.

Log Verification Methods

For detailed problem identification, debug-level output is effective.

# Start dewy with debug level
dewy server --log-level debug --registry ghr://myorg/myapp --port 8080 -- /opt/myapp/current/myapp

Debug-level logs record detailed signal reception information and inter-process communication status, making it easier to identify root causes of problems.