snowplow / snowbridge

For replicating streams across clouds, accounts and regions
Other
15 stars 8 forks source link

Consider alternatives for thread-safe runtime reuse of transformation engines #138

Open adatzer opened 2 years ago

adatzer commented 2 years ago

As @colmsnowplow pointed out here, it'd be nice to consider alternatives that can allow not having to spin-up custom transformation runtimes per event.

(edit: copying comment, to continue discussion here) Alternatives i only briefly thought with the libraries we use now:

  1. reuse the same JS runtime or Lua state: even though it feels that we could , since we just call a function, it is hard to know:

    1. did the user introduced state in their scripts? does that state mutate? is function a closure over that state?
    2. has some other thread pushed to the stack (lua), before return value was popped?
    3. what about the Go values that e.g. the goja runtimes wraps? what happens in the runtime global state?
  2. use a pool of runtimes: same concerns more or less

I was afraid that the performance penalty would be huge, but it turned out that it is not that bad, especially since we compile the code ahead saving the compile time. But again, definitely something to investigate and improve.

colmsnowplow commented 2 years ago

My assumption is that we'd have to go with option 2 realistically, otherwise we've got a choke point for our code. The pool could be equal to source_concurrent_writes for example. The same issues remain though, so let me pop down my thoughts on them while it's fresh in my mind:

did the user introduced state in their scripts? does that state mutate? is function a closure over that state?

Indeed this is a concern - realistically, the user shouldn't depend on state across events, or at least if they're currently trying to do so they're not going to be able to reliably do it. (Facilitating that kind of behaviour is another good idea, but realistically a longer-term prospect for which we'd need a new design model).

Currently, we basically put a hard blocker on doing this by spinning up a new vm for every event (if I'm correct?). I think we could have persistent pool of vms and keep the same model by either:

a) Just documenting that the user needs to ensure that environment state shouldn't impact upon the script (I don't like this though, since it doesn't prevent the behaviour) b) Enforce it by clearing the state somehow. Perhaps the vm has a means of doing so natively, perhaps we create some kind of 'bootstrap' script.

has some other thread pushed to the stack (lua), before return value was popped?

This is more straightforward - if we're to instrument persistent vms, then we must instrument a concurrency model that prevents this from happening. However doing so isn't so difficult - we do far more complicated queue management in our fork of kinsumer.

Examples of mechanisms we might use would be mutex or waitgroup.

what about the Go values that e.g. the goja runtimes wraps? what happens in the runtime global state?

I'm not sure I understand this one - but if it's what I think it is, it's basically the same scenario as above. In the Go runtime, we operate on a one-thread-per-event basis. Within a thread the value will be consistent. We might change that model but anything we do there necessarily has to manage concurrency appropriately (this is true independently of transformations).

As for the vm runtime - it's the same as the above issue - we need to manage the concurrency pattern to ensure we don't have conflicts.