Skip to content

Commit

Permalink
Improve the comments for EventLoop.RegisterCallback() (#2615)
Browse files Browse the repository at this point in the history
  • Loading branch information
na-- authored Aug 9, 2022
1 parent ff2b8b8 commit 5b70e70
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 18 deletions.
62 changes: 57 additions & 5 deletions js/eventloop/eventloop.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,63 @@ func (e *EventLoop) wakeup() {
}
}

// RegisterCallback register that a callback will be invoked on the loop, preventing it from returning/finishing.
// The returned function, upon invocation, will queue its argument and wakeup the loop if needed.
// If the eventLoop has since stopped, it will not be executed.
// This function *must* be called from within running on the event loop, but its result can be called from anywhere.
func (e *EventLoop) RegisterCallback() func(func() error) {
// RegisterCallback signals to the event loop that you are going to do some
// asynchronous work off the main thread and that you may need to execute some
// code back on the main thread when you are done. So, once you call this
// method, the event loop will wait for you to finish and give it the callback
// it needs to run back on the main thread before it can end the whole current
// script iteration.
//
// RegisterCallback() *must* be called from the main runtime thread, but its
// result enqueueCallback() is thread-safe and can be called from any goroutine.
// enqueueCallback() ensures that its callback parameter is added to the VU
// runtime's tasks queue, to be executed on the main runtime thread eventually,
// when the VU is done with the other tasks before it. Unless the whole event
// loop has been stopped, invoking enqueueCallback() will queue its argument and
// "wake up" the loop (if it was idle, but not stopped).
//
// Keep in mind that once you call RegisterCallback(), you *must* also call
// enqueueCallback() exactly once, even if don't actually need to run any code
// on the main thread. If that's the case, you can pass an empty no-op callback
// to it, but you must call it! The event loop will wait for the
// enqueueCallback() invocation and the k6 iteration won't finish and will be
// stuck until the VU itself has been stopped (e.g. because the whole test or
// scenario has ended). Any error returned by any callback on the main thread
// will abort the current iteration and no further event loop callbacks will be
// executed in the same iteration.
//
// A common pattern for async work is something like this:
//
// func doAsyncWork(vu modules.VU) *goja.Promise {
// enqueueCallback := vu.RegisterCallback()
// p, resolve, reject := vu.Runtime().NewPromise()
//
// // Do the actual async work in a new independent goroutine, but make
// // sure that the Promise resolution is done on the main thread:
// go func() {
// // Also make sure to abort early if the context is cancelled, so
// // the VU is not stuck when the scenario ends or Ctrl+C is used:
// result, err := doTheActualAsyncWork(vu.Context())
// enqueueCallback(func() error {
// if err != nil {
// reject(err)
// } else {
// resolve(result)
// }
// return nil // do not abort the iteration
// })
// }()
//
// return p
// }
//
// This ensures that the actual work happens asynchronously, while the Promise
// is immediately returned and the main thread resumes execution. It also
// ensures that the Promise resolution happens safely back on the main thread
// once the async work is done, as required by goja and all other JS runtimes.
//
// TODO: rename to ReservePendingCallback or something more appropriate?
func (e *EventLoop) RegisterCallback() (enqueueCallback func(func() error)) {
e.lock.Lock()
var callbackCalled bool
e.registeredCallbacks++
Expand Down
20 changes: 7 additions & 13 deletions js/modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,14 @@ type VU interface {
// Runtime returns the goja.Runtime for the current VU
Runtime() *goja.Runtime

// RegisterCallback lets a module declare that it wants to run a function on the event loop *at a later point in time*.
// It needs to be called from within the event loop, so not in a goroutine spawned by a module.
// Its result can be called with a function that will be executed *on the event loop* -
// possibly letting you call RegisterCallback again.
// Calling the result can be done at any time. The event loop will block until that happens, (if it doesn't
// have something else to run) so in the event of an iteration end or abort (for example due to an exception),
// It is the module responsibility to monitor the context and abort on it being done.
// This still means that the returned function here *needs* to be called to signal that the module
// has aborted the operation and will not do anything more, not doing so will block k6.
// RegisterCallback lets a JS module declare that it wants to run a function
// on the event loop *at a later point in time*. See the documentation for
// `EventLoop.RegisterCallback()` in the `k6/js/eventloop` Go module for
// the very important details on its usage and restrictions.
//
// Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a later release.
RegisterCallback() func(func() error)
// Notice: This API is EXPERIMENTAL and may be changed, renamed or
// completely removed in a later k6 release.
RegisterCallback() (enqueueCallback func(func() error))

// sealing field will help probably with pointing users that they just need to embed this in their Instance
// implementations
Expand Down

0 comments on commit 5b70e70

Please sign in to comment.