Skip to content

Stateful Processing

PSPF supports Stateful Stream Processing, allowing your handlers to maintain and update persistent state (e.g., counters, running totals, or session data).

State Stores

State is managed via a StateStore interface. Currently supported backends: - SQLite: Local, file-based state (perfect for small-to-medium datasets). - RocksDB: High-performance Key-Value store. Recommended for high-throughput state. Supports TTL-based eviction and background GC. - In-Memory: Fast, but state is lost on restart.

Using State in Handlers

To use state, your handler function must accept a Context object as its third argument.

from pspf import Context

async def process_word(msg_id, data, ctx: Context):
    word = data.get("word")

    # 1. Get current count (default to 0)
    current_count = await ctx.state.get(word, 0)

    # 2. Update state
    await ctx.state.put(word, current_count + 1)

## Windowing Strategies

PSPF provides built-in support for windowed aggregations:

- **Tumbling Windows**: Fixed-size, non-overlapping intervals (e.g., every 5 minutes).
- **Sliding Windows**: Overlapping intervals (e.g., 5-minute window sliding every 1 minute).
- **Session Windows**: Dynamic windows defined by activity gaps.
    - **Stateful Merging**: Unlike fixed windows, sessions grow as new events arrive. If an event arrives within the configured `gap_ms`, it is merged into the current session.

```python
from pspf.processing.windows import SessionWindow

# Define a session with a 10-second inactivity gap
@stream.window("user_activity", SessionWindow(gap_ms=10000))
async def track_session(event, current_state):
    # Logic to aggregate session data
    return (current_state or 0) + 1

## Configuring the Processor

When initializing your `Stream`, you can provide a `state_store` to the underlying `BatchProcessor`.

```python
from pspf.state.backends.sqlite import SQLiteStateStore
from pspf.processor import BatchProcessor

store = SQLiteStateStore(path="data/state.db")
processor = BatchProcessor(backend, state_store=store)
```python
from pspf.state.backends.rocksdb_store import RocksDBStateStore

# Configure RocksDB with custom TTL and performance options
options = {
    "block_cache_size": 64 * 1024 * 1024, # 64MB
    "write_buffer_size": 32 * 1024 * 1024 # 32MB
}
store = RocksDBStateStore(path="data/rocks_state", options=options)

# Keys can have a default TTL (in seconds)
await store.put("my_key", {"data": "..."} , ttl_seconds=3600)

RocksDB supports background Garbage Collection (GC) which runs every 60 seconds to evict expired keys.

Interactive Queries (REST API)

PSPF allows you to query the state stores of live workers using a built-in REST API. This is useful for building dashboards or exposing real-time service data.

1. Enable the Admin API

The Admin API is enabled by default in the BatchProcessor. You can configure the port via the admin_port setting.

2. Distributed Query Routing

The Admin API is transparently distributed. If you query a key that is managed by a different node in the cluster, the Admin API will: 1. Resolve the leader node for that key via the ClusterCoordinator. 2. Proxy the request to that node. 3. Return the merged result to you.

# Query any node; it will route to the owner automatically
curl http://any-worker:8001/state/user_id_123

Response:

{
  "key": "user_id_123",
  "value": {
    "points": 1500,
    "tier": "gold"
  }
}

Checkpointing & EOS

For production reliability, PSPF uses Atomic Checkpointing. This means the stream offset (where the worker is in the log) is stored in the same database as your state.

  • When a transaction commits, both your ctx.state.put() calls and the current message offset are saved together.
  • This provides Exactly-Once Semantics (EOS) even if the application process or the database connection is interrupted.