diff --git a/js/core/src/globalStore.ts b/js/core/src/globalStore.ts new file mode 100644 index 0000000..db9c161 --- /dev/null +++ b/js/core/src/globalStore.ts @@ -0,0 +1,104 @@ +const ConcurrentHashMap = Java.type("java.util.concurrent.ConcurrentHashMap"); + +interface ConcurrentHashMapStatic { + new (): ConcurrentHashMap; +} + +interface ConcurrentHashMap { + 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(); + +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(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(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(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(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(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 = (x: T | null): T | undefined => (x === null ? undefined : x); + +const undefinedToNull = (x: T | undefined): T | null => (x === undefined ? null : x); diff --git a/js/core/src/index.test.ts b/js/core/src/index.test.ts index 8db75d2..055821e 100644 --- a/js/core/src/index.test.ts +++ b/js/core/src/index.test.ts @@ -37,7 +37,8 @@ import { ssv, stressPeakUsers, tsv, - ProtocolBuilder + ProtocolBuilder, + GlobalStore } from "./index"; const runSimulationMock = (_: Simulation): void => {}; @@ -69,6 +70,15 @@ const byteArrayBody2 = ByteArrayBody((session) => [1]); //const records = csv("foo").readRecords(); const recordsCount = csv("foo").recordsCount(); +// global store +GlobalStore.getOrDefault("key", 0); +GlobalStore.put("key", 0); +GlobalStore.get("key"); +GlobalStore.containsKey("key"); +GlobalStore.update("key", (oldValue) => (oldValue === null ? 0 : oldValue + 1)); +GlobalStore.remove("key"); +GlobalStore.clear(); + // scenario const scn = scenario("scenario") // execs diff --git a/js/core/src/index.ts b/js/core/src/index.ts index 61b3f09..6a72ad3 100644 --- a/js/core/src/index.ts +++ b/js/core/src/index.ts @@ -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"; diff --git a/ts-simulation/src/test-simulation.gatling.ts b/ts-simulation/src/test-simulation.gatling.ts index cafb302..6cad4fc 100644 --- a/ts-simulation/src/test-simulation.gatling.ts +++ b/ts-simulation/src/test-simulation.gatling.ts @@ -14,7 +14,8 @@ import { nothingFor, doWhile, css, - global + global, + GlobalStore } from "@gatling.io/core"; import { http } from "@gatling.io/http"; @@ -36,9 +37,12 @@ export default simulation((setUp) => { .exec( group("Browse").on( exec(session => session.set("page", 1)), - doWhile((session: Session) => session.get("page") < 3).on( + doWhile((session: Session) => session.get("page") <= 3).on( http("Browse page #{page}").get("/computers?p=#{page}"), - exec(session => session.set("page", session.get("page") + 1)), + exec(session => { + GlobalStore.update("browsedPages", old => typeof old === "number" ? old + 1 : 1); + return session.set("page", session.get("page") + 1) + }), pause({ amount: 500, unit: "milliseconds" }) ) ), @@ -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(