You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2026/03/31 17:40:51 UTC

(nifi) branch main updated: NIFI-15772 Fixed inherited Parameter Context synchronization on versioned Process Group upgrades (#11082)

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 a08dcd708bd NIFI-15772 Fixed inherited Parameter Context synchronization on versioned Process Group upgrades (#11082)
a08dcd708bd is described below

commit a08dcd708bd7f9eb03a55cbf24cfd3f39b49f2b7
Author: Pierre Villard <pi...@gmail.com>
AuthorDate: Tue Mar 31 19:40:43 2026 +0200

    NIFI-15772 Fixed inherited Parameter Context synchronization on versioned Process Group upgrades (#11082)
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 .../StandardVersionedComponentSynchronizer.java    | 11 +++
 ...StandardVersionedComponentSynchronizerTest.java | 48 ++++++++++++
 .../registry/ParameterContextPreservationIT.java   | 86 ++++++++++++++++++++++
 3 files changed, 145 insertions(+)

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 ae823db265d..424b4782c2d 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
@@ -449,6 +449,17 @@ 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());
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
index fe1ae5b4f41..1dd5bd17f7f 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java
@@ -1305,6 +1305,54 @@ public class StandardVersionedComponentSynchronizerTest {
         verify(processGroup, times(1)).setParameterContext(any(ParameterContext.class));
     }
 
+    @Test
+    public void testNewParameterInInheritedContextAddedDuringSync() throws FlowSynchronizationException, InterruptedException, TimeoutException {
+        // Create P2 with paramA
+        final VersionedParameterContext versionedP2 = createVersionedParameterContext("P2", Map.of("paramA", "valueA"), Collections.emptySet());
+        synchronizer.synchronize(null, versionedP2, synchronizationOptions);
+
+        final ParameterContext p2 = parameterContextManager.getParameterContextNameMapping().get("P2");
+
+        // Create P1 with its own parameter, then set up inheritance from P2
+        final VersionedParameterContext versionedP1 = createVersionedParameterContext("P1", Map.of("paramOwn", "ownValue"), Collections.emptySet());
+        synchronizer.synchronize(null, versionedP1, synchronizationOptions);
+
+        final ParameterContext p1 = parameterContextManager.getParameterContextNameMapping().get("P1");
+        p1.setInheritedParameterContexts(List.of(p2));
+        assertEquals(1, p1.getInheritedParameterContexts().size());
+        assertEquals("P2", p1.getInheritedParameterContexts().get(0).getName());
+
+        // Prepare the process group mock bound to P1
+        final ProcessGroup processGroup = createMockProcessGroup();
+        when(processGroup.getParameterContext()).thenReturn(p1);
+
+        // Build proposed flow for version 2: P2 now has paramA and paramX
+        final VersionedParameterContext proposedP2 = createVersionedParameterContext("P2", Map.of("paramA", "valueA", "paramX", "valueX"), Collections.emptySet());
+        proposedP2.setInheritedParameterContexts(Collections.emptyList());
+
+        final VersionedParameterContext proposedP1 = createVersionedParameterContext("P1", Map.of("paramOwn", "ownValue"), Collections.emptySet());
+        proposedP1.setInheritedParameterContexts(List.of("P2"));
+
+        final Map<String, VersionedParameterContext> parameterContextMap = new HashMap<>();
+        parameterContextMap.put("P1", proposedP1);
+        parameterContextMap.put("P2", proposedP2);
+
+        final VersionedProcessGroup rootGroup = new VersionedProcessGroup();
+        rootGroup.setIdentifier(processGroup.getIdentifier());
+        rootGroup.setParameterContextName("P1");
+
+        final VersionedExternalFlow externalFlow = new VersionedExternalFlow();
+        externalFlow.setFlowContents(rootGroup);
+        externalFlow.setParameterContexts(parameterContextMap);
+
+        synchronizer.synchronize(processGroup, externalFlow, synchronizationOptions);
+
+        // Verify P2 now contains paramX
+        assertTrue(p2.getParameter("paramX").isPresent(), "paramX should have been added to inherited context P2");
+        assertEquals("valueX", p2.getParameter("paramX").get().getValue());
+        assertTrue(p2.getParameter("paramA").isPresent(), "paramA should still exist on P2");
+    }
+
     private VersionedParameterContext createVersionedParameterContext(final String name, final Map<String, String> parameters, final Set<String> sensitiveParamNames) {
         final Set<VersionedParameter> versionedParameters = new HashSet<>();
         for (final Map.Entry<String, String> entry : parameters.entrySet()) {
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 d1937b06271..592bd29958b 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
@@ -37,11 +37,14 @@ import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * System tests to verify that parameter context bindings and inheritance chains are preserved
@@ -148,6 +151,89 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
                 "NEW Process Group B should use the same parameter context as its parent A2, not be bound to the first parameter context P");
     }
 
+    /**
+     * Verifies that new parameters added to an inherited parameter context are properly synchronized
+     * when upgrading a versioned process group.
+     *
+     * Scenario: P1 inherits from P2. PG is bound to P1. In version 2, a new parameter is added to P2.
+     * After importing version 1 fresh and upgrading to version 2, P2 should contain the new parameter.
+     */
+    @Test
+    void testNewParameterInInheritedContextSynchronizedDuringUpgrade() throws NiFiClientException, IOException, InterruptedException {
+        final FlowRegistryClientEntity clientEntity = registerClient();
+        final NiFiClientUtil util = getClientUtil();
+
+        // Create P2 with parameter "paramA"
+        final ParameterContextEntity paramContextP2 = util.createParameterContext("P2", Map.of("paramA", "valueA"));
+
+        // Create P1 inheriting from P2 with its own parameter
+        final ParameterContextEntity paramContextP1 = util.createParameterContext("P1", Map.of("paramOwn", "ownValue"), List.of(paramContextP2.getId()), null);
+
+        // Create PG bound to P1 with a processor that references paramA (inherited from P2)
+        final ProcessGroupEntity groupPG = util.createProcessGroup("PG", "root");
+        util.setParameterContext(groupPG.getId(), paramContextP1);
+
+        final ProcessorEntity processor = util.createProcessor(PROCESSOR_TYPE, groupPG.getId());
+        util.updateProcessorProperties(processor, Collections.singletonMap(PROCESSOR_PROPERTY_TEXT, "#{paramA}"));
+        util.setAutoTerminatedRelationships(processor, RELATIONSHIP_SUCCESS);
+
+        // Save as version 1 (P2 has only paramA)
+        final VersionControlInformationEntity vciV1 = util.startVersionControl(groupPG, clientEntity, TEST_FLOWS_BUCKET, "InheritedParamFlow");
+        final String flowId = vciV1.getVersionControlInformation().getFlowId();
+
+        // Add new parameter "paramX" to P2 and save as version 2
+        final ParameterContextEntity currentP2 = getNifiClient().getParamContextClient().getParamContext(paramContextP2.getId(), false);
+        final ParameterContextUpdateRequestEntity updateRequest = util.updateParameterContext(currentP2, Map.of("paramA", "valueA", "paramX", "valueX"));
+        util.waitForParameterContextRequestToComplete(paramContextP2.getId(), updateRequest.getRequest().getRequestId());
+
+        final ProcessGroupEntity refreshedPG = getNifiClient().getProcessGroupClient().getProcessGroup(groupPG.getId());
+        util.saveFlowVersion(refreshedPG, clientEntity, vciV1);
+
+        // Clean up the original flow and parameter contexts
+        final ProcessGroupEntity pgForStopVc = getNifiClient().getProcessGroupClient().getProcessGroup(groupPG.getId());
+        getNifiClient().getVersionsClient().stopVersionControl(pgForStopVc);
+        util.deleteAll(groupPG.getId());
+        final ProcessGroupEntity pgToDelete = getNifiClient().getProcessGroupClient().getProcessGroup(groupPG.getId());
+        getNifiClient().getProcessGroupClient().deleteProcessGroup(pgToDelete);
+
+        final ParameterContextEntity p1ToDelete = getNifiClient().getParamContextClient().getParamContext(paramContextP1.getId(), false);
+        getNifiClient().getParamContextClient().deleteParamContext(paramContextP1.getId(), String.valueOf(p1ToDelete.getRevision().getVersion()));
+
+        final ParameterContextEntity p2ToDelete = getNifiClient().getParamContextClient().getParamContext(paramContextP2.getId(), false);
+        getNifiClient().getParamContextClient().deleteParamContext(paramContextP2.getId(), String.valueOf(p2ToDelete.getRevision().getVersion()));
+
+        // Import version 1 fresh -- creates new P1 and P2 with only paramA on P2
+        final ProcessGroupEntity importedPG = importFlowWithReplaceParameterContext(clientEntity.getId(), flowId, VERSION_1);
+
+        // Locate the imported P2 by finding P1's inherited context
+        final ProcessGroupEntity fetchedPG = getNifiClient().getProcessGroupClient().getProcessGroup(importedPG.getId());
+        final String importedP1Id = fetchedPG.getComponent().getParameterContext().getId();
+        final ParameterContextEntity importedP1 = getNifiClient().getParamContextClient().getParamContext(importedP1Id, false);
+        assertEquals(1, importedP1.getComponent().getInheritedParameterContexts().size());
+        final String importedP2Id = importedP1.getComponent().getInheritedParameterContexts().get(0).getId();
+
+        // Verify P2 has only paramA after importing version 1
+        final ParameterContextEntity importedP2 = getNifiClient().getParamContextClient().getParamContext(importedP2Id, false);
+        final Set<String> p2NamesAfterImport = getParameterNames(importedP2);
+        assertTrue(p2NamesAfterImport.contains("paramA"), "paramA should exist on P2 after importing version 1");
+        assertFalse(p2NamesAfterImport.contains("paramX"), "paramX should not exist on P2 after importing version 1");
+
+        // Upgrade from version 1 to version 2
+        util.changeFlowVersion(importedPG.getId(), VERSION_2);
+
+        // Verify P2 now contains paramX after the upgrade
+        final ParameterContextEntity p2AfterUpgrade = getNifiClient().getParamContextClient().getParamContext(importedP2Id, false);
+        final Set<String> p2NamesAfterUpgrade = getParameterNames(p2AfterUpgrade);
+        assertTrue(p2NamesAfterUpgrade.contains("paramA"), "paramA should still exist on P2 after upgrading to version 2");
+        assertTrue(p2NamesAfterUpgrade.contains("paramX"), "paramX should exist on P2 after upgrading to version 2");
+    }
+
+    private Set<String> getParameterNames(final ParameterContextEntity context) {
+        return context.getComponent().getParameters().stream()
+            .map(entity -> entity.getParameter().getName())
+            .collect(Collectors.toSet());
+    }
+
     private ProcessGroupEntity importFlowWithReplaceParameterContext(
             final String registryClientId,
             final String flowId,