Skip to content

Commit

Permalink
feat: Add global store to share values between virtual users
Browse files Browse the repository at this point in the history
  • Loading branch information
guilgaly authored and notdryft committed Jun 24, 2024
1 parent c990523 commit d2d6fbd
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 4 deletions.
104 changes: 104 additions & 0 deletions js/core/src/globalStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const ConcurrentHashMap = Java.type<ConcurrentHashMapStatic>("java.util.concurrent.ConcurrentHashMap");

interface ConcurrentHashMapStatic {
new <K, V>(): ConcurrentHashMap<K, V>;
}

interface ConcurrentHashMap<K, V> {
put(key: K, value: V): V | null;
remove(key: K): V | null;
get(key: K): V | null;
getOrDefault(key: K, defaultValue: V): V;
containsKey(key: K): boolean;
compute(key: K, remappingFunction: (k: K, v: V | null) => V | null): V | null;
clear(): void;
}

const javaStore = new ConcurrentHashMap<string, any>();

export interface GlobalStore {
/**
* Maps the specified key to the specified value in the global store. Neither the key nor the value can be null or
* undefined.
*
* @param key - key with which the specified value is to be associated
* @param value - value to be associated with the specified key
* @returns the previous value associated with key, or `null` if there was no mapping for key
*/
put<T>(key: string, value: T): T | undefined;

/**
* Attempts to compute a new value for the specified key and its currently mapped value (or `null` if there is no
* current value). The entire method invocation is performed atomically. The supplied function is invoked exactly once
* per invocation of this method. Some attempted update operations on this map by other threads may be blocked while
* computation is in progress, so the computation should be short and simple.
*
* The updateFunction function must not modify the global store during computation.
*
* @param key - key with which the specified value is to be associated
* @param updateFunction - the function to compute a new value to update the mapping; if it returns `null`, the
* mapping will be removed
* @returns the new value associated with the specified key, or `null` if none
*/
update<T>(key: string, updateFunction: (oldValue: T | undefined) => T | undefined): T | undefined;

/**
* Returns the value to which the specified key is mapped, or `null` if the global store contains no mapping for the
* key.
*
* @param key - the key whose associated value is to be returned
* @returns the mapping for the key, if present; else `null`
*/
get<T>(key: string): T | undefined;

/**
* Returns the value to which the specified key is mapped, or the given default value if the global store contains no
* mapping for the key.
*
* @param key - the key whose associated value is to be returned
* @param defaultValue - the value to return if the global store contains no mapping for the given key
* @returns the mapping for the key, if present; else the default value
*/
getOrDefault<T>(key: string, defaultValue: T): T;

/**
* Tests if the key is present in the global store.
*
* @param key - possible key
* @returns `true` if and only if the key is present in the global store; `false` otherwise
*/
containsKey(key: string): boolean;

/**
* Removes the mapping for a key from the global store if it is present.
*
* @param key - key whose mapping is to be removed from the map
* @returns the previous value associated with `key`, or `null` if there was no mapping for `key`.
*/
remove<T>(key: string): T;

/**
* Removes all of the mappings from the global store.
*/
clear(): void;
}

/**
* A global store which can be used to share data between different virtual users.
*/
export const GlobalStore: GlobalStore = {
put: (key, value) => nullToUndefined(javaStore.put(key, value)),
update: (key, updateFunction) =>
nullToUndefined(
javaStore.compute(key, (_, oldValue) => undefinedToNull(updateFunction(nullToUndefined(oldValue))))
),
get: (key) => nullToUndefined(javaStore.get(key)),
getOrDefault: (key, defaultValue) => javaStore.getOrDefault(key, defaultValue),
containsKey: (key) => javaStore.containsKey(key),
remove: (key) => nullToUndefined(javaStore.remove(key)),
clear: () => javaStore.clear()
};

const nullToUndefined = <T>(x: T | null): T | undefined => (x === null ? undefined : x);

const undefinedToNull = <T>(x: T | undefined): T | null => (x === undefined ? null : x);
12 changes: 11 additions & 1 deletion js/core/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import {
ssv,
stressPeakUsers,
tsv,
ProtocolBuilder
ProtocolBuilder,
GlobalStore
} from "./index";

const runSimulationMock = (_: Simulation): void => {};
Expand Down Expand Up @@ -69,6 +70,15 @@ const byteArrayBody2 = ByteArrayBody((session) => [1]);
//const records = csv("foo").readRecords();
const recordsCount = csv("foo").recordsCount();

// global store
GlobalStore.getOrDefault<number>("key", 0);
GlobalStore.put<number>("key", 0);
GlobalStore.get<number>("key");
GlobalStore.containsKey("key");
GlobalStore.update<number>("key", (oldValue) => (oldValue === null ? 0 : oldValue + 1));
GlobalStore.remove<number>("key");
GlobalStore.clear();

// scenario
const scn = scenario("scenario")
// execs
Expand Down
1 change: 1 addition & 0 deletions js/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from "./closedInjection";
export * from "./common";
export * from "./feeders";
export * from "./filters";
export { GlobalStore } from "./globalStore";
export * from "./openInjection";
export * from "./population";
export * from "./protocol";
Expand Down
17 changes: 14 additions & 3 deletions ts-simulation/src/test-simulation.gatling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
nothingFor,
doWhile,
css,
global
global,
GlobalStore
} from "@gatling.io/core";
import { http } from "@gatling.io/http";

Expand All @@ -36,9 +37,12 @@ export default simulation((setUp) => {
.exec(
group("Browse").on(
exec(session => session.set("page", 1)),
doWhile((session: Session) => session.get<number>("page") < 3).on(
doWhile((session: Session) => session.get<number>("page") <= 3).on(
http("Browse page #{page}").get("/computers?p=#{page}"),
exec(session => session.set("page", session.get<number>("page") + 1)),
exec(session => {
GlobalStore.update("browsedPages", old => typeof old === "number" ? old + 1 : 1);
return session.set("page", session.get<number>("page") + 1)
}),
pause({ amount: 500, unit: "milliseconds" })
)
),
Expand Down Expand Up @@ -76,6 +80,13 @@ export default simulation((setUp) => {
atOnceUsers(10),
nothingFor({ amount: 5, unit: "seconds" }),
constantUsersPerSec(2).during(30)
).andThen(
scenario("Post execution")
.exec(session => {
console.log(`browsedPages=${GlobalStore.get("browsedPages")}`);
return session;
})
.injectOpen(atOnceUsers(1))
)
).protocols(baseHttpProtocol)
.assertions(
Expand Down

0 comments on commit d2d6fbd

Please sign in to comment.