04:31 | <littledan> | I think that's tricky due to races - for example, the exact ordering of a promise resolving vs. the promise.then call would make one or the other context more recent. But I think we probably want to run in the same context (resolution vs registration) regardless of the order. |
16:35 | <Steve Hicks> | it's a little pedantic, but if you have something like
Then "the most specific (i.e. causally recent) relevant context" would presumably be the 2, since the callback was registered after the promise resolved. Without the |
16:42 | <Chengzhong Wu> | this smells like zalgo -- it depends on how the handlers are called |
17:49 | <Justin Ridgewell> | @Stephen Belanger pointed out that if we were to have an flows-through ExecutionVariable and implement scoped using mutations, we’d have a problem with propogating the execution state through the resolve
|
17:50 | <Justin Ridgewell> | Because the scoped mutation is disposed in the FunctionStatementList evaluation of the function, and the outer promise is resolved in the AsyncFunctionBody execution after StatementList is evaluated |
17:51 | <Justin Ridgewell> | Then there’s weird interactions with how you’re supposed to resolve the promise, and what if the disposals caused an exeception |
17:52 | <Steve Hicks> | this smells like zalgo -- it depends on how the handlers are called |
17:55 | <Steve Hicks> | Because the scoped mutation is disposed in the |
17:57 | <Justin Ridgewell> | We’d have to redefine the using methods to perform DisposeResources in each of the *FunctionBody node types |
17:58 | <Justin Ridgewell> | Instead of using the FunctionStatementList node |
17:58 | <Justin Ridgewell> | That way we’d dispose the variables after resolving |
18:30 | <Stephen Belanger> | But using needs to work with block scopes too, right? So it'd need to be conditional on if the block is the function body or if it is nested. |
18:31 | <Stephen Belanger> | It'd add a bit of complexity. |
18:31 | <Stephen Belanger> | This is why other languages like .NET went for just doing set/get and persisting until the context would change. |
18:32 | <littledan> |
|
18:32 | <littledan> | (so there is no await vs .then mismatch) |
18:34 | <Stephen Belanger> |
|
18:34 | <Steve Hicks> | This is why other languages like .NET went for just doing set/get and persisting until the context would change. |
18:35 | <littledan> | Is there some nuance here between persisting until the context would change vs. persisting until the end of a scope? Does the former somehow allow escaping a changed value out of a scope where the latter doesn't? I'm still trying to wrap my mind around how you can actually observe a changed value in a child scope if the change gets reverted before you resume. |
18:35 | <littledan> | so this is the difference between using and .set |
18:35 | <Steve Hicks> | In flow-through model that should always be 1, as that is what was set when the resolve happened. We don't much care about where a continuation is attached. await is flow-around, but then restores the resolution context, since that would still allow accessing all the flow-through causes as needed. |
18:36 | <Stephen Belanger> | Is there some nuance here between persisting until the context would change vs. persisting until the end of a scope? Does the former somehow allow escaping a changed value out of a scope where the latter doesn't? I'm still trying to wrap my mind around how you can actually observe a changed value in a child scope if the change gets reverted before you resume. |
18:37 | <Stephen Belanger> | Right, but I'm imagining a world where callingContext would be helpful for there. |
18:37 | <Stephen Belanger> | It's a bit awkward though. |
18:37 | <Stephen Belanger> | And probably would have terrible performance for APMs which would need to be doing that basically always. |
18:39 | <Steve Hicks> | And probably would have terrible performance for APMs which would need to be doing that basically always. |
18:40 | <Steve Hicks> | Not sure if that made any sense... basically, you could explain the semantics of callingContext as if were a userland implementation, but it could be implemented internally in a more efficient way. |
18:42 | <Stephen Belanger> | If we have to actually do that explicitly on every await though it doesn't matter how much we optimize it, it will always have significant overhead. We need a type that has the appropriate flow by default or we're basically doubling all the context state modifications. |
18:43 | <Stephen Belanger> | This is why I was suggesting either having a separate type for the different flow, or having a per-store configuration to pick the different flow. |
18:45 | <Stephen Belanger> | Requiring that users manually manage snapshots all over the place is going to get expensive fast. This is why I had previously suggested flow-through by default and then a per-store config to additionally snapshot around awaits as the functionality for that binding layer is quite discrete and could be handled fairly performantly by just keeping a separate list of variables which want to snapshot there. |
18:47 | <Stephen Belanger> | But it seems some are of the opinion that flow-around should be the default, even though it's the more costly option to escape from. |
18:53 | <Steve Hicks> | I think it's a sticky situation. The default is less relevant - even if flow-around is the user-facing default, I don't see why it couldn't be implemented internally as the option that adds additional processing on top of the flow-through codepath. What's not clear to me is whether flow-through is viable at all, even as an option. My understanding is that SES would block it outright, so even if we want it we can't really have it. |
18:53 | <littledan> | yeah I have been assuming that .then would always use the registration context, and that we were only talking about web APIs. |
18:58 | <Steve Hicks> | Steve Hicks: Thoughts on this? await , there will always be fundamental disagreement between the context propagation of callbacks and async-await. Promises has to choose one or the other. Registration aligns it with async-await, resolution aligns it with callbacks. It's not clear to me that one is fundamentally better than the other, but the latter opens up a possibility to access semantics that are impossible with the former. |
19:01 | <Stephen Belanger> | I think it's a sticky situation. The default is less relevant - even if flow-around is the user-facing default, I don't see why it couldn't be implemented internally as the option that adds additional processing on top of the flow-through codepath. What's not clear to me is whether flow-through is viable at all, even as an option. My understanding is that SES would block it outright, so even if we want it we can't really have it. |
19:01 | <Steve Hicks> | (of course, maybe that already makes it problematic for SES anyway) |
19:02 | <Steve Hicks> | I wasn't part of those earlier conversations, so I don't really understand the reasoning here |
19:02 | <Steve Hicks> | They've got some super-theoretical framework where they transformed SyncContext to AsyncContext and transposed the ownership and reasoned that nothing dangerous happened at any step... |
19:04 | <Steve Hicks> | But IIUC the concern was less about passing a value directly and more about causing an observable side effect in a variable (by controlling the global snapshotting) without actually having access to that variable. |
19:51 | <littledan> | Assuming we have flow-around for |
19:53 | <littledan> | But IIUC the concern was less about passing a value directly and more about causing an observable side effect in a variable (by controlling the global snapshotting) without actually having access to that variable. |
19:53 | <littledan> | aesthetically, I do like keeping things well-behaved, all else being equal! |
20:45 | <Stephen Belanger> | Being "well-behaved" is deeply dependant on what behaviour is expected. And as I expressed with my examples about confusion in regard to differences between callback and promise flows, flow-through semantic seem to be more well-behaved in this regard that if flow-through is what you expect then there is rarely, if ever, a need to patch things with snapshotting. |
20:46 | <Stephen Belanger> | Once you start trying to carve out sub-graphs it becomes a matter of opinion which sub-graphs are correct. |
20:49 | <littledan> | things "nesting" and "not accidentally escaping" feels well-behaved to someone without background in the domain. We'll need to be able to explain the argument for the other semantics in a clear way to get through to them. |
20:49 | <Stephen Belanger> | I expressed this in the doc and slides I shared previously, but to repeat that: there are many layers of execution rearrangement occurring in JS. The base level of the microtask scheduler is fairly obvious to bind around, but as you raise further and further out into layers closer to user code it becomes less and less obvious what the "correct" path is when flowing around. |
20:49 | <littledan> | BTW I mentioned in the previous TG3 (SES) meeting that this is under discussion |
20:50 | <Stephen Belanger> | Not accidentally escaping. Intentionally escaping. |
20:50 | <littledan> | I expressed this in the doc and slides I shared previously, but to repeat that: there are many layers of execution rearrangement occurring in JS. The base level of the microtask scheduler is fairly obvious to bind around, but as you raise further and further out into layers closer to user code it becomes less and less obvious what the "correct" path is when flowing around. |
20:50 | <littledan> | Not accidentally escaping. Intentionally escaping. |
20:51 | <Stephen Belanger> | Yep, that's why I aimed for through path first in my own RFC and provide tools for reducing the graph as-needed. My experience has been that it's a lot easier to reduce the graph than to expand it. |
20:51 | <littledan> | but the through path doesn't provide more information, it just provides different information; it still loses stuff that the around path had |
20:52 | <Stephen Belanger> | Admittedly though, ALS never had a way to expand the graph, so that experience is possibly incomplete. |
20:52 | <Stephen Belanger> | The through path can represent the around path if you don't change the value in the branches. |
20:52 | <Stephen Belanger> | The reverse is not true though. |
20:53 | <Stephen Belanger> | So you can get the same behaviour as flow-around if you use the variables carefully. |
20:54 | <Stephen Belanger> | And the bind tools let you reduce graphs to flow-around easily to prevent internal branch modifications from flowing out. |
20:55 | <Stephen Belanger> | Something like await bindPromise(promise) which would just capture context at the point bindPromise(...) is called and restore it when the given promise calls its continuation let you trivially restore around flow. |
20:55 | <littledan> | So you can get the same behaviour as flow-around if you use the variables carefully. |
20:55 | <littledan> | Something like |
20:56 | <Stephen Belanger> | So if you do an await doSomething() and then don't change the value anywhere in the through flow of doSomething() then what comes out the other side will be exactly the same as what was present when the await first started. |
20:58 | <Stephen Belanger> | The only exception being if you do await existingPromise , which is what bind was created for in the first place for the callback equivalent, and the bindPromise(...) can equally solve by binding to the correct place directly at the await. |
20:59 | <Stephen Belanger> | It just happens to be a bit annoying to need to do bindPromise(...) everywhere, so it makes sense to have some separate optimization to be able to inform the system that you want every await to apply a particular bind, which is what the around flow is essentially actually doing...it just doesn't give you a choice in the matter. |
21:03 | <Stephen Belanger> | Flow-around semantics are always a subset of flow-through semantics. They just look different because flow-around often orphans branches which would have otherwise flowed through so it appears like flow-through has return-based semantics when really it's just that flow-around decided to cut off the natural through flow to restore a snapshot captured before a given await. The flow is otherwise completely identical, apart from the single additional step of also capturing at resolve and restoring that unless a different link point has been established. |
21:04 | <Stephen Belanger> | With flow-around, the context still flows through the entire branch up to the point where a snapshot clobbers the value that had otherwise flowed through all the way to the resolve edge. |
21:10 | <Stephen Belanger> | It's probably more useful to think of it as flowing forward. Context always flows forward in time to any caused branches. The only question is if that context reaching the end of that branch where execution merges back should be raised out to continue flowing forward or if we want to intentionally cut that branch off for some reason. For the purpose of branch cancellation we would want a barrier where it does not raise out to unrelated execution, which is somewhat what the flow-around is solving. But for the purpose of flowing into future execution needed by tracing or most users wanting to store request-scoped data it needs to flow through backward merges all the way to the end of any execution caused by the initiation of that request. |
21:13 | <Stephen Belanger> | Flow-around may sometimes work for request-scoped data, but it needs to be set at a level which can never be merged back to or subsequent execution might not have a context available. This becomes problematic if you, for example, want to create a database connection in a middleware and flow that into subsequent middlewares or routers. |
21:18 | <Stephen Belanger> |
|
21:20 | <Stephen Belanger> | The further you get from the store logic, the easier it is to shoot yourself in the foot with behaviours like this. |
21:20 | <Stephen Belanger> | And the whole point of async context storage is that you can retrieve values distant from where the store is set. |