NIFI-16318 Preserve existing Parameter Contexts on registry import - #11645
NIFI-16318 Preserve existing Parameter Contexts on registry import#11645mattcasters wants to merge 1 commit into
Conversation
|
I think the failure on Jetty bound HTTPS on 5670, then the management server (standalone tests use 127.0.0.1:56730 in bootstrap.conf) could not bind. NiFi shut itself down. The test client then waited 5 minutes, got connection refused, and reported “Failed to start NiFi”. Why it is unrelated to this PR • The hang is a port conflict on the management server, not Parameter Context merge. The previous class, ContentClaimTruncationAfterRestartIT, uses isAllowFactoryReuse() = false and restarts NiFi. If that process (or a leftover standalone) still held 56730, the next standalone instance fails exactly this way. That is a known Linux CI flake under load (TIME_WAIT / leftover PID), not something this PR introduced. |
bbende
left a comment
There was a problem hiding this comment.
Thanks for looking into this! Left some comments/questions for discussion
|
|
||
| assertEquals(VALUE_XYZ, paramContext.getParameter(PARAM_ABC).get().getValue(), | ||
| "KEEP_EXISTING import must not overwrite an existing parameter value"); | ||
| assertEquals(ORIGINAL_PARAMETER_DESCRIPTION, paramContext.getParameter(PARAM_ABC).get().getDescriptor().getDescription(), |
There was a problem hiding this comment.
Interestingly, if you run these 3 new tests without the rest of the changes in this PR (i.e. using the behavior on main), they all pass except for this line about the description, the value check above passes indicating that the value is not altered.
Should we try to alter these tests to reproduce the problem you experienced to ensure it is fixed?
| && !Objects.equals(localParameter.getDescriptor().getDescription(), versionedParameter.getDescription())) { | ||
| final Parameter updatedParameter = new Parameter.Builder() | ||
| .fromParameter(existingParameter) | ||
| .fromParameter(localParameter) |
There was a problem hiding this comment.
Just wanted to clarify that the original intent here of using fromParameter was to preserve the value.
I think the actual bug was in the previous code at line 2428 when it used currentParameterContext.getParameter which returns the resolved effective parameter. So for a parameter whose value is exactly #{other} in a context that has inheritance, a description-only update materialized a local parameter holding the resolved literal and destroyed the reference.
Is that what you ran into?
There was a problem hiding this comment.
See the later comment about the tests, I think we should try to update them to test this scenario with losing the reference
| // To accomplish this, we call updateProcessGroupContents() passing 'true' for the updateSettings flag but null out the position. | ||
| flowSnapshot.getFlowContents().setPosition(null); | ||
| final boolean preserveExistingParameterContextEntries = | ||
| ParameterContextHandlingStrategy.KEEP_EXISTING.equals(parameterContextHandlingStrategy); |
There was a problem hiding this comment.
The existing code reads parameterContextHandlingStrategy exactly once, and it does so inside a block that is guarded so it only runs on the originating node:
ProcessGroupResource.java
Ln 1070–1091
if (versionControlInfo != null && requestProcessGroupEntity.getVersionedFlowSnapshot() == null) {
// ... fetch snapshot from registry ...
// Step 4: Replace parameter contexts if necessary
if (ParameterContextHandlingStrategy.REPLACE.equals(parameterContextHandlingStrategy)) {
parameterContextReplacer.replaceParameterContexts(flowSnapshot, serviceFacade.getParameterContexts());
}
The getVersionedFlowSnapshot() == null condition is the key. Step 6 of that same block sets the snapshot on the entity, and the entity is what gets replicated. So on the receiving nodes the snapshot is already populated, the whole block is skipped, and the query param is never consulted there. The renaming decision has already been baked into the request body. That's why REPLACE works correctly in a cluster today despite the query param being dropped by getAbsolutePath().
The PR adds a second read of the strategy, and it puts it inside the withWriteLock callback — which is the one place that runs on every node, always after replication. On the receiving nodes that read has no query param to work with, so it always resolves to the @DefaultValue("KEEP_EXISTING").
So the underlying mechanism (query params don't survive replication) is pre-existing, and the existing code is correctly written around it. The divergence between standalone and clustered behavior is new in this PR.
| // Use the effective parameter map so inherited parameters and resolved aliases are compared against | ||
| // the values components actually see. Using the local map treats an inherited name as "new" and | ||
| // incorrectly requires referencing components to be stopped. | ||
| verifyCanSetParameters(getEffectiveParameters(), updatedParameters, duringUpdate); |
There was a problem hiding this comment.
This is a correct and worthwhile fix, but it technically isn't required now that the synchronizer skips inherited parameters.
It's a separate behavioral change and is used by every parameter-context update in the code base. I would consider separating this to it's own JIRA with the new contract stated explicitly and tests for the provider-backed and sensitivity-shadowing cases.
During flow synchronization, using getParameter() returned the resolved
effective parameter instead of local raw parameters. As a result,
description-only updates materialized local overrides for inherited
parameters and flattened parameter reference aliases (#{other}) to
literal resolved values. Furthermore, materializing inherited parameters
on child contexts caused verification to treat them as runtime-affecting
additions, failing import when referencing components were active.
In StandardVersionedComponentSynchronizer:
- Preserve existing local parameter values and reference syntax
(#{other}) using raw local parameters from getParameters().
- Do not materialize local overrides for inherited parameters found in
getRawEffectiveParameters().
- Continue adding parameters that are absent from the effective set.
- Only invoke setParameters when updates or additions exist.
269985b to
2751706
Compare
|
Thanks @bbende for the thorough review and insights! In response to your feedback, we have streamlined the PR to focus squarely on the root cause in
|
Summary
NIFI-16318
When importing a versioned flow or upgrading flow versions with Parameter Contexts,
StandardVersionedComponentSynchronizer.addMissingConfigurationpreviously usedcurrentParameterContext.getParameter()to check for parameter existence. BecausegetParameter()returns the resolved effective parameter (resolving across inherited contexts and dereferencing parameter references#{...}):addMissingConfigurationconstructed an update and calledsetParameters()on the child context. This materialized an unwanted local override on the child context, severing parent inheritance.verifyCanSetParameters(), because the parameter was inherited and not in the child's local map (this.parameters), it was treated as a runtime-affecting addition. If a referencing component (such as a Controller Service) was active (ENABLED), the import failed with anIllegalStateException.#{otherParam}), usinggetParameter()returned the resolved literal string value. Updating the description withfromParameter()replaced the reference syntax with the literal string, destroying the alias reference.Root cause
StandardVersionedComponentSynchronizer.addMissingConfiguration:currentParameterContext.getParameter(name)(effective) rather than checking local parameters (getParameters()) and inherited parameters (getRawEffectiveParameters()) separately.#{...}) into literal values during description updates.setParameters()even when no additions or modifications were present.Fix
In
StandardVersionedComponentSynchronizer.addMissingConfiguration:currentParameterContext.getParameters().get(...)and build description updates usingfromParameter(localParameter). This preserves the raw local value and reference syntax (#{otherParam}).currentParameterContext.getRawEffectiveParameters().get(...)and skip them (continue;). This prevents materializing local overrides on child contexts for inherited parameters, preserves parent inheritance, and avoids triggering component state verification on active referencing components.setParameters()whenparametersis non-empty.import-from-registry.component.htmlto clarify that parameter values and inheritance chains are preserved.Tracking
mainVerification
./mvnw -pl nifi-framework-bundle/nifi-framework/nifi-framework-components,nifi-system-tests/nifi-system-test-suite checkstyle:checkStandardVersionedComponentSynchronizerTest(all 60 tests passing, including tests for inherited parameters with diverging descriptions, and parameter reference#{...}preservation)ParameterContextPreservationIT