We have a REST web service written in TypeScript that runs on Node, and a large legacy C++ library that provides services for the web service. We use NAPI to provide JS bindings for the C++ library, basically it’s a NAPI plugin. This has worked well now for several years.
We have recently added support for timeouts in the REST service (calls to the C++ library can be very slow for complex data). To do this we use the workerpool package which allows us to do the actual work in a Node worker thread. Our NAPI plugin is now being called from the worker thread.
Mostly this works perfectly, however we have found a single situation where we get this error and stack from our service:
FATAL ERROR: v8::HandleScope::CreateHandle() Cannot create a handle without a HandleScope
----- Native stack trace -----
1: 0xb80b5c node::OnFatalError(char const*, char const*) [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
2: 0xeedd86 v8::Utils::ReportApiFailure(char const*, char const*) [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
3: 0x1093f52 v8::internal::HandleScope::Extend(v8::internal::Isolate*) [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
4: 0xeef668 v8::EscapableHandleScope::EscapableHandleScope(v8::Isolate*) [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
5: 0xc5ba9a napi_open_escapable_handle_scope [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
6: 0x7ffff7c6065a Napi::Value NapiHelpers::CreateWrapperInstance<RedactedObjectNodeWrapper>(std::initializer_list<napi_value__*> const&) [/workspaces/Redacted-Web/node_modules/@redacted/Redactedsdk-node/dist/linux/Redactedsdk.node]
7: 0x7ffff7c60b2c Napi::Array NapiHelpers::CreateArrayOfWrappers<RedactedObjectNodeWrapper, RedactedSDK::RedactedObject>(Napi::CallbackInfo const&, std::vector<RedactedSDK::RedactedObject, std::allocator<RedactedSDK::RedactedObject> > const&) [/workspaces/Redacted-Web/node_modules/@redacted/Redactedsdk-node/dist/linux/Redactedsdk.node]
8: 0x7ffff7c5afbb [/workspaces/Redacted-Web/node_modules/@redacted/Redactedsdk-node/dist/linux/Redactedsdk.node]
9: 0x7ffff7c400d4 Napi::Value NapiHelpers::WithNapiScope<Napi::Value>(Napi::CallbackInfo const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<Napi::Value ()>) [/workspaces/Redacted-Web/node_modules/@redacted/Redactedsdk-node/dist/linux/Redactedsdk.node]
10: 0x7ffff7c5a448 DrawingNodeWrapper::GetRedactedObjects(Napi::CallbackInfo const&) [/workspaces/Redacted-Web/node_modules/@redacted/Redactedsdk-node/dist/linux/Redactedsdk.node]
11: 0x7ffff7c5e506 Napi::InstanceWrap<DrawingNodeWrapper>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*) [/workspaces/Redacted-Web/node_modules/@redacted/Redactedsdk-node/dist/linux/Redactedsdk.node]
12: 0xc4f059 [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
13: 0xf565df v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo) [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
14: 0xf56e4d [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
15: 0xf57315 v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
16: 0x1961df6 [/home/jenkins/.nvm/versions/node/v20.15.0/bin/node]
----- JavaScript stack trace -----
1: unstringifyRedactedObject (/workspaces/Redacted-Web/applications/Redacted-web-service/dist/RedactedObjectFactory.js:76:20)
2: calculateProperties (/workspaces/Redacted-Web/applications/Redacted-web-service/dist/workers/PropertyCalculatorWorkers.js:18:52)
3: /workspaces/Redacted-Web/node_modules/workerpool/src/worker.js:150:27
4: [nodejs.internal.kHybridDispatch] (node:internal/event_target:820:20)
5: node:internal/per_context/messageport:23:28
Our C++ library is thread safe (we lock a global mutex at every entry point), and this has been used in multi-threaded C++ and C# applications.
NapiHelpers::WithNapiScope
creates a HandleScope
:
template<class T>
T NapiHelpers::WithNapiScope(
const Napi::CallbackInfo& info,
const std::string& unknownErrorMessage,
std::function<T()> callback)
{
Napi::Env env = info.Env();
Napi::HandleScope scope(env);
try
{
return callback();
}
catch (const ChemDrawSDK::Exception& e)
{
Napi::Error error = Napi::Error::New(info.Env(), e.what());
error.Value().DefineProperty(Napi::PropertyDescriptor::Value("code", Napi::Number::New(info.Env(), e.GetErrorCode()), napi_default));
throw error;
}
catch (const std::exception& e)
{
throw Napi::Error::New(env, e.what());
}
catch (...)
{
throw Napi::Error::New(env, unknownErrorMessage);
}
}
Later on in the stack though, the HandleScope
is no longer valid. This feels like a reentrancy problem (where some other thread has changed the handle scope), however I would expect to see this problem a lot more than we have if that was the case.
Does anyone know what is happening here? In particular is it possible to safely call a NAPI plugin from a Node worker thread like this? Is there anything we need to do to isolate handle scopes to a particular thread?