External State in YTsaurus Flow (Python)
Use External State to work with external state stored in an external dynamic table in YTsaurus. You create the table to store the state yourself, on the same cluster where the pipeline is deployed.
For general information about stateful processing, see the Stateful processing section.
Overview
In the Python SDK, External State is represented by the ExternalStateAccessor class. This class is a subclass of Payload and gives you dict-like access to the columns of the external dynamic table. The state is bound to the message key (group_by_schema). For more details about StateAccessor and working with state, see StateAccessor.
Note
If you need only read-only access to the external state (a key-based join with TTL caching, without modification), the framework provides a separate mechanism: External State Joiner. Currently, the joiner is available only in C++ (External State Joiner) and is declared in the computation spec, in the top-level external_state_joiners section (at the same level as external_state_managers).
Difference from Internal State
| Characteristic | External State | Internal State |
|---|---|---|
| Storage | External dynamic table | Flow internal tables |
| Table creation | You create it yourself | Automatic |
| Data format | Typed Payload (table row) |
Arbitrary (YSON, Protobuf, raw bytes) |
| Access from other systems | Yes (sorted dynamic table) | No |
| Schema | Defined by the table schema | Defined by you |
For more details about Internal State, see Internal State.
Getting ExternalStateAccessor
You get ExternalStateAccessor through RuntimeContext (ctx):
# For a message
state = ctx.external_state("/state-name", message)
# For a timer
state = ctx.external_state("/state-name", timer)
Parameters:
"/state-name"— a string with the state name from theexternal_state_managerssection of the static spec. The name must start with/and match the key in the spec.message/timer— the message or timer for which you need to get the state for the key.
Warning
The external state name is validated: it must start with /, must not be empty, must not end with /, and must not contain two consecutive / characters. Calling ctx.external_state("state-name", message) (without the leading /) will raise a ValueError.
ExternalStateAccessor as Payload
ExternalStateAccessor inherits from the Payload class, which lets you read column values directly:
state = ctx.external_state("/shuffle-state", message)
# Dict-like access
value = state["count"] # Raises KeyError if the column doesn't exist
value = state.get("count") # Returns None if the column doesn't exist
exists = "count" in state # Checks existence
The Payload class also provides:
keys()— a list of column names with non-empty values.to_dict()— converts to a standard Python dict.
Main operations
Reading state
state = ctx.external_state("/join-state", message)
# Read a column value
hit_payload = state.get("hit_payload", str)
show_time = state.get("show_time")
# Dict-like access
try:
value = state["hit_payload"]
except KeyError:
value = None
Writing state with PayloadBuilder
To modify the state, use the to_builder() / set() / finish() pattern:
state = ctx.external_state("/join-state", message)
# Get a builder with current values
builder = state.to_builder()
# Update the required fields
builder.set("hit_payload", "some_value")
builder.set("show_time", 1234567890)
# Save the updated state
state.set(builder.finish())
The to_builder() method returns a PayloadBuilder pre-filled with the current state values. The builder.set(column, value) method returns the builder itself (it supports chaining calls). The builder.finish() method creates a new Payload and resets the builder.
Clearing state
state = ctx.external_state("/join-state", timer)
# Delete the row from the table
state.clear()
Note
An empty state corresponds to the absence of a row in the table. If the row doesn't exist, state.get("column") returns None. Calling clear() removes the row from the table.
Configuration in the static spec
To use External State, you must declare an external state manager in the external_state_managers section of the computation in the static spec:
"computations" = {
"reducer" = {
"computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation";
"group_by_schema" = [
{"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"};
{"name" = "key"; "type" = "string"};
];
"input_stream_ids" = ["input"];
"output_stream_ids" = ["output"];
"external_state_managers" = {
"/shuffle-state" = {
"external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager";
"parameters" = {
"path" = "//path/to/state/table";
};
};
};
"parameters" = {};
};
};
Key fields:
external_state_managers— a top-level section insideComputationthat describes external state managers (previouslyparameters/external_states).- The key inside
external_state_managers(for example,"/shuffle-state") — the state name you use in Python code when callingctx.external_state("/shuffle-state", message). The name must start with/. external_state_manager_class_name— the name of the registered external state manager class. For a typical scenario, use"NYT::NFlow::TSimpleExternalStateManager"; For more details, see the C++ documentation.parameters.path— the path to the YTsaurus dynamic table where the state is stored.
Creating a table for the state
You must create the table for External State in advance. The schema of the table's key columns must match the computation's group_by_schema.
Complete example — EventReducer from Shuffle
Example from shuffle:
from yt.yt.flow.library.python.companion.computation import RowFunction
class EventReducer(RowFunction):
"""Counts the number of events for each key using external state."""
def on_message(self, message, output, ctx):
state = ctx.external_state("/shuffle-state", message)
builder = state.to_builder()
builder.set("count", (state.get("count") or 0) + 1)
state.set(builder.finish())
Work pattern:
- Get the current state with
ctx.external_state(...). - Create a builder with
state.to_builder(). - Update the required fields with
builder.set(...). - Save with
state.set(builder.finish()).