Skip to content

Commit 99fc99d

Browse files
committed
Save + restore context during evaluate
Reverted changes not reqd anymore
1 parent 5a9598e commit 99fc99d

2 files changed

Lines changed: 70 additions & 58 deletions

File tree

src/main/java/com/dashjoin/jsonata/Functions.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,10 +1561,9 @@ public static List hofFuncArgs(Object func, Object arg1, Object arg2, Object arg
15611561
*/
15621562
public static Object funcApply(Object func, List funcArgs) throws Throwable {
15631563
Object res;
1564-
if (isLambda(func)) {
1565-
Jsonata.EvalContext ctx = Jsonata.evalContext.get();
1566-
res = ctx.instance.apply(func, funcArgs, null, ctx.environment);
1567-
} else
1564+
if (isLambda(func))
1565+
res = Jsonata.current.get().apply(func, funcArgs, null, Jsonata.current.get().environment);
1566+
else
15681567
res = ((JFunction)func).call(null, funcArgs);
15691568
return res;
15701569
}
@@ -2376,8 +2375,7 @@ public static Object functionEval(String expr, Object focus) {
23762375
if(expr == null) {
23772376
return null;
23782377
}
2379-
Jsonata.EvalContext ctx = Jsonata.evalContext.get();
2380-
Object input = ctx.input;
2378+
Object input = Jsonata.current.get().input; // = this.input;
23812379
if(focus != null) {
23822380
input = focus;
23832381
// if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input
@@ -2388,7 +2386,7 @@ public static Object functionEval(String expr, Object focus) {
23882386
}
23892387

23902388
Jsonata ast;
2391-
Jsonata.Frame env = ctx.environment;
2389+
Jsonata.Frame env = Jsonata.current.get().environment;
23922390
try {
23932391
ast = jsonata(expr);
23942392
} catch(Throwable err) {
@@ -2399,7 +2397,7 @@ public static Object functionEval(String expr, Object focus) {
23992397
}
24002398
Object result = null;
24012399
try {
2402-
result = ctx.instance.evaluate(ast.ast, input, env);
2400+
result = Jsonata.current.get().evaluate(ast.ast, input, env);
24032401
} catch(Throwable err) {
24042402
// error evaluating the expression passed to $eval
24052403
//populateMessage(err);
@@ -2414,15 +2412,15 @@ public static Object functionEval(String expr, Object focus) {
24142412
// return datetime.fromMillis(timestamp.getTime(), picture, timezone);
24152413
// }, "<s?s?:s>"));
24162414
public static String now(String picture, String timezone) {
2417-
long t = Jsonata.evalContext.get().timestamp;
2415+
long t = Jsonata.current.get().timestamp;
24182416
return dateTimeFromMillis(t, picture, timezone);
24192417
}
24202418

24212419
// environment.bind("millis", defineFunction(function() {
24222420
// return timestamp.getTime();
24232421
// }, "<:n>"));
24242422
public static long millis() {
2425-
long t = Jsonata.evalContext.get().timestamp;
2423+
long t = Jsonata.current.get().timestamp;
24262424
return t;
24272425
}
24282426
}

src/main/java/com/dashjoin/jsonata/Jsonata.java

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,29 @@ public void setEvaluateExitCallback(ExitCallback cb) {
127127
* @returns {*} Evaluated input data
128128
*/
129129
Object evaluate(Symbol expr, Object input, Frame environment) {
130-
return _evaluate(expr, input, environment);
130+
// Thread safety:
131+
// Make sure each evaluate is executed on an instance per thread
132+
Jsonata _this = getPerThreadInstance();
133+
// Save and restore the evaluation context so that nested
134+
// evaluations (e.g. $eval()) see the correct context.
135+
Object _input = _this.input;
136+
Frame _environment = _this.environment;
137+
try {
138+
return _this._evaluate(expr, input, environment);
139+
} finally {
140+
_this.input = _input;
141+
_this.environment = _environment;
142+
}
131143
}
132144

133145
Object _evaluate(Symbol expr, Object input, Frame environment) {
134146
Object result = null;
135147

136-
// Save and restore the evaluation context so that nested
137-
// evaluations (e.g. $eval()) see the correct context.
138-
// All mutable per-evaluation state lives on EvalContext, not on this instance.
139-
EvalContext ctx = evalContext.get();
140-
Object savedInput = ctx.input;
141-
Frame savedEnvironment = ctx.environment;
142-
ctx.input = input;
143-
ctx.environment = environment;
148+
// Store the current input + environment
149+
// This is required by Functions.functionEval for current $eval() input context
150+
this.input = input;
151+
this.environment = environment;
144152

145-
try {
146153
if (parser.dbg) System.out.println("eval expr="+expr+" type="+expr.type);//+" input="+input);
147154

148155
var entryCallback = environment.lookup("__evaluate_entry");
@@ -239,10 +246,6 @@ Object _evaluate(Symbol expr, Object input, Frame environment) {
239246
}
240247

241248
return result;
242-
} finally {
243-
ctx.input = savedInput;
244-
ctx.environment = savedEnvironment;
245-
}
246249
}
247250

248251
/**
@@ -1556,34 +1559,29 @@ boolean isFunctionLike(Object o) {
15561559
return Utils.isFunction(o) || Functions.isLambda(o) || (o instanceof Pattern);
15571560
}
15581561

1562+
final static ThreadLocal<Jsonata> current = new ThreadLocal<>();
1563+
15591564
/**
1560-
* Mutable evaluation context, stored on a static ThreadLocal.
1561-
* Holds all per-evaluation state that was previously stored as mutable fields
1562-
* on the Jsonata instance. This allows the Jsonata instance itself to remain
1563-
* immutable after construction, making it freely shareable across threads
1564-
* with no cloning needed.
1565+
* Returns a per thread instance of this parsed expression.
1566+
*
1567+
* @return
15651568
*/
1566-
static final class EvalContext {
1567-
Object input;
1568-
Frame environment;
1569-
long timestamp;
1570-
final Jsonata instance;
1571-
1572-
EvalContext(Jsonata instance) {
1573-
this.instance = instance;
1569+
Jsonata getPerThreadInstance() {
1570+
Jsonata threadInst = current.get();
1571+
// Fast path
1572+
if (threadInst!=null)
1573+
return threadInst;
1574+
1575+
synchronized(this) {
1576+
threadInst = current.get();
1577+
if (threadInst==null) {
1578+
threadInst = new Jsonata(this);
1579+
current.set(threadInst);
1580+
}
1581+
return threadInst;
15741582
}
15751583
}
15761584

1577-
/**
1578-
* Thread-local evaluation context. Set at the entry point of evaluate()
1579-
* and restored after evaluation completes. Read by Functions.java for
1580-
* $eval(), $now(), $millis(), and funcApply().
1581-
*
1582-
* This is the ONLY ThreadLocal needed. No per-instance ThreadLocals,
1583-
* no cloning, no mutable state on the Jsonata instance during evaluation.
1584-
*/
1585-
static final ThreadLocal<EvalContext> evalContext = new ThreadLocal<>();
1586-
15871585
/**
15881586
* Evaluate Object against input data
15891587
* @param {Object} expr - JSONata expression
@@ -1594,6 +1592,8 @@ static final class EvalContext {
15941592
/* async */ Object evaluateFunction(Symbol expr, Object input, Frame environment, Object applytoContext) {
15951593
Object result = null;
15961594

1595+
// this.current is set by getPerThreadInstance() at this point
1596+
15971597
// create the procedure
15981598
// can"t assume that expr.procedure is a lambda type directly
15991599
// could be an expression that evaluates to a Object (e.g. variable reference, parens expr etc.
@@ -2484,6 +2484,8 @@ Exception populateMessage(Exception err) {
24842484
List<Exception> errors;
24852485
Frame environment;
24862486
Symbol ast;
2487+
long timestamp;
2488+
Object input;
24872489

24882490
static {
24892491
staticFrame = new Frame(null);
@@ -2516,6 +2518,8 @@ public static Jsonata jsonata(String expression) {
25162518
}
25172519
environment = createFrame(staticFrame);
25182520

2521+
timestamp = System.currentTimeMillis(); // will be overridden on each call to evalute()
2522+
25192523
// Note: now and millis are implemented in Functions
25202524
// environment.bind("now", defineFunction(function(picture, timezone) {
25212525
// return datetime.fromMillis(timestamp.getTime(), picture, timezone);
@@ -2531,8 +2535,21 @@ public static Jsonata jsonata(String expression) {
25312535
// jsonata.RegexEngine = RegExp;
25322536
// }
25332537

2538+
// Set instance for this thread
2539+
current.set(this);
25342540
}
25352541

2542+
/**
2543+
* Creates a clone of the given Jsonata instance.
2544+
* Package-private copy constructor used to create per thread instances.
2545+
*
2546+
* @param other
2547+
*/
2548+
Jsonata(Jsonata other) {
2549+
this.ast = other.ast;
2550+
this.environment = other.environment;
2551+
this.timestamp = other.timestamp;
2552+
}
25362553

25372554
/**
25382555
* Flag: validate input objects to comply with JSON types
@@ -2574,10 +2591,13 @@ public Object evaluate(Object input, Frame bindings) { // FIXME:, callback) {
25742591
} else {
25752592
exec_env = environment;
25762593
}
2594+
// put the input document into the environment as the root object
25772595
exec_env.bind("$", input);
25782596

2579-
// Timestamp for $now() and $millis() — captured once per evaluation
2580-
long ts = System.currentTimeMillis();
2597+
// capture the timestamp and put it in the execution environment
2598+
// the $now() and $millis() functions will return this value - whenever it is called
2599+
timestamp = System.currentTimeMillis();
2600+
//exec_env.timestamp = timestamp;
25812601

25822602
// if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input
25832603
if((input instanceof List) && !Utils.isSequence(input)) {
@@ -2588,24 +2608,18 @@ public Object evaluate(Object input, Frame bindings) { // FIXME:, callback) {
25882608
if (validateInput)
25892609
Functions.validateInput(input);
25902610

2591-
EvalContext prev = evalContext.get();
2592-
EvalContext ctx = new EvalContext(this);
2593-
ctx.input = input;
2594-
ctx.environment = exec_env;
2595-
ctx.timestamp = ts;
2596-
evalContext.set(ctx);
2597-
25982611
Object it;
25992612
try {
26002613
it = /* await */ evaluate(ast, input, exec_env);
2614+
// if (typeof callback === "function") {
2615+
// callback(null, it);
2616+
// }
26012617
it = Utils.convertNulls(it);
26022618
return it;
26032619
} catch (Exception err) {
26042620
// insert error message into structure
26052621
populateMessage(err); // possible side-effects on `err`
26062622
throw err;
2607-
} finally {
2608-
evalContext.set(prev);
26092623
}
26102624
}
26112625

0 commit comments

Comments
 (0)