Skip to content

Commit 044f78d

Browse files
committed
Improve error message when erlang.call() used from non-worker thread
The previous error "No callback handler registered" was confusing. The new message explains the issue and suggests using execute_async() for concurrent calls from other threads. Added test case to verify the improved error message.
1 parent b6dbe95 commit 044f78d

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

c_src/py_callback.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,9 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) {
541541
(void)self;
542542

543543
if (tl_current_worker == NULL || !tl_current_worker->has_callback_handler) {
544-
PyErr_SetString(PyExc_RuntimeError, "No callback handler registered");
544+
PyErr_SetString(PyExc_RuntimeError,
545+
"erlang.call() must be called from worker thread. "
546+
"Use execute_async() for concurrent calls from other threads.");
545547
return NULL;
546548
}
547549

test/py_reentrant_SUITE.erl

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
test_callback_error_propagation/1,
2121
test_concurrent_reentrant/1,
2222
test_callback_with_complex_types/1,
23-
test_multiple_sequential_callbacks/1
23+
test_multiple_sequential_callbacks/1,
24+
test_call_from_non_worker_thread/1
2425
]).
2526

2627
all() ->
@@ -30,7 +31,8 @@ all() ->
3031
test_callback_error_propagation,
3132
test_concurrent_reentrant,
3233
test_callback_with_complex_types,
33-
test_multiple_sequential_callbacks
34+
test_multiple_sequential_callbacks,
35+
test_call_from_non_worker_thread
3436
].
3537

3638
init_per_suite(Config) ->
@@ -230,3 +232,30 @@ test_multiple_sequential_callbacks(_Config) ->
230232
15 = Result3,
231233

232234
ok.
235+
236+
%% @doc Test that erlang.call() from a non-worker thread gives a helpful error.
237+
%% When Python spawns a thread (e.g., via ThreadPoolExecutor) and tries to call
238+
%% erlang.call() from that thread, it should fail with an informative message.
239+
test_call_from_non_worker_thread(_Config) ->
240+
%% Register a simple function to call
241+
py:register_function(simple_add, fun([A, B]) -> A + B end),
242+
243+
%% Use an inline lambda to test calling from a thread
244+
%% The lambda imports, creates executor, runs in thread, catches error
245+
Code = <<"(lambda cf, erlang: (lambda executor: (lambda future: (('expected_error', str(e)) if 'worker thread' in str(e := future.exception()) and 'execute_async' in str(e) else ('wrong_error', str(e))) if future.exception() else ('unexpected_success', future.result()))(executor.submit(lambda: erlang.call('simple_add', 1, 2))))(cf.ThreadPoolExecutor(max_workers=1).__enter__()))(__import__('concurrent.futures', fromlist=['ThreadPoolExecutor']), __import__('erlang'))">>,
246+
{ok, Result} = py:eval(Code),
247+
248+
%% Verify we got the expected error with helpful message
249+
case Result of
250+
{<<"expected_error">>, Msg} ->
251+
ct:log("Got expected error message: ~s", [Msg]),
252+
ok;
253+
{<<"wrong_error">>, Msg} ->
254+
ct:fail({wrong_error_message, Msg});
255+
{<<"unexpected_success">>, Val} ->
256+
ct:fail({should_have_failed, Val})
257+
end,
258+
259+
%% Cleanup
260+
py:unregister_function(simple_add),
261+
ok.

0 commit comments

Comments
 (0)