Skip to content

Commit 7576e5b

Browse files
authored
Merge pull request #26 from benoitc/refactor/cast-spawn-call
Refactor py:cast to fire-and-forget, add py:spawn_call
2 parents f78610d + c514605 commit 7576e5b

File tree

9 files changed

+96
-31
lines changed

9 files changed

+96
-31
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ application:ensure_all_started(erlang_python).
6666
%% Evaluate with local variables
6767
{ok, 25} = py:eval(<<"x * y">>, #{x => 5, y => 5}).
6868

69-
%% Async calls
70-
Ref = py:cast(math, factorial, [100]),
69+
%% Async calls with await
70+
Ref = py:spawn_call(math, factorial, [100]),
7171
{ok, Result} = py:await(Ref).
7272

73+
%% Fire-and-forget (no result)
74+
ok = py:cast(erlang, send, [self(), {done, <<"task1">>}]).
75+
7376
%% Streaming from generators
7477
{ok, [0,1,4,9,16]} = py:stream_eval(<<"(x**2 for x in range(5))">>).
7578
```
@@ -443,10 +446,13 @@ escript examples/logging_example.erl
443446
{ok, Result} = py:call(Module, Function, Args, KwArgs).
444447
{ok, Result} = py:call(Module, Function, Args, KwArgs, Timeout).
445448

446-
%% Async
447-
Ref = py:cast(Module, Function, Args).
449+
%% Async with result
450+
Ref = py:spawn_call(Module, Function, Args).
448451
{ok, Result} = py:await(Ref).
449452
{ok, Result} = py:await(Ref, Timeout).
453+
454+
%% Fire-and-forget (no result returned)
455+
ok = py:cast(Module, Function, Args).
450456
```
451457

452458
### Expression Evaluation

docs/ai-integration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,9 @@ This demonstrates:
503503
For non-blocking LLM calls:
504504

505505
```erlang
506-
%% Start async LLM call
506+
%% Start async LLM call (returns ref for await)
507507
ask_async(Question) ->
508-
py:cast('__main__', generate, [Question, <<"">>]).
508+
py:spawn_call('__main__', generate, [Question, <<"">>]).
509509

510510
%% Gather multiple responses
511511
ask_many(Questions) ->

docs/getting-started.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,16 @@ All operations support optional timeouts:
9494
For non-blocking operations:
9595

9696
```erlang
97-
%% Start async call
98-
Ref = py:cast(math, factorial, [1000]).
97+
%% Start async call (returns ref for await)
98+
Ref = py:spawn_call(math, factorial, [1000]).
9999

100100
%% Do other work...
101101

102102
%% Wait for result
103103
{ok, HugeNumber} = py:await(Ref).
104+
105+
%% Fire-and-forget (no result)
106+
ok = py:cast(some_module, log_event, [EventData]).
104107
```
105108

106109
## Streaming from Generators

docs/migration.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This guide covers breaking changes and migration steps when upgrading from erlan
44

55
## Quick Checklist
66

7-
- [ ] Rename `py:call_async``py:cast`
7+
- [ ] Rename `py:call_async``py:spawn_call` (with await) or `py:cast` (fire-and-forget)
88
- [ ] Replace `py:bind`/`py:unbind` with `py_context_router`
99
- [ ] Replace `py:ctx_*` functions with `py_context:*`
1010
- [ ] Replace `erlang_asyncio` imports with `erlang`
@@ -148,7 +148,7 @@ N = py_context_router:num_contexts().
148148

149149
## API Changes
150150

151-
### `py:call_async` renamed to `py:cast`
151+
### `py:call_async` renamed to `py:spawn_call`
152152

153153
The function for non-blocking Python calls has been renamed to follow gen_server conventions:
154154

@@ -160,11 +160,13 @@ Ref = py:call_async(math, factorial, [100]),
160160

161161
**After (v2.0):**
162162
```erlang
163-
Ref = py:cast(math, factorial, [100]),
163+
Ref = py:spawn_call(math, factorial, [100]),
164164
{ok, Result} = py:await(Ref).
165165
```
166166

167-
The semantics are identical - only the name changed.
167+
The semantics are identical - `spawn_call` replaces `async_call`.
168+
169+
Note: `py:cast/3,4` is now fire-and-forget (returns `ok`, no await).
168170

169171
### `erlang_asyncio` module removed
170172

docs/pools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ py:register_pool(io, requests), %% API calls
221221

222222
process_batch(Items) ->
223223
%% Parallel fetch from S3 (io pool)
224-
Futures = [py:cast(boto3, download_file, [Key]) || Key <- Items],
224+
Futures = [py:spawn_call(boto3, download_file, [Key]) || Key <- Items],
225225
Files = [py:await(F) || F <- Futures],
226226

227227
%% Process with ML model (default pool - doesn't block I/O)

examples/basic_example.erl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ main(_) ->
5252

5353
io:format("~n=== Async Calls ===~n~n"),
5454

55-
%% Async call
56-
Ref1 = py:cast(math, factorial, [10]),
57-
Ref2 = py:cast(math, factorial, [20]),
55+
%% Async call with spawn_call/await
56+
Ref1 = py:spawn_call(math, factorial, [10]),
57+
Ref2 = py:spawn_call(math, factorial, [20]),
5858

5959
{ok, Fact10} = py:await(Ref1),
6060
{ok, Fact20} = py:await(Ref2),

examples/benchmark_compare.erl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,15 @@ bench_sync_eval() ->
101101
%% ============================================================================
102102

103103
bench_cast_single() ->
104-
Name = "Cast py:cast single",
104+
Name = "Cast py:spawn_call single",
105105
N = 1000,
106106

107107
io:format("~s~n", [Name]),
108108
io:format(" Iterations: ~p~n", [N]),
109109

110110
{Time, _} = timer:tc(fun() ->
111111
lists:foreach(fun(I) ->
112-
Ref = py:cast(math, sqrt, [I]),
112+
Ref = py:spawn_call(math, sqrt, [I]),
113113
{ok, _} = py:await(Ref, 5000)
114114
end, lists:seq(1, N))
115115
end),
@@ -125,7 +125,7 @@ bench_cast_single() ->
125125
{Name, PerCall, Throughput}.
126126

127127
bench_cast_multiple() ->
128-
Name = "Cast py:cast batch (10 calls)",
128+
Name = "Cast py:spawn_call batch (10 calls)",
129129
N = 100,
130130

131131
io:format("~s~n", [Name]),
@@ -134,7 +134,7 @@ bench_cast_multiple() ->
134134
{Time, _} = timer:tc(fun() ->
135135
lists:foreach(fun(Batch) ->
136136
%% Start 10 cast calls
137-
Refs = [py:cast(math, sqrt, [Batch * 10 + I])
137+
Refs = [py:spawn_call(math, sqrt, [Batch * 10 + I])
138138
|| I <- lists:seq(1, 10)],
139139
%% Await all
140140
[{ok, _} = py:await(Ref, 5000) || Ref <- Refs]
@@ -153,7 +153,7 @@ bench_cast_multiple() ->
153153
{Name, PerBatch, Throughput}.
154154

155155
bench_cast_parallel() ->
156-
Name = "Cast py:cast parallel (10 concurrent)",
156+
Name = "Cast py:spawn_call parallel (10 concurrent)",
157157
N = 100,
158158

159159
io:format("~s~n", [Name]),
@@ -162,7 +162,7 @@ bench_cast_parallel() ->
162162
{Time, _} = timer:tc(fun() ->
163163
lists:foreach(fun(Batch) ->
164164
%% Start 10 cast calls in parallel
165-
Refs = [py:cast(math, factorial, [20 + (Batch rem 10)])
165+
Refs = [py:spawn_call(math, factorial, [20 + (Batch rem 10)])
166166
|| _ <- lists:seq(1, 10)],
167167
%% Await all results
168168
[py:await(Ref, 5000) || Ref <- Refs]
@@ -230,7 +230,7 @@ bench_concurrent_cast() ->
230230
{Time, _} = timer:tc(fun() ->
231231
Pids = [spawn_link(fun() ->
232232
lists:foreach(fun(I) ->
233-
Ref = py:cast(math, factorial, [20 + I]),
233+
Ref = py:spawn_call(math, factorial, [20 + I]),
234234
{ok, _} = py:await(Ref, 5000)
235235
end, lists:seq(1, CallsPerProc)),
236236
Parent ! {done, self()}

src/py.erl

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
call/5,
4242
cast/3,
4343
cast/4,
44+
cast/5,
45+
spawn_call/3,
46+
spawn_call/4,
47+
spawn_call/5,
4448
await/1,
4549
await/2,
4650
eval/1,
@@ -270,16 +274,42 @@ exec(Ctx, Code) when is_pid(Ctx) ->
270274
%%% Asynchronous API
271275
%%% ============================================================================
272276

273-
%% @doc Cast a Python function call, returns immediately with a ref.
274-
%% The call executes in a spawned process. Use await/1,2 to get the result.
275-
-spec cast(py_module(), py_func(), py_args()) -> py_ref().
277+
%% @doc Fire-and-forget Python function call.
278+
-spec cast(py_module(), py_func(), py_args()) -> ok.
276279
cast(Module, Func, Args) ->
277280
cast(Module, Func, Args, #{}).
278281

279-
%% @doc Cast a Python function call with kwargs.
280-
-spec cast(py_module(), py_func(), py_args(), py_kwargs()) -> py_ref().
282+
%% @doc Fire-and-forget Python function call with context or kwargs.
283+
-spec cast(pid(), py_module(), py_func(), py_args()) -> ok;
284+
(py_module(), py_func(), py_args(), py_kwargs()) -> ok.
285+
cast(Ctx, Module, Func, Args) when is_pid(Ctx) ->
286+
cast(Ctx, Module, Func, Args, #{});
281287
cast(Module, Func, Args, Kwargs) ->
282-
%% Spawn a process to execute the call and return a ref
288+
spawn(fun() ->
289+
Ctx = py_context_router:get_context(),
290+
_ = py_context:call(Ctx, Module, Func, Args, Kwargs)
291+
end),
292+
ok.
293+
294+
%% @doc Fire-and-forget Python function call with context and kwargs.
295+
-spec cast(pid(), py_module(), py_func(), py_args(), py_kwargs()) -> ok.
296+
cast(Ctx, Module, Func, Args, Kwargs) when is_pid(Ctx) ->
297+
spawn(fun() ->
298+
_ = py_context:call(Ctx, Module, Func, Args, Kwargs)
299+
end),
300+
ok.
301+
302+
%% @doc Spawn a Python function call, returns immediately with a ref.
303+
-spec spawn_call(py_module(), py_func(), py_args()) -> py_ref().
304+
spawn_call(Module, Func, Args) ->
305+
spawn_call(Module, Func, Args, #{}).
306+
307+
%% @doc Spawn a Python function call with context or kwargs.
308+
-spec spawn_call(pid(), py_module(), py_func(), py_args()) -> py_ref();
309+
(py_module(), py_func(), py_args(), py_kwargs()) -> py_ref().
310+
spawn_call(Ctx, Module, Func, Args) when is_pid(Ctx) ->
311+
spawn_call(Ctx, Module, Func, Args, #{});
312+
spawn_call(Module, Func, Args, Kwargs) ->
283313
Ref = make_ref(),
284314
Parent = self(),
285315
spawn(fun() ->
@@ -289,6 +319,17 @@ cast(Module, Func, Args, Kwargs) ->
289319
end),
290320
Ref.
291321

322+
%% @doc Spawn a Python function call with context and kwargs.
323+
-spec spawn_call(pid(), py_module(), py_func(), py_args(), py_kwargs()) -> py_ref().
324+
spawn_call(Ctx, Module, Func, Args, Kwargs) when is_pid(Ctx) ->
325+
Ref = make_ref(),
326+
Parent = self(),
327+
spawn(fun() ->
328+
Result = py_context:call(Ctx, Module, Func, Args, Kwargs),
329+
Parent ! {py_response, Ref, Result}
330+
end),
331+
Ref.
332+
292333
%% @doc Wait for an async call to complete.
293334
-spec await(py_ref()) -> py_result().
294335
await(Ref) ->

test/py_SUITE.erl

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
test_eval_complex_locals/1,
1818
test_exec/1,
1919
test_cast/1,
20+
test_spawn_call/1,
2021
test_type_conversions/1,
2122
test_nested_types/1,
2223
test_timeout/1,
@@ -68,6 +69,7 @@ all() ->
6869
test_eval_complex_locals,
6970
test_exec,
7071
test_cast,
72+
test_spawn_call,
7173
test_type_conversions,
7274
test_nested_types,
7375
test_timeout,
@@ -204,9 +206,20 @@ def my_func():
204206
ok.
205207

206208
test_cast(_Config) ->
207-
Ref1 = py:cast(math, sqrt, [100]),
208-
Ref2 = py:cast(math, sqrt, [144]),
209+
%% Test fire-and-forget cast with erlang.send
210+
Self = erlang:self(),
211+
ok = py:cast(erlang, send, [Self, {<<"result">>, <<"msg1">>}]),
212+
ok = py:cast(erlang, send, [Self, {<<"result">>, <<"msg2">>}]),
213+
%% Wait for results (order may vary)
214+
R1 = receive {<<"result">>, V1} -> V1 after 5000 -> ct:fail(timeout1) end,
215+
R2 = receive {<<"result">>, V2} -> V2 after 5000 -> ct:fail(timeout2) end,
216+
true = lists:sort([R1, R2]) =:= [<<"msg1">>, <<"msg2">>],
217+
ok.
209218

219+
test_spawn_call(_Config) ->
220+
%% Test spawn_call with await
221+
Ref1 = py:spawn_call(math, sqrt, [100]),
222+
Ref2 = py:spawn_call(math, sqrt, [144]),
210223
{ok, 10.0} = py:await(Ref1),
211224
{ok, 12.0} = py:await(Ref2),
212225
ok.

0 commit comments

Comments
 (0)