Semian

repository·main·Indexed 23 days ago

https://github.com/shopify/semian

A Ruby library designed to prevent cascading failures in distributed systems by controlling access to slow or unresponsive external services. Semian implements circuit breakers and bulkheading to force applications to 'fail fast', preventing slow dependencies from exhausting application resources, causing response time spikes, or leading to system-wide capacity loss. It provides adapters for MySQL2, Redis, Net::HTTP, and Trilogy, and includes an ExperimentalResource adapter for simulating failure scenarios.

Tokens
11.5K
Snippets
22
Records
59
Agent score
80%

What's inside semian

  1. What is Semian and when should you use it?

    main

    Semian is a library that uses heuristics to implement 'failing fast'. It is designed to prevent slow or unresponsive resources from degrading the overall capacity and response time of an application.

    When to use Semian

    Semian is most beneficial for applications aiming to eliminate Single Points of Failure (SPOFs) that are experiencing issues with slow resources. It is particularly useful when a resource becomes unresponsive or consistently slow, causing requests to hit full timeouts.

    Key Problems Solved

    1. Response Time Spikes: Without Semian, if a resource (like a session store) is down or slow, every request attempting to access it must wait for the full timeout duration. Semian uses heuristics to detect these failures and fail the requests instantly, preventing the average response time from spiking.
    2. Capacity Loss: When workers (e.g., single-threaded web workers) wait for a slow resource to time out, they cannot serve other requests. This leads to a significant degradation in the cluster's total capacity. By failing fast, Semian frees up workers to handle other requests immediately.

    Important Considerations

    • Complexity: Semian introduces complexity and should be introduced with care. It works by raising exceptions based on heuristics; you must understand its behavior before using it in production.
    • Not a Magic Wand: It does not solve all latency problems; it specifically addresses the impact of slow/unresponsive resources.
    • Threaded vs. Evented: If your application is multithreaded or evented (unlike Resque or Unicorn), these problems may be less pressing, though Semian can still provide value.
  2. What is Semian and how does it prevent cascading failures?

    main

    Semian is a library designed to control access to slow or unresponsive external services. It prevents cascading failures—where slow resources occupy all available workers/threads and cause a system-wide loss of capacity—by forcing the application to 'fail fast'.

    Semian implements two primary patterns to achieve this:

    • Circuit breaker: Limits the number of requests sent to a dependency that is experiencing issues.
    • Bulkheading: Controls concurrent access to a single resource, coordinating access server-wide using SysV semaphores.

    When Semian determines a resource is unavailable or over capacity, it raises an exception immediately instead of waiting for a network timeout. This allows your application to rescue the exception and execute a graceful fallback (e.g., returning cached data or a default value) rather than blocking a thread.

  3. How Semian prevents capacity loss and response time spikes

    main

    Semian addresses the limitations of standard timeouts. While timeouts prevent a request from waiting forever, they do not prevent the 'slow failure' problem where every request incurs the cost of the timeout duration.

    In a typical scenario where a resource like Redis is slow:

    • Without Semian: Every request to the resource waits for the full timeout (e.g., 200ms). This increases the average response time for all pages and consumes worker capacity, as workers are stuck waiting for timeouts rather than serving fast requests.
    • With Semian: After a small amount of convergence time, Semian's heuristics identify the resource as failing and cause subsequent requests to fail instantly. This preserves the application's capacity and keeps response times low even during resource downtime.

    Example of a pattern Semian protects:

    # This code is resilient to failure but vulnerable to latency
    def index
      @user = fetch_user
      @posts = Post.all
    end
    
    private
    def fetch_user
      user = User.find(session[:user_id])
    rescue Redis::CannotConnectError
      nil
    end

    In the example above, if Redis is unavailable, the code handles the error. However, if Redis is merely slow, the fetch_user method will hang for the duration of the timeout on every single request, causing the issues Semian is designed to solve.

  4. How bulkheads and circuit breakers work together

    main

    Semian provides a multi-layered defense line for resource access:

    1. Circuit Breaker: The first check. If the circuit is open, Semian raises an exception immediately to trigger a fallback (defaulting to a 500 response). This prevents calls from even attempting to reach the resource when it is known to be failing.
    2. Bulkhead: If the circuit is closed, Semian checks the bulkhead. If too many workers are already querying the resource, it fails instantly. This limits the number of concurrent connections to a resource, preventing a single slow resource from consuming all application workers.
    3. Driver/Data Store: If both checks pass, the driver attempts to query the data store. If the data store is slow or fails, the driver raises an exception (e.g., after a timeout), which in turn affects the circuit breaker and bulkhead state for future calls.

    Key distinction:

    • Circuit Breakers allow for regaining 100% capacity once the resource recovers.
    • Bulkheads guarantee a minimum capacity by limiting concurrency, ensuring that even during an incident, a portion of your workers remain available for other tasks.
  5. How Semian works: Circuit Breaker and Bulkheading

    main

    Semian provides resiliency through two primary patterns:

    1. Circuit Breaker: Prevents a worker from repeatedly hitting a failing or slow service. When a threshold of errors is met, the circuit 'opens,' and subsequent calls fail instantly with an exception instead of waiting for a timeout. This protects the caller and gives the dependency time to recover.
    2. Bulkheading: Limits the number of concurrent requests to a specific resource using a 'ticket' system (implemented via SysV semaphores). This prevents a single slow dependency from consuming all available workers on a server. If no 'tickets' are available, workers wait for a specified timeout before failing.

    Note: The state of the circuit breaker is local to the worker and is not shared across all workers on a server. Bulkheading, however, uses semaphores to provide server-wide access control.

  6. Use the ExperimentalResource adapter for failure simulation

    main

    The ExperimentalResource class is an experimental adapter designed to simulate a distributed service with multiple endpoints. It is used to test how your application handles various failure scenarios and performance characteristics by simulating latencies and errors.

    Key Simulation Capabilities

    • Multiple Endpoints: Configure multiple endpoints, each with its own latency profile.
    • Statistical Latencies: Latencies can be assigned using statistical distributions (currently supports Log-normal distribution).
    • Latency Bounds: You can set minimum and maximum latency constraints.
    • Request Timeouts: You can configure a maximum timeout. If a request would exceed this timeout, the resource sleeps for the timeout duration and then raises an exception, simulating real-world timeout behavior.
    • Baseline Error Rate: You can configure a probability of request failure. Failed requests throw RequestError exceptions after partial processing.
    • Service-Wide Degradation: You can simulate service degradation across all endpoints, including:
      • Latency degradation: Adding fixed latency to all requests.
      • Error rate changes: Modifying the error rate for the entire service.
      • Gradual ramp-up: Both latency and error rate degradations support gradual transitions over time.
  7. How Semian adapters work

    main

    Semian works by intercepting resource access through monkey-patching the resource driver. When access is requested, Semian checks the circuit breaker and bulkheads. If the resource is unavailable, Semian raises an exception.

    Crucially, the exception raised by the driver always inherits from the driver's Base exception class. This allows you to rescue the driver's base class to catch both Semian-specific errors and original driver errors in a single block for graceful fallbacks.

  8. Use Quotas instead of Tickets for Bulkheads

    main

    Instead of manually calculating the number of tickets, you can use a quota. A quota is a proportion (0.0 to 1.0) of the active workers allowed to connect to the resource. This is ideal for environments with non-uniform worker distribution.

    Rules:

    • You must pass exactly one of tickets or quota.
    • Tickets available will be the ceiling of the quota ratio to the number of workers.
    • If using a forking web server (like Unicorn), call Semian.unregister_all_resources before/after forking.

    Note on Thread Safety: In threaded environments (Puma, Sidekiq), you should disable bulkheads (bulkhead: false) because Semian's internal use of SEM_UNDO is tied to the process, not the thread, which can lead to ticket starvation or deadlocks if threads are killed.

    client = Redis.new(semian: {
      name: "inventory",
      quota: 0.51, # Allow 51% of workers to connect
      success_threshold: 2,
      error_threshold: 4,
      error_timeout: 20
    })
  9. Disable Semian via environment variables

    main

    You can globally disable Semian or specific components using environment variables:

    • Disable all Semian functionality: SEMIAN_DISABLED=1
    • Disable only the Circuit Breaker: SEMIAN_CIRCUIT_BREAKER_DISABLED=1
    • Disable only the Bulkhead: SEMIAN_BULKHEAD_DISABLED=1
  10. Build Semian native extension with debug information

    main

    If you need to debug the native extension, follow these steps to clean the existing build, enable debug mode, and rebuild:

    1. Clean existing builds: bundle exec rake clean --trace
    2. Set the debug environment variable: export DEBUG=1
    3. Build the extension: bundle exec rake build
    4. Install the gems: bundle install
    $ bundle exec rake clean --trace
    $ export DEBUG=1
    $ bundle exec rake build
    $ bundle install
  11. Run Semian tests locally

    main

    You can run the test suite locally using Rake.

    Standard execution:

    $ bundle exec rake

    To skip flaky tests (useful for local development, though CI runs all tests):

    $ bundle exec rake SKIP_FLAKY_TESTS=true
  12. Debug Bulkhead semaphores on Linux

    main

    If you need to inspect the actual IPC (Inter-Process Communication) resources used by Semian's bulkheads, you can find the semaphore key via Ruby and then use the ipcs command.

    1. Find the key in Ruby:
    require 'semian'
    puts Semian::Resource.new(:your_resource_name, tickets: 42).key
    1. Use the key with ipcs on the host:
    ipcs -si $(ipcs -s | grep <YOUR_KEY> | awk '{print $2}')
    require 'semian'
    puts Semian::Resource.new(:your_resource_name, tickets: 42).key # do this from a dev machine
    # Output example: "0x48af51ea"