NIFI-8667: When marking a Controller Service as enabled, ensure that we release the write lock before calling validation methods of referencing components. Otherwise, we can encounter a deadlock.
Signed-off-by: Matthew Burgess <mattyb149@apache.org>
This closes #5134
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java
index ae19388..afcfec4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/ServiceStateTransition.java
@@ -77,18 +77,28 @@
logger.debug("{} transitioned to ENABLED", controllerServiceNode);
enabledFutures.forEach(future -> future.complete(null));
-
- final List<ComponentNode> referencingComponents = controllerServiceReference.findRecursiveReferences(ComponentNode.class);
- for (final ComponentNode component : referencingComponents) {
- component.performValidation();
- }
-
- stateChangeCondition.signalAll();
-
- return true;
} finally {
writeLock.unlock();
}
+
+ // We want to perform validation against other components outside of the write lock. Component validation could be expensive
+ // and more importantly could reference other controller services, which could result in a dead lock if we run the validation
+ // while holding this.writeLock.
+ final List<ComponentNode> referencingComponents = controllerServiceReference.findRecursiveReferences(ComponentNode.class);
+ for (final ComponentNode component : referencingComponents) {
+ component.performValidation();
+ }
+
+ // Now that the write lock was relinquished in order to perform validation on referencing components, we must re-acquire it
+ // in order to signal that a state change has completed.
+ writeLock.lock();
+ try {
+ stateChangeCondition.signalAll();
+ } finally {
+ writeLock.unlock();
+ }
+
+ return true;
}
public boolean transitionToDisabling(final ControllerServiceState expectedState, final CompletableFuture<?> disabledFuture) {