Работа со стейтами в YTsaurus Flow (Go)
Примечание
Данная страница описывает Go API для работы со стейтами. Общие концепции стейтов описаны в разделе Stateful-вычисления.
Аксессор стейта открывается свободной функцией flow.OpenXxxState(rt, name, input) с тремя аргументами:
rt—flow.Runtime, второй аргумент любого обработчика (см. Process Function).name— имя стейта, объявленное в спеке компьютейшена.input— вход, к ключу которого привязывается аксессор:flow.ExtendedMessage,flow.Timerилиflow.Visit. Все три реализуют интерфейсflow.Input, поэтому работа со стейтом в обработчике сообщения, таймера и визита выглядит одинаково.
Функции открытия — свободные, а не методы Runtime: YSON- и Proto-аксессоры параметризуются типом стейта, а методы в Go собственных типовых параметров не имеют.
Аксессор адресует стейт ровно одного ключа и живёт в пределах одного запроса. Воркеру уезжают только те записи, в которые компьютейшен писал: чтение стейта не отправляет его обратно.
YSON State
Самый простой способ работы со стейтом — YSON-формат. Стейт описывается обычной Go-структурой с тегами yson:
state, err := flow.OpenYSONState[wordCountState](rt, "word-state", msg)
if err != nil {
return err
}
if state.Empty() {
state.Value().Word = word
}
state.Value().Count++
return nil
flow.YSONState[T] предоставляет Empty(), Get() (*T, bool), Value() *T, Clear() и ReadOnly(). Value() возвращает изменяемое значение, а для отсутствующего стейта создаёт zero value. Отдельный Set не нужен: после успешной обработки батча SDK сериализует изменения автоматически; при ошибке обработчика они отбрасываются.
type wordCountState struct {
Word string `yson:"word"`
Count int64 `yson:"count"`
}
type wordCountMapper struct{}
var _ flow.RowFunction = (*wordCountMapper)(nil)
func (*wordCountMapper) OnMessage(
ctx context.Context,
rt flow.Runtime,
msg flow.ExtendedMessage,
out flow.OutputCollector,
) error {
var input wordMessage
if err := msg.ConvertTo(&input); err != nil {
return err
}
state, err := flow.OpenYSONState[wordCountState](rt, wordStateName, msg)
if err != nil {
return err
}
fresh := state.Empty()
counter := state.Value()
if fresh {
counter.Word = input.Word
}
counter.Count++
return nil
}
Стейт привязан к ключу сообщения, определяемому через group_by_schema в спеке компьютейшена. Для каждого уникального ключа хранится независимое значение.
Raw State
Для хранения стейта в виде сырых байтов:
state, err := flow.OpenRawState(rt, "raw-state", msg)
Возвращает flow.RawStateAccessor с методами:
Get() ([]byte, bool)— получить значение и признак его наличия.Or(fallback []byte) []byte— получить значение илиfallback.Set(data []byte) error— сохранить значение.Clear() error— удалить стейт.
YSON- и Proto-стейты — обёртки над сырым аксессором: сам RawStateAccessor нужен, когда сериализацию компьютейшен выполняет самостоятельно.
Proto State
Для хранения стейта в виде Protobuf-сообщения:
state, err := flow.OpenProtoState[TJoinState](rt, "join-state", msg)
Тип стейта называется по значению (TJoinState), а стейт работает с указателем на него (*TJoinState) — именно на указателе сгенерированный код реализует proto.Message.
Возвращает *flow.ProtoState[T, PT] с методами:
Empty() bool— проверить, отсутствует ли значение.Get() (PT, bool)— вернуть изменяемое сообщение.Or(fallback PT) PT— вернуть сохранённое сообщение или записать и вернутьfallback.Set(value PT) error— заменить значение целиком.Clear()— удалить стейт.ReadOnly() flow.ReadOnlyProtoState[T, PT]— представление только на чтение.
Сообщение изменяется на месте: изменения записываются по окончании батча без Set, см. Изменение значения на месте.
state, err := flow.OpenProtoState[TJoinState](rt, "join-state", msg)
if err != nil {
return err
}
window := state.Or(&TJoinState{})
window.HitPayload = payload
return nil
External State
Внешний стейт — строка пользовательской динамической таблицы. В пользовательском коде она представляется обычной Go-структурой с тегами yson:
state, err := flow.OpenExternalState(rt, "/shuffle-state", msg)
Имя стейта — абсолютный путь, совпадающий с ключом в секции external_state_managers статической спеки. Имя без ведущего / отвергается ошибкой flow.ErrInvalidStateName, необъявленное имя — ошибкой flow.ErrUnknownState.
Возвращает flow.ExternalStateAccessor. Основные операции:
ConvertTo(&value) (bool, error)— заполнить структуру сохранённой строкой;boolотличает отсутствующую строку.ConvertFrom(&value) error— сохранить поля структуры в строку стейта.Clear() error— удалить строку.
Низкоуровневые Get, Or, Builder, Set и Schema нужны только для динамических схем и поколоночной обработки.
Пример из Shuffle:
type shuffleState struct {
Count int64 `yson:"count"`
}
type eventReducer struct{}
var _ flow.RowFunction = (*eventReducer)(nil)
func (*eventReducer) OnMessage(
ctx context.Context,
rt flow.Runtime,
msg flow.ExtendedMessage,
out flow.OutputCollector,
) error {
state, err := flow.OpenExternalState(rt, shuffleStateName, msg)
if err != nil {
return err
}
var counter shuffleState
if _, err := state.ConvertTo(&counter); err != nil {
return err
}
counter.Count++
return state.ConvertFrom(&counter)
}
Паттерн работы с внешним стейтом:
- Открыть стейт через
flow.OpenExternalState(...). - Преобразовать строку в структуру через
state.ConvertTo(&value). - Изменить поля структуры и сохранить её через
state.ConvertFrom(&value).
Подробнее — в разделе External State (Go).
Joined External State
Если внешний стейт нужен только на чтение — например, для обогащения сообщений справочником, — компьютейшен объявляет его в секции external_state_joiners и открывает отдельной функцией:
reference, err := flow.OpenJoinedExternalState(rt, "/reference", msg)
Возвращает flow.JoinedExternalStateAccessor. Строка читается в структуру через ConvertTo(&value); записи у аксессора нет по устройству, потому что заджойненный стейт заполняется из запроса и никогда не едет обратно. Низкоуровневые Get, Or и Schema доступны для динамических схем.
Пространства имён не пересекаются: стейт, которым компьютейшен владеет, недоступен через flow.OpenJoinedExternalState, и наоборот.
type referenceState struct {
Name *string `yson:"name"`
}
type lookupJoin struct{}
var _ flow.RowFunction = (*lookupJoin)(nil)
func (*lookupJoin) OnMessage(
ctx context.Context,
rt flow.Runtime,
msg flow.ExtendedMessage,
out flow.OutputCollector,
) error {
var event eventMessage
if err := msg.ConvertTo(&event); err != nil {
return err
}
name, ok, err := referenceName(rt, msg)
if err != nil || !ok {
return err
}
enriched := flow.NewYSONMessage[enrichedMessage](enrichedStreamID)
enriched.Key = event.Key
enriched.Name = name
encoded, err := flow.ConvertFrom(rt, enriched)
if err != nil {
return err
}
out.AddMessage(encoded)
return nil
}
func referenceName(rt flow.Runtime, msg flow.ExtendedMessage) (string, bool, error) {
reference, err := flow.OpenJoinedExternalState(rt, referenceStateName, msg)
if errors.Is(err, flow.ErrStateNotRead) {
return "", false, nil
}
if err != nil {
return "", false, err
}
var state referenceState
exists, err := reference.ConvertTo(&state)
if err != nil || !exists || state.Name == nil {
return "", false, err
}
return *state.Name, true, nil
}
Важно
Воркер джойнит только те ключи, для которых нашёл строки. Батч, не совпавший ни с одним ключом справочника, приходит вообще без заджойненного стейта, и открытие возвращает flow.ErrStateNotRead — стейта с таким именем запрос просто не принёс. Это не ошибка обработки: её следует отличать через errors.Is и трактовать как отсутствие данных.
Стейт в таймерах
API работы со стейтом в обработчике таймеров идентичен — вместо сообщения передаётся таймер:
state, err := flow.OpenExternalState(rt, "/join-state", timer)
Пример из WaitClickJoin — окно закрывается по таймеру, и стейт очищается сразу после публикации результата:
func (*joinFunction) OnTimer(
ctx context.Context,
rt flow.Runtime,
timer flow.Timer,
out flow.OutputCollector,
) error {
if timer.StreamID != timerStream {
return fmt.Errorf("unhandled timer stream %q", timer.StreamID)
}
state, err := flow.OpenExternalState(rt, joinStateName, timer)
if err != nil {
return err
}
var window joinState
if _, err := state.ConvertTo(&window); err != nil {
return err
}
if window.ShowTime != nil && *window.ShowTime != 0 && window.HitPayload != nil {
var key joinKey
if err := timer.Key.ConvertTo(&key); err != nil {
return err
}
joined, err := joinedAction(rt, key, window)
if err != nil {
return err
}
out.AddMessage(joined)
}
return state.Clear()
}
Стейт, очищенный в этом запросе, дальше читается как отсутствующий: компьютейшен видит стейт таким, каким он станет после ответа воркеру.
Привязка стейта к ключу
В TTransformCompanionComputation стейт привязывается к ключу сообщения, определяемому через group_by_schema в спеке компьютейшена. Все сообщения с одинаковым ключом разделяют один стейт. Ключ, к которому привязан аксессор, берётся из входа — поэтому один обработчик не может случайно записать стейт чужого ключа.
В TTransformOrderedSourceCompanionComputation поле group_by_schema не поддерживается. Ключом внутреннего стейта служит ключ партиции источника, поэтому все сообщения одной партиции разделяют стейт. Подробнее о выборе класса для SourceComputation см. в разделе Computation (Go).
Подробнее о конфигурации ключей см. Stateful-вычисления.
Схема ключа доступна в обработчике через rt.KeySchema(), а сам ключ — через поле Key входа (msg.Key, timer.Key, visit.Key).
Конфигурация стейтов в спеке
Стейт, не объявленный в спеке, открыть нельзя: функция открытия вернёт flow.ErrUnknownState со списком объявленных имён. Объявляются стейты в трёх разных местах описания компьютейшена:
- Внутренние стейты (YSON, Raw, Proto) — списком имён в
parameters.internal_states. Имена произвольные, без ведущего/. - Внешние стейты, которыми компьютейшен владеет, — в секции
external_state_managers. Ключ секции задаёт имя стейта (абсолютный путь), а полеexternal_state_manager_class_name— зарегистрированный класс менеджера; для типового сценария это"NYT::NFlow::TSimpleExternalStateManager". - Внешние стейты, которые компьютейшен только читает, — в секции
external_state_joiners, рядом сexternal_state_managers.
"mapper" = {
"computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation";
"external_state_managers" = {
"/shuffle-state" = {
"external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager";
"parameters" = {
"path" = "<cluster=cluster_name>//path/to/state";
};
};
};
"external_state_joiners" = {
"/reference" = {
"external_state_joiner_class_name" = "NYT::NFlow::TSimpleExternalStateJoiner";
"parameters" = {
"path" = "//path/to/current";
};
};
};
"parameters" = {
"internal_states" = ["word-state"];
};
};
Таблицы внутренних стейтов создаёт и обслуживает Flow, таблицу внешнего стейта пользователь создаёт сам. Подробнее — в разделах Internal State (Go) и External State (Go).