OTP Supervision Tree Optimization

active advanced

OTP Supervision Tree Optimization

Hypothesis

We hypothesize that restructuring the Prismatic Platform's supervision trees from a flat :one_for_one topology to a domain-partitioned hierarchical topology with mixed restart strategies will reduce cascading failure propagation by 80% and improve cold-start time by 45%, while maintaining the platform's zero-downtime guarantee under single-node failure conditions.

Background

The Prismatic Platform runs 90 umbrella applications under OTP supervision. The original architecture followed a straightforward pattern: each application defines a top-level Supervisor with :one_for_one strategy, starting children in dependency order. This simplicity served the platform well through the first 40 applications, but scaling beyond that introduced three systemic problems.

First, cascading failures. When a storage backend process crashes, it can trigger restart storms in dependent processes across multiple applications. With :one_for_one, each process restarts independently, potentially reconnecting to backends that are still recovering. This creates thundering herd effects that can take 30-60 seconds to stabilize.

Second, cold-start time. Sequential process startup across 90 applications takes 12.4 seconds on average. Many of these startups are independent and could execute in parallel, but the flat supervision hierarchy enforces serial ordering.

Third, resource isolation. A misbehaving process in one domain (such as an agent consuming excessive memory) can affect processes in unrelated domains because they share the same supervision boundary.

The prismatic_supervisor application was built to address these issues through compositional supervision with dependency-aware startup, domain partitioning, and pluggable registry backends (ETS for development, Horde for production clustering).

Methodology

We evaluated four supervision topologies against identical failure injection scenarios:

Topology A: Flat (Baseline) -- Current production topology. Each of 90 applications has a :one_for_one supervisor with children started sequentially.

Topology B: Domain-Partitioned -- Applications grouped into 8 domains (Storage, Intelligence, Security, Web, Agent, Evolution, Infrastructure, Analysis). Each domain gets a dedicated supervisor with domain-appropriate restart strategy.

Topology C: Hierarchical with Rest-for-One -- Similar to B, but critical dependencies within domains use :rest_for_one to ensure dependent processes restart when their dependencies crash.

Topology D: Full Compositional -- The prismatic_supervisor implementation with dependency graph resolution, parallel startup within independent subgraphs, and runtime health monitoring.

Failure scenarios injected:

  1. Single process crash -- Kill one worker process, measure recovery time
  2. Storage backend failure -- Simulate PostgreSQL connection loss for 10 seconds
  3. Domain cascade -- Kill all processes in the Intelligence domain simultaneously
  4. Memory pressure -- Force one process to allocate 500MB, triggering OS memory pressure
  5. Network partition -- Simulate 5-second network partition between application groups

Setup

The domain-partitioned supervisor configuration:

defmodule PrismaticSupervisor.DomainConfig do
  @domains %{
    storage: %{
      strategy: :rest_for_one,
      max_restarts: 5,
      max_seconds: 30,
      apps: [
        :prismatic_storage_core,
        :prismatic_storage_ets,
        :prismatic_storage_ecto,
        :prismatic_storage_meilisearch,
        :prismatic_storage_kuzu
      ]
    },
    intelligence: %{
      strategy: :one_for_one,
      max_restarts: 10,
      max_seconds: 60,
      apps: [
        :prismatic_agents,
        :prismatic_osint,
        :prismatic_visitor_intelligence,
        :prismatic_deduction
      ]
    },
    security: %{
      strategy: :one_for_all,
      max_restarts: 3,
      max_seconds: 10,
      apps: [
        :prismatic_perimeter,
        :prismatic_auth,
        :prismatic_compliance,
        :prismatic_dark
      ]
    }
  }

  @spec domain_for(atom()) :: atom()
  def domain_for(app) do
    Enum.find_value(@domains, :uncategorized, fn {domain, config} ->
      if app in config.apps, do: domain
    end)
  end

  @spec config_for(atom()) :: map()
  def config_for(domain), do: Map.get(@domains, domain, default_config())

  defp default_config do
    %{strategy: :one_for_one, max_restarts: 5, max_seconds: 30}
  end
end

The dependency resolver builds a startup DAG:

defmodule PrismaticSupervisor.DependencyResolver do
  @spec resolve(map()) :: {:ok, [[atom()]]} | {:error, :cycle_detected}
  def resolve(dependency_graph) do
    case topological_sort(dependency_graph) do
      {:ok, sorted} -> {:ok, group_independent(sorted, dependency_graph)}
      {:error, _} = error -> error
    end
  end

  defp group_independent(sorted, graph) do
    sorted
    |> Enum.reduce({[], MapSet.new()}, fn app, {groups, started} ->
      deps = Map.get(graph, app, [])

      if MapSet.subset?(MapSet.new(deps), started) do
        case groups do
          [current | rest] ->
            {[current ++ [app] | rest], MapSet.put(started, app)}
          [] ->
            {[[app]], MapSet.put(started, app)}
        end
      else
        {[[app] | groups], MapSet.put(started, app)}
      end
    end)
    |> elem(0)
    |> Enum.reverse()
  end
end

Results

Failure recovery times (seconds to full operational status):

Failure ScenarioTopology ATopology BTopology CTopology D
Single process crash0.80.30.30.2
Storage backend failure34.28.14.73.1
Domain cascade47.812.49.86.2
Memory pressure22.115.311.78.4
Network partition38.714.910.27.1

Cold-start times:

TopologyCold Start (s)Improvement
A (Flat)12.4Baseline
B (Domain)8.2-33.9%
C (Hierarchical)7.1-42.7%
D (Compositional)6.4-48.4%

Cascade propagation (number of processes affected by a single process crash):

TopologyAvg AffectedMax AffectedCascade Rate
A14.78316.3%
B4.2124.7%
C2.883.1%
D2.152.3%

Analysis

Topology D (Full Compositional) exceeded our hypothesis targets. Cascading failure propagation dropped by 85.7% (target: 80%), and cold-start time improved by 48.4% (target: 45%). The key insight is that dependency-aware parallel startup and domain-partitioned restart strategies complement each other more than expected.

The storage backend failure scenario revealed the most dramatic improvement. Topology A's 34.2-second recovery was caused by a thundering herd of 83 processes simultaneously attempting reconnection. Topology D's dependency graph ensures that storage-dependent processes wait for backend health confirmation before restarting, reducing the affected set to 5 processes and recovery to 3.1 seconds.

The :one_for_all strategy for the security domain proved essential. Security processes share authentication state, and partial restarts create windows where some processes have stale credentials. Restarting all security processes together eliminates this inconsistency window.

Memory pressure recovery in Topology D benefited from the health monitoring component, which detected the memory spike 200ms before the OS triggered OOM actions, allowing graceful process migration.

Conclusions

  1. Domain-partitioned supervision is mandatory for umbrella platforms exceeding 40 applications.
  2. Mixed restart strategies per domain provide better fault isolation than global :one_for_one.
  3. Dependency-aware parallel startup provides nearly 50% cold-start improvement with no correctness trade-offs.
  4. Health monitoring enables proactive failure management that reduces recovery time by an additional 25% over reactive-only supervision.
  5. The compositional approach scales -- adding new applications requires only domain classification, not topology redesign.

Next Steps

  • Evaluate Horde-based distributed supervision for multi-node deployments
  • Implement adaptive restart rate limiting based on failure frequency patterns
  • Add chaos engineering scenarios using the Color Team framework
  • Benchmark supervision tree performance under sustained 100K process counts
  • Integrate with SEADF for autonomous topology evolution

Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL