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 <[email protected]>
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 <[email protected]>
---
 .../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) {

Reply via email to