This document describes the worker thread support implementation for @photostructure/sqlite.
Worker thread support is fully implemented and working in @photostructure/sqlite. The implementation provides complete thread safety through per-worker instance data management and thread validation, ensuring that database connections cannot be shared between threads.
- Full worker thread compatibility with no special initialization required
- Automatic per-worker instance data management
- Thread safety validation prevents cross-thread object usage
- Proper cleanup when worker threads terminate
- Clear error messages when thread violations occur
- 100% test success rate with full test coverage
SQLite supports three threading modes:
- Single-thread (
SQLITE_THREADSAFE=0): No thread safety, one thread only - Multi-thread (
SQLITE_THREADSAFE=2): Multiple threads, separate connections per thread - Serialized (
SQLITE_THREADSAFE=1): Full thread safety with mutexes (default) CURRENT
Current Configuration: Using the default SQLITE_THREADSAFE=1 (serialized mode). binding.gyp does not define SQLITE_THREADSAFE, so SQLite's own default applies. This provides full thread safety with mutex protection, which is safe for worker threads where each thread creates its own database connection.
Serialized mode is not merely a conservative default here — db.backup() depends on it. BackupJob steps the backup against the source connection on a libuv worker thread while the main thread may still be using that same connection, and SQLite only permits that "if SQLite is compiled and configured to support threadsafe database connections" (see the Online Backup API notes in sqlite3.h). Switching to SQLITE_THREADSAFE=2 would make every concurrent use during a backup a data race. If an async API ever needs to drop the per-connection mutex, do it per connection with SQLITE_OPEN_NOMUTEX at open time, not by changing the compile-time mode.
- Context-Aware Addons: N-API provides automatic context awareness
- Instance Data Management: Implemented using
napi_set_instance_datainsrc/binding.cpp - Cleanup Hooks: Implemented
CleanupAddonDatafor worker termination - Connection Isolation: Thread ID validation prevents cross-thread connection sharing
- Thread-Safe Functions: Not implemented (not required for basic worker thread support)
What was fixed:
The root cause was static Napi::FunctionReference constructors that were shared across all worker threads but belonged to the main thread's V8 isolate. When worker threads tried to access these static constructors, it caused the "HandleScope::HandleScope Entering the V8 API without proper locking in place" error.
The solution:
- Removed all static constructors from DatabaseSync, StatementSync, StatementSyncIterator, and Session classes
- Moved constructors to per-instance AddonData that is properly initialized for each worker thread context
- Updated all constructor usage to retrieve constructors from the addon data instead of static variables
- Added thread ID tracking to DatabaseSync and StatementSync objects
- Implemented ValidateThread() method to ensure objects are only used from their creation thread
Each worker thread has its own AddonData instance managed through N-API:
struct AddonData {
Napi::FunctionReference database_sync_constructor;
Napi::FunctionReference statement_sync_constructor;
Napi::FunctionReference statement_sync_iterator_constructor;
Napi::FunctionReference session_constructor;
DatabaseMap database_map;
std::mutex database_map_mutex;
};This data is:
- Created when the module is loaded in each worker thread
- Associated with the worker using
napi_set_instance_data - Automatically cleaned up via
CleanupAddonDatawhen the worker terminates
Each database and statement object tracks its creation thread:
class DatabaseSync {
std::thread::id creation_thread_;
void ValidateThread() {
if (std::this_thread::get_id() != creation_thread_) {
throw Napi::Error("Database objects cannot be used from different thread");
}
}
};This prevents cross-thread usage and provides clear error messages when violations occur
Connection Isolation Pattern (Recommended):
- Each worker thread creates its own Database instance
- No sharing of
sqlite3*handles between threads - Each thread's connections are completely independent
Implementation pattern:
// main.js
const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js", {
workerData: { dbPath: "./data.db" },
});
// worker.js
const { Database } = require("@photostructure/sqlite");
const { workerData } = require("worker_threads");
// Each worker creates its own connection
const db = new Database(workerData.dbPath);
// ... perform operations
db.close(); // Clean up when done-
Per-Worker Instance Data:
struct WorkerData { napi_env env; std::vector<DatabaseSync*> databases; std::vector<StatementSync*> statements; };
-
Cleanup Sequence:
- Worker thread termination triggers cleanup hook
- All statements are finalized
- All database connections are closed
- Instance data is deallocated
-
Reference Management:
- Use weak references where possible to prevent cycles
- Track object dependencies for proper cleanup order
- Implement RAII patterns for automatic cleanup
Cross-thread usage detection:
class DatabaseSync {
std::thread::id creation_thread_;
void ValidateThread() {
if (std::this_thread::get_id() != creation_thread_) {
throw std::runtime_error("Database connection cannot be used from different thread");
}
}
};Error categories:
- Thread Affinity Errors: Using objects from wrong thread
- Lifecycle Errors: Using objects after worker termination
- Resource Errors: Memory/handle leaks during cleanup
-
Basic Functionality:
- Create database in worker thread
- Execute SQL operations
- Clean shutdown
-
Concurrency Tests:
- Multiple workers accessing same file
- Concurrent read/write operations
- Worker thread pools
-
Error Scenarios:
- Cross-thread usage attempts
- Premature worker termination
- Resource exhaustion
-
Memory Tests:
- Valgrind leak detection
- Stress testing with many workers
- Long-running worker scenarios
test/worker-threads-simple.test.ts- Basic functionality teststest/worker-threads-simple-init.test.ts- Worker initialization teststest/worker-threads-initialization.test.ts- Module initialization in workerstest/worker-threads-error.test.ts- Error handling and cross-thread validation tests
- SQLite Thread Safety: Well-established multi-thread mode
- N-API Context Awareness: Proven technology
- Basic Pattern: Simple connection-per-thread model
- Current Segfaults: Need investigation but likely solvable
- Resource Management: Requires careful implementation
- Test Coverage: Need thorough validation
- Production Deployment: Should thoroughly test before release
- Performance Impact: Thread safety may add overhead
- Compatibility: Ensure compatibility across Node.js versions
- 100% test success rate with worker threads
- No HandleScope errors
- No special initialization required
- Each worker thread properly isolated
- Thread safety validation working correctly
- Proper cleanup on worker termination
- Clear error messages for cross-thread usage
- Zero memory leaks or stability issues
- Pattern: Each worker creates own connection
- Message Passing: Simple SQL execution via worker messages
- Isolation: No connection sharing between threads
- Documentation: Clear usage examples and limitations
- Context Awareness: Use N-API for automatic context handling
- Instance Data: Use
napi_set_instance_datafor per-worker state - Cleanup Hooks: Use
napi_add_env_cleanup_hookfor proper cleanup - Thread-Safe Functions: Use
Napi::ThreadSafeFunctionfor callbacks
- Thread Safety Modes: Multi-thread mode prevents connection sharing
- Connection Isolation: Each thread must have separate connections
- Performance: Minimal overhead in multi-thread mode
src/binding.cpp- Add instance data management and cleanup hookssrc/sqlite_impl.h- Add thread ID tracking and validation declarationssrc/sqlite_impl.cpp- Implement thread validation and cleanup logic
test/worker-threads-simple.test.ts- Basic worker thread functionalitytest/worker-threads-simple-init.test.ts- Worker initialization teststest/worker-threads-initialization.test.ts- Module initialization in workerstest/worker-threads-error.test.ts- Error handling and cross-thread validation
README.md- Add worker thread examplesdocs/WORKER-THREADS.md- Create detailed usage guideAGENTS.md- Update with worker thread implementation notes
binding.gyp- Ensure proper threading flags are setpackage.json- Update test scripts for worker thread testing
While worker thread support is fully functional, potential future work includes:
-
Thread-Safe Function support
- Implement
Napi::ThreadSafeFunctionfor cross-thread callbacks - Add async callback mechanisms from worker threads
- Support for progress notifications across threads
- Implement
-
Worker pool utilities
- Helper classes for managing worker thread pools
- Load balancing for database operations
- Connection pooling strategies
-
Performance optimizations
- Optimize further for multi-threaded scenarios
- Reduce lock contention where possible
- Memory allocation optimizations
-
Additional stress testing
- More stress tests with many concurrent workers
- Long-running worker scenarios
- Resource exhaustion testing