Skip to content

Commit 56784d1

Browse files
authored
[JS API] Improve error handling in AsyncInferQueue (#34857)
- exceptions thrown during asynchronous inference and callback execution in `async_infer_queue.cpp` are now caught and converted to rejected JavaScript promises - test cases in `async_infer_queue.test.js` now use `try/finally` blocks to guarantee that `inferQueue.release()` is called, even when tests fail or throw exceptions. - add tests to verify that `startAsync` correctly rejects promises when provided with input data of incorrect shape or incompatible tensor types ### Tickets: - CVS-170804
1 parent 434dfb7 commit 56784d1

File tree

2 files changed

+119
-54
lines changed

2 files changed

+119
-54
lines changed

src/bindings/js/node/src/async_infer_queue.cpp

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -108,22 +108,22 @@ void AsyncInferQueue::set_custom_callbacks(const Napi::CallbackInfo& info) {
108108
for (size_t handle = 0; handle < m_requests.size(); handle++) {
109109
m_requests[handle].set_callback([this, handle](std::exception_ptr exception_ptr) {
110110
try {
111-
if (exception_ptr) {
112-
std::rethrow_exception(exception_ptr);
113-
}
114-
auto ov_callback = [this, handle](Napi::Env env, Napi::Function user_callback) {
111+
auto ov_callback = [this, handle, exception_ptr](Napi::Env env, Napi::Function user_callback) {
115112
Napi::Object js_ir = InferRequestWrap::wrap(env, m_requests[handle]);
116113
const auto promise = m_user_ids[handle].second;
117114
try {
115+
if (exception_ptr) {
116+
std::rethrow_exception(exception_ptr);
117+
}
118118
auto user_data =
119119
m_user_ids[handle].first.Value().ToString().Utf8Value() == UNDEFINED_USER_DATA
120120
? env.Undefined()
121121
: m_user_ids[handle].first.Value();
122-
user_callback.Call({env.Null(), js_ir, user_data}); // CVS-170804
122+
user_callback.Call({env.Null(), js_ir, user_data});
123123
promise.Resolve(user_data);
124124
// returns before the promise's .then() is completed
125-
} catch (Napi::Error& e) {
126-
promise.Reject(Napi::Error::New(env, e.Message()).Value());
125+
} catch (const std::exception& e) {
126+
promise.Reject(Napi::Error::New(env, e.what()).Value());
127127
}
128128
// Start async inference on the next request or add idle handle to queue
129129
if (std::lock_guard<std::mutex> lock(m_mutex); m_awaiting_requests.size() > 0) {
@@ -151,21 +151,27 @@ void AsyncInferQueue::start_async_impl(const size_t handle,
151151
Napi::Object infer_data,
152152
Napi::Object user_data,
153153
Napi::Promise::Deferred deferred) {
154-
m_user_inputs[handle] = Napi::Persistent(infer_data); // keep reference to inputs so they are not garbage collected
155-
m_user_ids[handle] = std::make_pair(Napi::Persistent(user_data), deferred);
156-
157-
// CVS-166764
158-
const auto& keys = infer_data.GetPropertyNames();
159-
for (uint32_t i = 0; i < keys.Length(); ++i) {
160-
auto input_name = static_cast<Napi::Value>(keys[i]).ToString().Utf8Value();
161-
auto value = infer_data.Get(input_name);
162-
auto tensor = value_to_tensor(value, m_requests[handle], input_name);
154+
try {
155+
m_user_inputs[handle] =
156+
Napi::Persistent(infer_data); // keep reference to inputs so they are not garbage collected
157+
m_user_ids[handle] = std::make_pair(Napi::Persistent(user_data), deferred);
158+
159+
// CVS-166764
160+
const auto& keys = infer_data.GetPropertyNames();
161+
for (uint32_t i = 0; i < keys.Length(); ++i) {
162+
auto input_name = static_cast<Napi::Value>(keys[i]).ToString().Utf8Value();
163+
auto value = infer_data.Get(input_name);
164+
auto tensor = value_to_tensor(value, m_requests[handle], input_name);
165+
166+
m_requests[handle].set_tensor(input_name, tensor);
167+
}
163168

164-
m_requests[handle].set_tensor(input_name, tensor);
169+
OPENVINO_ASSERT(m_tsfn != nullptr,
170+
"Callback has to be set before starting inference. Use 'setCallback' method.");
171+
m_requests[handle].start_async(); // returns immediately, main event loop is free
172+
} catch (const std::exception& e) {
173+
deferred.Reject(Napi::Error::New(infer_data.Env(), e.what()).Value());
165174
}
166-
167-
OPENVINO_ASSERT(m_tsfn != nullptr, "Callback has to be set before starting inference. Use 'setCallback' method.");
168-
m_requests[handle].start_async(); // returns immediately, main event loop is free
169175
}
170176

171177
Napi::Value AsyncInferQueue::start_async(const Napi::CallbackInfo& info) {

src/bindings/js/node/tests/unit/async_infer_queue.test.js

Lines changed: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,18 @@ describe("Tests for AsyncInferQueue.", () => {
6060

6161
inferQueue.setCallback(callback);
6262

63-
for (let i = 0; i < jobs; i++) {
64-
const img = generateImage();
65-
// Start the inference request in non-blocking mode.
66-
// The results will be available in the callback function.
67-
await inferQueue.startAsync({ data: img }, i);
68-
}
63+
try {
64+
for (let i = 0; i < jobs; i++) {
65+
const img = generateImage();
66+
// Start the inference request in non-blocking mode.
67+
// The results will be available in the callback function.
68+
await inferQueue.startAsync({ data: img }, i);
69+
}
6970

70-
assert.strictEqual(jobsDone.filter((job) => job.finished).length, jobs);
71-
inferQueue.release();
71+
assert.strictEqual(jobsDone.filter((job) => job.finished).length, jobs);
72+
} finally {
73+
inferQueue.release();
74+
}
7275
});
7376

7477
it("Test startAsync fails without callback", async () => {
@@ -92,8 +95,11 @@ describe("Tests for AsyncInferQueue.", () => {
9295
}
9396
}
9497
inferQueue.setCallback(basicUserCallback);
95-
await inferQueue.startAsync({ data: generateImage() });
96-
inferQueue.release();
98+
try {
99+
await inferQueue.startAsync({ data: generateImage() });
100+
} finally {
101+
inferQueue.release();
102+
}
97103
});
98104

99105
it("test Promise.all()", async () => {
@@ -124,9 +130,17 @@ describe("Tests for AsyncInferQueue.", () => {
124130
promises.push(promise);
125131
}
126132

127-
await Promise.all(promises);
128-
assert.strictEqual(jobsDone.filter((job) => job.finished).length, jobs);
129-
inferQueue.release();
133+
const allSettled = Promise.allSettled(promises);
134+
135+
try {
136+
const results = await allSettled;
137+
const rejected = results.filter((r) => r.status === "rejected");
138+
assert.strictEqual(rejected.length, 0, "startAsync promise rejected");
139+
assert.strictEqual(jobsDone.filter((job) => job.finished).length, jobs);
140+
} finally {
141+
await allSettled;
142+
inferQueue.release();
143+
}
130144
});
131145

132146
it("Test AsyncInferQueue no freeze", async () => {
@@ -139,9 +153,12 @@ describe("Tests for AsyncInferQueue.", () => {
139153

140154
it("Test double set_callback", async () => {
141155
const inferQueue = new ov.AsyncInferQueue(compiledModel, numRequest);
142-
inferQueue.setCallback(() => {});
143-
inferQueue.setCallback(() => {});
144-
inferQueue.release();
156+
try {
157+
inferQueue.setCallback(() => {});
158+
inferQueue.setCallback(() => {});
159+
} finally {
160+
inferQueue.release();
161+
}
145162
});
146163

147164
it("Test repeated AsyncInferQueue.release()", async () => {
@@ -165,19 +182,25 @@ describe("Tests for AsyncInferQueue.", () => {
165182

166183
it("Test setCallback throws and list possible signatures", async () => {
167184
const inferQueue = new ov.AsyncInferQueue(compiledModel, numRequest);
168-
assert.throws(() => {
169-
inferQueue.setCallback();
170-
}, /'setCallback' method called with incorrect parameters./);
171-
inferQueue.release();
185+
try {
186+
assert.throws(() => {
187+
inferQueue.setCallback();
188+
}, /'setCallback' method called with incorrect parameters./);
189+
} finally {
190+
inferQueue.release();
191+
}
172192
});
173193

174194
it("Test startAsync throws and list possible signatures", async () => {
175195
const inferQueue = new ov.AsyncInferQueue(compiledModel, numRequest);
176-
inferQueue.setCallback(basicUserCallback);
177-
assert.throws(() => {
178-
inferQueue.startAsync({ data: generateImage() }, "user_data", "extra_param");
179-
}, /'startAsync' method called with incorrect parameters./);
180-
inferQueue.release();
196+
try {
197+
inferQueue.setCallback(basicUserCallback);
198+
assert.throws(() => {
199+
inferQueue.startAsync({ data: generateImage() }, "user_data", "extra_param");
200+
}, /'startAsync' method called with incorrect parameters./);
201+
} finally {
202+
inferQueue.release();
203+
}
181204
});
182205

183206
it("Test possibility to catch error in callback", async () => {
@@ -194,9 +217,12 @@ describe("Tests for AsyncInferQueue.", () => {
194217
}, /ReferenceError: jobsDone is not defined/);
195218
}
196219
}
197-
inferQueue.setCallback(callback);
198-
await inferQueue.startAsync({ data: generateImage() }, "data");
199-
inferQueue.release();
220+
try {
221+
inferQueue.setCallback(callback);
222+
await inferQueue.startAsync({ data: generateImage() }, "data");
223+
} finally {
224+
inferQueue.release();
225+
}
200226
});
201227

202228
it("Test error in callback and rejected promise", async () => {
@@ -212,11 +238,44 @@ describe("Tests for AsyncInferQueue.", () => {
212238
}
213239
}
214240

215-
inferQueue.setCallback(callback);
216-
await inferQueue.startAsync({ data: generateImage() }, "data").catch((err) => {
217-
assert(err instanceof Error);
218-
assert.strictEqual(err.message, "jobsDone is not defined");
219-
});
220-
inferQueue.release();
241+
try {
242+
inferQueue.setCallback(callback);
243+
await inferQueue.startAsync({ data: generateImage() }, "data").catch((err) => {
244+
assert(err instanceof Error);
245+
assert.strictEqual(err.message, "jobsDone is not defined");
246+
});
247+
} finally {
248+
inferQueue.release();
249+
}
250+
});
251+
252+
it("Test startAsync with incorrect data shape", async () => {
253+
const inferQueue = new ov.AsyncInferQueue(compiledModel, numRequest);
254+
inferQueue.setCallback(basicUserCallback);
255+
256+
const incorrectShapeData = generateImage([1, 2, 3]);
257+
try {
258+
await assert.rejects(
259+
async () => await inferQueue.startAsync({ data: incorrectShapeData }, "user_data"),
260+
/Memory allocated using shape and element::type mismatc/,
261+
);
262+
} finally {
263+
inferQueue.release();
264+
}
265+
});
266+
267+
it("Test startAsync with incorrect data shape2", async () => {
268+
const inferQueue = new ov.AsyncInferQueue(compiledModel, numRequest);
269+
inferQueue.setCallback(basicUserCallback);
270+
271+
const tensor1 = new ov.Tensor(ov.element.f32, [1, 3, 32, 31]);
272+
try {
273+
await assert.rejects(
274+
async () => await inferQueue.startAsync({ data: tensor1 }, "user_data"),
275+
/incompatible/,
276+
);
277+
} finally {
278+
inferQueue.release();
279+
}
221280
});
222281
});

0 commit comments

Comments
 (0)