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/04/23 16:53:42 UTC

(nifi) branch main updated: NIFI-15872 Excluded Parameter Description from Flow Version change determination (#11174)

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 0e11aeecd15 NIFI-15872 Excluded Parameter Description from Flow Version change determination (#11174)
0e11aeecd15 is described below

commit 0e11aeecd1584335fb3a9341c9bb8ae6b836be3d
Author: Pierre Villard <pi...@gmail.com>
AuthorDate: Thu Apr 23 18:53:35 2026 +0200

    NIFI-15872 Excluded Parameter Description from Flow Version change determination (#11174)
    
    Fixed flow version upgrades when the Parameter Description has changed
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 .../nifi/parameter/StandardParameterContext.java   | 33 +++++++++--
 .../parameter/TestStandardParameterContext.java    | 59 ++++++++++++++++++
 .../registry/ParameterContextPreservationIT.java   | 69 ++++++++++++++++++----
 3 files changed, 143 insertions(+), 18 deletions(-)

diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
index d5445d2a8c2..5bc98968961 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
@@ -741,9 +741,11 @@ public class StandardParameterContext implements ParameterContext {
         for (final Map.Entry<String, Parameter> entry : updatedParameters.entrySet()) {
             final String parameterName = entry.getKey();
             final Parameter parameter = entry.getValue();
+            final Parameter currentParameter = currentParameters.get(new ParameterDescriptor.Builder().name(parameterName).build());
+
             if (parameter == null) {
                 // parameter is being deleted.
-                validateReferencingComponents(parameterName, null, duringUpdate);
+                validateReferencingComponents(parameterName, currentParameter, null, duringUpdate);
                 continue;
             }
 
@@ -752,7 +754,7 @@ public class StandardParameterContext implements ParameterContext {
             }
 
             validateSensitiveFlag(currentParameters, parameter);
-            validateReferencingComponents(parameterName, parameter, duringUpdate);
+            validateReferencingComponents(parameterName, currentParameter, parameter, duringUpdate);
         }
     }
 
@@ -777,15 +779,18 @@ public class StandardParameterContext implements ParameterContext {
         }
     }
 
-    private void validateReferencingComponents(final String parameterName, final Parameter parameter, final boolean duringUpdate) {
+    private void validateReferencingComponents(final String parameterName, final Parameter currentParameter, final Parameter parameter, final boolean duringUpdate) {
         final boolean isDeletion = (parameter == null);
         final String action = isDeletion ? "remove" : "update";
+        final boolean runtimeAffectingChange = isRuntimeAffectingChange(currentParameter, parameter);
+        final boolean enforceReferencingState = isDeletion || (duringUpdate && runtimeAffectingChange);
+
         for (final ProcessorNode procNode : parameterReferenceManager.getProcessorsReferencing(this, parameterName)) {
             if (procNode.isExtensionMissing()) {
                 continue;
             }
 
-            if (procNode.isRunning() && (isDeletion || duringUpdate)) {
+            if (procNode.isRunning() && enforceReferencingState) {
                 throw new IllegalStateException("Cannot " + action + " parameter '" + parameterName + "' because it is referenced by " + procNode + ", which is currently running");
             }
 
@@ -800,7 +805,7 @@ public class StandardParameterContext implements ParameterContext {
             }
 
             final ControllerServiceState serviceState = serviceNode.getState();
-            if (serviceState != ControllerServiceState.DISABLED && (isDeletion || duringUpdate)) {
+            if (serviceState != ControllerServiceState.DISABLED && enforceReferencingState) {
                 throw new IllegalStateException("Cannot " + action + " parameter '" + parameterName + "' because it is referenced by "
                         + serviceNode + ", which currently has a state of " + serviceState);
             }
@@ -811,6 +816,24 @@ public class StandardParameterContext implements ParameterContext {
         }
     }
 
+    /**
+     * Determines whether an update from {@code currentParameter} to {@code proposedParameter} would affect the runtime
+     * behavior of components that reference the parameter. Metadata-only changes such as description or tag updates do
+     * not affect runtime behavior and therefore do not require referencing components to be stopped or disabled.
+     */
+    private boolean isRuntimeAffectingChange(final Parameter currentParameter, final Parameter proposedParameter) {
+        if (currentParameter == null || proposedParameter == null) {
+            return true;
+        }
+        if (currentParameter.getDescriptor().isSensitive() != proposedParameter.getDescriptor().isSensitive()) {
+            return true;
+        }
+        if (!Objects.equals(currentParameter.getValue(), proposedParameter.getValue())) {
+            return true;
+        }
+        return !Objects.equals(currentParameter.getReferencedAssets(), proposedParameter.getReferencedAssets());
+    }
+
     private void validateParameterSensitivity(final Parameter parameter, final ComponentNode componentNode) {
         final String paramName = parameter.getDescriptor().getName();
 
diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
index 31a67930182..d4641821e4b 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
@@ -47,6 +47,12 @@ import static org.mockito.Mockito.when;
 
 public class TestStandardParameterContext {
 
+    private static final String PARAM_NAME_ABC = "abc";
+    private static final String PARAM_VALUE = "value";
+    private static final String PARAM_VALUE_UPDATED = "new-value";
+    private static final String DESCRIPTION_ORIGINAL = "original description";
+    private static final String DESCRIPTION_UPDATED = "updated description";
+
     @Test
     public void testUpdatesApply() {
         final ParameterReferenceManager referenceManager = new HashMapParameterReferenceManager();
@@ -549,6 +555,59 @@ public class TestStandardParameterContext {
         assertThrows(IllegalStateException.class, () -> b.setInheritedParameterContexts(Collections.emptyList()));
     }
 
+    @Test
+    public void testDescriptionOnlyUpdateAllowedWhileReferencingProcessorRunning() {
+        final HashMapParameterReferenceManager referenceManager = new HashMapParameterReferenceManager();
+        final ParameterContext context = createStandardParameterContext(referenceManager);
+        final ProcessorNode procNode = getProcessorNode(PARAM_NAME_ABC, referenceManager);
+
+        final ParameterDescriptor originalDescriptor = new ParameterDescriptor.Builder().name(PARAM_NAME_ABC).description(DESCRIPTION_ORIGINAL).build();
+        context.setParameters(Collections.singletonMap(PARAM_NAME_ABC, createParameter(originalDescriptor, PARAM_VALUE)));
+
+        startProcessor(procNode);
+
+        final ParameterDescriptor updatedDescriptor = new ParameterDescriptor.Builder().name(PARAM_NAME_ABC).description(DESCRIPTION_UPDATED).build();
+        context.setParameters(Collections.singletonMap(PARAM_NAME_ABC, createParameter(updatedDescriptor, PARAM_VALUE)));
+
+        final Parameter updatedParam = context.getParameter(PARAM_NAME_ABC).get();
+        assertEquals(DESCRIPTION_UPDATED, updatedParam.getDescriptor().getDescription());
+        assertEquals(PARAM_VALUE, updatedParam.getValue());
+
+        // Changing the value while the processor is running is still rejected
+        final Parameter valueChanged = createParameter(updatedDescriptor, PARAM_VALUE_UPDATED);
+        assertThrows(IllegalStateException.class, () -> context.setParameters(Collections.singletonMap(PARAM_NAME_ABC, valueChanged)));
+        assertEquals(PARAM_VALUE, context.getParameter(PARAM_NAME_ABC).get().getValue());
+    }
+
+    @Test
+    public void testDescriptionOnlyUpdateAllowedWhileReferencingServiceEnabled() {
+        final HashMapParameterReferenceManager referenceManager = new HashMapParameterReferenceManager();
+        final ParameterContext context = createStandardParameterContext(referenceManager);
+        final ControllerServiceNode serviceNode = mock(ControllerServiceNode.class);
+        setControllerServiceState(serviceNode, ControllerServiceState.DISABLED);
+        referenceManager.addControllerServiceReference(PARAM_NAME_ABC, serviceNode);
+
+        final ParameterDescriptor originalDescriptor = new ParameterDescriptor.Builder().name(PARAM_NAME_ABC).description(DESCRIPTION_ORIGINAL).build();
+        context.setParameters(Collections.singletonMap(PARAM_NAME_ABC, createParameter(originalDescriptor, PARAM_VALUE)));
+
+        for (final ControllerServiceState state : EnumSet.of(ControllerServiceState.ENABLED, ControllerServiceState.ENABLING, ControllerServiceState.DISABLING)) {
+            setControllerServiceState(serviceNode, state);
+
+            final String newDescription = "updated while " + state;
+            final ParameterDescriptor updatedDescriptor = new ParameterDescriptor.Builder().name(PARAM_NAME_ABC).description(newDescription).build();
+            context.setParameters(Collections.singletonMap(PARAM_NAME_ABC, createParameter(updatedDescriptor, PARAM_VALUE)));
+
+            final Parameter updatedParam = context.getParameter(PARAM_NAME_ABC).get();
+            assertEquals(newDescription, updatedParam.getDescriptor().getDescription());
+            assertEquals(PARAM_VALUE, updatedParam.getValue());
+
+            // Attempting to change the value while the referencing service is active still fails
+            final Parameter valueChange = createParameter(updatedDescriptor, PARAM_VALUE_UPDATED + "-" + state);
+            assertThrows(IllegalStateException.class, () -> context.setParameters(Collections.singletonMap(PARAM_NAME_ABC, valueChange)));
+            assertEquals(PARAM_VALUE, context.getParameter(PARAM_NAME_ABC).get().getValue());
+        }
+    }
+
     @Test
     public void testChangingParameterForEnabledControllerService() {
         final HashMapParameterReferenceManager referenceManager = new HashMapParameterReferenceManager();
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 931763f3ebe..c9e26318a5e 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
@@ -23,6 +23,8 @@ import org.apache.nifi.toolkit.client.NiFiClientException;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.VersionControlInformationDTO;
 import org.apache.nifi.web.api.dto.flow.FlowDTO;
+import org.apache.nifi.web.api.entity.ControllerServiceEntity;
+import org.apache.nifi.web.api.entity.ControllerServicesEntity;
 import org.apache.nifi.web.api.entity.FlowRegistryClientEntity;
 import org.apache.nifi.web.api.entity.ParameterContextEntity;
 import org.apache.nifi.web.api.entity.ParameterContextReferenceEntity;
@@ -70,6 +72,15 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
     private static final String PARAMETER_DESCRIPTION_V2 = "Description for version 2";
     private static final String CONTEXT_DESCRIPTION_V1 = "Context description v1";
     private static final String CONTEXT_DESCRIPTION_V2 = "Context description v2";
+    private static final String COUNT_PARAMETER_NAME = "countStart";
+    private static final String COUNT_PARAMETER_VALUE = "0";
+    private static final String COUNT_PARAMETER_REFERENCE = "#{" + COUNT_PARAMETER_NAME + "}";
+    private static final String COUNT_PARAMETER_DESCRIPTION_V1 = "Counter start description v1";
+    private static final String COUNT_PARAMETER_DESCRIPTION_V2 = "Counter start description v2";
+    private static final String COUNT_SERVICE_TYPE = "StandardCountService";
+    private static final String COUNT_SERVICE_START_VALUE_PROPERTY = "Start Value";
+    private static final String CONTROLLER_SERVICE_STATE_ENABLED = "ENABLED";
+    private static final String CONTROLLER_SERVICE_STATE_DISABLED = "DISABLED";
 
     @Test
     void testNewProcessGroupUsesCorrectParameterContextDuringUpgrade() throws NiFiClientException, IOException, InterruptedException {
@@ -235,15 +246,20 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
     }
 
     /**
-     * Verifies that parameter descriptions are updated when upgrading a versioned process group from one version
-     * to the next, even when the parameter value itself remains unchanged.
+     * 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
+     * and the parameter is referenced by a Controller Service that is currently ENABLED on the target.
+     * Description-only updates do not affect runtime behavior, so referencing components should not have
+     * to be disabled for the upgrade to succeed.
      */
     @Test
     void testParameterDescriptionUpdatedDuringVersionUpgrade() throws NiFiClientException, IOException, InterruptedException {
         final FlowRegistryClientEntity clientEntity = registerClient();
         final NiFiClientUtil util = getClientUtil();
 
-        final Set<ParameterEntity> parameterEntitiesV1 = Set.of(util.createParameterEntity(PARAMETER_NAME, PARAMETER_DESCRIPTION_V1, false, PARAMETER_VALUE));
+        final Set<ParameterEntity> parameterEntitiesV1 = Set.of(
+                util.createParameterEntity(PARAMETER_NAME, PARAMETER_DESCRIPTION_V1, false, PARAMETER_VALUE),
+                util.createParameterEntity(COUNT_PARAMETER_NAME, COUNT_PARAMETER_DESCRIPTION_V1, false, COUNT_PARAMETER_VALUE));
         final ParameterContextEntity paramContext = getNifiClient().getParamContextClient().createParamContext(
             util.createParameterContextEntity(PARAMETER_CONTEXT_NAME, CONTEXT_DESCRIPTION_V1, parameterEntitiesV1));
 
@@ -254,12 +270,23 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
         util.updateProcessorProperties(processor, Collections.singletonMap(PROCESSOR_PROPERTY_TEXT, PARAMETER_REFERENCE));
         util.setAutoTerminatedRelationships(processor, RELATIONSHIP_SUCCESS);
 
+        final ControllerServiceEntity sourceService = util.createControllerService(COUNT_SERVICE_TYPE, groupA.getId());
+        util.updateControllerServiceProperties(sourceService, Map.of(COUNT_SERVICE_START_VALUE_PROPERTY, COUNT_PARAMETER_REFERENCE));
+
         final VersionControlInformationEntity vciV1 = util.startVersionControl(groupA, clientEntity, TEST_FLOWS_BUCKET, DESCRIPTION_UPDATE_FLOW_NAME);
         final String flowId = vciV1.getVersionControlInformation().getFlowId();
 
-        // Update the parameter description (keeping the same value) and context description, then save as version 2
+        // Enable the Controller Service on the source side so updating the parameter description exercises
+        // the "description-only update while a referencing service is ENABLED" path on both source and target.
+        final ControllerServiceEntity sourceServiceForEnable = getNifiClient().getControllerServicesClient().getControllerService(sourceService.getId());
+        util.enableControllerService(sourceServiceForEnable);
+        util.waitForControllerServicesEnabled(groupA.getId(), List.of(sourceService.getId()));
+
+        // Update both parameter descriptions (keeping values unchanged) plus the context description, then save as version 2
         final ParameterContextEntity currentContext = getNifiClient().getParamContextClient().getParamContext(paramContext.getId(), false);
-        final Set<ParameterEntity> parameterEntitiesV2 = Set.of(util.createParameterEntity(PARAMETER_NAME, PARAMETER_DESCRIPTION_V2, false, PARAMETER_VALUE));
+        final Set<ParameterEntity> parameterEntitiesV2 = Set.of(
+                util.createParameterEntity(PARAMETER_NAME, PARAMETER_DESCRIPTION_V2, false, PARAMETER_VALUE),
+                util.createParameterEntity(COUNT_PARAMETER_NAME, COUNT_PARAMETER_DESCRIPTION_V2, false, COUNT_PARAMETER_VALUE));
         final ParameterContextEntity entityUpdate = util.createParameterContextEntity(PARAMETER_CONTEXT_NAME, CONTEXT_DESCRIPTION_V2, parameterEntitiesV2);
         entityUpdate.setId(currentContext.getId());
         entityUpdate.setRevision(currentContext.getRevision());
@@ -271,6 +298,10 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
         util.saveFlowVersion(refreshedGroupA, clientEntity, vciV1);
 
         // Clean up the original flow
+        final ControllerServiceEntity sourceServiceForDisable = getNifiClient().getControllerServicesClient().getControllerService(sourceService.getId());
+        util.disableControllerService(sourceServiceForDisable);
+        util.waitForControllerServiceState(groupA.getId(), CONTROLLER_SERVICE_STATE_DISABLED, List.of(sourceService.getId()));
+
         final ProcessGroupEntity groupAForStopVc = getNifiClient().getProcessGroupClient().getProcessGroup(groupA.getId());
         getNifiClient().getVersionsClient().stopVersionControl(groupAForStopVc);
         util.deleteAll(groupA.getId());
@@ -288,22 +319,34 @@ class ParameterContextPreservationIT extends NiFiSystemIT {
 
         // Verify version 1 descriptions
         final ParameterContextEntity importedContext = getNifiClient().getParamContextClient().getParamContext(importedContextId, false);
-        final String descriptionAfterV1 = getParameterDescription(importedContext, PARAMETER_NAME);
-        assertEquals(PARAMETER_DESCRIPTION_V1, descriptionAfterV1);
+        assertEquals(PARAMETER_DESCRIPTION_V1, getParameterDescription(importedContext, PARAMETER_NAME));
+        assertEquals(COUNT_PARAMETER_DESCRIPTION_V1, getParameterDescription(importedContext, COUNT_PARAMETER_NAME));
         assertEquals(CONTEXT_DESCRIPTION_V1, importedContext.getComponent().getDescription());
 
-        // Upgrade from version 1 to version 2
+        // Enable the imported Controller Service before the upgrade. With the description-only fix, the
+        // subsequent upgrade must succeed without requiring the service to be disabled first.
+        final ControllerServicesEntity servicesInImportedGroup = getNifiClient().getFlowClient().getControllerServices(importedGroup.getId());
+        assertEquals(1, servicesInImportedGroup.getControllerServices().size());
+        final ControllerServiceEntity importedService = servicesInImportedGroup.getControllerServices().iterator().next();
+        util.enableControllerService(importedService);
+        util.waitForControllerServicesEnabled(importedGroup.getId(), List.of(importedService.getId()));
+
+        // Upgrade from version 1 to version 2 while the referencing Controller Service is ENABLED
         util.changeFlowVersion(importedGroup.getId(), VERSION_2);
 
         // Verify descriptions were updated to version 2
         final ParameterContextEntity contextAfterUpgrade = getNifiClient().getParamContextClient().getParamContext(importedContextId, false);
-        final String descriptionAfterV2 = getParameterDescription(contextAfterUpgrade, PARAMETER_NAME);
-        assertEquals(PARAMETER_DESCRIPTION_V2, descriptionAfterV2);
+        assertEquals(PARAMETER_DESCRIPTION_V2, getParameterDescription(contextAfterUpgrade, PARAMETER_NAME));
+        assertEquals(COUNT_PARAMETER_DESCRIPTION_V2, getParameterDescription(contextAfterUpgrade, COUNT_PARAMETER_NAME));
         assertEquals(CONTEXT_DESCRIPTION_V2, contextAfterUpgrade.getComponent().getDescription());
 
-        // Verify the value was not changed
-        final String valueAfterUpgrade = getParameterValue(contextAfterUpgrade, PARAMETER_NAME);
-        assertEquals(PARAMETER_VALUE, valueAfterUpgrade);
+        // Verify the parameter values were not changed
+        assertEquals(PARAMETER_VALUE, getParameterValue(contextAfterUpgrade, PARAMETER_NAME));
+        assertEquals(COUNT_PARAMETER_VALUE, getParameterValue(contextAfterUpgrade, COUNT_PARAMETER_NAME));
+
+        // Verify the Controller Service remained ENABLED throughout the upgrade
+        final ControllerServiceEntity serviceAfterUpgrade = getNifiClient().getControllerServicesClient().getControllerService(importedService.getId());
+        assertEquals(CONTROLLER_SERVICE_STATE_ENABLED, serviceAfterUpgrade.getComponent().getState());
     }
 
     private String getParameterDescription(final ParameterContextEntity context, final String parameterName) {