Skip to content

Repository files navigation

typhoon

CI

A protocol-agnostic load testing framework in Elixir.

Typhoon load tests anything you can call from Elixir — HTTP services, custom TCP protocols, proprietary binary wire formats, message brokers, databases. You supply the client that speaks your protocol; Typhoon drives the load around it — spawning concurrent virtual users, ramping and shaping load over time, replaying scenarios, and capturing per-step latency and throughput metrics.

Contents

Installation

Typhoon is not yet published to Hex, so add it as a Git dependency in your mix.exs:

def deps do
  [
    {:typhoon, git: "https://github.com/rahultumpala/typhoon.git"}
  ]
end

Pin to a specific tag, branch, or commit for reproducible builds:

def deps do
  [
    # a tag
    {:typhoon, git: "https://github.com/rahultumpala/typhoon.git", tag: "v0.1.0"},
    # or a branch
    {:typhoon, git: "https://github.com/rahultumpala/typhoon.git", branch: "main"},
    # or a commit
    {:typhoon, git: "https://github.com/rahultumpala/typhoon.git", ref: "a1b2c3d"}
  ]
end

Then fetch it:

mix deps.get

Features

A load test is one or more scenarios, each replayed concurrently by a pool of virtual users.

Implemented:

  • Virtual users with ramp up over a configurable duration
  • Test duration: users replay until a wall-clock deadline
  • Load profiles: multi-phase concurrency shaping (ramp up / hold / spike / ramp down, in any order and to any level)
  • Scenarios of ordered, isolated steps with delays between them
  • Per-user sessions, seeded once and threaded through every step
  • Previous step's output passed to the next step
  • before_scenario / after_scenario hooks with session access
  • Replay a single step N times
  • Replay a scenario N times or for a fixed duration (mutually exclusive)
  • Per-step and per-scenario metrics with latency percentiles, throughput (steps/sec), error rate, and an end-of-run summary
  • Datasets (fixed and circular) feeding one entry per replay
  • Scenarios composed across modules, with per-scenario config overrides
  • Aggregation of every replay's final step output for the teardown hook

Planned (see road-map.md):

  • Run-time config overrides (users/duration/ramp via CLI flags)
  • Supervision tree for crash-resilient runs
  • Metrics streaming (Telemetry events and Prometheus export) and thresholds

Usage

Each virtual user replays the whole scenario. How many times a user replays is controlled by two mutually exclusive config keys — set exactly one:

  • iterations — replay the scenario a fixed number of times, then stop (count based).
  • test_duration — keep replaying the scenario until the duration elapses (time based). A replay already in progress always runs to completion, so a run may exceed test_duration by at most one replay. When test_duration is set, iterations is ignored.

For more than a single ramp up, set a profile instead. Every config key is documented in the reference.

Count based (iterations)

defmodule CountLoadTest do
  use Typhoon

  typhoon_config do
    [
      users: 100,
      iterations: 50,
      ramp_up_duration: Duration.new!(minute: 2),
      ramp_down_duration: Duration.new!(minute: 2)
    ]
  end

  scenario "Protocol Handshake Test",
    before_scenario: &setup/1,
    after_scenario: &teardown/1 do
      step("Establish Connection", &test_establish_connection/1, 1)
      |> step("Login", &test_login/2, 1)
      |> delay(Duration.new!(second: 15))
      |> step("Send Packet", &test_send_packet/2, 1)
  end

  def setup(session), do: Map.put(session, :endpoint, "custom://localhost:8080/api")
  def teardown(session), do: session

  # 1-arity: ignores previous output, emits the connection as output.
  def test_establish_connection(session) do
    socket = connect(session.endpoint)
    {session, socket}
  end

  def test_login(session, socket) do
    token = login(socket)
    {Map.put(session, :token, token), socket}
  end

  # 2-arity: uses the previous step's output (the token here is also in session).
  def test_send_packet(session, socket) do
    resp = send_packet(socket)
    {Map.put(session, :resp, resp), socket}
  end

  typhoon_run do
    ["Protocol Handshake Test"]
  end
end

Every step returns a {session, output} tuple: the session threads through the whole scenario, and the output is handed to the next step. Runnable examples live in test/load_test.

Run it with the typhoon.run Mix task:

mix typhoon.run path/to/load_test.exs

Duration based (test_duration)

The scenario, hooks, steps and typhoon_run are the same as above; only the config differs — swap iterations for test_duration:

  typhoon_config do
    [
      users: 100,
      test_duration: Duration.new!(minute: 30),
      ramp_up_duration: Duration.new!(minute: 2),
      ramp_down_duration: Duration.new!(minute: 2)
    ]
  end

Contributing

mix test                        # the suite
mix compile --warnings-as-errors
mix format

CI runs all three on every push and pull request.

Enable the pre-commit hook once per clone — it runs mix format --check-formatted on staged .ex/.exs files, so unformatted code never reaches CI:

git config core.hooksPath .githooks

Git does not clone hook config, which is why this is a manual step. Skip a run with git commit --no-verify.

About

A Load testing framework in Elixir

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages