dop251 / goja_nodejs

Nodejs compatibility library for Goja
MIT License
336 stars 81 forks source link

Resolving promises causes deadlock if the eventloop is stopped #80

Closed gbl08ma closed 3 months ago

gbl08ma commented 3 months ago

Consider the following scenario:

package main

import (
    "time"

    "github.com/dop251/goja"
    "github.com/dop251/goja_nodejs/eventloop"
)

func main() {
    loop := eventloop.NewEventLoop()
    loop.Start()

    c := make(chan struct{})
    loop.RunOnLoop(func(vm *goja.Runtime) {
        p, resolve, _ := vm.NewPromise()
        vm.Set("p", p)
        go func() {
            time.Sleep(500 * time.Millisecond)
            loop.RunOnLoop(func(*goja.Runtime) {
                resolve("result")
                c <- struct{}{}
            })
        }()
    })

    loop.Stop()
    <-c
}

This causes a deadlock because the resolve function returned by NewPromise blocks if, at the time it is called, the loop is stopped. (I assume it unblocks once the loop is resumed.) Now, this makes sense intuitively, and I'm not entirely sure this is a bug on its own, but I'm having trouble figuring out how to synchronize the stopping and disposal of an event loop that has pending promises, without leaving lingering goroutines behind stuck on resolve (and thus often leaving the associated goja runtime in memory forever). Do I need to synchronize the stopping of the loop with all callers of all promise's resolve function? I guess I was looking for a behavior that was more like trying to run scripts on an interrupted VM, where resolve would return immediately with an error (or at least we'd have the option for it to return immediately) if the loop is stopped.

gbl08ma commented 3 months ago

Actually, the real reason it deadlocks in the example I provided is because (obviously) the function that would unblock the channel never gets scheduled before the loop stops. I will need to keep tinkering to understand why in some scenarios there are goroutine leaks associated with promises in my larger program, whenever the goja environments are stopped in the middle of a Promise that is being computed on the Go side of things...