Internal State в YTsaurus Flow (Python)

Internal State — механизм работы с внутренним состоянием (стейтом), хранящимся во внутренних таблицах Flow. В отличие от External State, пользователю не нужно самостоятельно создавать таблицы — Flow управляет ими автоматически.

Подробнее про StateAccessor и общие принципы работы со стейтом: StateAccessor.

Общие сведения о stateful-обработке описаны в разделе Stateful processing.

Обзор

Python SDK предоставляет три вида аксессоров для работы с Internal State, различающихся форматом сериализации. Для работы с внешним стейтом используется отдельный аксессор — ExternalStateAccessor.

Аксессор Формат Описание
YsonStateAccessor YSON (dict) Сериализация Python dict через YSON
RawStateAccessor bytes Без сериализации (сырые байты)
ProtoStateAccessor Protobuf Сериализация через Protobuf

Все аксессоры предоставляют единый набор методов: get(), set(value), clear(), get_or_default(default), read_only().

Изменение значения на месте

Значение, которое возвращают get() и get_or_default(), живое: для каждого ключа оно декодируется один раз за батч, все аксессоры этого ключа возвращают один и тот же объект, а изменения, сделанные в нём, записываются в стейт по окончании батча без вызова set(). Если значение не изменилось, запись не выполняется. Дефолт из get_or_default() привязывается к ключу, но не записывается: его можно сразу изменять, а значением стейта он становится только после изменения, поэтому нетронутый дефолт не создаёт строку в таблице стейтов:

ctx.state("word-state", message).get_or_default({"count": 0})["count"] += 1

set() по-прежнему заменяет значение целиком, clear() удаляет стейт. Дефолт None значением не является и не привязывается. Значение, которое кодируется в пустые байты — например, protobuf-сообщение со всеми полями по умолчанию, — тоже значением не является: его запись удаляет стейт. Дефолт get_or_default() у RawStateAccessor привязывается так же, как у остальных аксессоров, и его вернёт последующий get(). Но объект bytes неизменяем, изменить привязанный дефолт на месте нельзя, поэтому непустое значение сырого стейта попадает в YTsaurus только через set().

Чтобы отследить изменения, прочитанное значение перекодируется по окончании батча. Если вычисление только читает стейт, используйте read_only(): такой аксессор возвращает тот же объект, но не отслеживает его, get_or_default() не создаёт стейт, а set() и clear() бросают ReadOnlyStateError:

count = ctx.state("word-state", message).read_only().get_or_default({"count": 0})["count"]

Значение общее, а не копия: если раньше в этом же батче ключ прочитал пишущий аксессор, изменение через read-only-представление всё равно попадёт в стейт.

YsonStateAccessor

Исходный код

YsonStateAccessor использует YSON-сериализацию. Стейт хранится как Python dict, автоматически сериализуемый в YSON и обратно.

Получение аксессора

# Для сообщения
state = ctx.state("state-name", message)

# Для таймера
state = ctx.state("state-name", timer)

Методы

Метод Возвращаемый тип Описание
get() dict или None Десериализовать и вернуть текущее значение
set(value) — Сериализовать и сохранить значение (dict или bytes)
clear() — Удалить стейт для текущего ключа
get_or_default(default) dict Вернуть текущее значение или default (записывается только после изменения)

Пример из WordCount

class WordCountMapper(RowFunction):
    """Processes Word messages and maintains word count state."""

    def on_message(self, message, output, ctx):
        state = ctx.state("word-state", message)
        data = state.get_or_default({"word": message.payload["word"], "count": 0})
        data["count"] += 1
        state.set(data)

Полный исходный код

Здесь стейт привязан к ключу сообщения (определяемому через group_by_schema в спеке). Для каждого уникального слова хранится свой независимый счётчик.

RawStateAccessor

Исходный код

RawStateAccessor работает с сырыми байтами без сериализации/десериализации.

Получение аксессора

# Для сообщения
state = ctx.raw_state("state-name", message)

# Для таймера
state = ctx.raw_state("state-name", timer)

Методы

Метод Возвращаемый тип Описание
get() bytes или None Получить сырые байты
set(value: bytes) — Сохранить сырые байты
clear() — Удалить стейт для текущего ключа
get_or_default(default: bytes) bytes Вернуть текущее значение или привязать и вернуть default (в YTsaurus попадает только через set())

Пример использования

state = ctx.raw_state("raw-state", message)

data = state.get()
if data is not None:
    # Обработка сырых данных...
    pass

# Запись сырых данных
state.set(b"\x01\x02\x03")

# Очистка
state.clear()

ProtoStateAccessor

Исходный код

ProtoStateAccessor использует Protobuf-сериализацию. Стейт десериализуется в экземпляр указанного Protobuf-класса.

Получение аксессора

# Для сообщения
state = ctx.proto_state("state-name", message, TJoinState)

# Для таймера
state = ctx.proto_state("state-name", timer, TJoinState)

Третий аргумент — класс Protobuf-сообщения, используемый для десериализации.

Методы

Метод Возвращаемый тип Описание
get() Proto-объект или None Десериализовать и вернуть значение
set(value) — Сериализовать и сохранить Proto-объект
clear() — Удалить стейт для текущего ключа
get_or_default(default=None) Proto-объект Вернуть значение, default либо пустой экземпляр Proto-класса (записывается только после изменения)

Примечание

Метод get_or_default() без аргументов возвращает пустой экземпляр Proto-класса (эквивалент ProtoClass()). Это удобно для инициализации стейта при первом обращении; сам стейт появится, когда вы измените это сообщение.

Конфигурация в статической спеке

Internal State не требует создания внешних таблиц. Стейты автоматически хранятся во внутренних таблицах Flow.

Имена внутренних стейтов должны быть объявлены в секции internal_states параметров компьютейшена в статической спеке:

{
    "spec" = {
        "computations" = {
            "reader" = {
                "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation";
                "output_stream_ids" = ["words"];
                "source_streams" = {
                    "queue" = {
                        "source_class_name" = "NYT::NFlow::TQueueSource";
                        "parameters" = {
                            "queue_path" = "<cluster=cluster_name>//path/to/queue";
                            "consumer_path" = "<cluster=cluster_name>//path/to/consumer";
                            "finite" = false;
                        };
                    };
                };
                "parameters" = {};
            };
            "mapper" = {
                "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation";
                "group_by_schema" = [
                    {"name" = "hash"; "expression" = "farm_hash(word)"; "type" = "uint64"; required = %true;};
                    {"name" = "word"; "type" = "string";};
                ];
                "input_stream_ids" = ["words"];
                "output_stream_ids" = [];
                "required_resource_ids" = {
                    "CompanionManager" = {
                        "worker" = true;
                        "controller" = false;
                    };
                };
                "parameters" = {
                    "internal_states" = ["word-state"];
                };
            };
        };
        "streams" = {
            "words" = {
                "schema" = [
                    {name = "word"; type = "string";};
                ];
            };
        };
        "resources" = {
            "CompanionManager" = {
                "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager";
                "parameters" = {
                };
                "dependencies" = {};
            };
        };
    };
    "dynamic_spec" = {
        "computations" = {
            "reader" = {
                "parameters" = {
                };
            };
            "mapper" = {
                "parameters" = {
                };
            };
        };
    };
}

Имя стейта в коде (первый аргумент ctx.state(...), ctx.raw_state(...) или ctx.proto_state(...)) должно совпадать с именем, объявленным в internal_states.

Важно

Если имя стейта не объявлено в internal_states, при обращении к нему через ctx.state(...) будет выброшено исключение ValueError.

См. также

Предыдущая
Следующая