nodepyx 1.0.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.
- package/LICENSE +22 -0
- package/README.md +399 -0
- package/binding.gyp +73 -0
- package/dist/core/PyCallable.d.ts +65 -0
- package/dist/core/PyCallable.d.ts.map +1 -0
- package/dist/core/PyCallable.js +109 -0
- package/dist/core/PyCallable.js.map +1 -0
- package/dist/core/PyContext.d.ts +76 -0
- package/dist/core/PyContext.d.ts.map +1 -0
- package/dist/core/PyContext.js +228 -0
- package/dist/core/PyContext.js.map +1 -0
- package/dist/core/PyIterator.d.ts +84 -0
- package/dist/core/PyIterator.d.ts.map +1 -0
- package/dist/core/PyIterator.js +243 -0
- package/dist/core/PyIterator.js.map +1 -0
- package/dist/core/PyModule.d.ts +55 -0
- package/dist/core/PyModule.d.ts.map +1 -0
- package/dist/core/PyModule.js +172 -0
- package/dist/core/PyModule.js.map +1 -0
- package/dist/core/PyProxy.d.ts +65 -0
- package/dist/core/PyProxy.d.ts.map +1 -0
- package/dist/core/PyProxy.js +483 -0
- package/dist/core/PyProxy.js.map +1 -0
- package/dist/core/PyRuntime.d.ts +105 -0
- package/dist/core/PyRuntime.d.ts.map +1 -0
- package/dist/core/PyRuntime.js +438 -0
- package/dist/core/PyRuntime.js.map +1 -0
- package/dist/env/CondaManager.d.ts +118 -0
- package/dist/env/CondaManager.d.ts.map +1 -0
- package/dist/env/CondaManager.js +401 -0
- package/dist/env/CondaManager.js.map +1 -0
- package/dist/env/PackageInstaller.d.ts +233 -0
- package/dist/env/PackageInstaller.d.ts.map +1 -0
- package/dist/env/PackageInstaller.js +609 -0
- package/dist/env/PackageInstaller.js.map +1 -0
- package/dist/env/PythonDetector.d.ts +103 -0
- package/dist/env/PythonDetector.d.ts.map +1 -0
- package/dist/env/PythonDetector.js +381 -0
- package/dist/env/PythonDetector.js.map +1 -0
- package/dist/env/VenvManager.d.ts +117 -0
- package/dist/env/VenvManager.d.ts.map +1 -0
- package/dist/env/VenvManager.js +331 -0
- package/dist/env/VenvManager.js.map +1 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/Plugin.interface.d.ts +41 -0
- package/dist/plugins/Plugin.interface.d.ts.map +1 -0
- package/dist/plugins/Plugin.interface.js +12 -0
- package/dist/plugins/Plugin.interface.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +26 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +174 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
- package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/NumpyPlugin.js +41 -0
- package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
- package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/PandasPlugin.js +57 -0
- package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
- package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
- package/dist/plugins/builtin/TorchPlugin.js +50 -0
- package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +12 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/serialization/DataFrameBridge.d.ts +141 -0
- package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
- package/dist/serialization/DataFrameBridge.js +355 -0
- package/dist/serialization/DataFrameBridge.js.map +1 -0
- package/dist/serialization/MsgPackSerializer.d.ts +45 -0
- package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
- package/dist/serialization/MsgPackSerializer.js +242 -0
- package/dist/serialization/MsgPackSerializer.js.map +1 -0
- package/dist/serialization/NumpyBridge.d.ts +96 -0
- package/dist/serialization/NumpyBridge.d.ts.map +1 -0
- package/dist/serialization/NumpyBridge.js +323 -0
- package/dist/serialization/NumpyBridge.js.map +1 -0
- package/dist/serialization/Serializer.d.ts +78 -0
- package/dist/serialization/Serializer.d.ts.map +1 -0
- package/dist/serialization/Serializer.js +281 -0
- package/dist/serialization/Serializer.js.map +1 -0
- package/dist/types/PythonTypeMapper.d.ts +87 -0
- package/dist/types/PythonTypeMapper.d.ts.map +1 -0
- package/dist/types/PythonTypeMapper.js +449 -0
- package/dist/types/PythonTypeMapper.js.map +1 -0
- package/dist/types/StubCache.d.ts +109 -0
- package/dist/types/StubCache.d.ts.map +1 -0
- package/dist/types/StubCache.js +333 -0
- package/dist/types/StubCache.js.map +1 -0
- package/dist/types/TypeGenerator.d.ts +139 -0
- package/dist/types/TypeGenerator.d.ts.map +1 -0
- package/dist/types/TypeGenerator.js +372 -0
- package/dist/types/TypeGenerator.js.map +1 -0
- package/dist/types/addon.d.ts +114 -0
- package/dist/types/addon.d.ts.map +1 -0
- package/dist/types/addon.js +32 -0
- package/dist/types/addon.js.map +1 -0
- package/dist/types/config.d.ts +175 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/python.d.ts +235 -0
- package/dist/types/python.d.ts.map +1 -0
- package/dist/types/python.js +7 -0
- package/dist/types/python.js.map +1 -0
- package/dist/utils/ErrorTranslator.d.ts +83 -0
- package/dist/utils/ErrorTranslator.d.ts.map +1 -0
- package/dist/utils/ErrorTranslator.js +210 -0
- package/dist/utils/ErrorTranslator.js.map +1 -0
- package/dist/utils/Logger.d.ts +27 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +115 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/MemoryMonitor.d.ts +44 -0
- package/dist/utils/MemoryMonitor.d.ts.map +1 -0
- package/dist/utils/MemoryMonitor.js +143 -0
- package/dist/utils/MemoryMonitor.js.map +1 -0
- package/package.json +177 -0
- package/python/error_handler.py +433 -0
- package/python/nodepyx_runtime.py +575 -0
- package/python/serializer.py +379 -0
- package/python/type_inspector.py +288 -0
- package/scripts/build-native.js +68 -0
- package/scripts/download-prebuilds.js +99 -0
- package/scripts/generate-stubs.js +405 -0
- package/scripts/install.js +260 -0
- package/src/core/PyCallable.ts +137 -0
- package/src/core/PyContext.ts +296 -0
- package/src/core/PyIterator.ts +294 -0
- package/src/core/PyModule.ts +194 -0
- package/src/core/PyProxy.ts +605 -0
- package/src/core/PyRuntime.ts +504 -0
- package/src/env/CondaManager.ts +451 -0
- package/src/env/PackageInstaller.ts +738 -0
- package/src/env/PythonDetector.ts +414 -0
- package/src/env/VenvManager.ts +396 -0
- package/src/index.ts +425 -0
- package/src/native/gil_guard.cpp +26 -0
- package/src/native/gil_guard.h +175 -0
- package/src/native/nodepyx_addon.cpp +886 -0
- package/src/native/python_bridge.cpp +790 -0
- package/src/native/python_bridge.h +257 -0
- package/src/native/thread_pool.cpp +336 -0
- package/src/native/thread_pool.h +175 -0
- package/src/native/type_converter.cpp +901 -0
- package/src/native/type_converter.h +272 -0
- package/src/nextjs/PyProvider.tsx +123 -0
- package/src/nextjs/index.ts +21 -0
- package/src/nextjs/usePython.ts +106 -0
- package/src/nextjs/withnodepyx.ts +88 -0
- package/src/plugins/Plugin.interface.ts +51 -0
- package/src/plugins/PluginManager.ts +155 -0
- package/src/plugins/builtin/NumpyPlugin.ts +36 -0
- package/src/plugins/builtin/PandasPlugin.ts +49 -0
- package/src/plugins/builtin/TorchPlugin.ts +56 -0
- package/src/plugins/index.ts +7 -0
- package/src/serialization/DataFrameBridge.ts +398 -0
- package/src/serialization/MsgPackSerializer.ts +220 -0
- package/src/serialization/NumpyBridge.ts +332 -0
- package/src/serialization/Serializer.ts +320 -0
- package/src/types/PythonTypeMapper.ts +495 -0
- package/src/types/StubCache.ts +340 -0
- package/src/types/TypeGenerator.ts +491 -0
- package/src/types/addon.ts +170 -0
- package/src/types/config.ts +226 -0
- package/src/types/index.ts +55 -0
- package/src/types/python.ts +309 -0
- package/src/types/stubs/numpy.d.ts +441 -0
- package/src/types/stubs/pandas.d.ts +575 -0
- package/src/types/stubs/sklearn.d.ts +728 -0
- package/src/types/stubs/torch.d.ts +694 -0
- package/src/utils/ErrorTranslator.ts +220 -0
- package/src/utils/Logger.ts +119 -0
- package/src/utils/MemoryMonitor.ts +175 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — 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
|
+
|
|
16
|
+
#include "type_converter.h"
|
|
17
|
+
#include "thread_pool.h"
|
|
18
|
+
|
|
19
|
+
namespace nodepyx {
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for function calls.
|
|
23
|
+
*/
|
|
24
|
+
struct CallOptions {
|
|
25
|
+
std::vector<SerializedValue> args;
|
|
26
|
+
std::vector<std::pair<std::string, SerializedValue>> kwargs;
|
|
27
|
+
uint32_t timeoutMs = 0;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Attribute access result.
|
|
32
|
+
*/
|
|
33
|
+
struct AttributeResult {
|
|
34
|
+
bool success = false;
|
|
35
|
+
SerializedValue value;
|
|
36
|
+
bool isCallable = false;
|
|
37
|
+
bool isObject = false;
|
|
38
|
+
uint32_t objectId = 0;
|
|
39
|
+
std::string errorType;
|
|
40
|
+
std::string errorMessage;
|
|
41
|
+
std::string errorTraceback;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* PythonBridge — orchestrates Python operations.
|
|
46
|
+
*
|
|
47
|
+
* This class provides the business logic on top of raw Python C API calls.
|
|
48
|
+
* All operations are designed to be executed in worker threads (GIL held).
|
|
49
|
+
*/
|
|
50
|
+
class PythonBridge {
|
|
51
|
+
public:
|
|
52
|
+
static PythonBridge& getInstance();
|
|
53
|
+
|
|
54
|
+
// Non-copyable singleton
|
|
55
|
+
PythonBridge(const PythonBridge&) = delete;
|
|
56
|
+
PythonBridge& operator=(const PythonBridge&) = delete;
|
|
57
|
+
|
|
58
|
+
// ─── Python Lifecycle ────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize the Python interpreter.
|
|
62
|
+
* Must be called once from the main thread before any Python operations.
|
|
63
|
+
*/
|
|
64
|
+
void initializePython(
|
|
65
|
+
const std::string& pythonHome,
|
|
66
|
+
const std::string& pythonExecutable,
|
|
67
|
+
const std::vector<std::string>& extraSysPaths,
|
|
68
|
+
const std::vector<std::pair<std::string, std::string>>& envVars
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Finalize the Python interpreter.
|
|
73
|
+
* Must be called once from the main thread at shutdown.
|
|
74
|
+
*/
|
|
75
|
+
void finalizePython();
|
|
76
|
+
|
|
77
|
+
bool isInitialized() const noexcept { return _initialized; }
|
|
78
|
+
|
|
79
|
+
// ─── Module Import ───────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Import a Python module. Returns objectId.
|
|
83
|
+
* Sync version (GIL must be held by caller).
|
|
84
|
+
*/
|
|
85
|
+
TaskResult importModuleSync(const std::string& moduleName);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reload a module by objectId.
|
|
89
|
+
*/
|
|
90
|
+
TaskResult reloadModuleSync(uint32_t objectId);
|
|
91
|
+
|
|
92
|
+
// ─── Attribute Access ─────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get attribute from a Python object by name.
|
|
96
|
+
*/
|
|
97
|
+
TaskResult getAttributeSync(uint32_t objectId, const std::string& attrName);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get attribute path (chain of attribute accesses).
|
|
101
|
+
* e.g. path = ["groupby", "agg"] means obj.groupby.agg
|
|
102
|
+
*/
|
|
103
|
+
TaskResult getAttributePathSync(
|
|
104
|
+
uint32_t objectId,
|
|
105
|
+
const std::vector<std::string>& path
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set attribute on a Python object.
|
|
110
|
+
*/
|
|
111
|
+
TaskResult setAttributeSync(
|
|
112
|
+
uint32_t objectId,
|
|
113
|
+
const std::string& attrName,
|
|
114
|
+
const SerializedValue& value
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if an attribute exists.
|
|
119
|
+
*/
|
|
120
|
+
bool hasAttributeSync(uint32_t objectId, const std::string& attrName);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Delete an attribute.
|
|
124
|
+
*/
|
|
125
|
+
TaskResult deleteAttributeSync(uint32_t objectId, const std::string& attrName);
|
|
126
|
+
|
|
127
|
+
// ─── Function Calls ──────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Call a Python callable (resolving attribute path first).
|
|
131
|
+
*/
|
|
132
|
+
TaskResult callFunctionSync(
|
|
133
|
+
uint32_t objectId,
|
|
134
|
+
const std::vector<std::string>& path,
|
|
135
|
+
const CallOptions& options
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Call a method by name on a Python object.
|
|
140
|
+
*/
|
|
141
|
+
TaskResult callMethodSync(
|
|
142
|
+
uint32_t objectId,
|
|
143
|
+
const std::string& methodName,
|
|
144
|
+
const CallOptions& options
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// ─── Direct Evaluation ───────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Run arbitrary Python code (statements).
|
|
151
|
+
*/
|
|
152
|
+
TaskResult runCodeSync(const std::string& code);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Evaluate a Python expression and return the result.
|
|
156
|
+
*/
|
|
157
|
+
TaskResult evalExpressionSync(const std::string& expression);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run a Python file.
|
|
161
|
+
*/
|
|
162
|
+
TaskResult runFileSync(const std::string& filePath);
|
|
163
|
+
|
|
164
|
+
// ─── Object Operations ───────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get the serialized value of an object (resolve it).
|
|
168
|
+
*/
|
|
169
|
+
TaskResult getObjectValueSync(uint32_t objectId);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get repr() of an object.
|
|
173
|
+
*/
|
|
174
|
+
std::string getObjectReprSync(uint32_t objectId);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get len() of an object.
|
|
178
|
+
*/
|
|
179
|
+
TaskResult getObjectLengthSync(uint32_t objectId);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get list of attribute names (dir()).
|
|
183
|
+
*/
|
|
184
|
+
TaskResult getObjectKeysSync(uint32_t objectId);
|
|
185
|
+
|
|
186
|
+
// ─── Iterator Support ─────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create an iterator for an object.
|
|
190
|
+
*/
|
|
191
|
+
TaskResult createIteratorSync(uint32_t objectId, const std::vector<std::string>& path);
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get next value from an iterator.
|
|
195
|
+
*/
|
|
196
|
+
TaskResult iteratorNextSync(uint32_t iteratorId);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Destroy an iterator.
|
|
200
|
+
*/
|
|
201
|
+
void destroyIteratorSync(uint32_t iteratorId);
|
|
202
|
+
|
|
203
|
+
// ─── Memory ──────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
void collectGarbage();
|
|
206
|
+
size_t getTrackedObjectCount() const;
|
|
207
|
+
|
|
208
|
+
private:
|
|
209
|
+
PythonBridge() = default;
|
|
210
|
+
~PythonBridge() = default;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolve attribute path starting from an objectId.
|
|
214
|
+
* Returns new Python reference, or nullptr on error.
|
|
215
|
+
* GIL must be held.
|
|
216
|
+
*/
|
|
217
|
+
PyObject* _resolveAttributePath(
|
|
218
|
+
PyObject* obj,
|
|
219
|
+
const std::vector<std::string>& path
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Convert a SerializedValue back to a Python object.
|
|
224
|
+
* Returns new reference.
|
|
225
|
+
*/
|
|
226
|
+
PyObject* _deserialize(const SerializedValue& value);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build Python args tuple and kwargs dict from CallOptions.
|
|
230
|
+
*/
|
|
231
|
+
bool _buildCallArgs(
|
|
232
|
+
const CallOptions& options,
|
|
233
|
+
PyObject*& argsOut,
|
|
234
|
+
PyObject*& kwargsOut
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build a TaskResult from Python exception state.
|
|
239
|
+
*/
|
|
240
|
+
TaskResult _makeErrorResult();
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build a successful TaskResult from a Python object.
|
|
244
|
+
*/
|
|
245
|
+
TaskResult _makeSuccessResult(PyObject* obj);
|
|
246
|
+
|
|
247
|
+
bool _initialized = false;
|
|
248
|
+
PyThreadState* _mainThreadState = nullptr;
|
|
249
|
+
|
|
250
|
+
// Iterator registry: iteratorId → Python iterator object
|
|
251
|
+
std::map<uint32_t, PyObject*> _iterators;
|
|
252
|
+
std::mutex _iteratorsMutex;
|
|
253
|
+
std::atomic<uint32_t> _nextIteratorId{1};
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
} // namespace nodepyx
|
|
257
|
+
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nodepyx — 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 nodepyx {
|
|
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 << "[nodepyx] 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
|
+
|
|
53
|
+
_running.store(true);
|
|
54
|
+
_draining.store(false);
|
|
55
|
+
|
|
56
|
+
// Spawn worker threads
|
|
57
|
+
_threads.reserve(_numThreads);
|
|
58
|
+
for (uint32_t i = 0; i < _numThreads; ++i) {
|
|
59
|
+
_threads.emplace_back(&PythonThreadPool::_workerThread, this, i);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Task Submission ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
uint64_t PythonThreadPool::submit(
|
|
66
|
+
std::function<TaskResult()> work,
|
|
67
|
+
std::function<void(TaskResult)> callback,
|
|
68
|
+
uint32_t timeoutMs
|
|
69
|
+
) {
|
|
70
|
+
if (!_running.load()) {
|
|
71
|
+
TaskResult errorResult;
|
|
72
|
+
errorResult.success = false;
|
|
73
|
+
errorResult.errorType = "RuntimeError";
|
|
74
|
+
errorResult.errorMessage = "Thread pool is shut down";
|
|
75
|
+
errorResult.errorTraceback = "";
|
|
76
|
+
callback(std::move(errorResult));
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check queue capacity
|
|
81
|
+
{
|
|
82
|
+
std::lock_guard<std::mutex> lock(_queueMutex);
|
|
83
|
+
if (_queue.size() >= _maxQueueSize) {
|
|
84
|
+
// Backpressure: queue is full
|
|
85
|
+
TaskResult errorResult;
|
|
86
|
+
errorResult.success = false;
|
|
87
|
+
errorResult.errorType = "RuntimeError";
|
|
88
|
+
errorResult.errorMessage = "Python thread pool queue is full (max: " +
|
|
89
|
+
std::to_string(_maxQueueSize) + "). " +
|
|
90
|
+
"Consider increasing threadPoolSize or maxQueueSize.";
|
|
91
|
+
errorResult.errorTraceback = "";
|
|
92
|
+
callback(std::move(errorResult));
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
uint64_t taskId = _nextTaskId.fetch_add(1, std::memory_order_relaxed);
|
|
98
|
+
|
|
99
|
+
PythonTask task;
|
|
100
|
+
task.id = taskId;
|
|
101
|
+
task.work = std::move(work);
|
|
102
|
+
task.callback = std::move(callback);
|
|
103
|
+
task.submittedAt = std::chrono::steady_clock::now();
|
|
104
|
+
task.timeoutMs = timeoutMs;
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
std::lock_guard<std::mutex> lock(_queueMutex);
|
|
108
|
+
_queue.push_back(std::move(task));
|
|
109
|
+
_stats.totalTasksSubmitted.fetch_add(1, std::memory_order_relaxed);
|
|
110
|
+
_stats.currentQueueSize.fetch_add(1, std::memory_order_relaxed);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_queueCv.notify_one();
|
|
114
|
+
return taskId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Worker Thread ──────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
void PythonThreadPool::_workerThread(uint32_t threadId) {
|
|
120
|
+
// Give the thread a name for debugging
|
|
121
|
+
// Note: thread naming is platform-specific
|
|
122
|
+
#ifdef __linux__
|
|
123
|
+
pthread_setname_np(pthread_self(), ("nodepyx-worker-" + std::to_string(threadId)).c_str());
|
|
124
|
+
#endif
|
|
125
|
+
|
|
126
|
+
while (_running.load()) {
|
|
127
|
+
PythonTask task;
|
|
128
|
+
|
|
129
|
+
// Wait for a task
|
|
130
|
+
{
|
|
131
|
+
std::unique_lock<std::mutex> lock(_queueMutex);
|
|
132
|
+
_queueCv.wait(lock, [this] {
|
|
133
|
+
return !_queue.empty() || !_running.load();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!_running.load() && _queue.empty()) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (_queue.empty()) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
task = std::move(_queue.front());
|
|
145
|
+
_queue.pop_front();
|
|
146
|
+
_stats.currentQueueSize.fetch_sub(1, std::memory_order_relaxed);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check timeout before executing
|
|
150
|
+
if (_isTimedOut(task)) {
|
|
151
|
+
TaskResult timeoutResult;
|
|
152
|
+
timeoutResult.success = false;
|
|
153
|
+
timeoutResult.errorType = "TimeoutError";
|
|
154
|
+
timeoutResult.errorMessage = "Python task timed out after " +
|
|
155
|
+
std::to_string(task.timeoutMs) + "ms in queue";
|
|
156
|
+
timeoutResult.errorTraceback = "";
|
|
157
|
+
_stats.totalTasksTimedOut.fetch_add(1, std::memory_order_relaxed);
|
|
158
|
+
_postResult(task.id, std::move(timeoutResult), std::move(task.callback));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Execute the task with GIL held
|
|
163
|
+
_stats.activeThreads.fetch_add(1, std::memory_order_relaxed);
|
|
164
|
+
|
|
165
|
+
auto startTime = std::chrono::steady_clock::now();
|
|
166
|
+
TaskResult result;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// Acquire GIL — critical: must be released when we're done
|
|
170
|
+
GILAcquire gil;
|
|
171
|
+
|
|
172
|
+
// Check timeout again (task might have waited in queue)
|
|
173
|
+
if (task.timeoutMs > 0 && _isTimedOut(task)) {
|
|
174
|
+
result.success = false;
|
|
175
|
+
result.errorType = "TimeoutError";
|
|
176
|
+
result.errorMessage = "Python task timed out";
|
|
177
|
+
result.errorTraceback = "";
|
|
178
|
+
} else {
|
|
179
|
+
// Execute the Python work
|
|
180
|
+
result = task.work();
|
|
181
|
+
}
|
|
182
|
+
} catch (const std::exception& e) {
|
|
183
|
+
result.success = false;
|
|
184
|
+
result.errorType = "RuntimeError";
|
|
185
|
+
result.errorMessage = std::string("C++ exception in worker thread: ") + e.what();
|
|
186
|
+
result.errorTraceback = "";
|
|
187
|
+
} catch (...) {
|
|
188
|
+
result.success = false;
|
|
189
|
+
result.errorType = "RuntimeError";
|
|
190
|
+
result.errorMessage = "Unknown C++ exception in worker thread";
|
|
191
|
+
result.errorTraceback = "";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
auto endTime = std::chrono::steady_clock::now();
|
|
195
|
+
auto durationUs = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
|
|
196
|
+
_stats.totalExecutionTimeUs.fetch_add(static_cast<uint64_t>(durationUs), std::memory_order_relaxed);
|
|
197
|
+
|
|
198
|
+
if (result.success) {
|
|
199
|
+
_stats.totalTasksCompleted.fetch_add(1, std::memory_order_relaxed);
|
|
200
|
+
} else {
|
|
201
|
+
_stats.totalTasksFailed.fetch_add(1, std::memory_order_relaxed);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
_stats.activeThreads.fetch_sub(1, std::memory_order_relaxed);
|
|
205
|
+
|
|
206
|
+
// Post result back to Node.js event loop
|
|
207
|
+
_postResult(task.id, std::move(result), std::move(task.callback));
|
|
208
|
+
|
|
209
|
+
// Signal drain waiters
|
|
210
|
+
if (_draining.load()) {
|
|
211
|
+
std::lock_guard<std::mutex> lock(_queueMutex);
|
|
212
|
+
_drainCv.notify_all();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Result Delivery ────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
void PythonThreadPool::_postResult(
|
|
220
|
+
uint64_t taskId,
|
|
221
|
+
TaskResult result,
|
|
222
|
+
std::function<void(TaskResult)> callback
|
|
223
|
+
) {
|
|
224
|
+
// Package the callback + result into a heap allocation
|
|
225
|
+
// that will be freed in the JS thread
|
|
226
|
+
auto* payload = new std::pair<std::function<void(TaskResult)>, TaskResult>(
|
|
227
|
+
std::move(callback),
|
|
228
|
+
std::move(result)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Use ThreadSafeFunction to invoke the callback in the JS event loop
|
|
232
|
+
napi_status status = _tsfn.NonBlockingCall(
|
|
233
|
+
payload,
|
|
234
|
+
[](Napi::Env env, Napi::Function jsCallback, void* data) {
|
|
235
|
+
auto* p = static_cast<std::pair<std::function<void(TaskResult)>, TaskResult>*>(data);
|
|
236
|
+
try {
|
|
237
|
+
p->first(std::move(p->second));
|
|
238
|
+
} catch (...) {
|
|
239
|
+
// Don't let JS callback exceptions crash the worker
|
|
240
|
+
}
|
|
241
|
+
delete p;
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (status != napi_ok) {
|
|
246
|
+
// This can happen during shutdown; just clean up
|
|
247
|
+
auto* p = static_cast<std::pair<std::function<void(TaskResult)>, TaskResult>*>(payload);
|
|
248
|
+
try {
|
|
249
|
+
// Try to call with error result
|
|
250
|
+
TaskResult shutdownResult;
|
|
251
|
+
shutdownResult.success = false;
|
|
252
|
+
shutdownResult.errorType = "RuntimeError";
|
|
253
|
+
shutdownResult.errorMessage = "Thread pool shut down while task was running";
|
|
254
|
+
p->first(std::move(shutdownResult));
|
|
255
|
+
} catch (...) {}
|
|
256
|
+
delete p;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Timeout Check ──────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
bool PythonThreadPool::_isTimedOut(const PythonTask& task) const {
|
|
263
|
+
if (task.timeoutMs == 0) return false;
|
|
264
|
+
|
|
265
|
+
auto now = std::chrono::steady_clock::now();
|
|
266
|
+
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
267
|
+
now - task.submittedAt
|
|
268
|
+
).count();
|
|
269
|
+
|
|
270
|
+
return elapsed >= static_cast<long long>(task.timeoutMs);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Resize ─────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
void PythonThreadPool::resize(uint32_t newNumThreads) {
|
|
276
|
+
if (!_running.load()) return;
|
|
277
|
+
if (newNumThreads == 0) newNumThreads = 1;
|
|
278
|
+
|
|
279
|
+
// For simplicity: add threads if growing, remove by setting flag if shrinking
|
|
280
|
+
// A production implementation would use a more sophisticated approach
|
|
281
|
+
uint32_t current = static_cast<uint32_t>(_threads.size());
|
|
282
|
+
|
|
283
|
+
if (newNumThreads > current) {
|
|
284
|
+
for (uint32_t i = current; i < newNumThreads; ++i) {
|
|
285
|
+
_threads.emplace_back(&PythonThreadPool::_workerThread, this, i);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Shrinking is complex and rarely needed; skip for now
|
|
289
|
+
_numThreads = newNumThreads;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── Shutdown ───────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
void PythonThreadPool::shutdown() {
|
|
295
|
+
if (!_running.load()) return;
|
|
296
|
+
|
|
297
|
+
// Drain the queue first
|
|
298
|
+
_draining.store(true);
|
|
299
|
+
{
|
|
300
|
+
std::unique_lock<std::mutex> lock(_queueMutex);
|
|
301
|
+
_drainCv.wait_for(lock, std::chrono::seconds(30), [this] {
|
|
302
|
+
return _queue.empty() && _stats.activeThreads.load() == 0;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
_draining.store(false);
|
|
306
|
+
|
|
307
|
+
forceShutdown();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
void PythonThreadPool::forceShutdown() {
|
|
311
|
+
if (!_running.load()) return;
|
|
312
|
+
|
|
313
|
+
_running.store(false);
|
|
314
|
+
_queueCv.notify_all();
|
|
315
|
+
|
|
316
|
+
// Wait for all threads to finish
|
|
317
|
+
for (auto& thread : _threads) {
|
|
318
|
+
if (thread.joinable()) {
|
|
319
|
+
thread.join();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
_threads.clear();
|
|
323
|
+
|
|
324
|
+
// Release ThreadSafeFunction
|
|
325
|
+
_tsfn.Release();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── Queue Size ─────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
size_t PythonThreadPool::getQueueSize() const {
|
|
331
|
+
std::lock_guard<std::mutex> lock(_queueMutex);
|
|
332
|
+
return _queue.size();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
} // namespace nodepyx
|
|
336
|
+
|