Skip to content

Commit 5b27ea6

Browse files
fix: tear down a failed runtime bootstrap so it can be retried
A bootstrap can fail inside native initialization or afterwards, when ts_helpers.js throws. Either way the whole native runtime is unwound: the isolate, its event loop entry, its crash breadcrumb slot and the BuildMetadata buffers and directory handle. A failed main runtime hands the election back, including its readiness, so a retry becomes the main runtime again. The unwind leaves never-initialized Persistents alone, V8 and the metadata tree are initialized once per process, and the inspector finds the main runtime whatever id its attempt was given.
1 parent 6d16eb2 commit 5b27ea6

7 files changed

Lines changed: 121 additions & 21 deletions

File tree

‎test-app/runtime/src/main/cpp/JsV8InspectorClient.cpp‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,11 @@ JsV8InspectorClient* JsV8InspectorClient::GetInstance() {
909909
// handleMessageOnSocketThread also calls this from the socket thread, so a
910910
// concurrent first call is possible: construct, then publish with a CAS and
911911
// discard our copy if another thread won the race.
912-
auto* created = new JsV8InspectorClient(Runtime::GetRuntime(0)->GetIsolate());
912+
Runtime* mainRuntime = Runtime::GetMainRuntime();
913+
if (mainRuntime == nullptr) {
914+
throw NativeScriptException("Cannot create the inspector: the main runtime is not initialized");
915+
}
916+
auto* created = new JsV8InspectorClient(mainRuntime->GetIsolate());
913917
JsV8InspectorClient* expected = nullptr;
914918
if (!instance.compare_exchange_strong(expected, created, std::memory_order_acq_rel,
915919
std::memory_order_acquire)) {

‎test-app/runtime/src/main/cpp/MetadataNode.cpp‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,6 +2042,8 @@ void MetadataNode::BuildMetadata(const string& filesPath) {
20422042
throw NativeScriptException(ss.str());
20432043
}
20442044
}
2045+
// Only opened to tell a missing folder from a missing file.
2046+
closedir(dir);
20452047

20462048
string nodesFile = baseDir + "/treeNodeStream.dat";
20472049
string namesFile = baseDir + "/treeStringsStream.dat";
@@ -2068,9 +2070,11 @@ void MetadataNode::BuildMetadata(const string& filesPath) {
20682070
<< "-byte records. The metadata is truncated or corrupt.";
20692071
throw NativeScriptException(ss.str());
20702072
}
2071-
char* nodes = new char[lenNodes];
2073+
// Owned until the reader takes them, so a file that fails to open further
2074+
// down does not strand the buffers already read.
2075+
std::unique_ptr<char[]> nodes(new char[lenNodes]);
20722076
rewind(f);
2073-
fread(nodes, 1, lenNodes, f);
2077+
fread(nodes.get(), 1, lenNodes, f);
20742078
fclose(f);
20752079

20762080
const int _512KB = 524288;
@@ -2085,9 +2089,9 @@ void MetadataNode::BuildMetadata(const string& filesPath) {
20852089
}
20862090
fseek(f, 0, SEEK_END);
20872091
int lenNames = ftell(f);
2088-
char* names = new char[lenNames + _512KB];
2092+
std::unique_ptr<char[]> names(new char[lenNames + _512KB]);
20892093
rewind(f);
2090-
fread(names, 1, lenNames, f);
2094+
fread(names.get(), 1, lenNames, f);
20912095
fclose(f);
20922096

20932097
f = fopen(valuesFile.c_str(), "rb");
@@ -2115,11 +2119,9 @@ void MetadataNode::BuildMetadata(const string& filesPath) {
21152119

21162120
DEBUG_WRITE("time=%ld", (millis2 - millis1));
21172121

2118-
BuildMetadata(lenNodes, reinterpret_cast<uint8_t*>(nodes), lenNames, reinterpret_cast<uint8_t*>(names), lenValues, reinterpret_cast<uint8_t*>(values));
2119-
2120-
delete[] nodes;
2121-
//delete[] names;
2122-
//delete[] values;
2122+
// The reader keeps the names and values buffers for the life of the
2123+
// process and only reads the nodes buffer while it builds the tree.
2124+
BuildMetadata(lenNodes, reinterpret_cast<uint8_t*>(nodes.get()), lenNames, reinterpret_cast<uint8_t*>(names.release()), lenValues, reinterpret_cast<uint8_t*>(values));
21232125
}
21242126

21252127
void MetadataNode::BuildMetadata(uint32_t nodesLength, uint8_t* nodeData, uint32_t nameLength, uint8_t* nameData, uint32_t valueLength, uint8_t* valueData) {

‎test-app/runtime/src/main/cpp/ObjectManager.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ class ObjectManager {
260260

261261
static jmethodID CHECK_WEAK_OBJECTS_ARE_ALIVE_METHOD_ID;
262262

263-
v8::Persistent<v8::Function>* m_poJsWrapperFunc;
263+
v8::Persistent<v8::Function>* m_poJsWrapperFunc = nullptr;
264264
};
265265
} // namespace tns
266266

‎test-app/runtime/src/main/cpp/Runtime.cpp‎

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ Runtime::~Runtime() {
313313
s_isolate2RuntimesCache.erase(it);
314314
}
315315
}
316+
// Same backstop for the breadcrumb slot Init took: the table is small and
317+
// fixed, so slots lost to failed bootstraps would crowd out live runtimes.
318+
CrashBreadcrumbs::UnregisterRuntime(m_id);
316319

317320
delete this->m_objectManager;
318321
// idempotent backstop for the matched erase WorkerWrapper does right after
@@ -712,9 +715,11 @@ void Runtime::ElectMainRuntime() {
712715
s_mainRuntimeElected = true;
713716
s_mainRuntimeFailed = false;
714717
m_isMainThread = true;
715-
// Once per process: V8::Initialize freezes the flag list, and setting a
716-
// flag afterwards aborts.
717-
InitializeV8();
718+
// Once per process, not once per election: a main runtime that failed
719+
// hands the election back, and V8 aborts both on a second
720+
// InitializePlatform and on a flag set after V8::Initialize froze the list.
721+
static std::once_flag v8Initialized;
722+
std::call_once(v8Initialized, InitializeV8);
718723
return;
719724
}
720725

@@ -735,16 +740,39 @@ void Runtime::SignalMainRuntimeReady(bool failed) {
735740
{
736741
std::lock_guard<std::mutex> lock(s_mainInitMutex);
737742
if (failed) {
738-
// Hand the election back so a later bootstrap can retry.
743+
// Hand the election back so a later bootstrap can retry. A main runtime
744+
// that already signalled readiness and failed afterwards withdraws it
745+
// too, so nothing waiting on the next main runtime starts against this
746+
// one.
739747
s_mainRuntimeElected = false;
740748
s_mainRuntimeFailed = true;
749+
s_mainThreadInitialized.store(false, std::memory_order_release);
741750
} else {
742751
s_mainThreadInitialized.store(true, std::memory_order_release);
743752
}
744753
}
745754
s_mainInitReady.notify_all();
746755
}
747756

757+
void Runtime::UnwindFailedBootstrap(int runtimeId) {
758+
Runtime* runtime = nullptr;
759+
{
760+
std::lock_guard<std::mutex> lock(s_runtimeCacheMutex);
761+
auto it = s_id2RuntimeCache.find(runtimeId);
762+
if (it != s_id2RuntimeCache.end()) {
763+
runtime = it->second;
764+
}
765+
}
766+
if (runtime == nullptr) {
767+
return;
768+
}
769+
// Only the bootstrapping thread can reach this runtime: no application JS
770+
// has run on it, so nothing has handed it to another thread or started a
771+
// worker from it.
772+
runtime->UnwindFailedInit();
773+
delete runtime;
774+
}
775+
748776
void Runtime::UnwindFailedInit() {
749777
/*
750778
* Reuses the two teardown windows rather than adding a third cleanup path.
@@ -760,6 +788,12 @@ void Runtime::UnwindFailedInit() {
760788
DestroyRuntime();
761789
}
762790
m_isolate->Dispose();
791+
// The ~Runtime backstop keys on m_isolate, which is cleared below, so the
792+
// platform's loop entry has to go here. Left behind, it would hand the
793+
// stopped loop to the next isolate allocated at this address.
794+
if (m_eventLoop != nullptr) {
795+
NativeScriptPlatform::Instance()->IsolateDisposed(m_isolate, m_eventLoop);
796+
}
763797
m_isolate = nullptr;
764798
}
765799

@@ -1071,7 +1105,15 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath,
10711105
// Do not build metadata (which should be static for the process) for non-main
10721106
// threads
10731107
if (m_isMainThread) {
1074-
MetadataNode::BuildMetadata(filesPath);
1108+
// Once per process, like V8 itself: the tree is process-wide state that
1109+
// outlives the runtime that built it, so a main runtime elected after an
1110+
// earlier one failed past this point reads the tree already there. Only
1111+
// the elected main runtime gets here, one at a time.
1112+
static bool metadataBuilt = false;
1113+
if (!metadataBuilt) {
1114+
MetadataNode::BuildMetadata(filesPath);
1115+
metadataBuilt = true;
1116+
}
10751117
}
10761118

10771119
auto enableProfiler = !profilerOutputDir.empty();
@@ -1089,6 +1131,7 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath,
10891131
s_currentRuntime = this;
10901132

10911133
if (m_isMainThread) {
1134+
s_mainRuntime.store(this, std::memory_order_release);
10921135
// Releases any runtime waiting in ElectMainRuntime: the metadata tree and
10931136
// the main event loop they depend on are published by now.
10941137
SignalMainRuntimeReady(false /* failed */);
@@ -1164,6 +1207,9 @@ void Runtime::DestroyRuntime() {
11641207
if (s_currentRuntime == this) {
11651208
s_currentRuntime = nullptr;
11661209
}
1210+
Runtime* self = this;
1211+
s_mainRuntime.compare_exchange_strong(self, nullptr,
1212+
std::memory_order_acq_rel);
11671213
// The events state holds v8::Global handles (backing event target, dispatch
11681214
// closures and tracked promise rejections) - reset them while the isolate
11691215
// is still alive.
@@ -1251,6 +1297,7 @@ bool Runtime::s_mainRuntimeFailed = false;
12511297
v8::Platform* Runtime::platform = nullptr;
12521298
int Runtime::m_androidVersion = Runtime::GetAndroidVersion();
12531299
std::shared_ptr<EventLoop> Runtime::s_mainEventLoop;
1300+
std::atomic<Runtime*> Runtime::s_mainRuntime{nullptr};
12541301

12551302
thread_local Runtime* Runtime::s_currentRuntime = nullptr;
12561303
thread_local PendingIsolateSetup Runtime::s_pendingIsolateSetup;

‎test-app/runtime/src/main/cpp/Runtime.h‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ class Runtime {
8282
*/
8383
static void SetPendingIsolateSetup(PendingIsolateSetup setup);
8484

85+
/*
86+
* Tears down the native runtime of a bootstrap that failed on the
87+
* Java side after initNativeScript returned, such as a throwing
88+
* ts_helpers.js. No-op when no native runtime is registered under the
89+
* id, which is the case for a bootstrap that failed inside Init.
90+
*/
91+
static void UnwindFailedBootstrap(int runtimeId);
92+
8593
static Runtime* GetRuntime(int runtimeId);
8694

8795
static Runtime* GetRuntime(v8::Isolate* isolate);
@@ -202,6 +210,16 @@ class Runtime {
202210
static std::shared_ptr<EventLoop> GetMainEventLoop() {
203211
return s_mainEventLoop;
204212
}
213+
214+
/*
215+
* The main runtime, or null while there is none: before it finishes
216+
* initializing and after it is destroyed. Its id is whatever its
217+
* bootstrap attempt was handed, which is 0 only when the first attempt
218+
* succeeded.
219+
*/
220+
static Runtime* GetMainRuntime() {
221+
return s_mainRuntime.load(std::memory_order_acquire);
222+
}
205223
static JavaVM* GetJVM() {
206224
return s_jvm;
207225
}
@@ -351,7 +369,7 @@ class Runtime {
351369
v8::Persistent<v8::Function>* m_gcFunc;
352370
volatile bool m_runGC;
353371

354-
v8::Persistent<v8::Context>* m_context;
372+
v8::Persistent<v8::Context>* m_context = nullptr;
355373

356374
// Decided by ElectMainRuntime, before anything can read it.
357375
bool m_isMainThread = false;
@@ -383,10 +401,11 @@ class Runtime {
383401
static void SignalMainRuntimeReady(bool failed);
384402

385403
/*
386-
* Unwinds an initialization that threw after the isolate existed. The
387-
* Java-side rollback only unwinds Java state, which would otherwise
388-
* leave the isolate in the runtime caches and the half-built Runtime
389-
* holding everything it had allocated.
404+
* Unwinds an initialization that threw after the isolate existed, or
405+
* one that finished and then failed on the Java side. The Java-side
406+
* rollback only unwinds Java state, which would otherwise leave the
407+
* isolate in the runtime caches and the Runtime holding everything it
408+
* had allocated.
390409
*/
391410
void UnwindFailedInit();
392411
jobject ConvertJsValueToJavaObject(JEnv& env, const v8::Local<v8::Value>& value, int classReturnType);
@@ -422,6 +441,7 @@ class Runtime {
422441
static bool s_mainRuntimeFailed;
423442

424443
static std::shared_ptr<EventLoop> s_mainEventLoop;
444+
static std::atomic<Runtime*> s_mainRuntime;
425445

426446
static thread_local Runtime* s_currentRuntime;
427447
static thread_local PendingIsolateSetup s_pendingIsolateSetup;

‎test-app/runtime/src/main/cpp/com_tns_Runtime.cpp‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_initNativeScript(JNIEnv* _env, jo
107107
}
108108
}
109109

110+
extern "C" JNIEXPORT void Java_com_tns_Runtime_unwindFailedBootstrap(JNIEnv* _env, jclass clazz, jint runtimeId) {
111+
try {
112+
Runtime::UnwindFailedBootstrap(runtimeId);
113+
} catch (NativeScriptException& e) {
114+
e.ReThrowToJava();
115+
} catch (std::exception e) {
116+
stringstream ss;
117+
ss << "Error: c++ exception: " << e.what() << endl;
118+
NativeScriptException nsEx(ss.str());
119+
nsEx.ReThrowToJava();
120+
} catch (...) {
121+
NativeScriptException nsEx(std::string("Error: c++ exception!"));
122+
nsEx.ReThrowToJava();
123+
}
124+
}
125+
110126
Runtime* TryGetRuntime(int runtimeId) {
111127
Runtime* runtime = nullptr;
112128
try {

‎test-app/runtime/src/main/java/com/tns/Runtime.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ private native void initNativeScript(int runtimeId, String filesPath, String nat
4242

4343
private native Object runScript(int runtimeId, String filePath) throws NativeScriptException;
4444

45+
private static native void unwindFailedBootstrap(int runtimeId);
46+
4547
private native Object callJSMethodNative(int runtimeId, int javaObjectID, String methodName, int retType, boolean isConstructor, Object... packagedArgs) throws NativeScriptException;
4648

4749
private native void createJSInstanceNative(int runtimeId, Object javaObject, int javaObjectID, String canonicalName);
@@ -602,6 +604,15 @@ private static Runtime initRuntime(DynamicConfiguration dynamicConfiguration) {
602604
runtimeCache.remove(runtime.getRuntimeId());
603605
currentRuntime.remove();
604606
GcListener.unsubscribe(runtime);
607+
// ts_helpers.js runs after the native runtime is fully built, so a
608+
// failure there leaves the isolate behind unless it is torn down
609+
// here as well; after the unsubscribe, so no GC notification can
610+
// still be running against it
611+
try {
612+
unwindFailedBootstrap(runtime.getRuntimeId());
613+
} catch (Throwable unwindError) {
614+
t.addSuppressed(unwindError);
615+
}
605616
throw t;
606617
}
607618

0 commit comments

Comments
 (0)