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 2fe3fa79b5f NIFI-16086 Added Flow Registry Client Synchronization 
Interval property (#11404)
2fe3fa79b5f is described below

commit 2fe3fa79b5f6e30f831dd49a43343a3bbe12b485
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Jul 13 22:05:49 2026 +0200

    NIFI-16086 Added Flow Registry Client Synchronization Interval property 
(#11404)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../src/main/asciidoc/administration-guide.adoc    |   2 +-
 .../flow/git/AbstractGitFlowRegistryClient.java    |   1 +
 .../flow/NifiRegistryFlowRegistryClient.java       |   3 +-
 .../org/apache/nifi/controller/FlowController.java |  23 ++--
 .../RegistryFlowSynchronizationTask.java           | 119 +++++++++++++++++++++
 .../RegistryFlowSynchronizationTaskTest.java       |  69 ++++++++++++
 .../registry/FileSystemFlowRegistryClient.java     |   3 +-
 7 files changed, 201 insertions(+), 19 deletions(-)

diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc 
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index a2af47b3055..c3cec4e8024 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -2891,7 +2891,7 @@ This cleanup mechanism takes into account only 
automatically created archived _f
 |`nifi.flow.configuration.archive.max.count`*|The number of archive files 
allowed. NiFi will delete the oldest archive files so that only N latest 
archives can be kept, if this property is specified.
 |`nifi.flowcontroller.autoResumeState`|Indicates whether -upon restart- the 
components on the NiFi graph should return to their last state. When running in 
cluster, all nodes should have the same value. The default value is `true`.
 |`nifi.flowcontroller.graceful.shutdown.period`|Indicates the shutdown period. 
The default value is `10 secs`.
-|`nifi.flowcontroller.registry.sync.interval`|Specifies the recurring interval 
at which NiFi synchronizes the flow configuration with Flow Registry Clients. 
The default value is `30 min`.
+|`nifi.flowcontroller.registry.sync.interval`|Specifies the default recurring 
interval at which NiFi synchronizes the flow configuration with Flow Registry 
Clients. The default value is `30 min`. This value is used for any Flow 
Registry Client that does not configure its own `Synchronization Interval` 
property; a Flow Registry Client that sets that property is synchronized at its 
own interval instead.
 |`nifi.flowservice.writedelay.interval`|When many changes are made to the 
_flow.json_, this property specifies how long to wait before writing out the 
changes, so as to batch the changes into a single write. The default value is 
`500 ms`.
 |`nifi.administrative.yield.duration`|If a component allows an unexpected 
exception to escape, it is considered a bug. As a result, the framework will 
pause (or administratively yield) the component for this amount of time. This 
is done so that the component does not use up massive amounts of system 
resources, since it is known to have problems in the existing state. The 
default value is `30 secs`.
 |`nifi.bored.yield.duration`|When a component has no work to do (i.e., is 
"bored"), this is the amount of time it will wait before checking to see if it 
has new data to work on. This way, it does not use up CPU resources by checking 
for new work too often. When setting this property, be aware that it could add 
extra latency for components that do not constantly have work to do, as once 
they go into this "bored" state, they will wait this amount of time before 
checking for more work. The  [...]
diff --git 
a/nifi-extension-bundles/nifi-extension-utils/nifi-git-flow-registry/src/main/java/org/apache/nifi/registry/flow/git/AbstractGitFlowRegistryClient.java
 
b/nifi-extension-bundles/nifi-extension-utils/nifi-git-flow-registry/src/main/java/org/apache/nifi/registry/flow/git/AbstractGitFlowRegistryClient.java
index fc0f2bf2d79..2fa3fac9953 100644
--- 
a/nifi-extension-bundles/nifi-extension-utils/nifi-git-flow-registry/src/main/java/org/apache/nifi/registry/flow/git/AbstractGitFlowRegistryClient.java
+++ 
b/nifi-extension-bundles/nifi-extension-utils/nifi-git-flow-registry/src/main/java/org/apache/nifi/registry/flow/git/AbstractGitFlowRegistryClient.java
@@ -162,6 +162,7 @@ public abstract class AbstractGitFlowRegistryClient extends 
AbstractFlowRegistry
         combinedPropertyDescriptors.add(PARAMETER_CONTEXT_VALUES);
         combinedPropertyDescriptors.add(COMMIT_AUTHOR_SOURCE);
         combinedPropertyDescriptors.add(SSL_CONTEXT_SERVICE);
+        combinedPropertyDescriptors.add(SYNCHRONIZATION_INTERVAL);
         propertyDescriptors = 
Collections.unmodifiableList(combinedPropertyDescriptors);
 
         flowSnapshotSerializer = createFlowSnapshotSerializer();
diff --git 
a/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java
 
b/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java
index 4c82e0eaf58..8f3e06db762 100644
--- 
a/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java
+++ 
b/nifi-extension-bundles/nifi-flow-registry-client-bundle/nifi-flow-registry-client-services/src/main/java/org/apache/nifi/registry/flow/NifiRegistryFlowRegistryClient.java
@@ -114,7 +114,8 @@ public class NifiRegistryFlowRegistryClient extends 
AbstractFlowRegistryClient {
     protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
         return Arrays.asList(
                 PROPERTY_URL,
-                SSL_CONTEXT_SERVICE
+                SSL_CONTEXT_SERVICE,
+                SYNCHRONIZATION_INTERVAL
         );
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index ede57506249..674e2464307 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -1390,24 +1390,17 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
             }, 0L, 30L, TimeUnit.SECONDS);
 
             final String registrySyncInterval = 
nifiProperties.getProperty("nifi.flowcontroller.registry.sync.interval", "30 
min");
-            final long registrySyncIntervalSeconds = 
FormatUtils.getTimeDuration(registrySyncInterval, TimeUnit.SECONDS);
+            final long defaultRegistrySyncIntervalSeconds = 
FormatUtils.getTimeDuration(registrySyncInterval, TimeUnit.SECONDS);
 
-            LOG.info("Scheduled Flow Registry synchronization every {}", 
registrySyncInterval);
+            // The synchronization task runs on a fixed tick but synchronizes 
each Flow Registry Client's Process Groups only when
+            // that client's configured interval has elapsed. The tick is 
bounded so that short per-client intervals are honored
+            // reasonably closely while avoiding needlessly frequent 
iterations for the typical (minutes) interval.
+            final long registrySyncTickSeconds = Math.max(1, 
Math.min(defaultRegistrySyncIntervalSeconds, 30));
 
-            // Schedule the flow registry synchronization task
-            timerDrivenEngineRef.get().scheduleWithFixedDelay(() -> {
-                final ProcessGroup rootGroup = flowManager.getRootGroup();
-                final List<ProcessGroup> allGroups = 
rootGroup.findAllProcessGroups();
-                allGroups.add(rootGroup);
+            LOG.info("Scheduled Flow Registry with Sync Interval [{} s] Check 
Interval [{} s]", registrySyncInterval, registrySyncTickSeconds);
 
-                for (final ProcessGroup group : allGroups) {
-                    try {
-                        group.synchronizeWithFlowRegistry(flowManager);
-                    } catch (final Exception e) {
-                        LOG.error("Failed to synchronize {} with Flow 
Registry", group, e);
-                    }
-                }
-            }, 300, registrySyncIntervalSeconds, TimeUnit.SECONDS);
+            final RegistryFlowSynchronizationTask registrySynchronizationTask 
= new RegistryFlowSynchronizationTask(flowManager, 
defaultRegistrySyncIntervalSeconds);
+            
timerDrivenEngineRef.get().scheduleWithFixedDelay(registrySynchronizationTask, 
300, registrySyncTickSeconds, TimeUnit.SECONDS);
 
             initialized.set(true);
         } finally {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/RegistryFlowSynchronizationTask.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/RegistryFlowSynchronizationTask.java
new file mode 100644
index 00000000000..a2c94bbd36f
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/RegistryFlowSynchronizationTask.java
@@ -0,0 +1,119 @@
+/*
+ * 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.controller;
+
+import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.flow.AbstractFlowRegistryClient;
+import org.apache.nifi.registry.flow.FlowRegistryClientNode;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.util.FormatUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Periodic task that synchronizes version-controlled Process Groups with 
their external Flow Registries. The task is
+ * scheduled on a fixed tick, but each Process Group is only synchronized at 
the interval configured on its Flow
+ * Registry Client via {@link 
AbstractFlowRegistryClient#SYNCHRONIZATION_INTERVAL}. When a Flow Registry 
Client does not
+ * specify an interval, the configurable default (the {@code 
nifi.flowcontroller.registry.sync.interval} property) is
+ * used. Process Groups are grouped by their Flow Registry Client so that all 
groups belonging to a given client are
+ * synchronized together on that client's cadence.
+ */
+final class RegistryFlowSynchronizationTask implements Runnable {
+    private static final Logger logger = 
LoggerFactory.getLogger(RegistryFlowSynchronizationTask.class);
+
+    private final FlowManager flowManager;
+    private final long defaultIntervalSeconds;
+    private final Map<String, Long> lastSynchronizationTimestamps = new 
ConcurrentHashMap<>();
+
+    RegistryFlowSynchronizationTask(final FlowManager flowManager, final long 
defaultIntervalSeconds) {
+        this.flowManager = flowManager;
+        this.defaultIntervalSeconds = defaultIntervalSeconds;
+    }
+
+    @Override
+    public void run() {
+        final ProcessGroup rootGroup = flowManager.getRootGroup();
+        final List<ProcessGroup> allGroups = rootGroup.findAllProcessGroups();
+        allGroups.add(rootGroup);
+
+        final Map<String, List<ProcessGroup>> groupsByRegistryClientId = new 
HashMap<>();
+        for (final ProcessGroup group : allGroups) {
+            final VersionControlInformation versionControlInformation = 
group.getVersionControlInformation();
+            if (versionControlInformation == null) {
+                continue;
+            }
+
+            
groupsByRegistryClientId.computeIfAbsent(versionControlInformation.getRegistryIdentifier(),
 key -> new ArrayList<>()).add(group);
+        }
+
+        final long now = System.currentTimeMillis();
+        for (final Map.Entry<String, List<ProcessGroup>> entry : 
groupsByRegistryClientId.entrySet()) {
+            final String registryClientId = entry.getKey();
+            final long intervalSeconds = 
getEffectiveIntervalSeconds(registryClientId);
+            final Long lastSynchronization = 
lastSynchronizationTimestamps.get(registryClientId);
+
+            if (lastSynchronization != null && (now - lastSynchronization) < 
TimeUnit.SECONDS.toMillis(intervalSeconds)) {
+                continue;
+            }
+
+            for (final ProcessGroup group : entry.getValue()) {
+                try {
+                    group.synchronizeWithFlowRegistry(flowManager);
+                } catch (final Exception e) {
+                    logger.error("Failed to synchronize {} with Flow 
Registry", group, e);
+                }
+            }
+
+            lastSynchronizationTimestamps.put(registryClientId, now);
+        }
+
+        // Stop tracking Flow Registry Clients that no longer have any 
version-controlled Process Groups so that a client
+        // is synchronized immediately if it is removed and later re-added.
+        
lastSynchronizationTimestamps.keySet().retainAll(groupsByRegistryClientId.keySet());
+    }
+
+    long getEffectiveIntervalSeconds(final String registryClientId) {
+        final FlowRegistryClientNode clientNode = 
flowManager.getFlowRegistryClient(registryClientId);
+        if (clientNode == null) {
+            return defaultIntervalSeconds;
+        }
+
+        final String configuredInterval = 
clientNode.getEffectivePropertyValue(AbstractFlowRegistryClient.SYNCHRONIZATION_INTERVAL);
+        return parseIntervalSeconds(configuredInterval, 
defaultIntervalSeconds);
+    }
+
+    static long parseIntervalSeconds(final String configuredInterval, final 
long defaultIntervalSeconds) {
+        if (configuredInterval == null || configuredInterval.isBlank()) {
+            return defaultIntervalSeconds;
+        }
+
+        try {
+            return FormatUtils.getTimeDuration(configuredInterval.trim(), 
TimeUnit.SECONDS);
+        } catch (final IllegalArgumentException e) {
+            logger.warn("Configured Flow Registry Synchronization Interval 
[{}] is not valid; using default of {} seconds", configuredInterval, 
defaultIntervalSeconds);
+            return defaultIntervalSeconds;
+        }
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/RegistryFlowSynchronizationTaskTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/RegistryFlowSynchronizationTaskTest.java
new file mode 100644
index 00000000000..8683d73cad6
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/RegistryFlowSynchronizationTaskTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.controller;
+
+import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.registry.flow.AbstractFlowRegistryClient;
+import org.apache.nifi.registry.flow.FlowRegistryClientNode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RegistryFlowSynchronizationTaskTest {
+
+    private static final long DEFAULT_INTERVAL_SECONDS = 1800L;
+
+    @Test
+    void testParseIntervalSecondsFallsBackToDefaultWhenNotConfigured() {
+        assertEquals(DEFAULT_INTERVAL_SECONDS, 
RegistryFlowSynchronizationTask.parseIntervalSeconds(null, 
DEFAULT_INTERVAL_SECONDS));
+        assertEquals(DEFAULT_INTERVAL_SECONDS, 
RegistryFlowSynchronizationTask.parseIntervalSeconds("", 
DEFAULT_INTERVAL_SECONDS));
+        assertEquals(DEFAULT_INTERVAL_SECONDS, 
RegistryFlowSynchronizationTask.parseIntervalSeconds("   ", 
DEFAULT_INTERVAL_SECONDS));
+    }
+
+    @Test
+    void testParseIntervalSecondsParsesConfiguredDuration() {
+        assertEquals(300L, 
RegistryFlowSynchronizationTask.parseIntervalSeconds("5 min", 
DEFAULT_INTERVAL_SECONDS));
+        assertEquals(45L, 
RegistryFlowSynchronizationTask.parseIntervalSeconds("45 secs", 
DEFAULT_INTERVAL_SECONDS));
+        assertEquals(45L, 
RegistryFlowSynchronizationTask.parseIntervalSeconds("  45 secs  ", 
DEFAULT_INTERVAL_SECONDS));
+    }
+
+    @Test
+    void testParseIntervalSecondsFallsBackToDefaultWhenInvalid() {
+        assertEquals(DEFAULT_INTERVAL_SECONDS, 
RegistryFlowSynchronizationTask.parseIntervalSeconds("not-a-duration", 
DEFAULT_INTERVAL_SECONDS));
+    }
+
+    @Test
+    void testGetEffectiveIntervalSeconds() {
+        final FlowManager flowManager = mock(FlowManager.class);
+        final RegistryFlowSynchronizationTask task = new 
RegistryFlowSynchronizationTask(flowManager, DEFAULT_INTERVAL_SECONDS);
+
+        final FlowRegistryClientNode configuredClient = 
mock(FlowRegistryClientNode.class);
+        
when(configuredClient.getEffectivePropertyValue(AbstractFlowRegistryClient.SYNCHRONIZATION_INTERVAL)).thenReturn("10
 min");
+        
when(flowManager.getFlowRegistryClient("configured")).thenReturn(configuredClient);
+        assertEquals(600L, task.getEffectiveIntervalSeconds("configured"));
+
+        final FlowRegistryClientNode unconfiguredClient = 
mock(FlowRegistryClientNode.class);
+        
when(unconfiguredClient.getEffectivePropertyValue(AbstractFlowRegistryClient.SYNCHRONIZATION_INTERVAL)).thenReturn(null);
+        
when(flowManager.getFlowRegistryClient("unconfigured")).thenReturn(unconfiguredClient);
+        assertEquals(DEFAULT_INTERVAL_SECONDS, 
task.getEffectiveIntervalSeconds("unconfigured"));
+
+        when(flowManager.getFlowRegistryClient("missing")).thenReturn(null);
+        assertEquals(DEFAULT_INTERVAL_SECONDS, 
task.getEffectiveIntervalSeconds("missing"));
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/flow/registry/FileSystemFlowRegistryClient.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/flow/registry/FileSystemFlowRegistryClient.java
index 1d7eebf0eda..1910247ed2a 100644
--- 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/flow/registry/FileSystemFlowRegistryClient.java
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/flow/registry/FileSystemFlowRegistryClient.java
@@ -47,7 +47,6 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -87,7 +86,7 @@ public class FileSystemFlowRegistryClient extends 
AbstractFlowRegistryClient {
 
     @Override
     protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
-        return Collections.singletonList(DIRECTORY);
+        return List.of(DIRECTORY, SYNCHRONIZATION_INTERVAL);
     }
 
     @Override

Reply via email to