lightship

repository·main·Indexed 19 days ago

https://github.com/gajus/lightship

A Node.js utility for managing Kubernetes lifecycle probes (readiness, liveness, and startup) and graceful shutdowns. It provides HTTP endpoints (/health, /live, /ready) to support container probes, allows registration of sequential shutdown handlers, and includes mechanisms like queueBlockingTask and Beacons to manage service readiness and asynchronous teardown tasks.

Tokens
5.2K
Snippets
16
Records
25
Agent score
65%

What's inside lightship

  1. Use Beacons to delay shutdown handlers

    main

    Beacons are used to prevent the shutdown routine from executing until specific asynchronous tasks are complete.

    How Beacons work:

    • A beacon is created via lightship.createBeacon().
    • A beacon is considered 'live' upon creation.
    • Shutdown handlers are suspended as long as there is at least one live beacon.
    • To signal a task is finished, call beacon.die(). Once all beacons are dead, the shutdown handlers run.
    • Beacons can accept an optional context object for better logging.

    Use Case: Use beacons when processing jobs in a loop to ensure the process doesn't terminate while a job is in progress.

    for (const job of jobs) {
      if (lightship.isServerShuttingDown()) {
        break;
      }
    
      const beacon = lightship.createBeacon({ jobId: job.id });
    
      // ... perform the job ...
    
      await beacon.die();
    }
  2. Use Lightship HTTP endpoints for Kubernetes probes

    main

    Lightship provides specific HTTP endpoints to support Kubernetes container probes:

    • /health: For human inspection. Returns 200 (SERVER_IS_READY) when accepting connections, or 500 (SERVER_IS_NOT_READY or SERVER_IS_SHUTTING_DOWN) otherwise.
    • /live: Used for liveness probes. Returns 200 (SERVER_IS_NOT_SHUTTING_DOWN) or 500 (SERVER_IS_SHUTTING_DOWN).
    • /ready: Used for readiness probes. Returns 200 (SERVER_IS_READY) or 500 (SERVER_IS_NOT_READY).

    Note on Startup Probes: Per Kubernetes documentation, the startupProbe should point to the same endpoint as the liveness probe (/live).

  3. Integrate Lightship with Express.js

    main

    To use Lightship with an Express.js application, create a Lightship instance and use registerShutdownHandler to manage the server lifecycle.

    Important Lifecycle Notes:

    • The default state of Lightship is SERVER_IS_NOT_READY. You must explicitly call lightship.signalReady() once your server is capable of accepting connections.
    • Use registerShutdownHandler to define how your server (e.g., server.close()) should shut down.
    • Do not call process.exit() inside a shutdown handler; Lightship automatically calls process.exit() after all registered handlers have finished.
    import express from 'express';
    import { createLightship } from 'lightship';
    
    const app = express();
    const server = app.listen(8080, () => {
      // Signal that the server is now ready to accept connections
      lightship.signalReady();
    });
    
    const lightship = await createLightship();
    
    lightship.registerShutdownHandler(() => {
      server.close();
    });
  4. Configure Kubernetes probes for Lightship

    main

    When using Lightship with Kubernetes, ensure your livenessProbe and readinessProbe are configured to allow sufficient time for graceful shutdown.

    Best Practices:

    • Timeout Calculation: Ensure your timeout is sufficient. A safe rule of thumb is periodSeconds * failureThreshold (e.g., 90 seconds). Setting a very low timeout (like 1s) can cause false-positive failures during heavy load.
    • Graceful Shutdown: Do not stop accepting connections immediately upon receiving a SIGTERM. Wait a few seconds before closing the server to allow iptables/load balancer updates to propagate, preventing 'connection refused' errors for clients.
  5. Configure Kubernetes container probes

    main

    When using Lightship, use the following pattern for your Kubernetes probe configuration. Ensure timeoutSeconds is not set too low to avoid false-positives.

    readinessProbe:
      httpGet:
        path: /ready
        port: 9000
      failureThreshold: 1
      initialDelaySeconds: 5
      periodSeconds: 5
      successThreshold: 1
      timeoutSeconds: 5
    livenessProbe:
      httpGet:
        path: /live
        port: 9000
      failureThreshold: 3
      initialDelaySeconds: 10
      # Allow sufficient amount of time (90 seconds = periodSeconds * failureThreshold)
      # for the registered shutdown handlers to run to completion.
      periodSeconds: 30
      successThreshold: 1
      # Setting a very low timeout value (e.g. 1 second) can cause false-positive
      # checks and service interruption.
      timeoutSeconds: 5
    
    startupProbe:
      httpGet:
        path: /live
        port: 9000
      failureThreshold: 3
      initialDelaySeconds: 10
      periodSeconds: 30
      successThreshold: 1
      timeoutSeconds: 5
  6. Install and initialize Lightship

    main

    Use createLightship to initialize a new instance. Lightship abstracts readiness, liveness, and startup checks, as well as graceful shutdown for Node.js services, specifically optimized for Kubernetes environments.

    By default, Lightship detects if it is running in Kubernetes. If it is not (e.g., local development), it enters Local-mode:

    • It starts the HTTP service on an available port to avoid collisions.
    • shutdownDelay defaults to 0 for immediate shutdown.
    • You can force local-mode by setting { detectKubernetes: false } in the configuration.
    import {
      createLightship
    } from 'lightship';
    
    const configuration = {};
    
    const lightship = await createLightship(configuration);
  7. Understand Lightship server states

    main

    Lightship manages the application through several distinct states:

    • SERVER_IS_READY: The application is fully operational and ready to receive traffic.
    • SERVER_IS_NOT_READY: The application is not ready (e.g., during startup or while a blocking task is queued).
    • SERVER_IS_SHUTTING_DOWN: The shutdown sequence has been initiated.
    • SERVER_IS_NOT_SHUTTING_DOWN: The default state before shutdown begins.
  8. Configure Lightship via ConfigurationInput

    main

    When calling createLightship(configuration), you can provide the following options:

    OptionTypeDefaultDescription
    detectKubernetesbooleantrueWhether to auto-detect Kubernetes environment.
    gracefulShutdownTimeoutnumber30000Milliseconds to wait for the process to exit gracefully after shutdown() is called before force-terminating.
    portnumber9000The port for the Lightship HTTP service. Can be overridden via LIGHTSHIP_PORT env var.
    shutdownDelaynumber5000Delays the shutdown handler by X ms. Should match readinessProbe.periodSeconds.
    shutdownHandlerTimeoutnumber5000Milliseconds to wait for registered shutdown handlers to complete before force-terminating.
    signalsstring[]['SIGTERM']Array of signal events that trigger shutdown.
    terminate() => void() => { process.exit(1) }Method used to terminate the Node.js process.
  9. Troubleshoot event-loop blocking and probe failures

    main

    If your /live or /ready endpoints fail intermittently with Client.Timeout exceeded, it is likely due to event-loop blocking tasks.

    Solutions:

    1. Use worker_threads to move heavy computations off the main event loop.
    2. Refactor synchronous code into smaller, asynchronous chunks.

    If the process fails to exit after shutdown handlers run, use utilities like wtfnode or why-is-node-running inside a shutdown handler to identify active handles (like setTimeout) keeping the loop alive.

    lightship.registerShutdownHandler(() => {
      server.close();
      whyIsNodeRunning(); // Prints active handles to help debug why the process won't exit
    });
  10. Wait for service readiness with whenFirstReady()

    main

    Use whenFirstReady() to obtain a promise that resolves the first time the service transitions from SERVER_IS_NOT_READY to SERVER_IS_READY. This is useful for delaying tasks that depend on the server being fully operational (like integration tests).

    import express from 'express';
    import { createLightship } from 'lightship';
    
    const lightship = await createLightship();
    
    const app = express();
    
    app.get('/', (req, res) => {
      res.send('Hello, World!');
    });
    
    const server = app.listen(8080, () => {
      lightship.signalReady();
    });
    
    (async () => {
      // Resolves only once when the service becomes ready
      await lightship.whenFirstReady();
    
      await runIntegrationTests();
    })();
  11. Trigger application shutdown with shutdown()

    main

    Call lightship.shutdown() to transition the server state to SERVER_IS_SHUTTING_DOWN. This initiates the shutdown sequence, triggering all registered shutdown handlers. This is useful for graceful termination when a specific condition is met (e.g., a maximum number of requests reached).

    if (total === 1000) {
      lightship.shutdown();
    }
  12. Manage server readiness with signalReady and signalNotReady

    main

    You can dynamically change the server's readiness state to control traffic flow (e.g., during high load or maintenance).

    • lightship.signalReady(): Sets the state to SERVER_IS_READY, allowing the readiness probe to pass.
    • lightship.signalNotReady(): Sets the state to SERVER_IS_NOT_READY, causing the readiness probe to fail and signaling to orchestrators like Kubernetes to stop sending traffic to this instance.
    // Example: Throttling readiness based on a running total
    if (runningTotal < 100) {
      lightship.signalReady();
    } else {
      lightship.signalNotReady();
    }