Packages
Execute Python applications from Erlang using dirty NIFs
Retired package: Release invalid - Missing priv/erlang_loop.py
Current section
Files
Jump to
Current section
Files
docs/context-affinity.md
# Context Affinity
Context affinity allows you to bind an Erlang process to a dedicated Python worker, preserving Python state (variables, imports, objects) across multiple `py:call/eval/exec` invocations.
## Why Context Affinity?
By default, each call to `py:call`, `py:eval`, or `py:exec` may be handled by a different worker from the pool. This means:
- Variables defined in one call are not available in the next
- Imported modules must be re-imported
- Objects created in one call cannot be accessed later
Context affinity solves this by dedicating a worker to your process, ensuring all calls go to the same Python interpreter with preserved state.
## Process-Implicit Binding
The simplest approach binds the current Erlang process to a worker:
```erlang
%% Bind current process to a dedicated worker
ok = py:bind(),
%% Now all calls use the same worker - state persists!
ok = py:exec(<<"counter = 0">>),
ok = py:exec(<<"counter += 1">>),
{ok, 1} = py:eval(<<"counter">>),
ok = py:exec(<<"counter += 1">>),
{ok, 2} = py:eval(<<"counter">>),
%% Release the worker back to the pool
ok = py:unbind().
```
### Checking Binding Status
```erlang
false = py:is_bound(),
ok = py:bind(),
true = py:is_bound(),
ok = py:unbind(),
false = py:is_bound().
```
## Explicit Contexts
For more control, create explicit context handles. This allows multiple independent Python contexts within a single Erlang process:
```erlang
%% Create two independent contexts
{ok, Ctx1} = py:bind(new),
{ok, Ctx2} = py:bind(new),
%% Each context has its own namespace
ok = py:ctx_exec(Ctx1, <<"x = 'context one'">>),
ok = py:ctx_exec(Ctx2, <<"x = 'context two'">>),
%% Values are isolated
{ok, <<"context one">>} = py:ctx_eval(Ctx1, <<"x">>),
{ok, <<"context two">>} = py:ctx_eval(Ctx2, <<"x">>),
%% Release both
ok = py:unbind(Ctx1),
ok = py:unbind(Ctx2).
```
### Context-Aware Functions
When using explicit contexts, use these functions:
| Function | Description |
|----------|-------------|
| `py:ctx_call(Ctx, Module, Func, Args)` | Call with context |
| `py:ctx_call(Ctx, Module, Func, Args, Kwargs)` | Call with kwargs |
| `py:ctx_call(Ctx, Module, Func, Args, Kwargs, Timeout)` | Call with timeout |
| `py:ctx_eval(Ctx, Code)` | Evaluate expression |
| `py:ctx_eval(Ctx, Code, Locals)` | Evaluate with locals |
| `py:ctx_eval(Ctx, Code, Locals, Timeout)` | Evaluate with timeout |
| `py:ctx_exec(Ctx, Code)` | Execute statements |
## Scoped Helper
The `with_context/1` function provides automatic bind/unbind with cleanup on exception:
### Implicit Binding (arity-0 function)
```erlang
Result = py:with_context(fun() ->
ok = py:exec(<<"total = 0">>),
ok = py:exec(<<"for i in range(10): total += i">>),
py:eval(<<"total">>)
end),
{ok, 45} = Result.
%% Process is automatically unbound here
```
### Explicit Context (arity-1 function)
```erlang
Result = py:with_context(fun(Ctx) ->
ok = py:ctx_exec(Ctx, <<"import json">>),
ok = py:ctx_exec(Ctx, <<"data = {'key': 'value'}">>),
py:ctx_eval(Ctx, <<"json.dumps(data)">>)
end),
{ok, <<"{\"key\": \"value\"}">>} = Result.
```
## Automatic Cleanup
### Process Death
If a bound process dies, the worker is automatically returned to the pool:
```erlang
Pid = spawn(fun() ->
ok = py:bind(),
%% Do some work...
exit(normal) %% Worker automatically returned
end).
```
### Worker Crash
If a bound worker crashes, the binding is cleaned up and a new worker is created:
```erlang
ok = py:bind(),
%% If the worker crashes, binding is cleaned up
%% Next bind() will get a fresh worker
```
## Use Cases
### Stateful Computation
```erlang
py:with_context(fun() ->
%% Load a model once
py:exec(<<"
import pickle
with open('model.pkl', 'rb') as f:
model = pickle.load(f)
">>),
%% Use it multiple times
{ok, Pred1} = py:eval(<<"model.predict([[1, 2, 3]])">>),
{ok, Pred2} = py:eval(<<"model.predict([[4, 5, 6]])">>),
{Pred1, Pred2}
end).
```
### Database Connections
```erlang
ok = py:bind(),
%% Establish connection once
py:exec(<<"
import sqlite3
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
cursor.execute('CREATE TABLE users (id INTEGER, name TEXT)')
">>),
%% Use the connection across multiple calls
py:exec(<<"cursor.execute('INSERT INTO users VALUES (1, \"Alice\")')">>),
py:exec(<<"cursor.execute('INSERT INTO users VALUES (2, \"Bob\")')">>),
{ok, Users} = py:eval(<<"cursor.execute('SELECT * FROM users').fetchall()">>),
%% Clean up
py:exec(<<"conn.close()">>),
py:unbind().
```
### Incremental Processing
```erlang
{ok, Ctx} = py:bind(new),
%% Initialize accumulator
py:ctx_exec(Ctx, <<"results = []">>),
%% Process items one at a time
lists:foreach(fun(Item) ->
py:ctx_exec(Ctx, <<"results.append(process_item(item))">>,
#{item => Item})
end, Items),
%% Get final results
{ok, Results} = py:ctx_eval(Ctx, <<"results">>),
py:unbind(Ctx).
```
## Performance Considerations
- **Binding overhead**: `bind()` requires a gen_server call to checkout a worker
- **Lookup overhead**: Once bound, routing adds only an O(1) ETS lookup
- **Pool exhaustion**: Each bound context removes a worker from the pool
- **Recommendation**: Use `with_context/1` for short-lived operations; explicit `bind/unbind` for long-lived sessions
## Pool Statistics
Check how many workers are bound:
```erlang
Stats = py_pool:get_stats(),
#{
num_workers := 8,
available_workers := 6, %% 2 workers are checked out
checked_out := 2,
pending_requests := 0
} = Stats.
```
## Error Handling
### No Workers Available
```erlang
%% If all workers are bound
{error, no_workers_available} = py:bind().
```
### Context Not Bound
```erlang
%% Using a context after unbind raises an error
{ok, Ctx} = py:bind(new),
ok = py:unbind(Ctx),
%% This will crash with context_not_bound
py:ctx_eval(Ctx, <<"1 + 1">>). %% error(context_not_bound)
```
## Best Practices
1. **Always unbind**: Use `with_context/1` or ensure `unbind` in a `try/after` block
2. **Minimize binding time**: Don't hold workers longer than necessary
3. **Watch pool size**: Monitor `py_pool:get_stats()` to avoid exhaustion
4. **Use explicit contexts**: When you need multiple independent namespaces
5. **Prefer implicit binding**: For simple sequential operations in a single process