Computation в YTsaurus Flow (Python)
Примечание
На этой странице описаны Python-специфичные детали работы с компьютейшенами. Общие концепции описаны в разделе Computation.
Типы Computation
Во Flow есть два вида Computation: Swift и Transform. От их выбора зависит способ обеспечения exactly-once гарантий и то, какие преобразования возможно реализовать с их применением.
| Тип | Способ обеспечения гарантий | Применение |
|---|---|---|
Swift |
Код преобразования детерминирован, при необходимости будет вызываться повторно | Stateless преобразования |
Transform |
Результат работы обязательно сохраняется в YT, поэтому нет требований на какую-либо детерминированность преобразований | Stateful преобразования Подробнее |
Что именно означает детерминированность и что ломается при её нарушении — в разделе Требование детерминированности.
При использовании компаньона выбор Swift или Transform осуществляется через указание computation_class_name в статической спеке:
NYT::NFlow::NCompanion::TTransformCompanionComputation— дляTransform.NYT::NFlow::NCompanion::TSwiftMapCompanionComputation— дляSwift.NYT::NFlow::NCompanion::TTransformOrderedSourceCompanionComputation— дляTransform-сорса.NYT::NFlow::NCompanion::TSwiftOrderedSourceCompanionComputation— дляSwift-сорса.
Создание Computation
В Python компьютейшен создаётся через Pipeline.add() и регистрируется в PipelineContext автоматически. Пример из WordCount:
def main():
log.info("Starting companion execution")
pipeline = Pipeline()
pipeline.add("mapper", WordCountMapper())
log.info("Starting pipeline...")
pipeline.run()
log.info("Pipeline completed")
Важно
process_function=None недопустим: компьютейшены без бизнес-логики в Python не регистрируют. Если нужен passthrough — не регистрируйте компьютейшен в Python вовсе, а в статической спеке укажите C++-класс passthrough в computation_class_name (см. Passthrough Computation).
В статической спеке создаётся Computation с таким же id (в данном примере mapper):
"mapper" = {
"computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation";
"group_by_schema" = [
...
];
"input_stream_ids" = [...];
"output_stream_ids" = [...];
"required_resource_ids" = {
"CompanionManager" = {
"worker" = true;
"controller" = false;
};
};
"parameters" = {
...
};
};
Подробнее про спеку в разделе Spec, DynamicSpec и Config.
SourceComputation
SourceComputation — вершина в графе пайплайна, осуществляющая чтение данных из внешних источников. На стороне воркера ей соответствует TSwiftOrderedSourceComputation или TTransformOrderedSourceComputation.
В Python SourceComputation создаётся через передачу source=True в Pipeline.add(). Фильтрация сообщений выполняется внутри Process Function через флаг distribute.
В статической спеке для детерминированной обработки без пользовательского стейта указывают TSwiftOrderedSourceCompanionComputation. Если SourceComputation использует внутренний стейт или недетерминированную логику, указывают TTransformOrderedSourceCompanionComputation: воркер материализует выход и фиксирует его вместе со стейтом и смещением источника. Ключ внутреннего стейта в таком компьютейшене — ключ партиции источника.
Параметры
| Параметр | Обязательный | Описание |
|---|---|---|
computation_id |
Да | Уникальный идентификатор |
fn (process function) |
Да | Функция обработки сообщений |
Создание SourceComputation
pipeline.add("reader", MyParsingFunction(), source=True)
Для passthrough Source не используйте Python — укажите в спеке NYT::NFlow::TSwiftPassthroughOrderedSourceComputation в computation_class_name и оставьте компьютейшен незарегистрированным в Python-компаньоне. Подробнее — Passthrough Computation.
Взаимодействие с Worker
При инициализации Worker запрашивает у Python-компаньона информацию о зарегистрированных объектах Computation и SourceComputation. Source-компьютейшен на стороне воркера отправляет входные сообщения в Python-компаньон, который применяет к ним ProcessFunction и возвращает результат.
Process Function
Бизнес-логика обработки данных реализуется через Process Function. Для реализации необходимо выбрать один из двух базовых классов: RowFunction или BatchFunction.
Примечание
Использование RowFunction или BatchFunction — исключительно вопрос бизнес-логики. RowFunction не добавляет накладных расходов на обработку данных относительно использования BatchFunction благодаря тому, что Flow внутри себя осуществляет передачу данных батчами.
RowFunction
RowFunction получает сообщения и таймеры по одному. Класс предоставляет два метода:
on_message(message, output, ctx)— вызывается для каждого входного сообщения.on_timer(timer, output, ctx)— вызывается при срабатывании таймера (опционально).
Пример stateless-функции
from yt.yt.flow.library.python.companion.computation import RowFunction
class X2Mapper(RowFunction):
def on_message(self, message, output, ctx):
builder = ctx.message_builder("x2_numbers") # 1
number = message.payload["number"] # 2
builder.set("number_x2", number * 2) # 3
output.add_message(builder.finish()) # 4
Разберём построчно:
-
ctx.message_builder("x2_numbers")— создаётсяMessageBuilderдля output-стрима с id =x2_numbers. Стрим с таким идентификатором должен присутствовать в спискеoutput_stream_idsв статической спеке компьютейшена. -
message.payload["number"]— получаем значение поляnumberиз входящего сообщения. Payload поддерживает dict-like доступ к полям. -
builder.set("number_x2", number * 2)— записываем значение в полеnumber_x2. Это поле должно присутствовать в схеме стримаx2_numbersв статической спеке. -
output.add_message(builder.finish())— методfinish()возвращает готовое сообщение и сбрасывает билдер для повторного использования. Сообщение добавляется вOutputCollector.
BatchFunction
BatchFunction получает весь список сообщений и таймеров, пришедших от воркера. Класс предоставляет два метода:
on_messages(messages, output, ctx)— вызывается для батча сообщений.on_timers(timers, output, ctx)— вызывается для батча таймеров (опционально).
Важно
Батч соответствует одному запросу воркера и может содержать сообщения с разными ключами (см. Companion). По умолчанию родителями каждого выходного сообщения становятся все сообщения батча.
output.set_parent_ids(ids) заменяет этот дефолт: метод принимает message_id (строку или список строк) входных сообщений текущего батча и возвращает новый коллектор — родителями всего вывода, добавленного через него, станут именно эти сообщения. Родители — это id сообщений, с ключами они не связаны; указывайте message_id тех входных сообщений, из которых фактически получен данный вывод. Когда вызов обязателен (см. Когда задавать lineage явно):
- Swift: обязателен при батче из более чем одного сообщения — у каждого выходного сообщения должен быть ровно один родитель (
out = output.set_parent_ids(message.message_id)), иначе обработка завершится ошибкой. Несколько родителей допустимы только приallow_batching_with_relaxed_guarantees. - Transform: необязателен, но с дефолтом «весь батч» event timestamp каждого выходного сообщения равен минимальному по всему батчу.
- В
RowFunctionвызыватьset_parent_idsне нужно: родителем SDK автоматически назначает текущее входное сообщение.
Если нужна обработка по ключам (в Transform или в Swift с allow_batching_with_relaxed_guarantees), сгруппируйте батч в пользовательском коде и задайте родителей каждой группы:
def on_messages(self, messages, output, ctx):
groups = {}
for message in messages:
groups.setdefault(message.key["user_id"], []).append(message)
for key, group in groups.items():
out = output.set_parent_ids([m.message_id for m in group])
... # Обработка группы, вывод через out.
В Swift-компьютейшене группировка и назначение родителей обязаны быть детерминированными, включая порядок групп. dict в Python сохраняет порядок вставки, поэтому пример выше воспроизводим; неупорядоченные структуры (например, set) использовать нельзя. Подробнее — Требование детерминированности. Полный пример — batch_compaction: Swift-компьютейшен с allow_batching_with_relaxed_guarantees, сворачивающий сообщения одного ключа в одно.
Пример batch-функции
from yt.yt.flow.library.python.companion.computation import BatchFunction
class X2BatchMapper(BatchFunction):
def on_messages(self, messages, output, ctx):
builder = ctx.message_builder("x2_numbers") # 1
for message in messages: # 2
number = message.payload["number"] # 3
builder.set("number_x2", number * 2) # 4
output.add_message(builder.finish()) # 5
Ключевое отличие от RowFunction:
MessageBuilderсоздаётся один раз за весь батч (строка 1).- Метод
finish()одновременно с возвратом готового сообщения сбрасываетMessageBuilderв исходное состояние, после чего его можно переиспользовать для следующего сообщения (строка 5).
Фильтрация сообщений
Для фильтрации сообщений в source-компьютейшенах используется per-message-флаг distribute: сообщение эмитится из Process Function с distribute=False и не публикуется дальше по графу, но учитывается при оценке watermark.
Регистрация в Pipeline
Все компьютейшены регистрируются через Pipeline.add(). Внутри Pipeline использует PipelineContext для хранения и управления зарегистрированными объектами.
from yt.yt.flow.library.python.companion import Pipeline
pipeline = Pipeline()
# Transform-компьютейшен
pipeline.add("computation_id", my_function)
# Source-компьютейшен
pipeline.add("reader", my_function, source=True)
Также доступен декоратор @pipeline.computation:
pipeline = Pipeline()
@pipeline.computation("mapper")
def mapper(message, output, ctx):
word = message.payload["word"]
state = ctx.state("word-state", message)
data = state.get_or_default({"word": word, "count": 0})
data["count"] += 1
state.set(data)
Важно
Каждый Computation должен иметь уникальный идентификатор, соответствующий идентификаторам в статической спеке. Попытка зарегистрировать Computation с уже существующим идентификатором приведёт к ошибке и невозможности старта компаньона.
RuntimeContext
RuntimeContext (ctx) предоставляет доступ к контексту выполнения компьютейшена. Основные методы:
| Метод | Описание |
|---|---|
ctx.message_builder(stream_id) |
Создать MessageBuilder для указанного output-стрима |
ctx.parameters |
Словарь параметров компьютейшена из спеки |
ctx.min_watermark |
Минимальный вотермарк по всем входным стримам |
ctx.watermark(stream_id) |
Вотермарк конкретного стрима (int или None) |
ctx.state(name, message) |
Получить YSON-стейт, привязанный к ключу сообщения |
ctx.raw_state(name, message) |
Получить стейт в виде сырых байтов |
ctx.proto_state(name, message, ProtoClass) |
Получить Protobuf-стейт |
ctx.external_state(name, message) |
Получить внешний стейт |
Подробнее про работу со стейтами — в разделе Работа со стейтами (Python).
MessageBuilder
Для создания выходных сообщений используется MessageBuilder:
builder = ctx.message_builder("stream_id")
builder.set("field_name", value)
message = builder.finish()
output.add_message(message)
Метод finish() возвращает готовый объект Message и сбрасывает билдер для повторного использования. Поле stream_id должно присутствовать в списке output_stream_ids в статической спеке компьютейшена.
Параметры компьютейшена
wait_for_actions = ctx.parameters["wait_for_actions"]
Словарь ctx.parameters содержит параметры, указанные в статической спеке компьютейшена.
Вотермарки
# Минимальный вотермарк по всем входным стримам
min_wm = ctx.min_watermark
# Вотермарк конкретного стрима (int или None)
stream_wm = ctx.watermark("stream_id")
OutputCollector
OutputCollector используется для отправки результатов обработки:
| Метод | Описание |
|---|---|
output.add_message(message) |
Добавить выходное сообщение (объект Message, полученный через builder.finish()) |
output.add_message(message, options) |
Добавить сообщение с AddMessageOptions, задающим публикацию и суффикс message id для Swift |
output.add_timer(trigger_timestamp, event_timestamp, stream_id) |
Добавить таймер с указанным временем срабатывания |
output.set_parent_ids(parent_ids) |
Задать parent ID для отслеживания lineage сообщений. Возвращает новый OutputCollector |
В Swift-компьютейшене MessageIdSuffix.payload_hash() или MessageIdSuffix.user_defined(value) позволяет отвязать идентичность сообщения от порядка эмита:
output.add_message(
message,
AddMessageOptions(message_id_suffix=MessageIdSuffix.payload_hash()),
)
Options по умолчанию используют порядковый номер сообщения. Хеш payload и пользовательский суффикс поддерживаются только Swift-компьютейшенами.
Пример создания выходного сообщения и таймера:
def on_message(self, message, output, ctx):
# Создание выходного сообщения
builder = ctx.message_builder("output_stream")
builder.set("field", value)
output.add_message(builder.finish())
# Создание таймера
output.add_timer(trigger_timestamp=1000, event_timestamp=500)
ExtendedMessage
Входящее сообщение (ExtendedMessage) содержит:
message.payload— Payload с dict-like доступом к полям:message.payload["field"].message.stream_id— идентификатор входного стрима (str).message.key— ключ сообщения (Payload) изgroup_by_schema:message.key["field"].message.event_timestamp— event timestamp сообщения (int).
Timer
Объект таймера (Timer) содержит:
timer.key— ключ таймера (Payload):timer.key["field"].timer.stream_id— идентификатор стрима таймера (str).timer.trigger_timestamp— время срабатывания (int).timer.event_timestamp— event timestamp (int).
Конфигурация ресурса CompanionManager
Для запуска Python-компаньона необходимо объявить ресурс CompanionManager в статической спеке:
"CompanionManager" = {
"resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager";
"parameters" = {
"entrypoint" = {
"executable" = "./py_companion";
};
};
"dependencies" = {};
};
Параметр resource_class_name указывает на класс ресурса, который будет осуществлять запуск компаньона.
В случае Python-компаньона resource_class_name всегда должен быть NYT::NFlow::NCompanion::TCompanionManager.
Процесс компаньона описывается параметром entrypoint (executable, args, env); воркер сам запускает компаньон и следит за его жизненным циклом. При запуске пайплайна с хоста через pipeline.run() заполнять entrypoint вручную не нужно: Python-бинарь сам прописывает entrypoint = {"executable" = "./py_companion"}, а flow_server доставляет бинарь в джобу под этим именем.
Ключевое отличие от Java-конфигурации: для Java существует отдельный класс ресурса NYT::NFlow::NCompanion::TJavaCompanionManager с параметрами jdk_bin_path, classpath и main_class, тогда как Python-компаньон использует общий TCompanionManager с параметром entrypoint.
Подробнее про спеку в разделе Spec, DynamicSpec и Config.