This is an automated email from the ASF dual-hosted git repository.

pvillard31 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 8372ecbe096 NIFI-16123 Added Connection Status reporting to Process 
Session (#11441)
8372ecbe096 is described below

commit 8372ecbe0968892fe0317ccebc351c1ea1c55acc
Author: David Handermann <[email protected]>
AuthorDate: Mon Jul 20 04:47:58 2026 -0500

    NIFI-16123 Added Connection Status reporting to Process Session (#11441)
    
    - Added ConnectionStatusEvent interface to framework-api
    - Updated StandardProcessSession with optional recording of Connection 
Status
---
 .../metrics/ComponentMetricReporter.java           |  18 +++
 .../controller/metrics/ConnectionStatusEvent.java  |  74 +++++++++
 .../repository/AbstractRepositoryContext.java      |  11 ++
 .../controller/repository/RepositoryContext.java   |   5 +
 .../repository/StandardProcessSession.java         |  82 +++++++++-
 .../metrics/ConnectionStatusEventBuilder.java      |  89 +++++++++++
 .../metrics/StandardConnectionStatusEvent.java     |  71 ++++++++
 .../repository/StandardProcessSessionTest.java     | 178 +++++++++++++++++++++
 8 files changed, 520 insertions(+), 8 deletions(-)

diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/controller/metrics/ComponentMetricReporter.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/controller/metrics/ComponentMetricReporter.java
index aeeb61649ce..c6ce22109c8 100644
--- 
a/nifi-framework-api/src/main/java/org/apache/nifi/controller/metrics/ComponentMetricReporter.java
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/controller/metrics/ComponentMetricReporter.java
@@ -59,4 +59,22 @@ public interface ComponentMetricReporter extends Closeable {
      */
     default void recordProcessSessionEvent(ProcessSessionEvent 
processSessionEvent) {
     }
+
+    /**
+     * Record current Connection Status for operations completed during a 
Process Session
+     *
+     * @param connectionStatusEvent Connection Status Event containing 
component context and captured status
+     */
+    default void recordConnectionStatusEvent(ConnectionStatusEvent 
connectionStatusEvent) {
+    }
+
+    /**
+     * Status indicator for recording Connection Status Event defaults to 
disabled and requires overriding along with
+     * recordConnectionStatusEvent() to handle Connection Status Events
+     *
+     * @return Enabled or disabled status for recording Connection Status 
Events
+     */
+    default boolean isRecordConnectionStatusEventEnabled() {
+        return false;
+    }
 }
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/controller/metrics/ConnectionStatusEvent.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/controller/metrics/ConnectionStatusEvent.java
new file mode 100644
index 00000000000..d372489d59f
--- /dev/null
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/controller/metrics/ConnectionStatusEvent.java
@@ -0,0 +1,74 @@
+/*
+ * 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.metrics;
+
+import org.apache.nifi.controller.status.FlowFileAvailability;
+import org.apache.nifi.controller.status.LoadBalanceStatus;
+
+/**
+ * Event abstraction for Connection Status metrics collected during 
ProcessSession operations
+ */
+public interface ConnectionStatusEvent {
+    /**
+     * Get Component Metric Context describing the Component associated with 
the recorded ProcessSession operations
+     *
+     * @return Component Metric Context
+     */
+    ComponentMetricContext getComponentMetricContext();
+
+    /**
+     * Get configured Back Pressure Bytes Threshold
+     *
+     * @return Back Pressure Bytes Threshold
+     */
+    long getBackPressureBytesThreshold();
+
+    /**
+     * Get configured Back Pressure Object Threshold
+     *
+     * @return Back Pressure Object Threshold
+     */
+    long getBackPressureObjectThreshold();
+
+    /**
+     * Get bytes from queued FlowFiles for the Connection
+     *
+     * @return Queued Bytes
+     */
+    long getQueuedBytes();
+
+    /**
+     * Get count of FlowFiles queued for the Connection
+     *
+     * @return Queued FlowFiles
+     */
+    int getQueuedCount();
+
+    /**
+     * Get Load Balance Status for the Connection
+     *
+     * @return Load Balance Status
+     */
+    LoadBalanceStatus getLoadBalanceStatus();
+
+    /**
+     * Get FlowFile Availability for the Connection
+     *
+     * @return FlowFile Availability
+     */
+    FlowFileAvailability getFlowFileAvailability();
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/AbstractRepositoryContext.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/AbstractRepositoryContext.java
index c1e911828ae..f92390352ae 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/AbstractRepositoryContext.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/AbstractRepositoryContext.java
@@ -24,6 +24,7 @@ import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.metrics.ComponentMetricContext;
 import org.apache.nifi.controller.metrics.ComponentMetricReporter;
+import org.apache.nifi.controller.metrics.ConnectionStatusEvent;
 import org.apache.nifi.controller.metrics.CounterRecord;
 import org.apache.nifi.controller.metrics.GaugeRecord;
 import org.apache.nifi.controller.metrics.ProcessSessionEvent;
@@ -182,6 +183,16 @@ public abstract class AbstractRepositoryContext implements 
RepositoryContext {
         componentMetricReporter.recordProcessSessionEvent(event);
     }
 
+    @Override
+    public void recordConnectionStatusEvent(final ConnectionStatusEvent event) 
{
+        componentMetricReporter.recordConnectionStatusEvent(event);
+    }
+
+    @Override
+    public boolean isRecordConnectionStatusEventEnabled() {
+        return componentMetricReporter.isRecordConnectionStatusEventEnabled();
+    }
+
     @Override
     public ContentRepository getContentRepository() {
         return contentRepo;
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/RepositoryContext.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/RepositoryContext.java
index 212ab344775..a26a39bfff9 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/RepositoryContext.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/RepositoryContext.java
@@ -21,6 +21,7 @@ import org.apache.nifi.components.state.StateManager;
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.controller.metrics.ComponentMetricContext;
+import org.apache.nifi.controller.metrics.ConnectionStatusEvent;
 import org.apache.nifi.controller.metrics.GaugeRecord;
 import org.apache.nifi.controller.metrics.ProcessSessionEvent;
 import org.apache.nifi.controller.repository.claim.ContentClaimWriteCache;
@@ -70,6 +71,10 @@ public interface RepositoryContext {
 
     void recordProcessSessionEvent(ProcessSessionEvent event);
 
+    void recordConnectionStatusEvent(ConnectionStatusEvent event);
+
+    boolean isRecordConnectionStatusEventEnabled();
+
     ProvenanceEventBuilder createProvenanceEventBuilder();
 
     StateManager getStateManager();
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java
index db0a419db3b..bd60a85b593 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/StandardProcessSession.java
@@ -25,9 +25,11 @@ import org.apache.nifi.controller.BackoffMechanism;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.lifecycle.TaskTermination;
 import org.apache.nifi.controller.metrics.ComponentMetricContext;
+import org.apache.nifi.controller.metrics.ConnectionStatusEvent;
 import org.apache.nifi.controller.metrics.GaugeRecord;
 import org.apache.nifi.controller.metrics.ProcessSessionEvent;
 import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.queue.LoadBalanceStrategy;
 import org.apache.nifi.controller.queue.PollStrategy;
 import org.apache.nifi.controller.queue.QueueSize;
 import org.apache.nifi.controller.repository.claim.ContentClaim;
@@ -41,12 +43,16 @@ import 
org.apache.nifi.controller.repository.io.FlowFileAccessOutputStream;
 import org.apache.nifi.controller.repository.io.LimitedInputStream;
 import org.apache.nifi.controller.repository.io.TaskTerminationInputStream;
 import org.apache.nifi.controller.repository.io.TaskTerminationOutputStream;
+import 
org.apache.nifi.controller.repository.metrics.ConnectionStatusEventBuilder;
 import org.apache.nifi.controller.repository.metrics.PerformanceTracker;
 import 
org.apache.nifi.controller.repository.metrics.PerformanceTrackingInputStream;
 import 
org.apache.nifi.controller.repository.metrics.ProcessSessionEventBuilder;
 import org.apache.nifi.controller.state.StandardStateMap;
+import org.apache.nifi.controller.status.FlowFileAvailability;
+import org.apache.nifi.controller.status.LoadBalanceStatus;
 import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.processor.DataUnit;
 import org.apache.nifi.processor.FlowFileFilter;
 import org.apache.nifi.processor.ProcessSession;
 import org.apache.nifi.processor.Relationship;
@@ -145,6 +151,7 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
 
     private final Map<Long, StandardRepositoryRecord> records = new 
ConcurrentHashMap<>();
     private final Map<String, ProcessSessionEventBuilder> connectionCounts = 
new ConcurrentHashMap<>();
+    private final Map<String, Connection> processedConnections = new 
ConcurrentHashMap<>();
     private final Map<String, ComponentMetricContext> connectionMetricContexts 
= new ConcurrentHashMap<>();
     private final Map<FlowFileQueue, Set<FlowFileRecord>> 
unacknowledgedFlowFiles = new ConcurrentHashMap<>();
     private final Map<ContentClaim, ByteCountingOutputStream> 
appendableStreams = new ConcurrentHashMap<>();
@@ -807,11 +814,52 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
                 
context.getFlowFileEventRepository().updateRepository(connectionSessionEvent);
                 context.recordProcessSessionEvent(connectionSessionEvent);
             }
+
+            recordConnectionStatusEvents(checkpoint);
         } catch (final IOException ioe) {
             LOG.error("FlowFile Event Repository failed to update", ioe);
         }
     }
 
+    private void recordConnectionStatusEvents(final Checkpoint checkpoint) {
+        // Check enabled status to avoid building objects and calling methods 
when not used
+        if (context.isRecordConnectionStatusEventEnabled()) {
+            for (final Connection connection : 
checkpoint.processedConnections.values()) {
+                final ComponentMetricContext connectionMetricContext = 
checkpoint.connectionMetricContexts.get(connection.getIdentifier());
+                final FlowFileQueue flowFileQueue = 
connection.getFlowFileQueue();
+                final QueueSize queueSize = flowFileQueue.size();
+                final long backPressureBytesThreshold = 
DataUnit.parseDataSize(flowFileQueue.getBackPressureDataSizeThreshold(), 
DataUnit.B).longValue();
+                final LoadBalanceStatus loadBalanceStatus = 
getLoadBalanceStatus(flowFileQueue);
+                final FlowFileAvailability flowFileAvailability = 
flowFileQueue.getFlowFileAvailability();
+
+                final ConnectionStatusEvent connectionStatusEvent = 
ConnectionStatusEventBuilder.forComponent(connectionMetricContext)
+                        .backPressureBytesThreshold(backPressureBytesThreshold)
+                        
.backPressureObjectThreshold(flowFileQueue.getBackPressureObjectThreshold())
+                        .queuedBytes(queueSize.getByteCount())
+                        .queuedCount(queueSize.getObjectCount())
+                        .loadBalanceStatus(loadBalanceStatus)
+                        .flowFileAvailability(flowFileAvailability)
+                        .build();
+                context.recordConnectionStatusEvent(connectionStatusEvent);
+            }
+        }
+    }
+
+    private LoadBalanceStatus getLoadBalanceStatus(final FlowFileQueue 
flowFileQueue) {
+        final LoadBalanceStatus loadBalanceStatus;
+
+        final LoadBalanceStrategy loadBalanceStrategy = 
flowFileQueue.getLoadBalanceStrategy();
+        if (loadBalanceStrategy == LoadBalanceStrategy.DO_NOT_LOAD_BALANCE) {
+            loadBalanceStatus = LoadBalanceStatus.LOAD_BALANCE_NOT_CONFIGURED;
+        } else if (flowFileQueue.isActivelyLoadBalancing()) {
+            loadBalanceStatus = LoadBalanceStatus.LOAD_BALANCE_ACTIVE;
+        } else {
+            loadBalanceStatus = LoadBalanceStatus.LOAD_BALANCE_INACTIVE;
+        }
+
+        return loadBalanceStatus;
+    }
+
     private Map<String, Long> combineCounters(final Map<String, Long> first, 
final Map<String, Long> second) {
         final boolean firstEmpty = first == null || first.isEmpty();
         final boolean secondEmpty = second == null || second.isEmpty();
@@ -1453,6 +1501,7 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
         bytesRead = 0L;
         bytesWritten = 0L;
         connectionCounts.clear();
+        processedConnections.clear();
         connectionMetricContexts.clear();
         createdFlowFiles.clear();
         createdFlowFilesWithoutLineage.clear();
@@ -1652,8 +1701,18 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
                 final FlowFileQueue inputQueue = repoRecord.getOriginalQueue();
                 if (inputQueue != null) {
                     final String connectionId = inputQueue.getIdentifier();
-                    incrementConnectionOutputCounts(connectionId, -1, 
-repoRecord.getOriginal().getSize());
-                    newOwner.incrementConnectionOutputCounts(connectionId, 1, 
repoRecord.getOriginal().getSize());
+                    final long originalSize = 
repoRecord.getOriginal().getSize();
+
+                    // A FlowFile that has an original queue was dequeued 
within this session, so the source Connection is tracked and can be
+                    // reused to record Connection Status for the destination 
session. Adjust counts by identifier only when it is not tracked.
+                    final Connection connection = 
processedConnections.get(connectionId);
+                    if (connection == null) {
+                        incrementConnectionOutputCounts(connectionId, -1, 
-originalSize);
+                        newOwner.incrementConnectionOutputCounts(connectionId, 
1, originalSize);
+                    } else {
+                        incrementConnectionOutputCounts(connection, -1, 
-originalSize);
+                        newOwner.incrementConnectionOutputCounts(connection, 
1, originalSize);
+                    }
 
                     unacknowledgedFlowFiles.get(inputQueue).remove(flowFile);
                     
newOwner.unacknowledgedFlowFiles.computeIfAbsent(inputQueue, queue -> new 
HashSet<>()).add(flowFileRecord);
@@ -1838,15 +1897,14 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
     }
 
     private void incrementConnectionInputCounts(final Connection connection, 
final RepositoryRecord record) {
-        incrementConnectionInputCounts(connection, 1, 
record.getCurrent().getSize());
-    }
-
-    private void incrementConnectionInputCounts(final Connection connection, 
final int flowFileCount, final long bytes) {
         final String connectionId = connection.getIdentifier();
         cacheConnectionMetricContext(connection);
         final ProcessSessionEventBuilder connectionEvent = 
connectionCounts.computeIfAbsent(
                 connectionId, id -> 
ProcessSessionEventBuilder.forComponent(getConnectionMetricContext(connectionId)));
-        connectionEvent.addFlowFilesIn(flowFileCount).addContentSizeIn(bytes);
+        final long bytes = record.getCurrent().getSize();
+        connectionEvent.addFlowFilesIn(1).addContentSizeIn(bytes);
+
+        processedConnections.put(connectionId, connection);
     }
 
     private void incrementConnectionOutputCounts(final Connection connection, 
final FlowFileRecord record) {
@@ -1859,6 +1917,8 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
         final ProcessSessionEventBuilder connectionEvent = 
connectionCounts.computeIfAbsent(
                 connectionId, id -> 
ProcessSessionEventBuilder.forComponent(getConnectionMetricContext(connectionId)));
         
connectionEvent.addFlowFilesOut(flowFileCount).addContentSizeOut(bytes);
+
+        processedConnections.put(connectionId, connection);
     }
 
     private void incrementConnectionOutputCounts(final String connectionId, 
final int flowFileCount, final long bytes) {
@@ -3994,6 +4054,7 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
 
         private Map<Long, StandardRepositoryRecord> records;
         private Map<String, ProcessSessionEventBuilder> connectionCounts;
+        private Map<String, Connection> processedConnections;
         private Map<String, ComponentMetricContext> connectionMetricContexts;
 
         private Map<String, Long> countersOnCommit;
@@ -4034,6 +4095,7 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
 
             records = new ConcurrentHashMap<>();
             connectionCounts = new ConcurrentHashMap<>();
+            processedConnections = new ConcurrentHashMap<>();
             connectionMetricContexts = new ConcurrentHashMap<>();
 
             countersOnCommit = new HashMap<>();
@@ -4070,6 +4132,7 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
             this.records = session.records;
 
             this.connectionCounts = session.connectionCounts;
+            this.processedConnections = session.processedConnections;
             this.connectionMetricContexts = session.connectionMetricContexts;
             this.countersOnCommit = session.countersOnCommit == null ? 
Collections.emptyMap() : session.countersOnCommit;
             this.immediateCounters = session.immediateCounters == null ? 
Collections.emptyMap() : session.immediateCounters;
@@ -4118,7 +4181,10 @@ public class StandardProcessSession implements 
ProcessSession, ProvenanceEventEn
 
             this.records.putAll(session.records);
 
-            mergeMapsWithMutableValue(this.connectionCounts, 
session.connectionCounts, (destination, toMerge) -> 
destination.merge(toMerge.build()));
+            mergeMapsWithMutableValue(this.connectionCounts, 
session.connectionCounts,
+                    (destination, toMerge) -> 
destination.merge(toMerge.build())
+            );
+            mergeMaps(this.processedConnections, session.processedConnections, 
(existing, incoming) -> existing);
             mergeMaps(this.connectionMetricContexts, 
session.connectionMetricContexts, (existing, incoming) -> existing);
             mergeMaps(this.countersOnCommit, session.countersOnCommit, 
Long::sum);
             mergeMaps(this.immediateCounters, session.immediateCounters, 
Long::sum);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/metrics/ConnectionStatusEventBuilder.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/metrics/ConnectionStatusEventBuilder.java
new file mode 100644
index 00000000000..8c48a5e8e1a
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/metrics/ConnectionStatusEventBuilder.java
@@ -0,0 +1,89 @@
+/*
+ * 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.repository.metrics;
+
+import org.apache.nifi.controller.metrics.ComponentMetricContext;
+import org.apache.nifi.controller.metrics.ConnectionStatusEvent;
+import org.apache.nifi.controller.status.FlowFileAvailability;
+import org.apache.nifi.controller.status.LoadBalanceStatus;
+
+import java.util.Objects;
+
+/**
+ * Builder for Connection Status Events with Component Metric Context required
+ */
+public class ConnectionStatusEventBuilder {
+
+    private final ComponentMetricContext componentMetricContext;
+
+    private long backPressureBytesThreshold;
+    private long backPressureObjectThreshold;
+    private long queuedBytes;
+    private int queuedCount;
+    private LoadBalanceStatus loadBalanceStatus = 
LoadBalanceStatus.LOAD_BALANCE_NOT_CONFIGURED;
+    private FlowFileAvailability flowFileAvailability = 
FlowFileAvailability.ACTIVE_QUEUE_EMPTY;
+
+    private ConnectionStatusEventBuilder(final ComponentMetricContext 
componentMetricContext) {
+        this.componentMetricContext = 
Objects.requireNonNull(componentMetricContext, "Component Metric Context 
required");
+    }
+
+    public static ConnectionStatusEventBuilder forComponent(final 
ComponentMetricContext componentMetricContext) {
+        return new ConnectionStatusEventBuilder(componentMetricContext);
+    }
+
+    public ConnectionStatusEventBuilder backPressureBytesThreshold(final long 
backPressureBytesThreshold) {
+        this.backPressureBytesThreshold = backPressureBytesThreshold;
+        return this;
+    }
+
+    public ConnectionStatusEventBuilder backPressureObjectThreshold(final long 
backPressureObjectThreshold) {
+        this.backPressureObjectThreshold = backPressureObjectThreshold;
+        return this;
+    }
+
+    public ConnectionStatusEventBuilder queuedBytes(final long queuedBytes) {
+        this.queuedBytes = queuedBytes;
+        return this;
+    }
+
+    public ConnectionStatusEventBuilder queuedCount(final int queuedCount) {
+        this.queuedCount = queuedCount;
+        return this;
+    }
+
+    public ConnectionStatusEventBuilder loadBalanceStatus(final 
LoadBalanceStatus loadBalanceStatus) {
+        this.loadBalanceStatus = Objects.requireNonNull(loadBalanceStatus, 
"Load Balance Status required");
+        return this;
+    }
+
+    public ConnectionStatusEventBuilder flowFileAvailability(final 
FlowFileAvailability flowFileAvailability) {
+        this.flowFileAvailability = 
Objects.requireNonNull(flowFileAvailability, "FlowFile Availability required");
+        return this;
+    }
+
+    public ConnectionStatusEvent build() {
+        return new StandardConnectionStatusEvent(
+                componentMetricContext,
+                backPressureBytesThreshold,
+                backPressureObjectThreshold,
+                queuedBytes,
+                queuedCount,
+                loadBalanceStatus,
+                flowFileAvailability
+        );
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/metrics/StandardConnectionStatusEvent.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/metrics/StandardConnectionStatusEvent.java
new file mode 100644
index 00000000000..4f4b32a20a8
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/repository/metrics/StandardConnectionStatusEvent.java
@@ -0,0 +1,71 @@
+/*
+ * 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.repository.metrics;
+
+import org.apache.nifi.controller.metrics.ComponentMetricContext;
+import org.apache.nifi.controller.metrics.ConnectionStatusEvent;
+import org.apache.nifi.controller.status.FlowFileAvailability;
+import org.apache.nifi.controller.status.LoadBalanceStatus;
+
+/**
+ * Standard record representation of Connection Status Event with 
package-private visibility for Builder
+ */
+record StandardConnectionStatusEvent(
+        ComponentMetricContext componentMetricContext,
+        long backPressureBytesThreshold,
+        long backPressureObjectThreshold,
+        long queuedBytes,
+        int queuedCount,
+        LoadBalanceStatus loadBalanceStatus,
+        FlowFileAvailability flowFileAvailability
+) implements ConnectionStatusEvent {
+
+    @Override
+    public ComponentMetricContext getComponentMetricContext() {
+        return componentMetricContext;
+    }
+
+    @Override
+    public long getBackPressureBytesThreshold() {
+        return backPressureBytesThreshold;
+    }
+
+    @Override
+    public long getBackPressureObjectThreshold() {
+        return backPressureObjectThreshold;
+    }
+
+    @Override
+    public long getQueuedBytes() {
+        return queuedBytes;
+    }
+
+    @Override
+    public int getQueuedCount() {
+        return queuedCount;
+    }
+
+    @Override
+    public LoadBalanceStatus getLoadBalanceStatus() {
+        return loadBalanceStatus;
+    }
+
+    @Override
+    public FlowFileAvailability getFlowFileAvailability() {
+        return flowFileAvailability;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/controller/repository/StandardProcessSessionTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/controller/repository/StandardProcessSessionTest.java
index 18d468b10e7..79ce3f98a0f 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/controller/repository/StandardProcessSessionTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/controller/repository/StandardProcessSessionTest.java
@@ -17,15 +17,24 @@
 package org.apache.nifi.controller.repository;
 
 import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.FlowFileActivity;
 import org.apache.nifi.controller.lifecycle.TaskTermination;
+import org.apache.nifi.controller.metrics.ComponentMetricContext;
+import org.apache.nifi.controller.metrics.ConnectionStatusEvent;
 import org.apache.nifi.controller.metrics.GaugeRecord;
 import org.apache.nifi.controller.metrics.ProcessSessionEvent;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.queue.LoadBalanceStrategy;
+import org.apache.nifi.controller.queue.QueueSize;
 import org.apache.nifi.controller.repository.claim.ContentClaim;
 import org.apache.nifi.controller.repository.claim.ContentClaimWriteCache;
 import org.apache.nifi.controller.repository.metrics.PerformanceTracker;
+import org.apache.nifi.controller.status.FlowFileAvailability;
+import org.apache.nifi.controller.status.LoadBalanceStatus;
 import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.metrics.CommitTiming;
 import org.apache.nifi.provenance.InternalProvenanceReporter;
 import org.apache.nifi.provenance.ProvenanceRepository;
@@ -44,16 +53,20 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anySet;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -76,6 +89,11 @@ class StandardProcessSessionTest {
 
     private static final double GAUGE_VALUE = 64.5;
 
+    private static final String INPUT_CONNECTION_ID = "input-connection-id";
+    private static final String OUTPUT_CONNECTION_ID = "output-connection-id";
+    private static final String BACK_PRESSURE_DATA_SIZE_THRESHOLD = "1 MB";
+    private static final long BACK_PRESSURE_BYTES_THRESHOLD = 1048576;
+
     @Mock
     RepositoryContext repositoryContext;
 
@@ -118,6 +136,9 @@ class StandardProcessSessionTest {
     @Captor
     ArgumentCaptor<GaugeRecord> gaugeRecordCaptor;
 
+    @Captor
+    ArgumentCaptor<ConnectionStatusEvent> connectionStatusEventCaptor;
+
     StandardProcessSession session;
 
     @BeforeEach
@@ -132,6 +153,163 @@ class StandardProcessSessionTest {
         session = new StandardProcessSession(repositoryContext, 
taskTermination, performanceTracker);
     }
 
+    @Test
+    void testGetTransferConnectionStatusEventsDisabled() {
+        setRepositoryContext();
+        
when(repositoryContext.getContentRepository()).thenReturn(contentRepository);
+        
when(repositoryContext.isRecordConnectionStatusEventEnabled()).thenReturn(false);
+
+        final Connection connection = mock(Connection.class);
+        
when(repositoryContext.getPollableConnections()).thenReturn(List.of(connection));
+        final FlowFileRecord flowFileRecord = mock(FlowFileRecord.class);
+        when(connection.poll(anySet())).thenReturn(flowFileRecord);
+        final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+        when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+        when(connection.getIdentifier()).thenReturn(INPUT_CONNECTION_ID);
+
+        final FlowFile flowFile = session.get();
+        assertNotNull(flowFile);
+        session.transfer(flowFile);
+        session.commit();
+
+        verify(repositoryContext, 
never()).recordConnectionStatusEvent(connectionStatusEventCaptor.capture());
+    }
+
+    @Test
+    void testGetTransferConnectionStatusEvents() {
+        setRepositoryContext();
+        
when(repositoryContext.getContentRepository()).thenReturn(contentRepository);
+        
when(repositoryContext.isRecordConnectionStatusEventEnabled()).thenReturn(true);
+
+        final Connection connection = mock(Connection.class);
+        
when(repositoryContext.getPollableConnections()).thenReturn(List.of(connection));
+        final FlowFileRecord flowFileRecord = mock(FlowFileRecord.class);
+        when(connection.poll(anySet())).thenReturn(flowFileRecord);
+        final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+        when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+        when(connection.getIdentifier()).thenReturn(INPUT_CONNECTION_ID);
+
+        final FlowFile flowFile = session.get();
+        assertNotNull(flowFile);
+
+        final Connection outputConnection = mock(Connection.class);
+        
when(outputConnection.getIdentifier()).thenReturn(OUTPUT_CONNECTION_ID);
+        final FlowFileQueue outputFlowFileQueue = mock(FlowFileQueue.class);
+        
when(outputFlowFileQueue.getBackPressureDataSizeThreshold()).thenReturn(BACK_PRESSURE_DATA_SIZE_THRESHOLD);
+        
when(outputConnection.getFlowFileQueue()).thenReturn(outputFlowFileQueue);
+        final QueueSize outputQueueSize = mock(QueueSize.class);
+        when(outputFlowFileQueue.size()).thenReturn(outputQueueSize);
+        
when(outputFlowFileQueue.getFlowFileAvailability()).thenReturn(FlowFileAvailability.FLOWFILE_AVAILABLE);
+
+        final Relationship relationship = new 
Relationship.Builder().name(Relationship.class.getSimpleName()).build();
+        
when(repositoryContext.getConnections(eq(relationship))).thenReturn(List.of(outputConnection));
+        session.transfer(flowFile, relationship);
+
+        
when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn(BACK_PRESSURE_DATA_SIZE_THRESHOLD);
+        final QueueSize queueSize = mock(QueueSize.class);
+        final int objectCount = Integer.MAX_VALUE;
+        when(queueSize.getObjectCount()).thenReturn(objectCount);
+        final long byteCount = Long.MAX_VALUE;
+        when(queueSize.getByteCount()).thenReturn(byteCount);
+        when(flowFileQueue.size()).thenReturn(queueSize);
+        
when(flowFileQueue.getLoadBalanceStrategy()).thenReturn(LoadBalanceStrategy.ROUND_ROBIN);
+        
when(flowFileQueue.getFlowFileAvailability()).thenReturn(FlowFileAvailability.FLOWFILE_AVAILABLE);
+
+        session.commit();
+
+        verify(repositoryContext, 
times(2)).recordConnectionStatusEvent(connectionStatusEventCaptor.capture());
+        final List<ConnectionStatusEvent> events = 
connectionStatusEventCaptor.getAllValues();
+
+        final ConnectionStatusEvent firstConnectionStatusEvent = 
events.getFirst();
+        final ComponentMetricContext componentMetricContext = 
firstConnectionStatusEvent.getComponentMetricContext();
+        assertEquals(INPUT_CONNECTION_ID, componentMetricContext.id());
+        assertEquals(BACK_PRESSURE_BYTES_THRESHOLD, 
firstConnectionStatusEvent.getBackPressureBytesThreshold());
+        assertEquals(objectCount, firstConnectionStatusEvent.getQueuedCount());
+        assertEquals(byteCount, firstConnectionStatusEvent.getQueuedBytes());
+        assertEquals(LoadBalanceStatus.LOAD_BALANCE_INACTIVE, 
firstConnectionStatusEvent.getLoadBalanceStatus());
+
+        final ConnectionStatusEvent secondConnectionStatusEvent = 
events.getLast();
+        final ComponentMetricContext secondComponentMetricContext = 
secondConnectionStatusEvent.getComponentMetricContext();
+        assertEquals(OUTPUT_CONNECTION_ID, secondComponentMetricContext.id());
+    }
+
+    @Test
+    void testBatchedCheckpointRetainsConnectionMetricContext() {
+        setRepositoryContext();
+        
when(repositoryContext.getContentRepository()).thenReturn(contentRepository);
+        
when(repositoryContext.isRecordConnectionStatusEventEnabled()).thenReturn(true);
+
+        final Connection connection = mock(Connection.class);
+        
when(repositoryContext.getPollableConnections()).thenReturn(List.of(connection));
+        final FlowFileRecord flowFileRecord = mock(FlowFileRecord.class);
+        when(connection.poll(anySet())).thenReturn(flowFileRecord);
+        final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+        when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+        when(connection.getIdentifier()).thenReturn(INPUT_CONNECTION_ID);
+        when(connection.getName()).thenReturn("Connection Name");
+
+        final FlowFile flowFile = session.get();
+        assertNotNull(flowFile);
+
+        
when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn(BACK_PRESSURE_DATA_SIZE_THRESHOLD);
+        final QueueSize queueSize = mock(QueueSize.class);
+        when(flowFileQueue.size()).thenReturn(queueSize);
+        
when(flowFileQueue.getLoadBalanceStrategy()).thenReturn(LoadBalanceStrategy.DO_NOT_LOAD_BALANCE);
+        
when(flowFileQueue.getFlowFileAvailability()).thenReturn(FlowFileAvailability.FLOWFILE_AVAILABLE);
+
+        session.remove(flowFile);
+        session.checkpoint();
+        session.commit();
+
+        verify(repositoryContext, 
times(1)).recordConnectionStatusEvent(connectionStatusEventCaptor.capture());
+        final ConnectionStatusEvent connectionStatusEvent = 
connectionStatusEventCaptor.getValue();
+        assertEquals("Connection Name", 
connectionStatusEvent.getComponentMetricContext().name());
+    }
+
+    @Test
+    void testMigrateTracksConnectionStatusEventForNewOwner() {
+        setRepositoryContext();
+        
when(repositoryContext.getContentRepository()).thenReturn(contentRepository);
+        
when(repositoryContext.isRecordConnectionStatusEventEnabled()).thenReturn(true);
+
+        final Connection connection = mock(Connection.class);
+        
when(repositoryContext.getPollableConnections()).thenReturn(List.of(connection));
+        final FlowFileRecord flowFileRecord = mock(FlowFileRecord.class);
+        when(connection.poll(anySet())).thenReturn(flowFileRecord);
+        final FlowFileQueue flowFileQueue = mock(FlowFileQueue.class);
+        when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+        when(connection.getIdentifier()).thenReturn(INPUT_CONNECTION_ID);
+        when(flowFileQueue.getIdentifier()).thenReturn(INPUT_CONNECTION_ID);
+
+        final FlowFile flowFile = session.get();
+        assertNotNull(flowFile);
+
+        final StandardProcessSession newOwner = new 
StandardProcessSession(repositoryContext, taskTermination, performanceTracker);
+        session.migrate(newOwner);
+
+        
when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn(BACK_PRESSURE_DATA_SIZE_THRESHOLD);
+        final QueueSize queueSize = mock(QueueSize.class);
+        final int objectCount = Integer.MAX_VALUE;
+        when(queueSize.getObjectCount()).thenReturn(objectCount);
+        final long byteCount = Long.MAX_VALUE;
+        when(queueSize.getByteCount()).thenReturn(byteCount);
+        when(flowFileQueue.size()).thenReturn(queueSize);
+        
when(flowFileQueue.getLoadBalanceStrategy()).thenReturn(LoadBalanceStrategy.DO_NOT_LOAD_BALANCE);
+        
when(flowFileQueue.getFlowFileAvailability()).thenReturn(FlowFileAvailability.FLOWFILE_AVAILABLE);
+
+        newOwner.remove(flowFile);
+        newOwner.commit();
+
+        verify(repositoryContext, 
times(1)).recordConnectionStatusEvent(connectionStatusEventCaptor.capture());
+        final ConnectionStatusEvent connectionStatusEvent = 
connectionStatusEventCaptor.getValue();
+        assertEquals(INPUT_CONNECTION_ID, 
connectionStatusEvent.getComponentMetricContext().id());
+        assertEquals(BACK_PRESSURE_BYTES_THRESHOLD, 
connectionStatusEvent.getBackPressureBytesThreshold());
+        assertEquals(objectCount, connectionStatusEvent.getQueuedCount());
+        assertEquals(byteCount, connectionStatusEvent.getQueuedBytes());
+        assertEquals(LoadBalanceStatus.LOAD_BALANCE_NOT_CONFIGURED, 
connectionStatusEvent.getLoadBalanceStatus());
+        assertEquals(FlowFileAvailability.FLOWFILE_AVAILABLE, 
connectionStatusEvent.getFlowFileAvailability());
+    }
+
     @Test
     void testExportToPathFlowFileEventBytes() throws IOException {
         setRepositoryContext();


Reply via email to