MINIFICPP-2845 - Monotonic provenance event identifiers, iterations support, refactor - #2209
MINIFICPP-2845 - Monotonic provenance event identifiers, iterations support, refactor#2209adamdebreceni wants to merge 3 commits into
Conversation
| class SchedulingAgent { | ||
| public: | ||
| SchedulingAgent(const gsl::not_null<core::controller::ControllerServiceProvider*> controller_service_provider, std::shared_ptr<core::Repository> repo, std::shared_ptr<core::Repository> flow_repo, | ||
| SchedulingAgent(const gsl::not_null<core::controller::ControllerServiceProvider*> controller_service_provider, std::shared_ptr<provenance::ProvenanceRepository> repo, std::shared_ptr<core::Repository> flow_repo, |
There was a problem hiding this comment.
I believe the "repo" naming of identifiers that have to be provenance repo was a mistake. I'm glad the type makes it clearer now, but I'd change the parameter and member names too. provenance_repo or prov_repo seem like good options to me.
| Identifier& operator++(); | ||
| Identifier operator++(int); | ||
|
|
There was a problem hiding this comment.
why do we need these UUID increments? I believe the Identified class is not meant to represent integer auto-increment keys.
There was a problem hiding this comment.
currently the RocksDbProvenanceRepository uses the event_uuid for key, we could either bring back the retired SequentialIdGenerator, we could replace the increment operators with a next() method to not cause confusion, or we could use an uint64_t (event ordinal) as key, which one do you prefer?
a5b3ee2 to
af3f98a
Compare
7797653 to
82815c6
Compare
Co-authored-by: Márton Szász <szaszm@apache.org>
af3f98a to
f1b2dcd
Compare
There was a problem hiding this comment.
Pull request overview
Introduces a dedicated provenance repository API with monotonic event identifiers and cursor-based reporting.
Changes:
- Adds provenance-specific append and iteration APIs.
- Implements persistent monotonic identifiers in RocksDB.
- Refactors reporting, runtime wiring, and tests to use the new API.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
minifi-api/include/minifi-cpp/provenance/ProvenanceRepository.h |
Adds provenance repository interface. |
minifi-api/include/minifi-cpp/provenance/Provenance.h |
Adds event ordinals and vector-backed reporting. |
minifi-api/include/minifi-cpp/core/Repository.h |
Removes generic provenance operations. |
minifi-api/include/minifi-cpp/core/ProcessContext.h |
Returns typed provenance repositories. |
minifi-api/common/include/minifi-cpp/utils/Id.h |
Declares identifier increment operators. |
minifi_main/MiNiFiMain.cpp |
Casts configured repository to the provenance API. |
libminifi/test/unit/SchedulingAgentTests.cpp |
Updates test repository typing. |
libminifi/test/persistence-tests/PersistenceTests.cpp |
Updates persistence fixtures. |
libminifi/test/libtest/unit/TestControllerWithFlow.cpp |
Updates test flow setup. |
libminifi/test/libtest/unit/TestBase.h |
Updates test plan provenance types. |
libminifi/test/libtest/unit/TestBase.cpp |
Implements updated test plan API. |
libminifi/test/libtest/unit/ProvenanceTestHelper.h |
Implements test provenance repository operations. |
libminifi/test/libtest/integration/IntegrationBase.cpp |
Updates integration repository setup. |
libminifi/test/integration/ControllerServiceIntegrationTests.cpp |
Updates repository inference. |
libminifi/test/integration/C2PauseResumeTest.cpp |
Updates repository inference. |
libminifi/test/flow-tests/SessionTests.cpp |
Casts the no-op repository. |
libminifi/src/provenance/Provenance.cpp |
Uses batched provenance appends. |
libminifi/src/FlowController.cpp |
Accepts the typed repository. |
libminifi/src/core/reporting/SiteToSiteProvenanceReportingTask.cpp |
Adds cursor-based reporting and ordinals. |
libminifi/src/core/ProcessContextImpl.cpp |
Stores the typed repository. |
libminifi/src/core/flow/StructuredConfiguration.cpp |
Assigns reporting-task UUIDs. |
libminifi/include/TimerDrivenSchedulingAgent.h |
Updates repository type. |
libminifi/include/ThreadedSchedulingAgent.h |
Updates repository type. |
libminifi/include/SchedulingAgent.h |
Stores the typed repository. |
libminifi/include/provenance/Provenance.h |
Implements ordinals and vector-backed events. |
libminifi/include/FlowController.h |
Exposes the typed repository. |
libminifi/include/EventDrivenSchedulingAgent.h |
Updates repository type. |
libminifi/include/CronDrivenSchedulingAgent.h |
Updates repository type. |
libminifi/include/core/repository/VolatileProvenanceRepository.h |
Implements volatile event appends. |
libminifi/include/core/repository/NoOpThreadedRepository.h |
Implements no-op provenance operations. |
libminifi/include/core/reporting/SiteToSiteProvenanceReportingTask.h |
Uses typed event records. |
libminifi/include/core/ProcessContextImpl.h |
Updates context repository types. |
extensions/standard-processors/tests/unit/ProcessorTests.cpp |
Adapts reporting tests to getEvents. |
extensions/standard-processors/tests/integration/VerifyInvokeHTTP.h |
Updates test repository inference. |
extensions/rocksdb-repos/tests/RepoTests.cpp |
Uses renamed RocksDB repository. |
extensions/rocksdb-repos/tests/ProvenanceTests.cpp |
Tests the append API. |
extensions/rocksdb-repos/tests/DBProvenanceRepositoryTests.cpp |
Adds cursor and monotonicity tests. |
extensions/rocksdb-repos/RocksDbProvenanceRepository.h |
Defines the renamed RocksDB implementation. |
extensions/rocksdb-repos/RocksDbProvenanceRepository.cpp |
Implements counters, cursors, and iteration. |
extensions/rocksdb-repos/ProvenanceRepository.cpp |
Removes the previous implementation. |
core-framework/src/core/Repository.cpp |
Removes generic element storage. |
core-framework/include/core/Repository.h |
Removes generic provenance defaults. |
core-framework/common/src/utils/Id.cpp |
Implements identifier incrementing. |
Suppressed comments (4)
extensions/rocksdb-repos/RocksDbProvenanceRepository.cpp:131
- A persisted cursor is expected to be parsed so the reporting task can fall back when it is invalid, but every string is currently accepted. A malformed/high-valued state such as
zzzzwill seek past every UUID and make provenance reporting return no records forever; validate it as anIdentifierand returnnullptron failure.
std::unique_ptr<ProvenanceRepository::Cursor> RocksDbProvenanceRepository::cursorFromString(std::optional<std::string> cursor_str) {
if (cursor_str.has_value()) {
return std::make_unique<EventCursor>(cursor_str.value());
}
return std::make_unique<EventCursor>("");
}
extensions/rocksdb-repos/RocksDbProvenanceRepository.cpp:174
- The iterator status is never checked after traversal, so a RocksDB read error is returned as a successful partial result and the reporting task can persist its cursor. Check
it->status()before updating the cursor and returnstd::unexpectedon failure, consistent withRocksDbStateStorageiteration.
if (event_cursor) {
event_cursor->event_id_ = last_event_id;
}
return records;
libminifi/src/core/reporting/SiteToSiteProvenanceReportingTask.cpp:224
- A
falsereturn fromtransmitPayloadonly yields and then reaches this cursor update, marking records as consumed even though they were not transmitted. Return from the trigger on the false path, just as the exception path does, so the same cursor batch is retried.
if (cursor) {
// no need to delete just update the state
std::unordered_map<std::string, std::string> state_map;
state_map["cursor"] = cursor->toString();
if (!state_manager->set(state_map)) {
extensions/rocksdb-repos/RocksDbProvenanceRepository.cpp:169
- Failed deserialization is silently skipped while
last_event_idstill advances. Once a later valid record is returned, the reporting task persists that advanced cursor, permanently losing the malformed record from the reporting stream. Return an error instead of advancing past an event that could not be decoded.
if (eventRead->deserialize(stream)) {
records.push_back(eventRead);
if (--max_size == 0) {
break;
}
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| MultiPut(data); | ||
|
|
||
| return {}; |
| if (open_state_db->Get(options, NEXT_EVENT_UUID_KEY, &next_event_uuid_str).ok()) { | ||
| next_event_id_ = next_event_uuid_str; | ||
| } else { | ||
| logger_->log_error("Could not find '{}'", NEXT_EVENT_UUID_KEY); | ||
| next_event_id_ = utils::IdGenerator::getIdGenerator()->generate(); |
| if (auto event_ordinal = record->getEventOrdinal()) { | ||
| recordJson.AddMember("eventOrdinal", event_ordinal.value(), alloc); | ||
| } |
| explicit RocksDbProvenanceRepository(std::string_view repo_name = "", | ||
| std::string directory = PROVENANCE_DIRECTORY, | ||
| std::chrono::milliseconds maxPartitionMillis = MAX_PROVENANCE_ENTRY_LIFE_TIME, | ||
| int64_t maxPartitionBytes = MAX_PROVENANCE_STORAGE_SIZE, | ||
| std::chrono::milliseconds purgePeriod = PROVENANCE_PURGE_PERIOD) |
There was a problem hiding this comment.
nitpick: the parameters are not indented correctly.
| if (auto max_partition = utils::timeutils::StringToDuration<std::chrono::milliseconds>(value)) | ||
| max_partition_millis_ = *max_partition; |
There was a problem hiding this comment.
nitpick: we should put braces around this line to be consistent with other one line conditional statements
| testRepository->storeElement(record1); | ||
| testRepository->appendEvents({record1}); | ||
|
|
||
| utils::Identifier eventId = record1->getEventId(); |
There was a problem hiding this comment.
This variable is redeclared and does not compile
| testRepository->storeElement(record1); | ||
| testRepository->appendEvents({record1}); | ||
|
|
||
| utils::Identifier eventId = record1->getEventId(); |
There was a problem hiding this comment.
This variable is redeclared and does not compile
| int64_t max_buffer_size = 16 << 20; | ||
| cf_opts.write_buffer_size = gsl::narrow<size_t>(std::min(max_buffer_size, max_partition_bytes_)); |
There was a problem hiding this comment.
Can we use 16_MB from Literals.h here and omit the max_buffer_size variable?
| if (cursor_str.has_value()) { | ||
| return std::make_unique<EventCursor>(cursor_str.value()); | ||
| } | ||
| return std::make_unique<EventCursor>(""); |
There was a problem hiding this comment.
If the std::nullopt behavior is the same as the empty string, do we need to make this optional or can we just use empty string when we want a cursor from the start? Or is it possible in the future that an empty string and nullopt cases would differ?
| } | ||
|
|
||
| std::expected<void, std::string> RocksDbProvenanceRepository::appendEvents(const std::vector<std::shared_ptr<ProvenanceEventRecord>>& events) { | ||
| std::vector<std::pair<std::string, std::unique_ptr<io::BufferStream>>> data; |
There was a problem hiding this comment.
This type is kind of long and repeated quite a lot in the repository domian. It should be named with an alias because its variable is usually also just called "data" which is not really descriptive.
| } | ||
| for (auto& event : events) { | ||
| data.emplace_back(event->getUUIDStr(), std::make_unique<io::BufferStream>()); | ||
| event->serialize(*data.back().second); |
There was a problem hiding this comment.
Should we also handle if the event serialization fails?
| std::optional<std::string> cursor_str; | ||
| { | ||
| std::unordered_map<std::string, std::string> state_map; | ||
| if (state_manager->get(state_map)) { | ||
| if (auto it = state_map.find("cursor"); it != state_map.end()) { | ||
| cursor_str = it->second; | ||
| } | ||
| } | ||
| } | ||
| std::vector<std::shared_ptr<provenance::ProvenanceEventRecord>> records; | ||
| auto cursor = repo->cursorFromString(cursor_str); | ||
| if (cursor_str && !cursor) { | ||
| logger_->log_error("Failed to parse cursor, falling back to enumerating from the beginning"); | ||
| cursor = repo->cursorFromString(std::nullopt); | ||
| } |
There was a problem hiding this comment.
I would extract getting the cursor to a separate function in anonymous namespace.
| try { | ||
| std::map<std::string, std::string> attributes; | ||
| if (!protocol_->transmitPayload(context, jsonStr, attributes)) { | ||
| context.yield(); |
There was a problem hiding this comment.
With the added code should we return here after the yield?
| return std::unexpected{"Failed to append provenance events"}; | ||
| } | ||
|
|
||
| REGISTER_RESOURCE_AS(RocksDbProvenanceRepository, InternalResource, ("RocksDbProvenanceRepository", "ProvenanceRepository", "provenancerepository")); |
There was a problem hiding this comment.
With this registration the repository does not work when I set the provenance repo name in minifi.properties to "RocksDbProvenanceRepository", only with "ProvenanceRepository", I think the "RocksDbProvenanceRepository" as an option should be added.
| if (open_state_db->Get(options, NEXT_EVENT_UUID_KEY, &next_event_uuid_str).ok()) { | ||
| next_event_id_ = next_event_uuid_str; | ||
| } else { | ||
| logger_->log_error("Could not find '{}'", NEXT_EVENT_UUID_KEY); |
There was a problem hiding this comment.
This error occurs on first startup when the id is not yet stored, that should not be displayed as an error in that case, maybe just as a debug log.
Depends on #2195
Thank you for submitting a contribution to Apache NiFi - MiNiFi C++.
In order to streamline the review of the contribution we ask you to ensure the following steps have been taken:
For all changes:
Is there a JIRA ticket associated with this PR? Is it referenced in the commit message?
Does your PR title start with MINIFICPP-XXXX where XXXX is the JIRA number you are trying to resolve? Pay particular attention to the hyphen "-" character.
Has your PR been rebased against the latest commit within the target branch (typically main)?
Is your initial contribution a single, squashed commit?
For code changes:
For documentation related changes:
Note:
Please ensure that once the PR is submitted, you check GitHub Actions CI results for build issues and submit an update to your PR as soon as possible.