| 419 | } |
| 420 | |
| 421 | bool DataSourceRuntimeHost::cbEnsureParserBinding( |
| 422 | void* ctx, const PJ_parser_binding_request_t* request, PJ_parser_binding_handle_t* out, |
| 423 | PJ_error_t* out_error) noexcept { |
| 424 | auto* self = static_cast<DataSourceRuntimeHost*>(ctx); |
| 425 | try { |
| 426 | const std::string_view encoding(request->parser_encoding.data, request->parser_encoding.size); |
| 427 | const std::string_view topic_name(request->topic_name.data, request->topic_name.size); |
| 428 | const std::string_view type_name(request->type_name.data, request->type_name.size); |
| 429 | |
| 430 | const LoadedMessageParser* parser_entry = |
| 431 | self->catalog_.findParserByEncoding(QString::fromUtf8(encoding.data(), static_cast<int>(encoding.size()))); |
| 432 | if (parser_entry == nullptr) { |
| 433 | return self->fail(out_error, ("no parser found for encoding '" + std::string(encoding) + "'").c_str()); |
| 434 | } |
| 435 | |
| 436 | auto parser = std::make_unique<MessageParserHandle>(parser_entry->library.createHandle()); |
| 437 | if (!parser->valid()) { |
| 438 | return self->fail(out_error, ("failed to create parser instance for '" + std::string(encoding) + "'").c_str()); |
| 439 | } |
| 440 | |
| 441 | auto topic_or = self->engine_.createTopic(self->dataset_id_, TopicDescriptor{.name = std::string(topic_name)}); |
| 442 | if (!topic_or.has_value()) { |
| 443 | return self->fail( |
| 444 | out_error, ("failed to create topic '" + std::string(topic_name) + "': " + topic_or.error()).c_str()); |
| 445 | } |
| 446 | const PJ_topic_handle_t topic_handle{static_cast<uint32_t>(*topic_or)}; |
| 447 | |
| 448 | // Lockstep-mirror into the secondary engine with the SAME TopicId. The two |
| 449 | // engines' TopicId counters drift whenever the primary gets topics the |
| 450 | // secondary doesn't (e.g. a file loaded between streams), so we force the |
| 451 | // secondary topic id to match the primary's instead of relying on the |
| 452 | // counters staying in step. A later push uses one id against whichever |
| 453 | // engine is the active target, so the ids MUST match. |
| 454 | if (self->secondary_data_engine_ != nullptr) { |
| 455 | auto mirrored = self->secondary_data_engine_->createTopic( |
| 456 | self->dataset_id_, TopicDescriptor{.name = std::string(topic_name)}, *topic_or); |
| 457 | if (!mirrored.has_value()) { |
| 458 | return self->fail( |
| 459 | out_error, |
| 460 | ("failed to mirror topic '" + std::string(topic_name) + "' into secondary engine: " + mirrored.error()) |
| 461 | .c_str()); |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | auto write_host = std::make_unique<DatastoreParserWriteHost>(self->engine_, topic_handle); |
| 466 | // Same lockstep wiring as the source-level write host (see the runtime |
| 467 | // host constructor). Closes the latent FieldHandle-stale bug for parser |
| 468 | // plugins that cache handles across messages (parser_protobuf et al.): |
| 469 | // every ensureField inside parserAppendRecord/appendBoundRecord is now |
| 470 | // mirrored to the secondary engine via DataEngine::createTopicField with |
| 471 | // the same FieldId, so the cached handle resolves after a pause/resume |
| 472 | // target swap. |
| 473 | write_host->setSecondaryEngine(self->secondary_data_engine_); |
| 474 | |
| 475 | // Build the service registry the parser binds against. The builder must |
| 476 | // outlive bind() because the plugin may hold a view into it; we move it |
| 477 | // into the ParserBinding so its lifetime matches the parser's. |
| 478 | auto registry_builder = std::make_unique<ServiceRegistryBuilder>(); |
nothing calls this directly
no test coverage detected