Skip to content

Commit d28b923

Browse files
committed
Support Erlang/OTP 28 and 29
Validate build and the full Common Test suite on OTP 28 and 29 across Python 3.12-3.14. Set minimum_otp_vsn to 28 and update CI to test both releases with rebar3 3.25. Replace deprecated prefix-catch cleanup calls with try/catch to clear the new OTP 29 default warning; behavior is unchanged.
1 parent 906e29b commit d28b923

18 files changed

Lines changed: 116 additions & 76 deletions

.github/workflows/ci.yml

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,35 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
include:
18-
# Linux with different OTP/Python combinations
18+
# Linux - OTP 28
1919
- os: ubuntu-24.04
20-
otp: "27.0"
20+
otp: "28"
2121
python: "3.12"
2222
- os: ubuntu-24.04
23-
otp: "27.0"
23+
otp: "28"
2424
python: "3.13"
2525
- os: ubuntu-24.04
26-
otp: "27.0"
26+
otp: "28"
2727
python: "3.14"
28-
# macOS
28+
# Linux - OTP 29
29+
- os: ubuntu-24.04
30+
otp: "29"
31+
python: "3.12"
32+
- os: ubuntu-24.04
33+
otp: "29"
34+
python: "3.13"
35+
- os: ubuntu-24.04
36+
otp: "29"
37+
python: "3.14"
38+
# macOS (brew installs the latest OTP)
2939
- os: macos-15
30-
otp: "27"
40+
otp: "29"
3141
python: "3.12"
3242
- os: macos-15
33-
otp: "27"
43+
otp: "29"
3444
python: "3.13"
3545
- os: macos-15
36-
otp: "27"
46+
otp: "29"
3747
python: "3.14"
3848

3949
steps:
@@ -50,7 +60,7 @@ jobs:
5060
uses: erlef/setup-beam@v1
5161
with:
5262
otp-version: ${{ matrix.otp }}
53-
rebar3-version: "3.24"
63+
rebar3-version: "3.25"
5464

5565
- name: Set up Erlang (macOS)
5666
if: startsWith(matrix.os, 'macos')
@@ -139,7 +149,7 @@ jobs:
139149
python${{ matrix.python }} --version
140150
141151
# Build with rebar3
142-
fetch https://github.com/erlang/rebar3/releases/download/3.24.0/rebar3 -o rebar3
152+
fetch https://github.com/erlang/rebar3/releases/download/3.25.0/rebar3 -o rebar3
143153
chmod +x rebar3
144154
145155
# Compile and test (uses CMake-based build)
@@ -160,8 +170,8 @@ jobs:
160170
- name: Set up Erlang
161171
uses: erlef/setup-beam@v1
162172
with:
163-
otp-version: "27.0"
164-
rebar3-version: "3.24"
173+
otp-version: "29"
174+
rebar3-version: "3.25"
165175

166176
- name: Set up free-threaded Python
167177
uses: actions/setup-python@v5
@@ -234,8 +244,8 @@ jobs:
234244
- name: Set up Erlang
235245
uses: erlef/setup-beam@v1
236246
with:
237-
otp-version: "27.0"
238-
rebar3-version: "3.24"
247+
otp-version: "29"
248+
rebar3-version: "3.25"
239249

240250
- name: Install dependencies
241251
run: |
@@ -280,8 +290,8 @@ jobs:
280290
- name: Set up Erlang
281291
uses: erlef/setup-beam@v1
282292
with:
283-
otp-version: "27.0"
284-
rebar3-version: "3.24"
293+
otp-version: "29"
294+
rebar3-version: "3.25"
285295

286296
- name: Set up Python
287297
uses: actions/setup-python@v5
@@ -313,8 +323,8 @@ jobs:
313323
- name: Set up Erlang
314324
uses: erlef/setup-beam@v1
315325
with:
316-
otp-version: "27.0"
317-
rebar3-version: "3.24"
326+
otp-version: "29"
327+
rebar3-version: "3.25"
318328

319329
- name: Set up Python
320330
uses: actions/setup-python@v5

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Changed
6+
7+
- **Support Erlang/OTP 28 and 29** - Validated builds and the full Common Test
8+
suite on OTP 28 and 29. Minimum supported OTP is now 28 (`minimum_otp_vsn`).
9+
CI tests OTP 28 and 29 across Python 3.12/3.13/3.14.
10+
- Replaced deprecated `catch Expr` cleanup calls with `try ... catch ... end`
11+
to silence the new OTP 29 default warning; behavior is unchanged.
12+
313
## 3.0.0 (2026-05-03)
414

515
### Breaking Changes

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Key features:
3737

3838
## Requirements
3939

40-
- Erlang/OTP 27+
40+
- Erlang/OTP 28+ (tested on OTP 28 and 29)
4141
- Python 3.12+ (3.13+ for free-threading)
4242
- C compiler (gcc, clang)
4343

docs/scalability.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,24 @@ The `PERF_BUILD` option enables:
609609
{ok, 2}
610610
```
611611

612+
### Erlang/OTP Version
613+
614+
erlang_python is built and tested on **Erlang/OTP 28 and 29** (minimum OTP 28).
615+
The heavy lifting happens in the C NIF, so the BEAM-side glue is light, but newer
616+
OTP releases still help for free:
617+
618+
- **OTP 29 JIT and scheduler improvements** apply to the Erlang side (routing,
619+
context bookkeeping, callback dispatch) with no code change. Just run on OTP 29.
620+
- **Consistent map iteration order** is now guaranteed across map operations in
621+
OTP 29. The router and state modules do not depend on iteration order, so this
622+
changes nothing here, but it removes a class of subtle bugs if you build on top.
623+
- **Building the NIF stays the same** across OTP 28/29 (NIF API 2.18); no version
624+
guards are needed in `c_src`.
625+
626+
For doc-example testing, OTP 29 ships `ct_doctest`. This project keeps its own
627+
`scripts/lint_doc_snippets.escript` because that linter also validates the Python
628+
blocks in the docs, which `ct_doctest` does not cover.
629+
612630
## See Also
613631

614632
- [Getting Started](getting-started.md) - Basic usage

rebar.config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{erl_opts, [debug_info]}.
22

3+
{minimum_otp_vsn, "28"}.
4+
35
{xref_checks, [
46
undefined_function_calls,
57
undefined_functions,

src/erlang_python_app.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ start(_StartType, _StartArgs) ->
2525
stop(_State) ->
2626
%% Stop pools before application shutdown to ensure proper cleanup
2727
%% of subinterpreters before NIF resources are garbage collected
28-
catch py_context_router:stop_pool(io),
29-
catch py_context_router:stop_pool(default),
28+
try py_context_router:stop_pool(io) catch _:_ -> ok end,
29+
try py_context_router:stop_pool(default) catch _:_ -> ok end,
3030
ok.

src/py_context.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,12 +651,12 @@ terminate(_Reason, #state{ref = Ref, event_state = EventState, callback_handler
651651
%% Stop the event worker first (if it exists and is still alive)
652652
case EventState of
653653
#{worker_pid := WorkerPid} ->
654-
catch gen_server:stop(WorkerPid, normal, 5000);
654+
try gen_server:stop(WorkerPid, normal, 5000) catch _:_ -> ok end;
655655
_ ->
656656
ok
657657
end,
658658
%% Destroy the Python context
659-
catch py_nif:context_destroy(Ref),
659+
try py_nif:context_destroy(Ref) catch _:_ -> ok end,
660660
ok.
661661

662662
%% ============================================================================

src/py_context_router.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ stop_pool(Pool) when is_atom(Pool) ->
355355
Contexts ->
356356
lists:foreach(
357357
fun(Ctx) ->
358-
catch py_context_sup:stop_context(Ctx)
358+
try py_context_sup:stop_context(Ctx) catch _:_ -> ok end
359359
end,
360360
Contexts
361361
)
@@ -365,12 +365,12 @@ stop_pool(Pool) when is_atom(Pool) ->
365365
Size = persistent_term:get(?POOL_SIZE_KEY(Pool), 0),
366366
lists:foreach(
367367
fun(N) ->
368-
catch persistent_term:erase(?POOL_CONTEXT_KEY(Pool, N))
368+
try persistent_term:erase(?POOL_CONTEXT_KEY(Pool, N)) catch _:_ -> ok end
369369
end,
370370
lists:seq(1, Size)
371371
),
372-
catch persistent_term:erase(?POOL_SIZE_KEY(Pool)),
373-
catch persistent_term:erase(?POOL_CONTEXTS_KEY(Pool)),
372+
try persistent_term:erase(?POOL_SIZE_KEY(Pool)) catch _:_ -> ok end,
373+
try persistent_term:erase(?POOL_CONTEXTS_KEY(Pool)) catch _:_ -> ok end,
374374
ok.
375375

376376
%% @doc Check if a pool has been started and is still alive.

src/py_event_loop.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ import sys, asyncio
442442
if sys.version_info < (3, 14):
443443
asyncio.set_event_loop_policy(None)
444444
">>,
445-
catch py:exec(Code),
445+
try py:exec(Code) catch _:_ -> ok end,
446446
ok.
447447

448448
code_change(_OldVsn, State, _Extra) ->

src/py_event_loop_pool.erl

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ handle_info({'DOWN', MonRef, process, Pid, _Reason}, State) ->
510510
case SessionPid =:= Pid andalso SessionMonRef =:= MonRef of
511511
true ->
512512
%% Destroy session in worker
513-
catch py_nif:owngil_destroy_session(WorkerId, HandleId),
513+
try py_nif:owngil_destroy_session(WorkerId, HandleId) catch _:_ -> ok end,
514514
%% Remove from ETS
515515
ets:delete(Tid, Key);
516516
false ->
@@ -531,15 +531,15 @@ terminate(_Reason, State) ->
531531
Tid ->
532532
%% Destroy all sessions
533533
ets:foldl(fun(#owngil_session{worker_id = WorkerId, handle_id = HandleId}, _) ->
534-
catch py_nif:owngil_destroy_session(WorkerId, HandleId),
534+
try py_nif:owngil_destroy_session(WorkerId, HandleId) catch _:_ -> ok end,
535535
ok
536536
end, ok, Tid),
537-
catch ets:delete(Tid)
537+
try ets:delete(Tid) catch _:_ -> ok end
538538
end,
539539

540540
%% Stop OWN_GIL thread pool if it was started
541541
case State#state.owngil_enabled of
542-
true -> catch py_nif:subinterp_thread_pool_stop();
542+
true -> try py_nif:subinterp_thread_pool_stop() catch _:_ -> ok end;
543543
false -> ok
544544
end,
545545

@@ -548,15 +548,15 @@ terminate(_Reason, State) ->
548548
{} -> ok;
549549
Loops ->
550550
lists:foreach(fun({LoopRef, WorkerPid}) ->
551-
catch py_event_worker:stop(WorkerPid),
552-
catch py_nif:event_loop_destroy(LoopRef)
551+
try py_event_worker:stop(WorkerPid) catch _:_ -> ok end,
552+
try py_nif:event_loop_destroy(LoopRef) catch _:_ -> ok end
553553
end, tuple_to_list(Loops))
554554
end,
555555

556-
catch persistent_term:erase(?PT_LOOPS),
557-
catch persistent_term:erase(?PT_NUM_LOOPS),
558-
catch persistent_term:erase(?PT_OWNGIL_ENABLED),
559-
catch persistent_term:erase(?PT_SESSIONS),
556+
try persistent_term:erase(?PT_LOOPS) catch _:_ -> ok end,
557+
try persistent_term:erase(?PT_NUM_LOOPS) catch _:_ -> ok end,
558+
try persistent_term:erase(?PT_OWNGIL_ENABLED) catch _:_ -> ok end,
559+
try persistent_term:erase(?PT_SESSIONS) catch _:_ -> ok end,
560560
ok.
561561

562562
%%% ============================================================================
@@ -638,7 +638,7 @@ create_session(Tid, Pid, LoopIdx) ->
638638
false ->
639639
%% Another process created the session first, destroy ours
640640
erlang:demonitor(MonRef, [flush]),
641-
catch py_nif:owngil_destroy_session(WorkerId, HandleId),
641+
try py_nif:owngil_destroy_session(WorkerId, HandleId) catch _:_ -> ok end,
642642
%% Retry lookup
643643
case ets:lookup(Tid, {Pid, LoopIdx}) of
644644
[#owngil_session{worker_id = W, handle_id = H}] ->

0 commit comments

Comments
 (0)