LiveLoad (LiveLoad v0.0.1-rc.55)

Copy Markdown View Source

A load testing framework for simulating real, distributed, live load on your application.

DISCLAIMER

The LiveLoad repo is currently private as I am building out the functionality and validating that it works as expected.

If you come across this package and are installing it for some reason - just know that it's not public yet because I haven't actually tested it at all. There might not even be any code in here yet - I literally just started the project and set up my CI/CD pipelines (it's a default thing I do when I start a project, I'm weird like that). I will make it public (and update this description to remove this disclaimer) as I build and test it out. Or - if it doesn't work out, I'll simply delete it.

Installation

LiveLoad is available on Hex.

To install, add it to you dependencies in your project's mix.exs.

def deps do
  [
    {:live_load, ">= 0.0.1"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/live_load.

Summary

Types

Defines the LiveLoad.Browser.Connection implementation to use for this run.

An error returned by a scenario when the cluster creation process fails to connect within the 30 second timeout for connecting to all of the nodes in the specified cluster.

Options passed in to the LiveLoad.Cluster initialization.

Configures the run to be distributed.

An error returned by a scenario when the cluster creation process fails to connect to the nodes specified in the cluster.

Defines the FLAME.Backend module to use when running a distributed load test.

Initialization options for running a LiveLoad.Scenario.

Defines the OTP application to load test.

Defines the duration of the entire load test for a specific scenario.

Defines the timeout for a single iteration of a scenario.

Run a single scenario module.

The result of a LiveLoad.Scenario run returned by LiveLoad.run/1.

Run a list of scenario modules.

Configures the number of user processes to use for the run.

Functions

Runs all discovered LiveLoad.Scenario modules and returns a map of results for each scenario run.

Types

browser_connection_adapter_opt()

@type browser_connection_adapter_opt() ::
  {:browser_connection_adapter, LiveLoad.Browser.Connection.t()}

Defines the LiveLoad.Browser.Connection implementation to use for this run.

Defaults to LiveLoad.Browser.Connection.Playwright.

browser_connection_opts_opt()

@type browser_connection_opts_opt() ::
  {:browser_connection_opts, LiveLoad.Browser.Connection.opts()}

Options passed to the given browser_connection_adapter_opt/0 on initialization of the LiveLoad.Browser.Connection.

Defaults to an empty list.

cluster_connection_timeout_error()

@type cluster_connection_timeout_error() ::
  {:error, {:waiting_for_cluster, %{optional(atom()) => any()}}}

An error returned by a scenario when the cluster creation process fails to connect within the 30 second timeout for connecting to all of the nodes in the specified cluster.

The error contains the current status of the cluster with details of what nodes are still waiting to connect, what nodes failed, and what nodes succeeded.

cluster_opts_opt()

@type cluster_opts_opt() :: {:cluster_opts, [LiveLoad.Cluster.option()]}

Options passed in to the LiveLoad.Cluster initialization.

This is a list of LiveLoad.Cluster.option/0 that is passed directly into the initialization.

See LiveLoad.Cluster for all available options.

distributed_run_opt()

@type distributed_run_opt() :: {:distributed?, boolean()}

Configures the run to be distributed.

When set to true, LiveLoad will use FLAME to build an ad-hoc pool of nodes based on the given FLAME.Pool configuration and evenly distribute the users across these nodes during the run.

Defaults to false.

failed_to_connect_cluster_error()

@type failed_to_connect_cluster_error() ::
  {:error, {:failed_to_connect, failed :: [term()]}}

An error returned by a scenario when the cluster creation process fails to connect to the nodes specified in the cluster.

The error contains the list of nodes that failed to connect.

flame_backend_opt()

@type flame_backend_opt() :: {:flame_backend, LiveLoad.Cluster.flame_backend()}

Defines the FLAME.Backend module to use when running a distributed load test.

The LiveLoad.Cluster can be additionally configured by passing the cluster_opts_opt/0 option to LiveLoad.run/1. See LiveLoad.Cluster for more details.

This option is required when running a distributed load test and setting the distributed_run_opt/0 option to true.

option()

Initialization options for running a LiveLoad.Scenario.

These are split between options for the overall run configuration (distributed_run_opt/0, users_count_opt/0, flame_backend_opt/0, cluster_opts_opt/0), options for the runner itself (browser_connection_adapter_opt/0, scenario_iteration_timeout_opt/0, scenario_duration_opt/0) and any other options that should be passed in as configuration to the scenario LiveLoad.Scenario.config/1 callback.

otp_app_opt()

@type otp_app_opt() :: {:otp_app, atom()}

Defines the OTP application to load test.

This option is used in order to automatically discover LiveLoad.Scenario modules implemented in the given application. Similarly to Ecto Migrations, LiveLoad will scan the given OTP application, find all LiveLoad.Scenario modules, and then run these scenarios for a load test.

This option is required unless a scenario_opt/0 or a scenarios_opt/0 is given, in which case only the given scenario modules will be run.

This option takes the lowest priority.

scenario_duration_opt()

@type scenario_duration_opt() :: {:scenario_duration, timeout()}

Defines the duration of the entire load test for a specific scenario.

When running a load test, the scenario's LiveLoad.Scenario.run/3 callback will be run in a loop multiple times until this value is reached. Once reached, the runner will transition to a terminating state and wait for the latest iteration of the scenario to complete, and then report its completion.

Defaults to 10 minutes.

Note: while the type here is set to timeout/0, the :infinity value is invalid and an error will be returned if it is passed.

scenario_iteration_timeout_opt()

@type scenario_iteration_timeout_opt() :: {:iteration_timeout, timeout()}

Defines the timeout for a single iteration of a scenario.

If this timeout is reached and the scenario has not completed, it will be killed and the user's status will reported as a failure. No other iterations will take place for that user.

Defaults to 2 minutes.

Note: while the type here is set to timeout/0, the :infinity value is invalid and an error will be returned if it is passed.

scenario_opt()

@type scenario_opt() :: {:scenario, LiveLoad.Scenario.t()}

Run a single scenario module.

This option is mutually exclusive with scenarios_opt/0 and otp_app_opt/0, each of which configure which scenarios should be run.

This option takes the highest priority.

scenario_result()

The result of a LiveLoad.Scenario run returned by LiveLoad.run/1.

This may either be a LiveLoad.Result or an error. If the given distributed_run_opt/0 is set to true, the error may include one of the possible t:Cluster.cluster_initialization_error/0 errors.

scenarios_opt()

@type scenarios_opt() :: {:scenarios, [LiveLoad.Scenario.t()]}

Run a list of scenario modules.

This option is mutually exclusive with scenario_opt/0 and otp_app_opt/0, each of which configure which scenarios should be run.

This option takes the second highest priority.

users_count_opt()

@type users_count_opt() :: {:users, pos_integer()}

Configures the number of user processes to use for the run.

Defaults to a single user.

Functions

run(opts \\ [])

@spec run(opts :: [option()]) :: %{
  required(LiveLoad.Scenario.t()) => scenario_result()
}

Runs all discovered LiveLoad.Scenario modules and returns a map of results for each scenario run.

run/1 is the main entrypoint for LiveLoad. It accepts a list of option/0 values to configure the load test, discovers which scenarios to run, runs each one to completion, and returns a map of LiveLoad.Scenario.t/0 keys to scenario_result/0 values.

Scenarios are run:

run/1 is synchronous and will block until all discovered scenarios have finished.

Errors encountered during a scenario are captured in the result map against the scenario that produced them and do not prevent other scenarios from running.

Scenario Discovery

Which scenarios are run is determined by the options given. The following options are mutually exclusive, and take priority in the order listed:

  1. scenario_opt/0 — a single LiveLoad.Scenario module.
  2. scenarios_opt/0 — a list of LiveLoad.Scenario modules.
  3. otp_app_opt/0 — an OTP application atom. LiveLoad will scan the given application for all modules implementing the LiveLoad.Scenario behaviour and run each of them.

Scenario Configuration

Any additional options passed to run/1 that are not consumed as part of the run configuration (such as distributed_run_opt/0, users_count_opt/0, flame_backend_opt/0, cluster_opts_opt/0) or runner options (such as browser_connection_adapter_opt/0, scenario_iteration_timeout_opt/0, and scenario_duration_opt/0) are forwarded to each scenario's LiveLoad.Scenario.config/1 callback as the opts argument. This allows you to pass arbitrary, scenario-specific configuration to each LiveLoad.Scenario run during the load test.

Examples

Run all scenarios discovered in :my_app with 50 concurrent users for 5 minutes:

LiveLoad.run(
  otp_app: :my_app,
  users: 50,
  scenario_duration: to_timeout(minute: 5)
)

Run a specific scenario with 25 concurrent users for 2 minutes:

LiveLoad.run(
  scenario: MyApp.LoadTest.CheckoutScenario,
  users: 25,
  scenario_duration: to_timeout(minute: 2)
)

Run a list of specific scenarios with 100 concurrent users for 15 minutes:

LiveLoad.run(
  scenarios: [MyApp.LoadTest.CheckoutScenario, MyApp.LoadTest.DeliveryStatusScenario],
  users: 100,
  scenario_duration: to_timeout(minute: 15)
)

Pass custom configuration to allow configuring a scenario's options via the LiveLoad.Scenario.config/1 callback:

LiveLoad.run(
  scenario: MyApp.LoadTest.CheckoutScenario,
  users: 10,
  base_url: "https://staging.myapp.com",
)

Run a distributed load test across a FLAME-provisioned cluster using the FLAME.FlyBackend using Fly machines with 8 CPUs and 16 GB of RAM, with a maximum of 100 nodes allowed:

LiveLoad.run(
  otp_app: :my_app,
  users: 10_000,
  distributed?: true,
  flame_backend: FLAME.FlyBackend,
  cluster_opts: [
    flame_backend_opts: [app: :live_load, cpus: 8, memory_mb: 16 * 1024],
    max_allowed_nodes: 100
  ]
)

Consuming Results

run/1 returns a map of LiveLoad.Scenario.t/0 keys to scenario_result/0 values. If the LiveLoad.Scenario completed successfully, the result with be a LiveLoad.Result value. LiveLoad.Result is a JSON serializable struct that contains all information necessary for a deep analysis of what occurred during the load test, including histograms, timelines, and stats broken down by various dimensions. The consumer of the result can write this data anywhere, and run independent analysis on it without requiring knowledge of LiveLoad.

An example of writing the data to a file to be analyzed later would look something like the following:

results = LiveLoad.run(
  otp_app: :my_app,
  users: 10_000,
  distributed?: true,
  flame_backend: FLAME.FlyBackend,
  cluster_opts: [
    flame_backend_opts: [app: :live_load, cpus: 8, memory_mb: 16 * 1024],
    max_allowed_nodes: 100
  ]
)

results
|> Enum.map(fn
  # The scenario name is encapsulated within the result, so we don't need it on success
  {_scenario, %LiveLoad.Result{} = result} -> result
  # Format the errors as maps for JSON serialization
  {scenario, {:error, reason}} -> %{scenario: inspect(scenario), error: inspect(reason)}
end)
|> then(&File.write!("./liveload_results.json", JSON.encode_to_iodata!(&1)))

For more information about what data is contained in the result, see the LiveLoad.Result module.