Release a murder of crows upon your enemies.
Crows is a distributed load and stress testing runner. The tests can be written in any language that can compile to WASM given the bindings for the library are available. At the moment the bindings are available only for Rust, but once the ABIs are stable it should be relatively straightforward to add more languages.
A sample scenario written in Rust looks like this:
#[config]
fn config() -> ExecutorConfig {
let config = ConstantArrivalRateConfig {
duration: Duration::from_secs(5),
rate: 10,
allocated_vus: 10,
..Default::default()
};
ExecutorConfig::ConstantArrivalRate(config)
}
#[export_name = "scenario"]
pub fn scenario() {
http_request(
"https://google.com".into(), GET, HashMap::new(), "".into(),
);
}
It will send 10 requests per second to google.com. For information on how to compile and run it please go to the Usage section
This project is at a "working proof of concept" stage. It has solid foundation as I put a lot of thought into figuring out the best way to implement it, but there are a lot of details that are either missing or are knowingly implemented in a not optimal/best way. I've started thinking about the project about 3 years ago and I've started the current iteration about 1.5 years ago and I decided I have to finish it as soon as possible or it will end up where most of my personal projects - aiming for perfection, half finished and never released.
What you should expect then? Most of the basic stuff works, but error handling is quite poor. Stuff will probably break here and there. Error messages may make no sense or may not even reach the CLI if they happen on the server side. Also, there is only one excutor: constant arrival rate. Use if you're curious and/or you would like to contribute, but it's probably not the best idea to use it for anything important or if you value your time. I will update the README once I feel the project is stable enough and feature complete enough for a usable version.
There are a lot of tools for stress testing available on the market. Why have I decided to write Crows then? A few reasons:
Crows is not production ready, so it misses some of the features, but my personal high level wishlist is:
At the moment you can install the crows only through the cargo command:
cargo install crows
The simplest way to use Crows is to use the local mode. After you obtain the crows binary you can run a scenario compiled to WebAssembly with a single command:
crows run scenario.wasm
In order to create a scenarios you have to generate a Rust appliction with:
cargo new crows-sample
Then you need to set the crate type and add the crows-bindings dependency:
[lib]
crate-type = ["cdylib"]
[dependencies]
crows-bindings = "0.3"
then add your scenario in src/lib.rs:
use crows_bindings::{
config, http_request, ConstantArrivalRateConfig, ExecutorConfig, HTTPMethod::*,
};
use std::time::Duration;
#[config]
fn config() -> ExecutorConfig {
let config = ConstantArrivalRateConfig {
duration: Duration::from_secs(5),
rate: 10,
..Default::default()
};
ExecutorConfig::ConstantArrivalRate(config)
}
#[export_name = "scenario"]
pub fn scenario() {
println!("Send request to https://test.k6.io/");
http_request(
"https://test.k6.io".to_string(), GET, Default::default(), "".to_string(),
);
}
I'm hoping K6 maintainers won't be angry about using their test URL. It's a great tool, btw, if you're into stress testing you should check it out!
Now you can compile the scenario with:
cargo build --release --target wasm32-wasi
and run with:
crows run target/wasm32-wasi/release/crows_sample.wasm
In a distributed mode you need to start a coordinator and at least one worker. You interact with the coordinator using the CLI.
cargo install crows-coordinator
cargo install crows-worker
First you start the coordinator:
crows-coordinator
and then at least one worker:
WORKER_NAME=worker-1 crows-worker
Now you can upload and run a scenario:
crows upload --name test --path scenario.wasm
crows start --name test --workers-number 1
I plan to prepare a more thorough description with diagrams and stuff, so in here I just wanted to give a brief summary.
A high level overview is as follows:
A coordinator is a single point of failure, but in the case of stress and load testing I think it's a sensible trade-off. On the upside this kind of architecture is extremely simple and with persistance layer it might be possible to tolerate coordinator crashes. In the future I'm open to making the coordinator distributed, but I don't think it's a pressing issue. Most of the issues I can identify with a single point of contact are not very problematic:
On top of that I really want to avoid the situation where every node can potentially be either a coordinator or a worker, cause I think it would complicate the project quite a lot without that many advantages.
Connections between components are plain TCPSocket connections with messages being lenght delimited JSON messages (ie. each message starts with 4 bytes describing the message length and then the message itself). The messages are abstracted with a custom RPC system that allows two way communication with either side initiating a message. In other words both sides can send a request and receive a response. One of the things that was really important for me was not having a requirement to expose the network address of a worker, cause this drastically simplifies server setup - it can be literally any machine or a virtual machine that connect to the coordinator. Even your laptop behind a NAT.
In the future I think I might change the serialization format to something like FlatBuffers, so it's easier to work with different versions, but for now JSON is enough and its simplicity makes development easier.
Worker is one of the most interesting parts of the project, in my humble opinion. A workers runs on top of a Tokio asynchronous runtime. All of the scenarios are executed in the context of the async runtime, thus even though the scenarios are written like you would normally write synchronous code, they will be executed in an asynchronous wait under the hood.
When a scenario is being executed a WASM runtime, using Wasmtime, is created. All the IO operations are done on the host side and thus WASM modules only call the host defined functions. For example in order to make an HTTP request:
HTTPRequest object and writes it to WASM linear memoryhttp_request binding that in turn executes code in the Tokio runtimeObjects shared between the host and the WASM module are serialized and deserialized using JSON.
WASM modules are compiled as WASI enabled modules, which means it's possible to support common I/O operations like printing to STDOUT or reading files. At the moment only the former is supported and anything printed to STDOUT or STDERR will be passed to the coordinator and then potentially to the client. In the future I would also like to support reading files and allowing to distribute files between workers.
Executors define how scenarios are being executed. They define how and when to allocated new WASM instances and when to run iterations. At the moment only one executor is available constant arrival rate. If you want to see some other examples of possible executors take a look at (executors available in the K6 project)[https://k6.io/docs/using-k6/scenarios/executors/] (which is a great tool, which I've successfully used in the past, btw. kudos to the K6 team!).
I don't have a very precise plan on where I want Crows to go, but some loose thoughts on the stuff I'd like to work on if I have time (which is scarce and you can help by supporting me (on GitHub!)[link to gh sponsors]):
wrk does by default between requests, but it should be configurable and in general we should have more granular control on how it works$ claude mcp add crows \
-- python -m otcore.mcp_server <graph>