Async Request в YTsaurus Flow (Java)
Данный пример пайплайна реализует событийно-ориентированный цикл запрос–ответ. Входящие события порождают запросы к обработчику, ответы возвращаются обратно в тот же компьютейшен и накапливаются во внешнем стейте. Пример демонстрирует циклическую топологию пайплайна и совместное использование внешнего стейта.
Компоненты
StateKeeperFunction
Обрабатывает два входных стрима — event и response. По событию event генерирует запрос с уникальным request_id и эмитирует его в стрим request. По событию response накапливает суммарную длину ответов в поле total_length внешнего стейта:
@Override
public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) {
String streamId = message.getStreamId();
if ("event".equals(streamId)) {
handleEvent(message, output, ctx);
} else if ("response".equals(streamId)) {
handleResponse(message, ctx);
} else {
throw new IllegalArgumentException("Unknown streamId: " + streamId);
}
}
override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) {
val streamId = message.getStreamId()
if ("event" == streamId) {
handleEvent(message, output, ctx)
} else if ("response" == streamId) {
handleResponse(message, ctx)
} else {
throw IllegalArgumentException("Unknown streamId: $streamId")
}
}
RequestProcessorFunction
Stateless-компьютейшен: получает запрос из стрима request, вычисляет длину строки и немедленно отправляет ответ в стрим response:
@Override
public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) {
long requestId = message.get("request_id", Long.class);
long key = message.get("key", Long.class);
String request = message.get("request", String.class);
long length = request == null ? 0L : (long) request.length();
log.debug("Processing request (requestId={}, key={}, length={})", requestId, key, length);
var builder = ctx.createMessageBuilder("response");
builder.set("request_id", requestId)
.set("key", key)
.set("length", length);
output.addMessage(builder.finish());
}
override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) {
val requestId = message.get("request_id", Long::class.java)
val key = message.get("key", Long::class.java)
val request = message.get("request", String::class.java)
val length = if (request == null) 0L else request.length.toLong()
log.debug("Processing request (requestId={}, key={}, length={})", requestId, key, length)
val builder = ctx.createMessageBuilder("response")
builder.set("request_id", requestId)
.set("key", key)
.set("length", length)
output.addMessage(builder.finish())
}
Регистрация компьютейшенов
Компьютейшены state и processor регистрируются аннотацией @FlowComputation на классах их process-функций:
@FlowComputation(id = "state")
@FlowComputation(id = "processor")
@FlowComputation(id = "state")
@FlowComputation(id = "processor")
PipelineMain
Единственная точка входа (запускает пайплайн или обслуживает его как компаньон — по YT_FLOW_MODE):
public static void main(String[] args) throws Exception {
new SpringApplicationBuilder(PipelineMain.class).run(args);
}
@JvmStatic
fun main(args: Array<String>) {
SpringApplicationBuilder(PipelineMain::class.java).run(*args)
}
Ключевые паттерны
- Циклическая топология — стрим
responseвозвращается обратно вstate-компьютейшен, замыкая циклevent → request → response → state. Flow поддерживает такие графы явно. - Маршрутизация по
streamId— одна функция обрабатывает несколько входных стримов, определяя тип сообщения черезmessage.getStreamId(). - ExternalStateAccessor с PayloadBuilder — поле
total_lengthобновляется точечно:current.toBuilder()→ изменение →stateAccessor.set(updated.finish()). - Конфигурация через Spring Boot — компьютейшены регистрируются аннотацией
@FlowComputation;flow-spring-boot-starterуправляет жизненным циклом gRPC-сервера.