State Adapters

BPTK API Documentation for the FileAdapter, PostgresAdapter and RedisAdapter state adapters
Keywords

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.Redis client.

    • 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.