State Adapters
bptk server, state, postgres, redis, stateless, bptk, bptk-py, python, business prototyping
State Adapters
Three concrete implementations of ExternalStateAdapter. An adapter lets BptkServer keep its instance state somewhere other than its own memory, so a server can be restarted, scaled out or load-balanced without the sessions dying with the process.
All three take the same two decisions: where the state is written, and whether it is compressed on the way there.
They all live behind the server extra:
pip install "BPTK-Py[server]"FileAdapter
FileAdapter(compress, path)
Writes each instance to a file below path. The simplest of the three and the one to develop against: the state is readable on disk, so you can look at what the server stored.
Not suited to more than one server process — two processes writing the same directory have no way to coordinate.
Parameters
compress – Boolean. Whether to compress the state before writing it.
path – String. The directory to write the instance files to.
from BPTK_Py import BptkServer, FileAdapter
adapter = FileAdapter(compress=True, path="state/")
application = BptkServer(
__name__, bptk_factory, external_state_adapter=adapter
)PostgresAdapter
PostgresAdapter(postgres_client, compress)
Stores each instance as a row in a state table. The choice when the state has to outlive the server and be shared between several of them.
You pass a connection rather than connection details, so the pooling and the credentials stay where the rest of your application keeps them.
Parameters
postgres_client – A psycopg connection. Used as
postgres_client.cursor(), so a connection or a pooled connection both work.compress – Boolean. Whether to compress the state before writing it.
import psycopg
from BPTK_Py import BptkServer
from BPTK_Py.externalstateadapter.postgres_adapter import PostgresAdapter
connection = psycopg.connect("postgresql://user:password@localhost/bptk")
adapter = PostgresAdapter(postgres_client=connection, compress=True)The table
The adapter does not create the table, so it has to exist before the first request:
CREATE TABLE "state" (
"state" text,
"instance_id" text,
"time" text,
"timeout.weeks" bigint,
"timeout.days" bigint,
"timeout.hours" bigint,
"timeout.minutes" bigint,
"timeout.seconds" bigint,
"timeout.milliseconds" bigint,
"timeout.microseconds" bigint,
"step" bigint
);The order of the columns matters: the adapter reads a row with SELECT * and takes the values by position, so a table with the same columns in a different order loads the wrong values into the wrong fields.
RedisAdapter
RedisAdapter(redis_client, compress=True, key_prefix=‘bptk:state’)
Stores each instance under a Redis key. The choice when sessions are short-lived and speed matters more than durability.
Parameters
redis_client – A
redis.Redisclient.compress – Boolean (Default=True). Whether to compress the state before writing it.
key_prefix – String (Default=‘bptk:state’). The prefix every key is written under, so BPTK’s keys stay separable from everything else in the same Redis.
import redis
from BPTK_Py import BptkServer
from BPTK_Py.externalstateadapter.redis_adapter import RedisAdapter
client = redis.Redis(host="localhost", port=6379)
adapter = RedisAdapter(redis_client=client, compress=True)Keys expire on their own
Every save sets a TTL on the key, computed from the instance’s own timeout - weeks through microseconds added up to seconds. An instance that is never touched again therefore disappears from Redis by itself, and only a timeout of zero leaves a key without an expiry.
Note that compress is accepted for symmetry with the other two adapters but is currently not applied here: the Redis adapter stores the state as it is.
A managed Redis
Nothing in the adapter cares where Redis runs, so a hosted one works the same way - the client is the only difference:
import redis
from BPTK_Py.externalstateadapter.redis_adapter import RedisAdapter
client = redis.from_url("rediss://your-instance-url")
adapter = RedisAdapter(redis_client=client)Running the server statelessly
An adapter on its own keeps the state as well as the server’s memory. With externalize_state_completely=True on BptkServer, the instance is dropped from memory after every request and read back from the adapter on the next one - and that is what turns the store into the only place the state lives.
What that buys is a server you can operate like any stateless web application:
- No session affinity. Any instance can answer any request for any simulation, so round-robin, least-connections or random all work; a load balancer needs no sticky sessions.
- Horizontal scaling. Add processes, no coordination between them.
- A crash costs a request, not a session. The state is in Postgres or Redis, so another process continues where the dead one stopped.
A session keeps the simulation backend it was started on. What that means for a session that comes back from external state - and why a stochastic model needs an explicit seed to resume predictably - is on Execution Backends.
The interface all three implement
Each adapter answers the same three calls, and BptkServer is the one making them — you rarely call them yourself.
| Method | What it does |
|---|---|
| save_instance(state) | Writes one InstanceState |
| load_instance(instance_uuid) | Reads one back, or returns None when there is none |
| delete_instance(instance_uuid) | Removes it, which is what /stop-instance triggers |
To write an adapter for a store not covered here, subclass ExternalStateAdapter; that page walks through it.