A k6 extension providing a persistent key-value store for sharing state across Virtual Users (VUs) during load testing.
Note: For extremely high-performance requirements, consider using the k6 Redis module instead.
go install go.k6.io/xk6/cmd/xk6@latest
xk6 build --with github.com/oleiade/xk6-kv
import { openKv } from "k6/x/kv";
./k6 run script.js
import { openKv } from "k6/x/kv";
// Open a key-value store with the default backend (disk)
const kv = openKv();
// Or specify a backend explicitly
// const kv = openKv({ backend: "disk" }); // Disk-based persistent backend (default)
// const kv = openKv({ backend: "memory" }); // In-memory backend
export async function setup() {
// Start with a clean state
await kv.clear();
}
export default async function () {
// Set a bunch of keys
await kv.set("foo", "bar");
await kv.set("abc", 123);
await kv.set("easy as", [1, 2, 3]);
const abcExists = await kv.exists("a b c")
if (!abcExists) {
await kv.set("a b c", { "123": "baby you and me girl"});
}
console.log(`current size of the KV store: ${kv.size()}`)
const entries = await kv.list({ prefix: "a" });
for (const entry of entries) {
console.log(`found entry: ${JSON.stringify(entry)}`);
}
await kv.delete("foo");
}
openKv(options?: OpenKvOptions): KVOpens a key-value store with the specified backend. Must be called in the init context.
interface OpenKvOptions {
backend?: "memory" | "disk"; // Default is "memory"
}
While both backends are optimized for performance and suitable for most load testing scenarios, be aware that:
set(key: string, value: any): Promise<any>
get(key: string): Promise<any>
delete(key: string): Promise<void>
exists(key: string): Promise<boolean>
list(options: ListOptions): Promise<Array<Entry>>
clear(): Promise<void>
size(): number
interface ListOptions {
prefix?: string; // Filter by key prefix
limit?: number; // Max number of results
}
A common use case for xk6-kv is sharing state between VUs for workflows such as producer-consumer patterns or rendez-vous points. The following example demonstrates a producer-consumer workflow where one VU produces tokens and another consumes them, coordinating through the shared key-value store.
import { sleep } from "k6";
import { openKv } from "k6/x/kv";
export let options = {
scenarios: {
producer: {
executor: "shared-iterations",
vus: 1,
iterations: 10,
exec: "producer",
},
consumer: {
executor: "shared-iterations",
vus: 1,
iterations: 10,
startTime: "5s",
exec: "consumer",
},
},
};
const kv = openKv({ backend: "memory" });
export async function producer() {
let latestProducerID = 0;
if (await kv.exists(`latest-producer-id`)) {
latestProducerID = await kv.get(`latest-producer-id`);
}
console.log(`[producer]-> adding token ${latestProducerID}`);
await kv.set(`token-${latestProducerID}`, "token-value");
await kv.set(`latest-producer-id`, latestProducerID + 1);
// Let's simulate a delay between producing tokens
sleep(1);
}
export async function consumer() {
console.log("[consumer]<- waiting for next token");
// Let's list the existing tokens, and consume the first we find
const entries = await kv.list({ prefix: "token-" });
if (entries.length > 0) {
await kv.get(entries[0].key);
console.log(`[consumer]<- consumed token ${entries[0].key}`);
await kv.delete(entries[0].key);
} else {
console.log("[consumer]<- no tokens available");
}
// Let's simulate a delay between consuming tokens
sleep(1);
}
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature')git push origin feature/AmazingFeature)Go
100.0%
A k6 extension providing a persistent key-value store for sharing state across Virtual Users (VUs) during load testing.
Note: For extremely high-performance requirements, consider using the k6 Redis module instead.
go install go.k6.io/xk6/cmd/xk6@latest
xk6 build --with github.com/oleiade/xk6-kv
import { openKv } from "k6/x/kv";
./k6 run script.js
import { openKv } from "k6/x/kv";
// Open a key-value store with the default backend (disk)
const kv = openKv();
// Or specify a backend explicitly
// const kv = openKv({ backend: "disk" }); // Disk-based persistent backend (default)
// const kv = openKv({ backend: "memory" }); // In-memory backend
export async function setup() {
// Start with a clean state
await kv.clear();
}
export default async function () {
// Set a bunch of keys
await kv.set("foo", "bar");
await kv.set("abc", 123);
await kv.set("easy as", [1, 2, 3]);
const abcExists = await kv.exists("a b c")
if (!abcExists) {
await kv.set("a b c", { "123": "baby you and me girl"});
}
console.log(`current size of the KV store: ${kv.size()}`)
const entries = await kv.list({ prefix: "a" });
for (const entry of entries) {
console.log(`found entry: ${JSON.stringify(entry)}`);
}
await kv.delete("foo");
}
openKv(options?: OpenKvOptions): KVOpens a key-value store with the specified backend. Must be called in the init context.
interface OpenKvOptions {
backend?: "memory" | "disk"; // Default is "memory"
}
While both backends are optimized for performance and suitable for most load testing scenarios, be aware that:
set(key: string, value: any): Promise<any>
get(key: string): Promise<any>
delete(key: string): Promise<void>
exists(key: string): Promise<boolean>
list(options: ListOptions): Promise<Array<Entry>>
clear(): Promise<void>
size(): number
interface ListOptions {
prefix?: string; // Filter by key prefix
limit?: number; // Max number of results
}
A common use case for xk6-kv is sharing state between VUs for workflows such as producer-consumer patterns or rendez-vous points. The following example demonstrates a producer-consumer workflow where one VU produces tokens and another consumes them, coordinating through the shared key-value store.
import { sleep } from "k6";
import { openKv } from "k6/x/kv";
export let options = {
scenarios: {
producer: {
executor: "shared-iterations",
vus: 1,
iterations: 10,
exec: "producer",
},
consumer: {
executor: "shared-iterations",
vus: 1,
iterations: 10,
startTime: "5s",
exec: "consumer",
},
},
};
const kv = openKv({ backend: "memory" });
export async function producer() {
let latestProducerID = 0;
if (await kv.exists(`latest-producer-id`)) {
latestProducerID = await kv.get(`latest-producer-id`);
}
console.log(`[producer]-> adding token ${latestProducerID}`);
await kv.set(`token-${latestProducerID}`, "token-value");
await kv.set(`latest-producer-id`, latestProducerID + 1);
// Let's simulate a delay between producing tokens
sleep(1);
}
export async function consumer() {
console.log("[consumer]<- waiting for next token");
// Let's list the existing tokens, and consume the first we find
const entries = await kv.list({ prefix: "token-" });
if (entries.length > 0) {
await kv.get(entries[0].key);
console.log(`[consumer]<- consumed token ${entries[0].key}`);
await kv.delete(entries[0].key);
} else {
console.log("[consumer]<- no tokens available");
}
// Let's simulate a delay between consuming tokens
sleep(1);
}
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature')git push origin feature/AmazingFeature)Go
100.0%