This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new d1f0d7e0860 NIFI-15945 Fixed Parameter addition for multiple suffixed
Parameter Contexts (#11255)
d1f0d7e0860 is described below
commit d1f0d7e0860d18260f2111cad3b5570e985b6af6
Author: Pierre Villard <[email protected]>
AuthorDate: Sun May 17 04:32:17 2026 +0200
NIFI-15945 Fixed Parameter addition for multiple suffixed Parameter
Contexts (#11255)
- Corrected scenario where Parameter was not correctly added to Parameter
Context when having multiple suffixed parameter contexts
Signed-off-by: David Handermann <[email protected]>
---
.../StandardVersionedComponentSynchronizer.java | 61 ++++++++++++-----
.../registry/ParameterContextPreservationIT.java | 80 ++++++++++++++++++++++
2 files changed, 123 insertions(+), 18 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
index e3704ead9ad..d5f1db9d7be 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java
@@ -454,17 +454,6 @@ public class StandardVersionedComponentSynchronizer
implements VersionedComponen
// parameter contexts that inherit from one another and neither the
inheriting nor inherited parameter context exists.
if (versionedParameterContexts != null) {
versionedParameterContexts.values().forEach(this::createParameterContextWithoutReferences);
-
- // After ensuring all contexts exist, add any missing parameters
to all existing contexts from the proposed definitions.
- // This is necessary because
createParameterContextWithoutReferences skips contexts that already exist, so
new parameters
- // added to inherited contexts (e.g., a parent P2 inherited by the
group's bound context P1) would otherwise be missed.
- final ComponentIdGenerator componentIdGenerator =
context.getComponentIdGenerator();
- for (final Map.Entry<String, VersionedParameterContext> entry :
versionedParameterContexts.entrySet()) {
- final ParameterContext existingContext =
getParameterContextByName(entry.getKey());
- if (existingContext != null) {
- addMissingConfiguration(entry.getValue(), existingContext,
versionedParameterContexts, parameterProviderReferences, componentIdGenerator);
- }
- }
}
updateParameterContext(group, proposed, versionedParameterContexts,
parameterProviderReferences, context.getComponentIdGenerator());
@@ -2404,6 +2393,17 @@ public class StandardVersionedComponentSynchronizer
implements VersionedComponen
final Map<String,
VersionedParameterContext> versionedParameterContexts,
final Map<String,
ParameterProviderReference> parameterProviderReferences,
final ComponentIdGenerator
componentIdGenerator) {
+ addMissingConfiguration(versionedParameterContext,
currentParameterContext, versionedParameterContexts,
parameterProviderReferences, componentIdGenerator, new HashSet<>());
+ }
+
+ private void addMissingConfiguration(final VersionedParameterContext
versionedParameterContext, final ParameterContext currentParameterContext,
+ final Map<String,
VersionedParameterContext> versionedParameterContexts,
+ final Map<String,
ParameterProviderReference> parameterProviderReferences,
+ final ComponentIdGenerator
componentIdGenerator, final Set<String> visitedParameterContextIds) {
+ if
(!visitedParameterContextIds.add(currentParameterContext.getIdentifier())) {
+ return;
+ }
+
final Map<String, Parameter> parameters = new HashMap<>();
for (final VersionedParameter versionedParameter :
versionedParameterContext.getParameters()) {
final Optional<Parameter> parameterOption =
currentParameterContext.getParameter(versionedParameter.getName());
@@ -2429,13 +2429,38 @@ public class StandardVersionedComponentSynchronizer
implements VersionedComponen
currentParameterContext.setDescription(versionedParameterContext.getDescription());
}
- // If the current parameter context doesn't have any inherited param
contexts but the versioned one does,
- // add the versioned ones.
- if (currentParameterContext.getInheritedParameterContexts().isEmpty()
- && versionedParameterContext.getInheritedParameterContexts()
!= null &&
!versionedParameterContext.getInheritedParameterContexts().isEmpty()) {
-
currentParameterContext.setInheritedParameterContexts(versionedParameterContext.getInheritedParameterContexts().stream()
- .map(name ->
selectParameterContext(versionedParameterContexts.get(name),
versionedParameterContexts, parameterProviderReferences, componentIdGenerator))
- .collect(Collectors.toList()));
+ final List<String> proposedInheritedNames =
versionedParameterContext.getInheritedParameterContexts();
+ final List<ParameterContext> currentInheritedContexts =
currentParameterContext.getInheritedParameterContexts();
+ if (proposedInheritedNames != null &&
!proposedInheritedNames.isEmpty()) {
+ if (currentInheritedContexts.isEmpty()) {
+ // The local parameter context has no inheritance configured
yet, so adopt the versioned chain
+ // by selecting (or creating) a matching parameter context for
each inherited name.
+
currentParameterContext.setInheritedParameterContexts(proposedInheritedNames.stream()
+ .map(name ->
selectParameterContext(versionedParameterContexts.get(name),
versionedParameterContexts, parameterProviderReferences, componentIdGenerator))
+ .collect(Collectors.toList()));
+ } else {
+ // Walk the local inheritance chain in lockstep with the
versioned chain so updates to inherited
+ // contexts are applied to the contexts actually referenced by
this parameter context, even when
+ // the local names were suffix-renamed at import time (for
example, P (2) instead of P). Pairs that
+ // do not match by exact name or by name-with-suffix are
skipped to avoid corrupting a chain that
+ // was rewired locally.
+ final int matchedDepth =
Math.min(currentInheritedContexts.size(), proposedInheritedNames.size());
+ for (int i = 0; i < matchedDepth; i++) {
+ final ParameterContext liveInheritedContext =
currentInheritedContexts.get(i);
+ final String proposedInheritedName =
proposedInheritedNames.get(i);
+ final VersionedParameterContext proposedInheritedContext =
versionedParameterContexts == null ? null :
versionedParameterContexts.get(proposedInheritedName);
+ if (liveInheritedContext == null ||
proposedInheritedContext == null) {
+ continue;
+ }
+ final String liveInheritedName =
liveInheritedContext.getName();
+ if (!liveInheritedName.equals(proposedInheritedName)
+ &&
!ParameterContextNameUtils.isNameWithSuffix(liveInheritedName,
proposedInheritedName)) {
+ continue;
+ }
+ addMissingConfiguration(proposedInheritedContext,
liveInheritedContext, versionedParameterContexts, parameterProviderReferences,
+ componentIdGenerator, visitedParameterContextIds);
+ }
+ }
}
if (versionedParameterContext.getParameterProvider() != null &&
currentParameterContext.getParameterProvider() == null) {
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
index c9e26318a5e..80318021501 100644
---
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/registry/ParameterContextPreservationIT.java
@@ -245,6 +245,86 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
assertTrue(p2NamesAfterUpgrade.contains("paramX"), "paramX should
exist on P2 after upgrading to version 2");
}
+ /**
+ * Verifies that a new parameter introduced by a new flow version is
applied only to the parameter context
+ * actually bound to the upgraded process group, even when the local flow
has multiple deployments of the
+ * same versioned flow with REPLACE-strategy suffix-renamed parameter
contexts (P, P (1), P (2)).
+ *
+ * Scenario: Flow F has parameter context P. F is imported three times
with the REPLACE strategy, producing
+ * deployments bound to P, P (1), and P (2) respectively. Version 2 of F
adds parameter Z to P. Upgrading
+ * the third deployment must apply Z only to P (2); P and P (1) must
remain unchanged.
+ */
+ @Test
+ void testNewParameterAppliedOnlyToBoundSuffixedContextDuringUpgrade()
throws NiFiClientException, IOException, InterruptedException {
+ final FlowRegistryClientEntity clientEntity = registerClient();
+ final NiFiClientUtil util = getClientUtil();
+
+ final ParameterContextEntity sourceParamContextP =
util.createParameterContext(PARAMETER_CONTEXT_NAME, Map.of(PARAMETER_NAME,
PARAMETER_VALUE));
+ final ProcessGroupEntity sourceGroupA =
util.createProcessGroup(GROUP_A_NAME, "root");
+ util.setParameterContext(sourceGroupA.getId(), sourceParamContextP);
+
+ final ProcessorEntity processor = util.createProcessor(PROCESSOR_TYPE,
sourceGroupA.getId());
+ util.updateProcessorProperties(processor,
Collections.singletonMap(PROCESSOR_PROPERTY_TEXT, PARAMETER_REFERENCE));
+ util.setAutoTerminatedRelationships(processor, RELATIONSHIP_SUCCESS);
+
+ final VersionControlInformationEntity vciV1 =
util.startVersionControl(sourceGroupA, clientEntity, TEST_FLOWS_BUCKET,
FLOW_NAME);
+ final String flowId = vciV1.getVersionControlInformation().getFlowId();
+
+ final String paramZName = "paramZ";
+ final String paramZValue = "valueZ";
+ final ParameterContextEntity currentSourceP =
getNifiClient().getParamContextClient().getParamContext(sourceParamContextP.getId(),
false);
+ final ParameterContextUpdateRequestEntity sourceUpdate =
util.updateParameterContext(currentSourceP,
+ Map.of(PARAMETER_NAME, PARAMETER_VALUE, paramZName,
paramZValue));
+
util.waitForParameterContextRequestToComplete(sourceParamContextP.getId(),
sourceUpdate.getRequest().getRequestId());
+
+ final ProcessGroupEntity sourceGroupARefreshed =
getNifiClient().getProcessGroupClient().getProcessGroup(sourceGroupA.getId());
+ util.saveFlowVersion(sourceGroupARefreshed, clientEntity, vciV1);
+
+ final ProcessGroupEntity sourceForStopVc =
getNifiClient().getProcessGroupClient().getProcessGroup(sourceGroupA.getId());
+
getNifiClient().getVersionsClient().stopVersionControl(sourceForStopVc);
+ util.deleteAll(sourceGroupA.getId());
+ final ProcessGroupEntity sourceToDelete =
getNifiClient().getProcessGroupClient().getProcessGroup(sourceGroupA.getId());
+
getNifiClient().getProcessGroupClient().deleteProcessGroup(sourceToDelete);
+
+ final ParameterContextEntity sourceContextToDelete =
getNifiClient().getParamContextClient().getParamContext(sourceParamContextP.getId(),
false);
+
getNifiClient().getParamContextClient().deleteParamContext(sourceParamContextP.getId(),
+
String.valueOf(sourceContextToDelete.getRevision().getVersion()));
+
+ final ProcessGroupEntity importedA1 =
importFlowWithReplaceParameterContext(clientEntity.getId(), flowId, VERSION_1);
+ final String paramContextId1 =
getNifiClient().getProcessGroupClient().getProcessGroup(importedA1.getId())
+ .getComponent().getParameterContext().getId();
+
+ final ProcessGroupEntity importedA2 =
importFlowWithReplaceParameterContext(clientEntity.getId(), flowId, VERSION_1);
+ final String paramContextId2 =
getNifiClient().getProcessGroupClient().getProcessGroup(importedA2.getId())
+ .getComponent().getParameterContext().getId();
+
+ final ProcessGroupEntity importedA3 =
importFlowWithReplaceParameterContext(clientEntity.getId(), flowId, VERSION_1);
+ final String paramContextId3 =
getNifiClient().getProcessGroupClient().getProcessGroup(importedA3.getId())
+ .getComponent().getParameterContext().getId();
+
+ assertNotEquals(paramContextId1, paramContextId2);
+ assertNotEquals(paramContextId2, paramContextId3);
+ assertNotEquals(paramContextId1, paramContextId3);
+
+
assertFalse(getParameterNames(getNifiClient().getParamContextClient().getParamContext(paramContextId1,
false)).contains(paramZName));
+
assertFalse(getParameterNames(getNifiClient().getParamContextClient().getParamContext(paramContextId2,
false)).contains(paramZName));
+
assertFalse(getParameterNames(getNifiClient().getParamContextClient().getParamContext(paramContextId3,
false)).contains(paramZName));
+
+ util.changeFlowVersion(importedA3.getId(), VERSION_2);
+
+ final ParameterContextEntity context3AfterUpgrade =
getNifiClient().getParamContextClient().getParamContext(paramContextId3, false);
+
assertTrue(getParameterNames(context3AfterUpgrade).contains(paramZName),
+ "paramZ should be added to the parameter context bound to the
upgraded deployment");
+
+ final ParameterContextEntity context1AfterUpgrade =
getNifiClient().getParamContextClient().getParamContext(paramContextId1, false);
+
assertFalse(getParameterNames(context1AfterUpgrade).contains(paramZName),
+ "paramZ should not leak into the canonical parameter context
bound to a different deployment");
+
+ final ParameterContextEntity context2AfterUpgrade =
getNifiClient().getParamContextClient().getParamContext(paramContextId2, false);
+
assertFalse(getParameterNames(context2AfterUpgrade).contains(paramZName),
+ "paramZ should not leak into the suffixed parameter context
bound to a different deployment");
+ }
+
/**
* Verifies that parameter and parameter context descriptions are updated
when upgrading a versioned
* process group from one version to the next, even when the parameter
value itself remains unchanged