Skip to content

Commit 1d1f8af

Browse files
committed
fix(worker): add cleanup hooks and exception handling
Implement environment cleanup hooks for all N-API references to prevent SIGSEGV crashes on Alpine/musl during worker thread termination. Move ValueStorage from static globals to per-environment storage for proper worker isolation. Wrap JavaScript callbacks in comprehensive try-catch to prevent C++ exceptions from crossing C boundaries into SQLite. Follows Node-API best practices for context-aware addons per official node-addon-examples documentation. Resolves Alpine/musl instability.
1 parent 10cfc93 commit 1d1f8af

File tree

7 files changed

+306
-85
lines changed

7 files changed

+306
-85
lines changed

src/aggregate_function.cpp

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,68 @@ static constexpr int64_t kMaxSafeJsInteger = 9007199254740991LL;
1515

1616
namespace photostructure::sqlite {
1717

18-
// Static member definitions for ValueStorage
19-
std::unordered_map<int32_t, Napi::Reference<Napi::Value>>
20-
ValueStorage::storage_;
21-
std::mutex ValueStorage::mutex_;
22-
std::atomic<int32_t> ValueStorage::next_id_{0};
23-
2418
// ValueStorage implementation
25-
int32_t ValueStorage::Store([[maybe_unused]] Napi::Env env, Napi::Value value) {
26-
const std::lock_guard<std::mutex> lock(mutex_);
27-
const int32_t id = ++next_id_;
28-
29-
// Try direct napi_value persistence instead of Napi::Reference
19+
int32_t ValueStorage::Store(Napi::Env env, Napi::Value value) {
20+
AddonData *addon_data = GetAddonData(env);
21+
if (!addon_data)
22+
throw Napi::Error::New(env, "Addon data not found");
23+
24+
std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
25+
const int32_t id =
26+
addon_data->next_value_id.fetch_add(1, std::memory_order_relaxed);
3027
try {
31-
storage_[id] = Napi::Reference<Napi::Value>::New(value, 1);
28+
addon_data->value_storage[id] = Napi::Reference<Napi::Value>::New(value, 1);
3229
} catch (...) {
3330
// If Reference creation fails, throw to let caller handle
3431
throw;
3532
}
36-
3733
return id;
3834
}
3935

4036
Napi::Value ValueStorage::Get(Napi::Env env, int32_t id) {
41-
const std::lock_guard<std::mutex> lock(mutex_);
42-
auto it = storage_.find(id);
43-
return (it != storage_.end()) ? it->second.Value() : env.Undefined();
37+
AddonData *addon_data = GetAddonData(env);
38+
if (!addon_data)
39+
return env.Null();
40+
41+
std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
42+
auto it = addon_data->value_storage.find(id);
43+
if (it == addon_data->value_storage.end() || it->second.IsEmpty()) {
44+
return env.Null();
45+
}
46+
return it->second.Value();
4447
}
4548

46-
void ValueStorage::Remove(int32_t id) {
47-
const std::lock_guard<std::mutex> lock(mutex_);
48-
auto it = storage_.find(id);
49-
if (it != storage_.end()) {
50-
// Don't call Reset() explicitly - let the Napi::Reference destructor
51-
// handle cleanup naturally when erased. Explicit Reset() during GC
52-
// causes JIT corruption on Alpine/musl.
53-
// See: nodejs/node-addon-api#660, P02-investigate-flaky-native-crashes.md
54-
storage_.erase(it);
49+
void ValueStorage::Remove(Napi::Env env, int32_t id) {
50+
AddonData *addon_data = GetAddonData(env);
51+
if (!addon_data)
52+
return;
53+
54+
std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
55+
auto it = addon_data->value_storage.find(id);
56+
if (it != addon_data->value_storage.end()) {
57+
// Don't call Reset() here - it's unsafe during environment teardown on
58+
// musl. The Cleanup() hook will reset all remaining references safely. For
59+
// refs removed during normal operation, they'll be cleaned up by GC.
60+
addon_data->value_storage.erase(it);
5561
}
5662
}
5763

64+
void CustomAggregate::CleanupHook(void *arg) {
65+
// Called before environment teardown - safe to Reset() references here
66+
auto *self = static_cast<CustomAggregate *>(arg);
67+
68+
if (!self->start_fn_.IsEmpty())
69+
self->start_fn_.Reset();
70+
if (!self->object_ref_.IsEmpty())
71+
self->object_ref_.Reset();
72+
if (!self->step_fn_.IsEmpty())
73+
self->step_fn_.Reset();
74+
if (!self->inverse_fn_.IsEmpty())
75+
self->inverse_fn_.Reset();
76+
if (!self->result_fn_.IsEmpty())
77+
self->result_fn_.Reset();
78+
}
79+
5880
CustomAggregate::CustomAggregate(Napi::Env env, DatabaseSync *db,
5981
bool use_bigint_args, Napi::Value start,
6082
Napi::Function step_fn,
@@ -99,14 +121,22 @@ CustomAggregate::CustomAggregate(Napi::Env env, DatabaseSync *db,
99121
result_fn_ = Napi::Reference<Napi::Function>::New(result_fn, 1);
100122
}
101123

124+
// Register cleanup hook to Reset() references before environment teardown.
125+
// This is required for worker thread support per Node-API best practices.
126+
// See:
127+
// https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/
128+
napi_add_env_cleanup_hook(env_, CleanupHook, this);
129+
102130
// Don't create async context immediately - we'll create it lazily if needed
103131
async_context_ = nullptr;
104132
}
105133

106134
CustomAggregate::~CustomAggregate() {
107-
// Let Napi::FunctionReference destructors handle cleanup naturally.
108-
// Explicitly calling Reset() during GC causes JIT corruption on Alpine/musl.
109-
// See: nodejs/node-addon-api#660, P02-investigate-flaky-native-crashes.md
135+
// Remove cleanup hook if still registered
136+
napi_remove_env_cleanup_hook(env_, CleanupHook, this);
137+
138+
// Don't call Reset() on references here - CleanupHook already handled them,
139+
// or the environment is being torn down and Reset() would be unsafe.
110140

111141
// Check if environment is still valid before N-API operations.
112142
napi_handle_scope scope;
@@ -132,7 +162,10 @@ void CustomAggregate::xInverse(sqlite3_context *ctx, int argc,
132162
xStepBase(ctx, argc, argv, &CustomAggregate::inverse_fn_);
133163
}
134164

135-
void CustomAggregate::xFinal(sqlite3_context *ctx) { xValueBase(ctx, true); }
165+
void CustomAggregate::xFinal(sqlite3_context *ctx) {
166+
xValueBase(ctx, true);
167+
DestroyAggregateData(ctx);
168+
}
136169

137170
void CustomAggregate::xValue(sqlite3_context *ctx) { xValueBase(ctx, false); }
138171

@@ -556,13 +589,16 @@ CustomAggregate::GetAggregate(sqlite3_context *ctx) {
556589
}
557590

558591
void CustomAggregate::DestroyAggregateData(sqlite3_context *ctx) {
592+
CustomAggregate *self =
593+
static_cast<CustomAggregate *>(sqlite3_user_data(ctx));
559594
AggregateData *agg = static_cast<AggregateData *>(
560595
sqlite3_aggregate_context(ctx, sizeof(AggregateData)));
561596

562-
if (agg && agg->initialized) {
563-
ValueStorage::Remove(agg->value_id);
564-
agg->initialized = false;
597+
if (!self || !agg || !agg->initialized) {
598+
return;
565599
}
600+
ValueStorage::Remove(self->env_, agg->value_id);
601+
agg->initialized = false;
566602
}
567603

568604
Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {

src/aggregate_function.h

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,10 @@ class DatabaseSync;
1919
// Thread-safe external storage for N-API values
2020
// This solves the problem of storing N-API objects in SQLite-allocated memory
2121
class ValueStorage {
22-
private:
23-
static std::unordered_map<int32_t, Napi::Reference<Napi::Value>> storage_;
24-
static std::mutex mutex_;
25-
static std::atomic<int32_t> next_id_;
26-
2722
public:
2823
static int32_t Store(Napi::Env env, Napi::Value value);
2924
static Napi::Value Get(Napi::Env env, int32_t id);
30-
static void Remove(int32_t id);
25+
static void Remove(Napi::Env env, int32_t id);
3126
};
3227

3328
class CustomAggregate {
@@ -46,6 +41,8 @@ class CustomAggregate {
4641
static void xDestroy(void *self);
4742

4843
private:
44+
// Environment cleanup hook - called before environment teardown
45+
static void CleanupHook(void *arg);
4946
// Comprehensive aggregate value storage (no Napi::Reference)
5047
struct AggregateValue {
5148
enum Type {

src/binding.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <napi.h>
33
#include <set>
44

5+
#include "aggregate_function.h"
56
#include "sqlite_impl.h"
67

78
namespace photostructure::sqlite {
@@ -17,6 +18,17 @@ void CleanupAddonData([[maybe_unused]] napi_env env, void *finalize_data,
1718
addon_data->databases.clear();
1819
}
1920

21+
// Clean up ValueStorage references
22+
{
23+
std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
24+
for (auto &[id, ref] : addon_data->value_storage) {
25+
if (!ref.IsEmpty()) {
26+
ref.Reset();
27+
}
28+
}
29+
addon_data->value_storage.clear();
30+
}
31+
2032
// Let Napi::FunctionReference destructors handle cleanup naturally.
2133
// Explicitly calling Reset() during worker termination causes JIT corruption
2234
// on Alpine/musl. The references will be cleaned up when addon_data is

0 commit comments

Comments
 (0)