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 054f49a16cc NIFI-16507 Add rebase handlers for name, concurrent tasks, 
scheduled state and scheduling changes (#11853)
054f49a16cc is described below

commit 054f49a16cc7521533bbca57c7f13614b5b73414
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Oct 26 20:11:14 2026 +0100

    NIFI-16507 Add rebase handlers for name, concurrent tasks, scheduled state 
and scheduling changes (#11853)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../diff/ConcurrentTasksChangedRebaseHandler.java  |  76 +++++++++++
 .../flow/diff/NameChangedRebaseHandler.java        |  56 ++++++++
 .../registry/flow/diff/RebaseConflictCode.java     |  20 +++
 .../registry/flow/diff/RebaseHandlerUtils.java     |  41 ++++++
 .../diff/ScheduledStateChangedRebaseHandler.java   |  77 +++++++++++
 .../flow/diff/SchedulingChangedRebaseHandler.java  |  99 ++++++++++++++
 .../registry/flow/diff/StandardRebaseEngine.java   |   5 +
 .../ConcurrentTasksChangedRebaseHandlerTest.java   | 147 +++++++++++++++++++++
 .../flow/diff/NameChangedRebaseHandlerTest.java    | 127 ++++++++++++++++++
 .../nifi/registry/flow/diff/RebaseEngineTest.java  |  19 ++-
 .../ScheduledStateChangedRebaseHandlerTest.java    | 145 ++++++++++++++++++++
 .../diff/SchedulingChangedRebaseHandlerTest.java   | 143 ++++++++++++++++++++
 12 files changed, 954 insertions(+), 1 deletion(-)

diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConcurrentTasksChangedRebaseHandler.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConcurrentTasksChangedRebaseHandler.java
new file mode 100644
index 00000000000..70034b38781
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConcurrentTasksChangedRebaseHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedPort;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.flow.VersionedRemoteGroupPort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class ConcurrentTasksChangedRebaseHandler implements RebaseHandler {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(ConcurrentTasksChangedRebaseHandler.class);
+
+    @Override
+    public DifferenceType getSupportedType() {
+        return DifferenceType.CONCURRENT_TASKS_CHANGED;
+    }
+
+    @Override
+    public RebaseAnalysis.ClassifiedDifference classify(final FlowDifference 
localDifference, final Set<FlowDifference> upstreamDifferences,
+                                                        final 
VersionedProcessGroup targetSnapshot) {
+        final VersionedComponent localComponent = 
localDifference.getComponentB();
+        if (getSetter(localComponent).isEmpty()) {
+            return 
RebaseAnalysis.ClassifiedDifference.unsupported(localDifference, 
RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE,
+                    "Concurrent tasks change is not supported for component %s 
of type %s".formatted(localComponent.getIdentifier(), 
localComponent.getComponentType()));
+        }
+        if 
(RebaseHandlerUtils.findConflictingUpstreamDifference(localDifference, 
upstreamDifferences).isPresent()) {
+            return 
RebaseAnalysis.ClassifiedDifference.conflicting(localDifference, 
RebaseConflictCode.SAME_CONCURRENT_TASKS,
+                    "Both local and upstream flows modified the concurrent 
tasks of component " + localComponent.getIdentifier());
+        }
+        return RebaseAnalysis.ClassifiedDifference.compatible(localDifference);
+    }
+
+    @Override
+    public void apply(final FlowDifference localDifference, final 
VersionedProcessGroup mergedFlow) {
+        final String componentIdentifier = 
localDifference.getComponentB().getIdentifier();
+        final VersionedComponent component = 
RebaseHandlerUtils.findComponentById(mergedFlow, componentIdentifier);
+        if (component == null) {
+            logger.warn("Unable to apply concurrent tasks change: component 
[{}] not found in merged flow", componentIdentifier);
+            return;
+        }
+        getSetter(component).ifPresent(setter -> setter.accept((Integer) 
localDifference.getValueB()));
+    }
+
+    private Optional<Consumer<Integer>> getSetter(final VersionedComponent 
component) {
+        return switch (component) {
+            case VersionedProcessor processor -> 
Optional.of(processor::setConcurrentlySchedulableTaskCount);
+            case VersionedPort port -> 
Optional.of(port::setConcurrentlySchedulableTaskCount);
+            case VersionedRemoteGroupPort remoteGroupPort -> 
Optional.of(remoteGroupPort::setConcurrentlySchedulableTaskCount);
+            case VersionedProcessGroup processGroup -> 
Optional.of(processGroup::setMaxConcurrentTasks);
+            default -> Optional.empty();
+        };
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/NameChangedRebaseHandler.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/NameChangedRebaseHandler.java
new file mode 100644
index 00000000000..23c530086e3
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/NameChangedRebaseHandler.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+
+public class NameChangedRebaseHandler implements RebaseHandler {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(NameChangedRebaseHandler.class);
+
+    @Override
+    public DifferenceType getSupportedType() {
+        return DifferenceType.NAME_CHANGED;
+    }
+
+    @Override
+    public RebaseAnalysis.ClassifiedDifference classify(final FlowDifference 
localDifference, final Set<FlowDifference> upstreamDifferences,
+                                                        final 
VersionedProcessGroup targetSnapshot) {
+        if 
(RebaseHandlerUtils.findConflictingUpstreamDifference(localDifference, 
upstreamDifferences).isPresent()) {
+            return 
RebaseAnalysis.ClassifiedDifference.conflicting(localDifference, 
RebaseConflictCode.SAME_COMPONENT_NAME,
+                    "Both local and upstream flows modified the name of 
component " + localDifference.getComponentB().getIdentifier());
+        }
+        return RebaseAnalysis.ClassifiedDifference.compatible(localDifference);
+    }
+
+    @Override
+    public void apply(final FlowDifference localDifference, final 
VersionedProcessGroup mergedFlow) {
+        final String componentIdentifier = 
localDifference.getComponentB().getIdentifier();
+        final VersionedComponent component = 
RebaseHandlerUtils.findComponentById(mergedFlow, componentIdentifier);
+        if (component == null) {
+            logger.warn("Unable to apply name change: component [{}] not found 
in merged flow", componentIdentifier);
+            return;
+        }
+        component.setName((String) localDifference.getValueB());
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
index ad651b6d017..60b165c433c 100644
--- 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseConflictCode.java
@@ -47,6 +47,26 @@ public enum RebaseConflictCode {
      */
     SAME_COMPONENT_COMMENTS,
 
+    /**
+     * Both the local and upstream flows modified the name of the same 
component to different values.
+     */
+    SAME_COMPONENT_NAME,
+
+    /**
+     * Both the local and upstream flows modified the concurrent tasks of the 
same component to different values.
+     */
+    SAME_CONCURRENT_TASKS,
+
+    /**
+     * Both the local and upstream flows modified the scheduled state of the 
same component to different values.
+     */
+    SAME_SCHEDULED_STATE,
+
+    /**
+     * Both the local and upstream flows modified the scheduling strategy or 
run schedule of the same processor to different values.
+     */
+    SAME_SCHEDULING,
+
     /**
      * The component targeted by the local change does not exist in the target 
version.
      */
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
index c1d2702d5c6..da4c82efa0b 100644
--- 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/RebaseHandlerUtils.java
@@ -26,8 +26,13 @@ import org.apache.nifi.flow.VersionedLabel;
 import org.apache.nifi.flow.VersionedPort;
 import org.apache.nifi.flow.VersionedProcessGroup;
 import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.flow.VersionedRemoteGroupPort;
 import org.apache.nifi.flow.VersionedRemoteProcessGroup;
 
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
 class RebaseHandlerUtils {
 
     private RebaseHandlerUtils() {
@@ -77,6 +82,14 @@ class RebaseHandlerUtils {
             if (identifier.equals(remoteProcessGroup.getIdentifier())) {
                 return remoteProcessGroup;
             }
+            final VersionedRemoteGroupPort remoteInputPort = 
findRemoteGroupPortById(remoteProcessGroup.getInputPorts(), identifier);
+            if (remoteInputPort != null) {
+                return remoteInputPort;
+            }
+            final VersionedRemoteGroupPort remoteOutputPort = 
findRemoteGroupPortById(remoteProcessGroup.getOutputPorts(), identifier);
+            if (remoteOutputPort != null) {
+                return remoteOutputPort;
+            }
         }
 
         for (final VersionedProcessGroup childGroup : 
group.getProcessGroups()) {
@@ -89,6 +102,34 @@ class RebaseHandlerUtils {
         return null;
     }
 
+    private static VersionedRemoteGroupPort findRemoteGroupPortById(final 
Set<VersionedRemoteGroupPort> remotePorts, final String identifier) {
+        if (remotePorts == null) {
+            return null;
+        }
+        for (final VersionedRemoteGroupPort remotePort : remotePorts) {
+            if (identifier.equals(remotePort.getIdentifier())) {
+                return remotePort;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Finds an upstream difference of the same type on the same component 
whose new value differs from the local new value.
+     */
+    static Optional<FlowDifference> findConflictingUpstreamDifference(final 
FlowDifference localDifference, final Set<FlowDifference> upstreamDifferences) {
+        final String componentIdentifier = 
localDifference.getComponentB().getIdentifier();
+        for (final FlowDifference upstreamDifference : upstreamDifferences) {
+            if (upstreamDifference.getDifferenceType() == 
localDifference.getDifferenceType()
+                    && upstreamDifference.getComponentA() != null
+                    && 
componentIdentifier.equals(upstreamDifference.getComponentA().getIdentifier())
+                    && !Objects.equals(localDifference.getValueB(), 
upstreamDifference.getValueB())) {
+                return Optional.of(upstreamDifference);
+            }
+        }
+        return Optional.empty();
+    }
+
     static VersionedProcessGroup findProcessGroupById(final 
VersionedProcessGroup group, final String identifier) {
         if (identifier.equals(group.getIdentifier()) || 
identifier.equals(group.getInstanceIdentifier())) {
             return group;
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ScheduledStateChangedRebaseHandler.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ScheduledStateChangedRebaseHandler.java
new file mode 100644
index 00000000000..1a64ee56419
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ScheduledStateChangedRebaseHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.ScheduledState;
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedPort;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.flow.VersionedRemoteGroupPort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class ScheduledStateChangedRebaseHandler implements RebaseHandler {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(ScheduledStateChangedRebaseHandler.class);
+
+    @Override
+    public DifferenceType getSupportedType() {
+        return DifferenceType.SCHEDULED_STATE_CHANGED;
+    }
+
+    @Override
+    public RebaseAnalysis.ClassifiedDifference classify(final FlowDifference 
localDifference, final Set<FlowDifference> upstreamDifferences,
+                                                        final 
VersionedProcessGroup targetSnapshot) {
+        final VersionedComponent localComponent = 
localDifference.getComponentB();
+        if (getSetter(localComponent).isEmpty()) {
+            return 
RebaseAnalysis.ClassifiedDifference.unsupported(localDifference, 
RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE,
+                    "Scheduled state change is not supported for component %s 
of type %s".formatted(localComponent.getIdentifier(), 
localComponent.getComponentType()));
+        }
+        if 
(RebaseHandlerUtils.findConflictingUpstreamDifference(localDifference, 
upstreamDifferences).isPresent()) {
+            return 
RebaseAnalysis.ClassifiedDifference.conflicting(localDifference, 
RebaseConflictCode.SAME_SCHEDULED_STATE,
+                    "Both local and upstream flows modified the scheduled 
state of component " + localComponent.getIdentifier());
+        }
+        return RebaseAnalysis.ClassifiedDifference.compatible(localDifference);
+    }
+
+    @Override
+    public void apply(final FlowDifference localDifference, final 
VersionedProcessGroup mergedFlow) {
+        final String componentIdentifier = 
localDifference.getComponentB().getIdentifier();
+        final VersionedComponent component = 
RebaseHandlerUtils.findComponentById(mergedFlow, componentIdentifier);
+        if (component == null) {
+            logger.warn("Unable to apply scheduled state change: component 
[{}] not found in merged flow", componentIdentifier);
+            return;
+        }
+        getSetter(component).ifPresent(setter -> 
setter.accept((ScheduledState) localDifference.getValueB()));
+    }
+
+    private Optional<Consumer<ScheduledState>> getSetter(final 
VersionedComponent component) {
+        return switch (component) {
+            case VersionedProcessor processor -> 
Optional.of(processor::setScheduledState);
+            case VersionedPort port -> Optional.of(port::setScheduledState);
+            case VersionedRemoteGroupPort remoteGroupPort -> 
Optional.of(remoteGroupPort::setScheduledState);
+            case VersionedProcessGroup processGroup -> 
Optional.of(processGroup::setScheduledState);
+            default -> Optional.empty();
+        };
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/SchedulingChangedRebaseHandler.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/SchedulingChangedRebaseHandler.java
new file mode 100644
index 00000000000..e41e56dc226
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/SchedulingChangedRebaseHandler.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Handles scheduling strategy and run schedule changes on processors. The two 
fields are evaluated together because a run
+ * schedule is only meaningful for its scheduling strategy: an upstream change 
to either field conflicts with the local change
+ * unless it results in the same value as the local processor.
+ */
+public class SchedulingChangedRebaseHandler implements RebaseHandler {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(SchedulingChangedRebaseHandler.class);
+
+    private final DifferenceType supportedType;
+
+    public SchedulingChangedRebaseHandler(final DifferenceType supportedType) {
+        if (supportedType != DifferenceType.SCHEDULING_STRATEGY_CHANGED && 
supportedType != DifferenceType.RUN_SCHEDULE_CHANGED) {
+            throw new IllegalArgumentException("Unsupported difference type " 
+ supportedType);
+        }
+        this.supportedType = supportedType;
+    }
+
+    @Override
+    public DifferenceType getSupportedType() {
+        return supportedType;
+    }
+
+    @Override
+    public RebaseAnalysis.ClassifiedDifference classify(final FlowDifference 
localDifference, final Set<FlowDifference> upstreamDifferences,
+                                                        final 
VersionedProcessGroup targetSnapshot) {
+        final VersionedComponent localComponent = 
localDifference.getComponentB();
+        final String componentIdentifier = localComponent.getIdentifier();
+        if (!(localComponent instanceof VersionedProcessor localProcessor)) {
+            return 
RebaseAnalysis.ClassifiedDifference.unsupported(localDifference, 
RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE,
+                    "Scheduling change is not supported for component %s of 
type %s".formatted(componentIdentifier, localComponent.getComponentType()));
+        }
+
+        for (final FlowDifference upstreamDifference : upstreamDifferences) {
+            if (upstreamDifference.getComponentA() == null || 
!componentIdentifier.equals(upstreamDifference.getComponentA().getIdentifier()))
 {
+                continue;
+            }
+
+            final String localValue;
+            if (upstreamDifference.getDifferenceType() == 
DifferenceType.SCHEDULING_STRATEGY_CHANGED) {
+                localValue = localProcessor.getSchedulingStrategy();
+            } else if (upstreamDifference.getDifferenceType() == 
DifferenceType.RUN_SCHEDULE_CHANGED) {
+                localValue = localProcessor.getSchedulingPeriod();
+            } else {
+                continue;
+            }
+
+            if (!Objects.equals(localValue, upstreamDifference.getValueB())) {
+                return 
RebaseAnalysis.ClassifiedDifference.conflicting(localDifference, 
RebaseConflictCode.SAME_SCHEDULING,
+                        "Both local and upstream flows modified the scheduling 
of processor " + componentIdentifier);
+            }
+        }
+        return RebaseAnalysis.ClassifiedDifference.compatible(localDifference);
+    }
+
+    @Override
+    public void apply(final FlowDifference localDifference, final 
VersionedProcessGroup mergedFlow) {
+        final String componentIdentifier = 
localDifference.getComponentB().getIdentifier();
+        if (!(RebaseHandlerUtils.findComponentById(mergedFlow, 
componentIdentifier) instanceof VersionedProcessor processor)) {
+            logger.warn("Unable to apply scheduling change: processor [{}] not 
found in merged flow", componentIdentifier);
+            return;
+        }
+
+        final String value = (String) localDifference.getValueB();
+        if (supportedType == DifferenceType.SCHEDULING_STRATEGY_CHANGED) {
+            processor.setSchedulingStrategy(value);
+        } else {
+            processor.setSchedulingPeriod(value);
+        }
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
index 57153781cdf..9ebbcb6ecb6 100644
--- 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardRebaseEngine.java
@@ -50,6 +50,11 @@ public class StandardRebaseEngine implements RebaseEngine {
         registerHandler(new PropertyChangedRebaseHandler());
         registerHandler(new PropertyAddedRebaseHandler());
         registerHandler(new CommentsChangedRebaseHandler());
+        registerHandler(new NameChangedRebaseHandler());
+        registerHandler(new ConcurrentTasksChangedRebaseHandler());
+        registerHandler(new ScheduledStateChangedRebaseHandler());
+        registerHandler(new 
SchedulingChangedRebaseHandler(DifferenceType.SCHEDULING_STRATEGY_CHANGED));
+        registerHandler(new 
SchedulingChangedRebaseHandler(DifferenceType.RUN_SCHEDULE_CHANGED));
     }
 
     public StandardRebaseEngine(final Map<DifferenceType, RebaseHandler> 
handlerRegistry) {
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ConcurrentTasksChangedRebaseHandlerTest.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ConcurrentTasksChangedRebaseHandlerTest.java
new file mode 100644
index 00000000000..6969d4fd007
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ConcurrentTasksChangedRebaseHandlerTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.flow.VersionedRemoteGroupPort;
+import org.apache.nifi.flow.VersionedRemoteProcessGroup;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ConcurrentTasksChangedRebaseHandlerTest {
+
+    private static final String ROOT = "root";
+    private static final String PROC_A = "proc-a";
+    private static final String PROC_B = "proc-b";
+    private static final String SERVICE_A = "service-a";
+    private static final String REMOTE_GROUP = "remote-group";
+    private static final String REMOTE_INPUT_PORT = "remote-input-port";
+    private static final int ORIGINAL_TASKS = 1;
+    private static final int LOCAL_TASKS = 6;
+    private static final int UPSTREAM_TASKS = 3;
+
+    private ConcurrentTasksChangedRebaseHandler handler;
+
+    @BeforeEach
+    void setup() {
+        handler = new ConcurrentTasksChangedRebaseHandler();
+    }
+
+    @Test
+    void testNoUpstreamChange() {
+        final FlowDifference localDifference = 
createDifference(createProcessor(PROC_A), LOCAL_TASKS);
+        final FlowDifference upstreamOtherComponent = 
createDifference(createProcessor(PROC_B), UPSTREAM_TASKS);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamOtherComponent), createRoot());
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testUpstreamChangedToSameValue() {
+        final FlowDifference localDifference = 
createDifference(createProcessor(PROC_A), LOCAL_TASKS);
+        final FlowDifference upstreamDifference = 
createDifference(createProcessor(PROC_A), LOCAL_TASKS);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamDifference), createRoot());
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testUpstreamChangedToDifferentValue() {
+        final FlowDifference localDifference = 
createDifference(createProcessor(PROC_A), LOCAL_TASKS);
+        final FlowDifference upstreamDifference = 
createDifference(createProcessor(PROC_A), UPSTREAM_TASKS);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamDifference), createRoot());
+
+        assertEquals(RebaseClassification.CONFLICTING, 
result.getClassification());
+        assertEquals(RebaseConflictCode.SAME_CONCURRENT_TASKS, 
result.getConflictCode());
+    }
+
+    @Test
+    void testUnsupportedComponentType() {
+        final VersionedControllerService service = new 
VersionedControllerService();
+        service.setIdentifier(SERVICE_A);
+        final FlowDifference localDifference = createDifference(service, 
LOCAL_TASKS);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), createRoot());
+
+        assertEquals(RebaseClassification.UNSUPPORTED, 
result.getClassification());
+        assertEquals(RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE, 
result.getConflictCode());
+    }
+
+    @Test
+    void testApplyProcessor() {
+        final VersionedProcessGroup mergedFlow = createRoot();
+        final VersionedProcessor processor = createProcessor(PROC_A);
+        processor.setConcurrentlySchedulableTaskCount(ORIGINAL_TASKS);
+        mergedFlow.getProcessors().add(processor);
+
+        handler.apply(createDifference(createProcessor(PROC_A), LOCAL_TASKS), 
mergedFlow);
+
+        assertEquals(LOCAL_TASKS, 
processor.getConcurrentlySchedulableTaskCount());
+    }
+
+    @Test
+    void testApplyRemoteGroupPort() {
+        final VersionedProcessGroup mergedFlow = createRoot();
+        final VersionedRemoteGroupPort remotePort = createRemoteGroupPort();
+        remotePort.setConcurrentlySchedulableTaskCount(ORIGINAL_TASKS);
+        final VersionedRemoteProcessGroup remoteProcessGroup = new 
VersionedRemoteProcessGroup();
+        remoteProcessGroup.setIdentifier(REMOTE_GROUP);
+        remoteProcessGroup.setInputPorts(Set.of(remotePort));
+        mergedFlow.getRemoteProcessGroups().add(remoteProcessGroup);
+
+        final FlowDifference localDifference = 
createDifference(createRemoteGroupPort(), LOCAL_TASKS);
+        assertEquals(RebaseClassification.COMPATIBLE, 
handler.classify(localDifference, Collections.emptySet(), 
mergedFlow).getClassification());
+        handler.apply(localDifference, mergedFlow);
+
+        assertEquals(LOCAL_TASKS, 
remotePort.getConcurrentlySchedulableTaskCount());
+    }
+
+    private FlowDifference createDifference(final VersionedComponent 
component, final int newValue) {
+        return new 
StandardFlowDifference(DifferenceType.CONCURRENT_TASKS_CHANGED, component, 
component,
+                ORIGINAL_TASKS, newValue, "Concurrent tasks changed");
+    }
+
+    private VersionedProcessor createProcessor(final String identifier) {
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(identifier);
+        return processor;
+    }
+
+    private VersionedRemoteGroupPort createRemoteGroupPort() {
+        final VersionedRemoteGroupPort remotePort = new 
VersionedRemoteGroupPort();
+        remotePort.setIdentifier(REMOTE_INPUT_PORT);
+        return remotePort;
+    }
+
+    private VersionedProcessGroup createRoot() {
+        final VersionedProcessGroup root = new VersionedProcessGroup();
+        root.setIdentifier(ROOT);
+        return root;
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/NameChangedRebaseHandlerTest.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/NameChangedRebaseHandlerTest.java
new file mode 100644
index 00000000000..8beae1c5f1e
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/NameChangedRebaseHandlerTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class NameChangedRebaseHandlerTest {
+
+    private static final String ROOT = "root";
+    private static final String CHILD_GROUP = "child-group";
+    private static final String PROC_A = "proc-a";
+    private static final String PROC_B = "proc-b";
+    private static final String ORIGINAL_NAME = "Original";
+    private static final String LOCAL_NAME = "Local";
+    private static final String UPSTREAM_NAME = "Upstream";
+
+    private NameChangedRebaseHandler handler;
+
+    @BeforeEach
+    void setup() {
+        handler = new NameChangedRebaseHandler();
+    }
+
+    @Test
+    void testNoUpstreamChange() {
+        final FlowDifference localDifference = createNameDifference(PROC_A, 
LOCAL_NAME);
+        final FlowDifference upstreamOtherComponent = 
createNameDifference(PROC_B, UPSTREAM_NAME);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamOtherComponent), createRoot());
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testUpstreamChangedToSameName() {
+        final FlowDifference localDifference = createNameDifference(PROC_A, 
LOCAL_NAME);
+        final FlowDifference upstreamDifference = createNameDifference(PROC_A, 
LOCAL_NAME);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamDifference), createRoot());
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testUpstreamChangedToDifferentName() {
+        final FlowDifference localDifference = createNameDifference(PROC_A, 
LOCAL_NAME);
+        final FlowDifference upstreamDifference = createNameDifference(PROC_A, 
UPSTREAM_NAME);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamDifference), createRoot());
+
+        assertEquals(RebaseClassification.CONFLICTING, 
result.getClassification());
+        assertEquals(RebaseConflictCode.SAME_COMPONENT_NAME, 
result.getConflictCode());
+    }
+
+    @Test
+    void testApplyProcessorName() {
+        final VersionedProcessGroup mergedFlow = createRoot();
+        final VersionedProcessor processor = createProcessor(PROC_A, 
ORIGINAL_NAME);
+        mergedFlow.getProcessors().add(processor);
+
+        handler.apply(createNameDifference(PROC_A, LOCAL_NAME), mergedFlow);
+
+        assertEquals(LOCAL_NAME, processor.getName());
+    }
+
+    @Test
+    void testApplyProcessGroupName() {
+        final VersionedProcessGroup mergedFlow = createRoot();
+        final VersionedProcessGroup childGroup = createGroup(CHILD_GROUP, 
ORIGINAL_NAME);
+        mergedFlow.getProcessGroups().add(childGroup);
+
+        final VersionedProcessGroup localGroup = createGroup(CHILD_GROUP, 
LOCAL_NAME);
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.NAME_CHANGED, createGroup(CHILD_GROUP, 
ORIGINAL_NAME), localGroup,
+                ORIGINAL_NAME, LOCAL_NAME, "Name changed");
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
handler.classify(localDifference, Collections.emptySet(), 
mergedFlow).getClassification());
+        handler.apply(localDifference, mergedFlow);
+
+        assertEquals(LOCAL_NAME, childGroup.getName());
+    }
+
+    private FlowDifference createNameDifference(final String identifier, final 
String newName) {
+        return new StandardFlowDifference(DifferenceType.NAME_CHANGED, 
createProcessor(identifier, ORIGINAL_NAME), createProcessor(identifier, 
newName),
+                ORIGINAL_NAME, newName, "Name changed");
+    }
+
+    private VersionedProcessor createProcessor(final String identifier, final 
String name) {
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(identifier);
+        processor.setName(name);
+        return processor;
+    }
+
+    private VersionedProcessGroup createGroup(final String identifier, final 
String name) {
+        final VersionedProcessGroup group = new VersionedProcessGroup();
+        group.setIdentifier(identifier);
+        group.setName(name);
+        return group;
+    }
+
+    private VersionedProcessGroup createRoot() {
+        return createGroup(ROOT, ROOT);
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
index 66b9287b3a6..5bdd3e93a37 100644
--- 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/RebaseEngineTest.java
@@ -415,10 +415,22 @@ class RebaseEngineTest {
                 "oldVal", "newVal", "Property propX changed"));
         localDifferences.add(new 
StandardFlowDifference(DifferenceType.COMMENTS_CHANGED, processorA, 
localProcessorA,
                 "old comments", "new comments", "Comments changed"));
+        localDifferences.add(new 
StandardFlowDifference(DifferenceType.NAME_CHANGED, processorA, localProcessorA,
+                "ProcessorA", "Renamed ProcessorA", "Name changed"));
+        localDifferences.add(new 
StandardFlowDifference(DifferenceType.CONCURRENT_TASKS_CHANGED, processorA, 
localProcessorA,
+                1, 6, "Concurrent tasks changed"));
+        localDifferences.add(new 
StandardFlowDifference(DifferenceType.SCHEDULED_STATE_CHANGED, processorA, 
localProcessorA,
+                ScheduledState.DISABLED, ScheduledState.ENABLED, "Scheduled 
state changed"));
+        localDifferences.add(new 
StandardFlowDifference(DifferenceType.SCHEDULING_STRATEGY_CHANGED, processorA, 
localProcessorA,
+                "TIMER_DRIVEN", "CRON_DRIVEN", "Scheduling strategy changed"));
+        localDifferences.add(new 
StandardFlowDifference(DifferenceType.RUN_SCHEDULE_CHANGED, processorA, 
localProcessorA,
+                "1 min", "0 0 1 * * ?", "Run schedule changed"));
 
         final VersionedProcessor targetProcessor = 
createProcessorWithProperty("proc-a", "ProcessorA", "propX", "oldVal");
         targetProcessor.setPosition(new Position(10.0, 20.0));
         targetProcessor.setComments("old comments");
+        targetProcessor.setConcurrentlySchedulableTaskCount(1);
+        targetProcessor.setScheduledState(ScheduledState.DISABLED);
 
         final VersionedProcessGroup targetSnapshot = new 
VersionedProcessGroup();
         targetSnapshot.setIdentifier("root");
@@ -427,7 +439,7 @@ class RebaseEngineTest {
         final RebaseAnalysis analysis = engine.analyze(localDifferences, 
Collections.emptySet(), targetSnapshot);
 
         assertTrue(analysis.isRebaseAllowed());
-        assertEquals(3, analysis.getClassifiedLocalChanges().size());
+        assertEquals(8, analysis.getClassifiedLocalChanges().size());
         for (final RebaseAnalysis.ClassifiedDifference classified : 
analysis.getClassifiedLocalChanges()) {
             assertEquals(RebaseClassification.COMPATIBLE, 
classified.getClassification());
         }
@@ -440,6 +452,11 @@ class RebaseEngineTest {
         assertEquals(60.0, mergedProc.getPosition().getY());
         assertEquals("newVal", mergedProc.getProperties().get("propX"));
         assertEquals("new comments", mergedProc.getComments());
+        assertEquals("Renamed ProcessorA", mergedProc.getName());
+        assertEquals(6, mergedProc.getConcurrentlySchedulableTaskCount());
+        assertEquals(ScheduledState.ENABLED, mergedProc.getScheduledState());
+        assertEquals("CRON_DRIVEN", mergedProc.getSchedulingStrategy());
+        assertEquals("0 0 1 * * ?", mergedProc.getSchedulingPeriod());
     }
 
     @Test
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ScheduledStateChangedRebaseHandlerTest.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ScheduledStateChangedRebaseHandlerTest.java
new file mode 100644
index 00000000000..3679a50c9ec
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/ScheduledStateChangedRebaseHandlerTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.ScheduledState;
+import org.apache.nifi.flow.VersionedComponent;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.flow.VersionedRemoteGroupPort;
+import org.apache.nifi.flow.VersionedRemoteProcessGroup;
+import org.apache.nifi.flow.VersionedReportingTask;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ScheduledStateChangedRebaseHandlerTest {
+
+    private static final String ROOT = "root";
+    private static final String PROC_A = "proc-a";
+    private static final String PROC_B = "proc-b";
+    private static final String TASK_A = "task-a";
+    private static final String REMOTE_GROUP = "remote-group";
+    private static final String REMOTE_OUTPUT_PORT = "remote-output-port";
+
+    private ScheduledStateChangedRebaseHandler handler;
+
+    @BeforeEach
+    void setup() {
+        handler = new ScheduledStateChangedRebaseHandler();
+    }
+
+    @Test
+    void testNoUpstreamChange() {
+        final FlowDifference localDifference = 
createDifference(createProcessor(PROC_A), ScheduledState.ENABLED);
+        final FlowDifference upstreamOtherComponent = 
createDifference(createProcessor(PROC_B), ScheduledState.RUNNING);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamOtherComponent), createRoot());
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testUpstreamChangedToSameState() {
+        final FlowDifference localDifference = 
createDifference(createProcessor(PROC_A), ScheduledState.ENABLED);
+        final FlowDifference upstreamDifference = 
createDifference(createProcessor(PROC_A), ScheduledState.ENABLED);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamDifference), createRoot());
+
+        assertEquals(RebaseClassification.COMPATIBLE, 
result.getClassification());
+    }
+
+    @Test
+    void testUpstreamChangedToDifferentState() {
+        final FlowDifference localDifference = 
createDifference(createProcessor(PROC_A), ScheduledState.ENABLED);
+        final FlowDifference upstreamDifference = 
createDifference(createProcessor(PROC_A), ScheduledState.RUNNING);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Set.of(upstreamDifference), createRoot());
+
+        assertEquals(RebaseClassification.CONFLICTING, 
result.getClassification());
+        assertEquals(RebaseConflictCode.SAME_SCHEDULED_STATE, 
result.getConflictCode());
+    }
+
+    @Test
+    void testUnsupportedComponentType() {
+        final VersionedReportingTask reportingTask = new 
VersionedReportingTask();
+        reportingTask.setIdentifier(TASK_A);
+        final FlowDifference localDifference = createDifference(reportingTask, 
ScheduledState.ENABLED);
+
+        final RebaseAnalysis.ClassifiedDifference result = 
handler.classify(localDifference, Collections.emptySet(), createRoot());
+
+        assertEquals(RebaseClassification.UNSUPPORTED, 
result.getClassification());
+        assertEquals(RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE, 
result.getConflictCode());
+    }
+
+    @Test
+    void testApplyProcessor() {
+        final VersionedProcessGroup mergedFlow = createRoot();
+        final VersionedProcessor processor = createProcessor(PROC_A);
+        processor.setScheduledState(ScheduledState.DISABLED);
+        mergedFlow.getProcessors().add(processor);
+
+        handler.apply(createDifference(createProcessor(PROC_A), 
ScheduledState.ENABLED), mergedFlow);
+
+        assertEquals(ScheduledState.ENABLED, processor.getScheduledState());
+    }
+
+    @Test
+    void testApplyRemoteGroupPort() {
+        final VersionedProcessGroup mergedFlow = createRoot();
+        final VersionedRemoteGroupPort remotePort = createRemoteGroupPort();
+        remotePort.setScheduledState(ScheduledState.DISABLED);
+        final VersionedRemoteProcessGroup remoteProcessGroup = new 
VersionedRemoteProcessGroup();
+        remoteProcessGroup.setIdentifier(REMOTE_GROUP);
+        remoteProcessGroup.setOutputPorts(Set.of(remotePort));
+        mergedFlow.getRemoteProcessGroups().add(remoteProcessGroup);
+
+        final FlowDifference localDifference = 
createDifference(createRemoteGroupPort(), ScheduledState.ENABLED);
+        assertEquals(RebaseClassification.COMPATIBLE, 
handler.classify(localDifference, Collections.emptySet(), 
mergedFlow).getClassification());
+        handler.apply(localDifference, mergedFlow);
+
+        assertEquals(ScheduledState.ENABLED, remotePort.getScheduledState());
+    }
+
+    private FlowDifference createDifference(final VersionedComponent 
component, final ScheduledState newState) {
+        return new 
StandardFlowDifference(DifferenceType.SCHEDULED_STATE_CHANGED, component, 
component,
+                ScheduledState.DISABLED, newState, "Scheduled state changed");
+    }
+
+    private VersionedProcessor createProcessor(final String identifier) {
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(identifier);
+        return processor;
+    }
+
+    private VersionedRemoteGroupPort createRemoteGroupPort() {
+        final VersionedRemoteGroupPort remotePort = new 
VersionedRemoteGroupPort();
+        remotePort.setIdentifier(REMOTE_OUTPUT_PORT);
+        return remotePort;
+    }
+
+    private VersionedProcessGroup createRoot() {
+        final VersionedProcessGroup root = new VersionedProcessGroup();
+        root.setIdentifier(ROOT);
+        return root;
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/SchedulingChangedRebaseHandlerTest.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/SchedulingChangedRebaseHandlerTest.java
new file mode 100644
index 00000000000..80ec4c6eb09
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/SchedulingChangedRebaseHandlerTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.registry.flow.diff;
+
+import org.apache.nifi.flow.VersionedPort;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SchedulingChangedRebaseHandlerTest {
+
+    private static final String ROOT = "root";
+    private static final String PROC_A = "proc-a";
+    private static final String PROC_B = "proc-b";
+    private static final String PORT_A = "port-a";
+    private static final String TIMER_DRIVEN = "TIMER_DRIVEN";
+    private static final String CRON_DRIVEN = "CRON_DRIVEN";
+    private static final String TIMER_PERIOD = "1 min";
+    private static final String UPSTREAM_TIMER_PERIOD = "5 min";
+    private static final String CRON_EXPRESSION = "0 0 1 * * ?";
+
+    private SchedulingChangedRebaseHandler strategyHandler;
+    private SchedulingChangedRebaseHandler scheduleHandler;
+
+    @BeforeEach
+    void setup() {
+        strategyHandler = new 
SchedulingChangedRebaseHandler(DifferenceType.SCHEDULING_STRATEGY_CHANGED);
+        scheduleHandler = new 
SchedulingChangedRebaseHandler(DifferenceType.RUN_SCHEDULE_CHANGED);
+    }
+
+    @Test
+    void testNoUpstreamChange() {
+        final VersionedProcessor original = createProcessor(PROC_A, 
TIMER_DRIVEN, TIMER_PERIOD);
+        final VersionedProcessor local = createProcessor(PROC_A, CRON_DRIVEN, 
CRON_EXPRESSION);
+        final VersionedProcessor otherOriginal = createProcessor(PROC_B, 
TIMER_DRIVEN, TIMER_PERIOD);
+        final VersionedProcessor otherUpstream = createProcessor(PROC_B, 
TIMER_DRIVEN, UPSTREAM_TIMER_PERIOD);
+        final Set<FlowDifference> upstreamDifferences = 
Set.of(scheduleDifference(otherOriginal, otherUpstream));
+
+        assertEquals(RebaseClassification.COMPATIBLE,
+                strategyHandler.classify(strategyDifference(original, local), 
upstreamDifferences, createRoot()).getClassification());
+        assertEquals(RebaseClassification.COMPATIBLE,
+                scheduleHandler.classify(scheduleDifference(original, local), 
upstreamDifferences, createRoot()).getClassification());
+    }
+
+    @Test
+    void testUpstreamChangedRunScheduleOnlyConflictsWithLocalStrategyChange() {
+        final VersionedProcessor original = createProcessor(PROC_A, 
TIMER_DRIVEN, TIMER_PERIOD);
+        final VersionedProcessor local = createProcessor(PROC_A, CRON_DRIVEN, 
CRON_EXPRESSION);
+        final VersionedProcessor upstream = createProcessor(PROC_A, 
TIMER_DRIVEN, UPSTREAM_TIMER_PERIOD);
+        final Set<FlowDifference> upstreamDifferences = 
Set.of(scheduleDifference(original, upstream));
+
+        final RebaseAnalysis.ClassifiedDifference result = 
strategyHandler.classify(strategyDifference(original, local), 
upstreamDifferences, createRoot());
+
+        assertEquals(RebaseClassification.CONFLICTING, 
result.getClassification());
+        assertEquals(RebaseConflictCode.SAME_SCHEDULING, 
result.getConflictCode());
+    }
+
+    @Test
+    void testUpstreamChangedToSameScheduling() {
+        final VersionedProcessor original = createProcessor(PROC_A, 
TIMER_DRIVEN, TIMER_PERIOD);
+        final VersionedProcessor local = createProcessor(PROC_A, CRON_DRIVEN, 
CRON_EXPRESSION);
+        final VersionedProcessor upstream = createProcessor(PROC_A, 
CRON_DRIVEN, CRON_EXPRESSION);
+        final Set<FlowDifference> upstreamDifferences = 
Set.of(strategyDifference(original, upstream), scheduleDifference(original, 
upstream));
+
+        assertEquals(RebaseClassification.COMPATIBLE,
+                strategyHandler.classify(strategyDifference(original, local), 
upstreamDifferences, createRoot()).getClassification());
+        assertEquals(RebaseClassification.COMPATIBLE,
+                scheduleHandler.classify(scheduleDifference(original, local), 
upstreamDifferences, createRoot()).getClassification());
+    }
+
+    @Test
+    void testUnsupportedComponentType() {
+        final VersionedPort port = new VersionedPort();
+        port.setIdentifier(PORT_A);
+        final FlowDifference localDifference = new 
StandardFlowDifference(DifferenceType.RUN_SCHEDULE_CHANGED, port, port,
+                TIMER_PERIOD, UPSTREAM_TIMER_PERIOD, "Run schedule changed");
+
+        final RebaseAnalysis.ClassifiedDifference result = 
scheduleHandler.classify(localDifference, Collections.emptySet(), createRoot());
+
+        assertEquals(RebaseClassification.UNSUPPORTED, 
result.getClassification());
+        assertEquals(RebaseConflictCode.UNSUPPORTED_COMPONENT_TYPE, 
result.getConflictCode());
+    }
+
+    @Test
+    void testApplyTimerDrivenToCronDriven() {
+        final VersionedProcessor original = createProcessor(PROC_A, 
TIMER_DRIVEN, TIMER_PERIOD);
+        final VersionedProcessor local = createProcessor(PROC_A, CRON_DRIVEN, 
CRON_EXPRESSION);
+        final VersionedProcessor merged = createProcessor(PROC_A, 
TIMER_DRIVEN, TIMER_PERIOD);
+        final VersionedProcessGroup mergedFlow = createRoot();
+        mergedFlow.getProcessors().add(merged);
+
+        strategyHandler.apply(strategyDifference(original, local), mergedFlow);
+        scheduleHandler.apply(scheduleDifference(original, local), mergedFlow);
+
+        assertEquals(CRON_DRIVEN, merged.getSchedulingStrategy());
+        assertEquals(CRON_EXPRESSION, merged.getSchedulingPeriod());
+    }
+
+    private FlowDifference strategyDifference(final VersionedProcessor before, 
final VersionedProcessor after) {
+        return new 
StandardFlowDifference(DifferenceType.SCHEDULING_STRATEGY_CHANGED, before, 
after,
+                before.getSchedulingStrategy(), after.getSchedulingStrategy(), 
"Scheduling strategy changed");
+    }
+
+    private FlowDifference scheduleDifference(final VersionedProcessor before, 
final VersionedProcessor after) {
+        return new StandardFlowDifference(DifferenceType.RUN_SCHEDULE_CHANGED, 
before, after,
+                before.getSchedulingPeriod(), after.getSchedulingPeriod(), 
"Run schedule changed");
+    }
+
+    private VersionedProcessor createProcessor(final String identifier, final 
String strategy, final String period) {
+        final VersionedProcessor processor = new VersionedProcessor();
+        processor.setIdentifier(identifier);
+        processor.setSchedulingStrategy(strategy);
+        processor.setSchedulingPeriod(period);
+        return processor;
+    }
+
+    private VersionedProcessGroup createRoot() {
+        final VersionedProcessGroup root = new VersionedProcessGroup();
+        root.setIdentifier(ROOT);
+        return root;
+    }
+}

Reply via email to