Misc functions
customFunction
Commentary
added in
Invokes custom functions defined in either Python, Ruby, or JavaScript.
For the most part, ShadowTraffic's core library is sufficient to generate a wide range of data. But sometimes, the existing functions are going to be missing something that only you can implement. customFunction
is the escape hatch to let you generate any kind of data you want.
ShadowTraffic natively executes code for each language using GraalVM 23+. This is important to know because the Graal implementation of each language may have slight difference to other platforms. For reference:
- Python is executed with GraalPy
- Ruby is executed with TruffleRuby
- JavaScript is executed with GraalJS
All custom functions must be defined with a single parameter, which is a map. This map gives you function useful context about its invocation, such as:
args
: a map of name -> values passed as arguments to the function 1call-id
: a unique ID for this functioncall-state
: a map of ID -> state for all functions in this evaluation tree 2vars
: a map of variable name -> value for any defined variables
Further, all functions must return a map with at least the key value
, which indicates the value that has been created. It may additionally return a key state
which returns an updated view of `call-state.
Examples
Defining a formula
The easiest example to demonstrate is a scalar function. Imagine you have some Python that looks like this:
factor = 387.34575
def formula(obj):
return {
"value": factor * (obj["args"]["x"] + obj["args"]["y"]),
}
To execute this code, mount it into the container at, say, path /home/python/basic_formula.py
.
Then invoke customFunction
, specifying the language
, the file
where the code can be found, and the function
name.
Optionally, you can pass named arguments in with args
. ShadowTraffic will evaluate any functions within and pass their make their results available to the custom function.
{
"_gen": "customFunction",
"language": "python",
"file": "/home/python/basic_formula.py",
"function": "my_formula",
"args": {
"x": {
"_gen": "normalDistribution",
"mean": 100,
"sd": 10
},
"y": {
"_gen": "uniformDistribution",
"bounds": [
50,
75
]
}
}
}
Carrying state
Some functions are only useful if they can remember prior values that they generated. To do this, ShadowTraffic has an internal state mechanism that's available to use in customFunction
.
Here's a simple Python function that generates a series of alternating boolean values: false
, true
, false
, true
, and so on.
def stateful_function(obj):
call_id = obj.get("call-id")
state = obj.get("call-state") or {call_id: False}
v = state.get(call_id)
return {
"value": v,
"state": {
call_id: not(v)
}
}
The Python code first obtains the call-id
of this function. It then grabs the state for this function, or initializes a value if it doesn't yet exist. Finally, it returns an updated call state by negating the previous boolean value.
Invoke the code like so:
{
"_gen": "customFunction",
"language": "python",
"file": "/home/python/stateful_function.py",
"function": "my_stateful_function"
}
Writing sequentialInteger
To better understand how custom functions with state work, we can implement an existing function - sequentialInteger
- from scratch.
In the same manner as the previous example, grab the call-id
of this function and initialize its state to 0
if it doesn't yet have a value.
Then return that value, plus an updated view of the state by incrementing the value by 1
.
def seq_int(obj):
call_id = obj.get("call-id")
state = obj.get("call-state") or {call_id: 0}
v = state.get(call_id)
return {
"value": v,
"state": {
call_id: v + 1
}
}
Finally, invoke the function inside ShadowTraffic.
{
"_gen": "customFunction",
"language": "python",
"file": "/home/python/seq_int.py",
"function": "sequential_integer"
}
Specification
JSON schema
{
"type": "object",
"properties": {
"language": {
"type": "string",
"enum": [
"python",
"ruby",
"js"
]
},
"file": {
"type": "string"
},
"function": {
"type": "string"
}
},
"required": [
"language",
"file",
"function"
]
}