pynect 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +478 -2
  3. package/SECURITY.md +34 -0
  4. package/binding.gyp +60 -0
  5. package/dist/cli/codegen.d.ts +3 -0
  6. package/dist/cli/codegen.d.ts.map +1 -0
  7. package/dist/cli/codegen.js +543 -0
  8. package/dist/cli/codegen.js.map +1 -0
  9. package/dist/cli/sync.d.ts +3 -0
  10. package/dist/cli/sync.d.ts.map +1 -0
  11. package/dist/cli/sync.js +48 -0
  12. package/dist/cli/sync.js.map +1 -0
  13. package/dist/config/project.d.ts +57 -0
  14. package/dist/config/project.d.ts.map +1 -0
  15. package/dist/config/project.js +176 -0
  16. package/dist/config/project.js.map +1 -0
  17. package/dist/core/IsolatedWorker.d.ts +39 -0
  18. package/dist/core/IsolatedWorker.d.ts.map +1 -0
  19. package/dist/core/IsolatedWorker.js +178 -0
  20. package/dist/core/IsolatedWorker.js.map +1 -0
  21. package/dist/core/PyCallable.d.ts +67 -0
  22. package/dist/core/PyCallable.d.ts.map +1 -0
  23. package/dist/core/PyCallable.js +104 -0
  24. package/dist/core/PyCallable.js.map +1 -0
  25. package/dist/core/PyContext.d.ts +76 -0
  26. package/dist/core/PyContext.d.ts.map +1 -0
  27. package/dist/core/PyContext.js +239 -0
  28. package/dist/core/PyContext.js.map +1 -0
  29. package/dist/core/PyIterator.d.ts +84 -0
  30. package/dist/core/PyIterator.d.ts.map +1 -0
  31. package/dist/core/PyIterator.js +243 -0
  32. package/dist/core/PyIterator.js.map +1 -0
  33. package/dist/core/PyModule.d.ts +53 -0
  34. package/dist/core/PyModule.d.ts.map +1 -0
  35. package/dist/core/PyModule.js +162 -0
  36. package/dist/core/PyModule.js.map +1 -0
  37. package/dist/core/PyProxy.d.ts +75 -0
  38. package/dist/core/PyProxy.d.ts.map +1 -0
  39. package/dist/core/PyProxy.js +563 -0
  40. package/dist/core/PyProxy.js.map +1 -0
  41. package/dist/core/PyRuntime.d.ts +112 -0
  42. package/dist/core/PyRuntime.d.ts.map +1 -0
  43. package/dist/core/PyRuntime.js +546 -0
  44. package/dist/core/PyRuntime.js.map +1 -0
  45. package/dist/env/PackageInstaller.d.ts +232 -0
  46. package/dist/env/PackageInstaller.d.ts.map +1 -0
  47. package/dist/env/PackageInstaller.js +628 -0
  48. package/dist/env/PackageInstaller.js.map +1 -0
  49. package/dist/env/ProjectEnvironment.d.ts +63 -0
  50. package/dist/env/ProjectEnvironment.d.ts.map +1 -0
  51. package/dist/env/ProjectEnvironment.js +379 -0
  52. package/dist/env/ProjectEnvironment.js.map +1 -0
  53. package/dist/env/PyfigAdapter.d.ts +32 -0
  54. package/dist/env/PyfigAdapter.d.ts.map +1 -0
  55. package/dist/env/PyfigAdapter.js +10 -0
  56. package/dist/env/PyfigAdapter.js.map +1 -0
  57. package/dist/env/PythonDetector.d.ts +48 -0
  58. package/dist/env/PythonDetector.d.ts.map +1 -0
  59. package/dist/env/PythonDetector.js +182 -0
  60. package/dist/env/PythonDetector.js.map +1 -0
  61. package/dist/env/PythonProvisioner.d.ts +16 -0
  62. package/dist/env/PythonProvisioner.d.ts.map +1 -0
  63. package/dist/env/PythonProvisioner.js +42 -0
  64. package/dist/env/PythonProvisioner.js.map +1 -0
  65. package/dist/env/VenvManager.d.ts +55 -0
  66. package/dist/env/VenvManager.d.ts.map +1 -0
  67. package/dist/env/VenvManager.js +153 -0
  68. package/dist/env/VenvManager.js.map +1 -0
  69. package/dist/env/executables.d.ts +3 -0
  70. package/dist/env/executables.d.ts.map +1 -0
  71. package/dist/env/executables.js +14 -0
  72. package/dist/env/executables.js.map +1 -0
  73. package/dist/index.d.ts +182 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +393 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/native/NativeAddonLoader.d.ts +11 -0
  78. package/dist/native/NativeAddonLoader.d.ts.map +1 -0
  79. package/dist/native/NativeAddonLoader.js +75 -0
  80. package/dist/native/NativeAddonLoader.js.map +1 -0
  81. package/dist/plugins/Plugin.interface.d.ts +41 -0
  82. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  83. package/dist/plugins/Plugin.interface.js +12 -0
  84. package/dist/plugins/Plugin.interface.js.map +1 -0
  85. package/dist/plugins/PluginManager.d.ts +26 -0
  86. package/dist/plugins/PluginManager.d.ts.map +1 -0
  87. package/dist/plugins/PluginManager.js +174 -0
  88. package/dist/plugins/PluginManager.js.map +1 -0
  89. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  90. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  91. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  92. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  93. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  94. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  95. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  96. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  97. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  98. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  99. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  100. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  101. package/dist/plugins/index.d.ts +7 -0
  102. package/dist/plugins/index.d.ts.map +1 -0
  103. package/dist/plugins/index.js +12 -0
  104. package/dist/plugins/index.js.map +1 -0
  105. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  106. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  107. package/dist/serialization/DataFrameBridge.js +355 -0
  108. package/dist/serialization/DataFrameBridge.js.map +1 -0
  109. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  110. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  111. package/dist/serialization/MsgPackSerializer.js +242 -0
  112. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  113. package/dist/serialization/NumpyBridge.d.ts +96 -0
  114. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  115. package/dist/serialization/NumpyBridge.js +323 -0
  116. package/dist/serialization/NumpyBridge.js.map +1 -0
  117. package/dist/serialization/Serializer.d.ts +78 -0
  118. package/dist/serialization/Serializer.d.ts.map +1 -0
  119. package/dist/serialization/Serializer.js +281 -0
  120. package/dist/serialization/Serializer.js.map +1 -0
  121. package/dist/types/addon.d.ts +131 -0
  122. package/dist/types/addon.d.ts.map +1 -0
  123. package/dist/types/addon.js +32 -0
  124. package/dist/types/addon.js.map +1 -0
  125. package/dist/types/config.d.ts +233 -0
  126. package/dist/types/config.d.ts.map +1 -0
  127. package/dist/types/config.js +60 -0
  128. package/dist/types/config.js.map +1 -0
  129. package/dist/types/index.d.ts +10 -0
  130. package/dist/types/index.d.ts.map +1 -0
  131. package/dist/types/index.js +12 -0
  132. package/dist/types/index.js.map +1 -0
  133. package/dist/types/python.d.ts +235 -0
  134. package/dist/types/python.d.ts.map +1 -0
  135. package/dist/types/python.js +7 -0
  136. package/dist/types/python.js.map +1 -0
  137. package/dist/utils/ErrorTranslator.d.ts +83 -0
  138. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  139. package/dist/utils/ErrorTranslator.js +210 -0
  140. package/dist/utils/ErrorTranslator.js.map +1 -0
  141. package/dist/utils/Locking.d.ts +3 -0
  142. package/dist/utils/Locking.d.ts.map +1 -0
  143. package/dist/utils/Locking.js +10 -0
  144. package/dist/utils/Locking.js.map +1 -0
  145. package/dist/utils/Logger.d.ts +27 -0
  146. package/dist/utils/Logger.d.ts.map +1 -0
  147. package/dist/utils/Logger.js +115 -0
  148. package/dist/utils/Logger.js.map +1 -0
  149. package/dist/utils/MemoryMonitor.d.ts +43 -0
  150. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  151. package/dist/utils/MemoryMonitor.js +143 -0
  152. package/dist/utils/MemoryMonitor.js.map +1 -0
  153. package/package.json +114 -4
  154. package/python/ast_inspector.py +248 -0
  155. package/python/error_handler.py +432 -0
  156. package/python/isolated_worker.py +94 -0
  157. package/python/pynect_dispatcher.py +382 -0
  158. package/python/pynect_runtime.py +574 -0
  159. package/python/serializer.py +373 -0
  160. package/python/type_inspector.py +315 -0
  161. package/scripts/build-native.js +50 -0
  162. package/scripts/cli.js +21 -0
  163. package/scripts/ensure-native.js +85 -0
  164. package/scripts/postinstall.js +40 -0
  165. package/scripts/pyfig.js +113 -0
  166. package/scripts/python-provisioner.js +23 -0
  167. package/scripts/smoke-python-boundary.js +103 -0
  168. package/src/native/NativeAddonLoader.ts +98 -0
  169. package/src/native/gil_guard.cpp +25 -0
  170. package/src/native/gil_guard.h +174 -0
  171. package/src/native/pynect_addon.cpp +942 -0
  172. package/src/native/python_bridge.cpp +904 -0
  173. package/src/native/python_bridge.h +263 -0
  174. package/src/native/thread_pool.cpp +375 -0
  175. package/src/native/thread_pool.h +185 -0
  176. package/src/native/type_converter.cpp +906 -0
  177. package/src/native/type_converter.h +271 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * pynect - Python Bridge Header
3
+ * High-level operations on Python objects: import, call, attribute access.
4
+ * Uses TypeConverter for serialization and ThreadPool for async execution.
5
+ */
6
+
7
+ #pragma once
8
+
9
+ #include <Python.h>
10
+ #include <napi.h>
11
+ #include <string>
12
+ #include <vector>
13
+ #include <memory>
14
+ #include <functional>
15
+ #include <map>
16
+
17
+ #include "type_converter.h"
18
+ #include "thread_pool.h"
19
+
20
+ namespace pynect {
21
+
22
+ /**
23
+ * Options for function calls.
24
+ */
25
+ struct CallOptions {
26
+ std::vector<SerializedValue> args;
27
+ std::vector<std::pair<std::string, SerializedValue>> kwargs;
28
+ uint32_t timeoutMs = 0;
29
+ uint32_t executionTimeoutMs = 0;
30
+ std::string requestId;
31
+ };
32
+
33
+ /**
34
+ * Attribute access result.
35
+ */
36
+ struct AttributeResult {
37
+ bool success = false;
38
+ SerializedValue value;
39
+ bool isCallable = false;
40
+ bool isObject = false;
41
+ uint32_t objectId = 0;
42
+ std::string errorType;
43
+ std::string errorMessage;
44
+ std::string errorTraceback;
45
+ };
46
+
47
+ /**
48
+ * PythonBridge - orchestrates Python operations.
49
+ *
50
+ * This class provides the business logic on top of raw Python C API calls.
51
+ * All operations are designed to be executed in worker threads (GIL held).
52
+ */
53
+ class PythonBridge {
54
+ public:
55
+ static PythonBridge& getInstance();
56
+
57
+ // Non-copyable singleton
58
+ PythonBridge(const PythonBridge&) = delete;
59
+ PythonBridge& operator=(const PythonBridge&) = delete;
60
+
61
+ // Python Lifecycle
62
+
63
+ /**
64
+ * Initialize the Python interpreter.
65
+ * Must be called once from the main thread before any Python operations.
66
+ */
67
+ void initializePython(
68
+ const std::string& pythonHome,
69
+ const std::string& pythonExecutable,
70
+ const std::string& pythonLibrary,
71
+ const std::vector<std::string>& extraSysPaths,
72
+ const std::vector<std::pair<std::string, std::string>>& envVars
73
+ );
74
+
75
+ /**
76
+ * Finalize the Python interpreter.
77
+ * Must be called once from the main thread at shutdown.
78
+ */
79
+ void finalizePython();
80
+
81
+ bool isInitialized() const noexcept { return _initialized; }
82
+
83
+ // Module Import
84
+
85
+ /**
86
+ * Import a Python module. Returns objectId.
87
+ * Sync version (GIL must be held by caller).
88
+ */
89
+ TaskResult importModuleSync(const std::string& moduleName);
90
+
91
+ /**
92
+ * Reload a module by objectId.
93
+ */
94
+ TaskResult reloadModuleSync(uint32_t objectId);
95
+
96
+ // Attribute Access
97
+
98
+ /**
99
+ * Get attribute from a Python object by name.
100
+ */
101
+ TaskResult getAttributeSync(uint32_t objectId, const std::string& attrName);
102
+
103
+ /**
104
+ * Get attribute path (chain of attribute accesses).
105
+ * e.g. path = ["groupby", "agg"] means obj.groupby.agg
106
+ */
107
+ TaskResult getAttributePathSync(
108
+ uint32_t objectId,
109
+ const std::vector<std::string>& path
110
+ );
111
+
112
+ /**
113
+ * Set attribute on a Python object.
114
+ */
115
+ TaskResult setAttributeSync(
116
+ uint32_t objectId,
117
+ const std::string& attrName,
118
+ const SerializedValue& value
119
+ );
120
+
121
+ /**
122
+ * Check if an attribute exists.
123
+ */
124
+ bool hasAttributeSync(uint32_t objectId, const std::string& attrName);
125
+
126
+ /**
127
+ * Delete an attribute.
128
+ */
129
+ TaskResult deleteAttributeSync(uint32_t objectId, const std::string& attrName);
130
+
131
+ // Function Calls
132
+
133
+ /**
134
+ * Call a Python callable (resolving attribute path first).
135
+ */
136
+ TaskResult callFunctionSync(
137
+ uint32_t objectId,
138
+ const std::vector<std::string>& path,
139
+ const CallOptions& options
140
+ );
141
+
142
+ /**
143
+ * Call a method by name on a Python object.
144
+ */
145
+ TaskResult callMethodSync(
146
+ uint32_t objectId,
147
+ const std::string& methodName,
148
+ const CallOptions& options
149
+ );
150
+
151
+ /** Cooperatively cancel a running asynchronous call. */
152
+ TaskResult cancelCallSync(const std::string& requestId);
153
+
154
+ // Direct Evaluation
155
+
156
+ /**
157
+ * Run arbitrary Python code (statements).
158
+ */
159
+ TaskResult runCodeSync(const std::string& code);
160
+
161
+ /**
162
+ * Evaluate a Python expression and return the result.
163
+ */
164
+ TaskResult evalExpressionSync(const std::string& expression);
165
+
166
+ /**
167
+ * Run a Python file.
168
+ */
169
+ TaskResult runFileSync(const std::string& filePath);
170
+
171
+ // Object Operations
172
+
173
+ /**
174
+ * Get the serialized value of an object (resolve it).
175
+ */
176
+ TaskResult getObjectValueSync(uint32_t objectId);
177
+
178
+ /**
179
+ * Get repr() of an object.
180
+ */
181
+ std::string getObjectReprSync(uint32_t objectId);
182
+
183
+ /**
184
+ * Get len() of an object.
185
+ */
186
+ TaskResult getObjectLengthSync(uint32_t objectId);
187
+
188
+ /**
189
+ * Get list of attribute names (dir()).
190
+ */
191
+ TaskResult getObjectKeysSync(uint32_t objectId);
192
+
193
+ // Iterator Support
194
+
195
+ /**
196
+ * Create an iterator for an object.
197
+ */
198
+ TaskResult createIteratorSync(uint32_t objectId, const std::vector<std::string>& path);
199
+
200
+ /**
201
+ * Get next value from an iterator.
202
+ */
203
+ TaskResult iteratorNextSync(uint32_t iteratorId);
204
+
205
+ /**
206
+ * Destroy an iterator.
207
+ */
208
+ void destroyIteratorSync(uint32_t iteratorId);
209
+
210
+ // Memory
211
+
212
+ void collectGarbage();
213
+ size_t getTrackedObjectCount() const;
214
+
215
+ private:
216
+ PythonBridge() = default;
217
+ ~PythonBridge() = default;
218
+
219
+ /**
220
+ * Resolve attribute path starting from an objectId.
221
+ * Returns new Python reference, or nullptr on error.
222
+ * GIL must be held.
223
+ */
224
+ PyObject* _resolveAttributePath(
225
+ PyObject* obj,
226
+ const std::vector<std::string>& path
227
+ );
228
+
229
+ /**
230
+ * Convert a SerializedValue back to a Python object.
231
+ * Returns new reference.
232
+ */
233
+ PyObject* _deserialize(const SerializedValue& value);
234
+
235
+ /**
236
+ * Build Python args tuple and kwargs dict from CallOptions.
237
+ */
238
+ bool _buildCallArgs(
239
+ const CallOptions& options,
240
+ PyObject*& argsOut,
241
+ PyObject*& kwargsOut
242
+ );
243
+
244
+ /**
245
+ * Build a TaskResult from Python exception state.
246
+ */
247
+ TaskResult _makeErrorResult();
248
+
249
+ /**
250
+ * Build a successful TaskResult from a Python object.
251
+ */
252
+ TaskResult _makeSuccessResult(PyObject* obj);
253
+
254
+ bool _initialized = false;
255
+ PyThreadState* _mainThreadState = nullptr;
256
+
257
+ // Iterator registry: iteratorId -> Python iterator object
258
+ std::map<uint32_t, PyObject*> _iterators;
259
+ std::mutex _iteratorsMutex;
260
+ std::atomic<uint32_t> _nextIteratorId{1};
261
+ };
262
+
263
+ } // namespace pynect
@@ -0,0 +1,375 @@
1
+ /**
2
+ * pynect - Thread Pool Implementation
3
+ * Worker thread management for executing Python code off the event loop.
4
+ */
5
+
6
+ #include "thread_pool.h"
7
+ #include "gil_guard.h"
8
+
9
+ #include <Python.h>
10
+ #include <algorithm>
11
+ #include <cassert>
12
+ #include <stdexcept>
13
+ #include <iostream>
14
+
15
+ namespace pynect {
16
+
17
+ // Constructor / Destructor
18
+
19
+ PythonThreadPool::PythonThreadPool(uint32_t numThreads, uint32_t maxQueueSize)
20
+ : _numThreads(numThreads > 0 ? numThreads : 1)
21
+ , _maxQueueSize(maxQueueSize > 0 ? maxQueueSize : 1000)
22
+ {
23
+ // Validate parameters
24
+ if (_numThreads > 64) {
25
+ std::cerr << "[pynect] Warning: Thread pool size " << _numThreads
26
+ << " is very large. Consider using <= 16 threads.\n";
27
+ }
28
+ }
29
+
30
+ PythonThreadPool::~PythonThreadPool() {
31
+ if (_running.load()) {
32
+ forceShutdown();
33
+ }
34
+ }
35
+
36
+ // Initialization
37
+
38
+ void PythonThreadPool::initialize(Napi::Env env) {
39
+ if (_running.load()) {
40
+ throw std::runtime_error("PythonThreadPool already initialized");
41
+ }
42
+
43
+ // Create a ThreadSafeFunction for delivering results back to the JS event loop.
44
+ // This is the safe way to call JS functions from non-JS threads.
45
+ _tsfn = Napi::ThreadSafeFunction::New(
46
+ env,
47
+ Napi::Function::New(env, [](const Napi::CallbackInfo&) {}), // dummy JS function
48
+ "PythonThreadPoolCallback",
49
+ 0, // unlimited queue size
50
+ 1 // initial thread count (we manage threads ourselves)
51
+ );
52
+ _env = env;
53
+ _tsfn.Unref(env);
54
+
55
+ _running.store(true);
56
+ _draining.store(false);
57
+
58
+ // Spawn worker threads
59
+ _threads.reserve(_numThreads);
60
+ for (uint32_t i = 0; i < _numThreads; ++i) {
61
+ _threads.emplace_back(&PythonThreadPool::_workerThread, this, i);
62
+ }
63
+ }
64
+
65
+ // Task Submission
66
+
67
+ uint64_t PythonThreadPool::submit(
68
+ std::function<TaskResult()> work,
69
+ std::function<void(TaskResult)> callback,
70
+ uint32_t timeoutMs,
71
+ std::string cancelKey
72
+ ) {
73
+ if (!_running.load()) {
74
+ TaskResult errorResult;
75
+ errorResult.success = false;
76
+ errorResult.errorType = "RuntimeError";
77
+ errorResult.errorMessage = "Thread pool is shut down";
78
+ errorResult.errorTraceback = "";
79
+ callback(std::move(errorResult));
80
+ return 0;
81
+ }
82
+
83
+ // Check queue capacity
84
+ {
85
+ std::lock_guard<std::mutex> lock(_queueMutex);
86
+ if (_queue.size() >= _maxQueueSize) {
87
+ // Backpressure: queue is full
88
+ TaskResult errorResult;
89
+ errorResult.success = false;
90
+ errorResult.errorType = "RuntimeError";
91
+ errorResult.errorMessage = "Python thread pool queue is full (max: " +
92
+ std::to_string(_maxQueueSize) + "). " +
93
+ "Consider increasing threadPoolSize or maxQueueSize.";
94
+ errorResult.errorTraceback = "";
95
+ callback(std::move(errorResult));
96
+ return 0;
97
+ }
98
+ }
99
+
100
+ uint64_t taskId = _nextTaskId.fetch_add(1, std::memory_order_relaxed);
101
+
102
+ PythonTask task;
103
+ task.id = taskId;
104
+ task.work = std::move(work);
105
+ task.callback = std::move(callback);
106
+ task.submittedAt = std::chrono::steady_clock::now();
107
+ task.timeoutMs = timeoutMs;
108
+ task.cancelKey = std::move(cancelKey);
109
+
110
+ // Keep Node alive only while a submitted task still owes JavaScript a result.
111
+ if (_pendingResults.fetch_add(1, std::memory_order_acq_rel) == 0) {
112
+ _tsfn.Ref(_env);
113
+ }
114
+
115
+ {
116
+ std::lock_guard<std::mutex> lock(_queueMutex);
117
+ _queue.push_back(std::move(task));
118
+ _stats.totalTasksSubmitted.fetch_add(1, std::memory_order_relaxed);
119
+ _stats.currentQueueSize.fetch_add(1, std::memory_order_relaxed);
120
+ }
121
+
122
+ _queueCv.notify_one();
123
+ return taskId;
124
+ }
125
+
126
+ bool PythonThreadPool::cancel(const std::string& cancelKey) {
127
+ if (cancelKey.empty()) return false;
128
+ PythonTask task;
129
+ {
130
+ std::lock_guard<std::mutex> lock(_queueMutex);
131
+ auto found = std::find_if(_queue.begin(), _queue.end(), [&cancelKey](const PythonTask& candidate) {
132
+ return candidate.cancelKey == cancelKey;
133
+ });
134
+ if (found == _queue.end()) return false;
135
+ task = std::move(*found);
136
+ _queue.erase(found);
137
+ _stats.currentQueueSize.fetch_sub(1, std::memory_order_relaxed);
138
+ _stats.totalTasksFailed.fetch_add(1, std::memory_order_relaxed);
139
+ }
140
+ TaskResult cancelled;
141
+ cancelled.success = false;
142
+ cancelled.errorType = "CancelledError";
143
+ cancelled.errorMessage = "Python call was cancelled before execution";
144
+ _postResult(task.id, std::move(cancelled), std::move(task.callback));
145
+ if (_draining.load()) _drainCv.notify_all();
146
+ return true;
147
+ }
148
+
149
+ // Worker Thread
150
+
151
+ void PythonThreadPool::_workerThread(uint32_t threadId) {
152
+ // Give the thread a name for debugging
153
+ // Note: thread naming is platform-specific
154
+ #ifdef __linux__
155
+ pthread_setname_np(pthread_self(), ("pynect-worker-" + std::to_string(threadId)).c_str());
156
+ #endif
157
+
158
+ while (_running.load()) {
159
+ PythonTask task;
160
+
161
+ // Wait for a task
162
+ {
163
+ std::unique_lock<std::mutex> lock(_queueMutex);
164
+ _queueCv.wait(lock, [this] {
165
+ return !_queue.empty() || !_running.load();
166
+ });
167
+
168
+ if (!_running.load() && _queue.empty()) {
169
+ break;
170
+ }
171
+
172
+ if (_queue.empty()) {
173
+ continue;
174
+ }
175
+
176
+ task = std::move(_queue.front());
177
+ _queue.pop_front();
178
+ _stats.currentQueueSize.fetch_sub(1, std::memory_order_relaxed);
179
+ }
180
+
181
+ // Check timeout before executing
182
+ if (_isTimedOut(task)) {
183
+ TaskResult timeoutResult;
184
+ timeoutResult.success = false;
185
+ timeoutResult.errorType = "TimeoutError";
186
+ timeoutResult.errorMessage = "Python task timed out after " +
187
+ std::to_string(task.timeoutMs) + "ms in queue";
188
+ timeoutResult.errorTraceback = "";
189
+ _stats.totalTasksTimedOut.fetch_add(1, std::memory_order_relaxed);
190
+ _postResult(task.id, std::move(timeoutResult), std::move(task.callback));
191
+ continue;
192
+ }
193
+
194
+ // Execute the task with GIL held
195
+ _stats.activeThreads.fetch_add(1, std::memory_order_relaxed);
196
+
197
+ auto startTime = std::chrono::steady_clock::now();
198
+ TaskResult result;
199
+
200
+ try {
201
+ // Acquire GIL - critical: must be released when we're done
202
+ GILAcquire gil;
203
+
204
+ // Check timeout again (task might have waited in queue)
205
+ if (task.timeoutMs > 0 && _isTimedOut(task)) {
206
+ result.success = false;
207
+ result.errorType = "TimeoutError";
208
+ result.errorMessage = "Python task timed out";
209
+ result.errorTraceback = "";
210
+ } else {
211
+ // Execute the Python work
212
+ result = task.work();
213
+ }
214
+ } catch (const std::exception& e) {
215
+ result.success = false;
216
+ result.errorType = "RuntimeError";
217
+ result.errorMessage = std::string("C++ exception in worker thread: ") + e.what();
218
+ result.errorTraceback = "";
219
+ } catch (...) {
220
+ result.success = false;
221
+ result.errorType = "RuntimeError";
222
+ result.errorMessage = "Unknown C++ exception in worker thread";
223
+ result.errorTraceback = "";
224
+ }
225
+
226
+ auto endTime = std::chrono::steady_clock::now();
227
+ auto durationUs = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
228
+ _stats.totalExecutionTimeUs.fetch_add(static_cast<uint64_t>(durationUs), std::memory_order_relaxed);
229
+
230
+ if (result.success) {
231
+ _stats.totalTasksCompleted.fetch_add(1, std::memory_order_relaxed);
232
+ } else {
233
+ _stats.totalTasksFailed.fetch_add(1, std::memory_order_relaxed);
234
+ }
235
+
236
+ _stats.activeThreads.fetch_sub(1, std::memory_order_relaxed);
237
+
238
+ // Post result back to Node.js event loop
239
+ _postResult(task.id, std::move(result), std::move(task.callback));
240
+
241
+ // Signal drain waiters
242
+ if (_draining.load()) {
243
+ std::lock_guard<std::mutex> lock(_queueMutex);
244
+ _drainCv.notify_all();
245
+ }
246
+ }
247
+ }
248
+
249
+ // Result Delivery
250
+
251
+ void PythonThreadPool::_postResult(
252
+ uint64_t taskId,
253
+ TaskResult result,
254
+ std::function<void(TaskResult)> callback
255
+ ) {
256
+ // Package the callback + result into a heap allocation
257
+ // that will be freed in the JS thread
258
+ auto* payload = new std::pair<std::function<void(TaskResult)>, TaskResult>(
259
+ std::move(callback),
260
+ std::move(result)
261
+ );
262
+
263
+ // Use ThreadSafeFunction to invoke the callback in the JS event loop
264
+ napi_status status = _tsfn.NonBlockingCall(
265
+ payload,
266
+ [this](Napi::Env env, Napi::Function jsCallback, void* data) {
267
+ auto* p = static_cast<std::pair<std::function<void(TaskResult)>, TaskResult>*>(data);
268
+ try {
269
+ p->first(std::move(p->second));
270
+ } catch (...) {
271
+ // Don't let JS callback exceptions crash the worker
272
+ }
273
+ delete p;
274
+ _resultDelivered(env);
275
+ }
276
+ );
277
+
278
+ if (status != napi_ok) {
279
+ // This can happen during shutdown; just clean up
280
+ auto* p = static_cast<std::pair<std::function<void(TaskResult)>, TaskResult>*>(payload);
281
+ try {
282
+ // Try to call with error result
283
+ TaskResult shutdownResult;
284
+ shutdownResult.success = false;
285
+ shutdownResult.errorType = "RuntimeError";
286
+ shutdownResult.errorMessage = "Thread pool shut down while task was running";
287
+ p->first(std::move(shutdownResult));
288
+ } catch (...) {}
289
+ delete p;
290
+ _pendingResults.fetch_sub(1, std::memory_order_acq_rel);
291
+ }
292
+ }
293
+
294
+ void PythonThreadPool::_resultDelivered(Napi::Env env) {
295
+ if (_pendingResults.fetch_sub(1, std::memory_order_acq_rel) == 1) {
296
+ _tsfn.Unref(env);
297
+ }
298
+ }
299
+
300
+ // Timeout Check
301
+
302
+ bool PythonThreadPool::_isTimedOut(const PythonTask& task) const {
303
+ if (task.timeoutMs == 0) return false;
304
+
305
+ auto now = std::chrono::steady_clock::now();
306
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
307
+ now - task.submittedAt
308
+ ).count();
309
+
310
+ return elapsed >= static_cast<long long>(task.timeoutMs);
311
+ }
312
+
313
+ // Resize
314
+
315
+ void PythonThreadPool::resize(uint32_t newNumThreads) {
316
+ if (!_running.load()) return;
317
+ if (newNumThreads == 0) newNumThreads = 1;
318
+
319
+ // For simplicity: add threads if growing, remove by setting flag if shrinking
320
+ // A production implementation would use a more sophisticated approach
321
+ uint32_t current = static_cast<uint32_t>(_threads.size());
322
+
323
+ if (newNumThreads > current) {
324
+ for (uint32_t i = current; i < newNumThreads; ++i) {
325
+ _threads.emplace_back(&PythonThreadPool::_workerThread, this, i);
326
+ }
327
+ }
328
+ // Shrinking is complex and rarely needed; skip for now
329
+ _numThreads = newNumThreads;
330
+ }
331
+
332
+ // Shutdown
333
+
334
+ void PythonThreadPool::shutdown() {
335
+ if (!_running.load()) return;
336
+
337
+ // Drain the queue first
338
+ _draining.store(true);
339
+ {
340
+ std::unique_lock<std::mutex> lock(_queueMutex);
341
+ _drainCv.wait_for(lock, std::chrono::seconds(30), [this] {
342
+ return _queue.empty() && _stats.activeThreads.load() == 0;
343
+ });
344
+ }
345
+ _draining.store(false);
346
+
347
+ forceShutdown();
348
+ }
349
+
350
+ void PythonThreadPool::forceShutdown() {
351
+ if (!_running.load()) return;
352
+
353
+ _running.store(false);
354
+ _queueCv.notify_all();
355
+
356
+ // Wait for all threads to finish
357
+ for (auto& thread : _threads) {
358
+ if (thread.joinable()) {
359
+ thread.join();
360
+ }
361
+ }
362
+ _threads.clear();
363
+
364
+ // Release ThreadSafeFunction
365
+ _tsfn.Release();
366
+ }
367
+
368
+ // Queue Size
369
+
370
+ size_t PythonThreadPool::getQueueSize() const {
371
+ std::lock_guard<std::mutex> lock(_queueMutex);
372
+ return _queue.size();
373
+ }
374
+
375
+ } // namespace pynect