Search code examples
c++node.jsc++11v8node.js-addon

Blocking calls in a Node.js Addon


I'm developing a Node.js application that incorporates a Windows DLL. The DLL manages scientific equipment, for context.

My interface from Node to the DLL is going well, however the DLL has some non-deterministic calls that depend on the network topology and RF signals in the room. These calls can take anywhere from 10 seconds to 10 minutes.

I'd like to get these calls off Node's event loop, and even avoid AsyncWorkers. I'd like to put them in their own C++ threads. I'm worried that I don't know enough Node/V8 to approach the problem correctly, though I've attempted twice now.

Below is my attempt at spawning a thread to call a js callback, though I'm not sure if this is a good approach. I need the result of the call, and what I have so far is a 'daemon' in my node app that checks on a regular interval to retrieve results for completed tasks.

mTp in the snippet below is a threadpool implementation I've written. Runtask takes a C++ lambda as a parameter to be pushed onto my worker thread queue. mThreadStatus is a map from my thread 'handle', which is a string, to thread_status_t enum. mThreadResults is another map from the thread handle to a v8::Value that gets returned by the callback.

void
MyObj::SpawnThread(functionInput info) {
    MyObj* obj = ObjectWrap::Unwrap<MyObj>(info.Holder());      
    obj->mTp.RunTask([&]() {
        v8::Isolate::CreateParams cp;
        v8::Isolate* tpIsolate = v8::Isolate::New(cp);
        v8::Locker locker(tpIsolate);
        v8::Isolate::Scope isolateScope(tpIsolate);

        Nan::HandleScope scope;

        auto global = obj->mContext.Get(tpIsolate)->Global();

        auto handle = std::string(*v8::String::Utf8Value(info[0]->ToString()));

        {
            std::unique_lock<std::shared_mutex> lock(obj->mThreadStatusMutex);
            obj->mThreadStatus[handle] = thread_status_t::running;
        }

        v8::Handle<v8::Function> f = v8::Handle<v8::Function>::Cast(info[1]);
        v8::TryCatch trycatch(tpIsolate);
        v8::Handle<v8::Value> result = f->Call(global, 0, nullptr);
        if (result.IsEmpty()) {
            v8::Local<v8::Value> exception = trycatch.Exception();
            std::unique_lock<std::shared_mutex> lock(obj->mThreadStatusMutex);
            obj->mThreadStatus[handle] = thread_status_t::error;
            return;
        }

        {
            std::unique_lock<std::shared_mutex> resultLock(obj->mThreadResultsMutex);
            obj->mThreadResults[handle] = result;
        }

        std::unique_lock<std::shared_mutex> lock(obj->mThreadStatusMutex);
        obj->mThreadStatus[handle] = completed;

        tpIsolate->Dispose();
    });

I'm envisioning my js looking like this to spawn a thread:

var ctx = this
this.myObj.spawnThread('startMeasurements', () => {
    return ctx.myObj.startMeasurements()
})

And like this to get the result, in my 'daemon':

var status = this.myObj.getThreadStatus('startMeasurements')
if ( status === 'complete') {
    // Publish returned information to front-end
}
else if (status === 'error') {
    // Handle error
}

Has anyone solved this problem before? Does this look like a decent approach? Help with v8 is greatly appreciated. Thank you!


Solution

  • I have not solved a similar problem before, but the general way I would go about it is:

    • let the JavaScript code be oblivious of the threading
    • expose a function getMeasurements(callback) to JavaScript, implemented in C++
    • when the function is called, it gets itself a thread (either newly created, or from the pool) and instructs it to do the blocking external call; when that call is completed the thread signals its result to the main thread, which invokes the callback with it.
    • that way all communication with JavaScript code (i.e. all interaction with V8) happens on the main thread, and you only use background threads for the blocking calls.

    I hope this helps!