Skip to content

kai-niemi/battery

Repository files navigation

Java CI

About

Battery is a simple and flexible SQL database load testing tool. A load test is composed by a set of scripts using either a custom DSL or plain SQL, or a combination thereof to simulate some actual production database workload.

A load test runs through a scenario, which is a sequence of phases that ramp up threads to simulate virtual users running through some SQL workflow. A scenario can be much anything, such as reading a set of rows, performing an aggregation query or updating and inserting rows.

Battery instruments and collects execution times, error rates and other metrics visualized in its web UI and exposed via a prometheus endpoint. It also provides a REST API for automation and for coordinating distributed load test, where multiple instances on different hosts can generate load in the aggregate towards a target database.

Dashboard page in the web UI, for illustration:

demo1.png

Workload statistics:

demo2.png

Terms of Use

This tool is not supported by Cockroach Labs. Use of this tool is entirely at your own risk and Cockroach Labs makes no guarantees or warranties about its operation.

See MIT for terms and conditions.

Usage

Battery is configured using a YAML file that defines the test structure, along with SQL scripts and/or battery scripts to run different preparation steps and the actual load generating steps.

YAML Files

All load test configuration properties are specified in a local config/XX/application-XX.yml file where XX is some arbitrary profile name. It overrides the baseline configuration in application.yml.

See the config/ directory to get an idea of the layout of load test configurations.

SQL Scripts

A SQL file is simply a multi-statement SQL file that can run transactional or implicitly with auto commits (per statement). Parameter placeholders are also supported, see more below for details.

Battery Scripts

Battery script is a custom-made DSL that supports variables, expressions, flow control structures and asynchronous fork/join statements, along with a function library for executing arbitrary SQL statements and generating random data.

Typically, you'd use it to craft workload dynamics that mimics some actual application behavior, which is hard to do with just plain SQL file templates. Rather than resorting to typical programming language idioms to create such dynamics, this scripting language offers a focused and user-friendly middle ground.

See the battery script tutorial for a language introduction.

Basic example to insert 10 rows:

// Loop 10 times and insert one row per cycle
for y from 1 to 10 {
   jdbc.update("insert into account (id,city,balance) values(unordered_unique_rowid(),?,?)",
                [randomWord(10 + y), randomDouble(10.00,100.00)]);
}

Another example a of a decomposed UPSERT with execution branch logic (i.e. different paths depending on some pivot):

ids = jdbc.queryForList("select id from account limit 100");
// Pick random id and pass as parameter
id = random(ids).get("id");
a = jdbc.queryForMap("select * from account where id=?",[id]);
// Either insert or update 
if (a.isEmpty()) {
    jdbc.update("insert into account (id,city,balance) values(unordered_unique_rowid(),?,?)",
                [randomWord(12),randomDouble(10.00,100.00)]);
} else {
    jdbc.update("update account set balance=balance+?::decimal where id=?",
                [randomDouble(1.00,100.00), a.get("id") ]);
}

Another more complex example with parallel fork and join operations:

f1 = fork {
    id = jdbc.update("insert into account (id,name,balance) values(unordered_unique_id(),?,?) returning id",
            [randomWord(12), randomDouble(10.00, 100.00)] );
    return id;
};

f2 = fork {
    id = jdbc.update("insert into account (id,name,balance) values(unordered_unique_id(),?,?) returning id",
            [randomWord(12), randomDouble(10.00, 100.00)] );
    return id;
};

f3 = fork {
    id = jdbc.update("insert into account (id,name,balance) values(unordered_unique_id(),?,?) returning id",
            [randomWord(12), randomDouble(10.00, 100.00)] );
    return id;
};

// Wait for f1-f3 to run to completion
join [f1, f2, f3];

ids = [f1.get(), f2.get(), f3.get()];

jdbc.update("insert into audit_log (a,b,c) values ('inserted','account',?)", [ids]);

Configuration

The load test configuration YAML file must include a battery prefix section that describe the structure of the load test. This includes any before steps, after steps, ramping phases and scenarios with steps.

battery:
  baseDir: config/XX/
  before:
    ..
  after:
    ..
  phases:
    ..
  scenarios:
    ..

In addition, you can override any baseline setting in application.yml. For example the datasource and concurrency settings:

# override datasource and concurrency settings
spring:
  datasource:
    url: jdbc:postgresql://bigone.aws-eu-north-1.cockroachlabs.cloud:26257/battery?sslmode=require
    username: craig
    password: ..
concurrency:
  concurrencyLimit: -1
  corePoolSize: 2
  queueCapacity: 32
  threadType: virtual

battery:
  ...

Before Steps

Before steps are optional preparation steps executed at one time to allow for things like schema creation and initial test data loading. A before step either executes a SQL script or a battery script, which in turn can execute a sequence of SQL statements.

Script variables can be passed along to scenario scripts by using the capture flag.

    battery:
      phases: ..
      before:
        steps:
          - script: create-schema.sql
          - script: insert-users.sql
          - script: load-users.b
            capture: true

After Steps

After steps are optional cleanup steps executed at most one time after a load test scenario has been ramped up. Notice that after steps are invoked potentially before all workloads have completed unless awaitCompletion is true.

    battery:
      phases: ..
      before: ..
      scenarios: ..
      after:
        awaitCompletion: true
        steps:
          - name: Print Report
            script: report.sql

Ramping Phases

A load test scenario is composed of workloads (next section) that run concurrently to simulate virtual users.

The phases section defines at which rate these virtual user are created or ramped up. It follows a linear progression curve to simulate phases, such as a warmup phase, a sustained steady state phase and a final stress phase.

The example below defines 3 phases: warmup, steady and sustained. The warmup phase runs for a total of 60 seconds with an arrival rate of 2 up to a limit of 30 virtual users. This translates to approximately 2 threads created at 2 second intervals.

The steady state phase ramps for a total of 90s, starting 10 threads approximately every 9:th second (interval formula is: duration / (rampTo / rampRate)).

The last sustained state doesn't start any virtual users but simply pauses for 60s to simulate a sustained steady state until the test ends.

    battery:
      phases:
        - name: warmup
          duration: 60s
          rampRate: 2
          rampTo: 30
        - name: steady
          duration: 90s
          rampRate: 10
          rampTo: 100
        - name: sustained
          duration: 60s

Scenarios

Scenarios define the set of SQL operations (encapsulated by SQL or battery scripts) that should be executed concurrently for a specified duration. A scenario has one or more steps that are executed in sequence but also concurrently in scope of a virtual user.

The concurrency level is determined by the phases. The steps to execute are defined by scenarios and only one scenario can run at any given time. A scenario step either executes a SQL script or a battery script which in turn can execute a sequence of SQL statements. Variables can be passed to both scripts from the "before" steps. Random scenarios can be picked by using a weight factor.

    battery:
      phases: ..
      before: ..
      scenarios:
        - name: Read Users
          duration: 2m
          weight: 1
          steps:
            - script: select-users.b
        - name: Update Users
          duration: 3m
          weight: 2
          steps:
            - script: update-users.b
        - name: Insert Users
          duration: 4m
          weight: 3
          steps:
            - script: insert-users.b

Compatibility

  • MacOS (main platform)
  • Linux
  • JDK 21
  • Databases (bundled JDBC drivers)
    • CockroachDB v25.2+
    • PostgreSQL 9.x+
    • MySQL 8.x+
    • Oracle

Setup

Things you need to build and run battery locally.

Prerequisites

Install the JDK

MacOS (using sdkman):

curl -s "https://get.sdkman.io" | bash
sdk list java
sdk install java 21.0 (use TAB to pick edition)  

Ubuntu:

sudo apt-get install openjdk-21-jdk

Database Setup

The following instructions assumes MacOS and brew.

CockroachDB Setup

See start a local cluster for setup instructions. You can also use CockroachDB Cloud basic, standard or advanced. Whether a license is needed or not depends on if your SQL schema or statements uses enterprise features.

PostgreSQL Setup

Install PSQL on MacOS using brew:

brew install postgresql

Starting:

brew services start postgresql@14

Create the database:

psql postgres
$ create database test;
$ quit

Stopping:

brew services stop postgresql@14

MySQL Setup

Install MySQL on MacOS using brew:

brew install mysql

Starting:

brew services start colima
brew services start mysql

Create the database:

mysql -u root
$ create database test;
$ quit

Stopping:

brew services stop mysql

Oracle Setup

Install Oracle 23 Free on MacOS using brew:

brew install colima
brew install docker
colima start --arch x86_64 --memory 4

Starting:

docker run -d -p 1521:1521 -e ORACLE_PASSWORD=root -v oracle-volume:/opt/oracle/oradata gvenzl/oracle-free

Stopping:

docker ps -al
docker stop $(docker ps -a -q)

Building

Instructions for building the project locally.

Clone the project

git clone git@github.com:kai-niemi/battery.git && cd battery

Build the artifact

chmod +x mvnw
./mvnw -Pdistribution clean install

Battery is a single server component (executable jar) available at target/battery.jar.

With the distribution Maven profile activated, it also provides a target/battery-<version>.tar.gz assembly.

Running

Starting battery requires at least one profile name that denotes which configuration to use.

Run with an interactive shell

This command will start the battery web server with an internal interactive shell. You can either use the shell, web UI or REST API to run load tests.

./run-server.sh

Start in the background

To run shell commands in headless mode, first add the commands to a plain text file. For example:

echo "help" > cmd.txt
echo "version" >> cmd.txt
echo "quit" >> cmd.txt

Then start the server in the background by passing a command file name as argument with a @ prefix:

./start-server.sh <profile> @cmd.txt

Stop background server

This command stops the server if it's running:

./stop-server.sh

About

A SQL database load testing tool for CockroachDB, PostgreSQL, MySQL and Oracle.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published