mirror of https://github.com/apache/nifi.git
NIFI-11289: Avoid obtaining read locks on queues when fetching Group Status, except in those few specific situations where it's needed.
This closes #7046 Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
parent
776f180d15
commit
450a46bc58
|
@ -93,8 +93,9 @@ public abstract class AbstractEventAccess implements EventAccess {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ProcessGroupStatus getGroupStatus(final String groupId) {
|
public ProcessGroupStatus getGroupStatus(final String groupId) {
|
||||||
final RepositoryStatusReport repoStatusReport = generateRepositoryStatusReport();
|
final RepositoryStatusReport statusReport = generateRepositoryStatusReport();
|
||||||
return getGroupStatus(groupId, repoStatusReport);
|
final ProcessGroup group = flowManager.getGroup(groupId);
|
||||||
|
return getGroupStatus(group, statusReport, authorizable -> true, Integer.MAX_VALUE, 1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,7 +111,7 @@ public abstract class AbstractEventAccess implements EventAccess {
|
||||||
final ProcessGroup group = flowManager.getGroup(groupId);
|
final ProcessGroup group = flowManager.getGroup(groupId);
|
||||||
|
|
||||||
// this was invoked with no user context so the results will be unfiltered... necessary for aggregating status history
|
// this was invoked with no user context so the results will be unfiltered... necessary for aggregating status history
|
||||||
return getGroupStatus(group, statusReport, authorizable -> true, Integer.MAX_VALUE, 1);
|
return getGroupStatus(group, statusReport, authorizable -> true, Integer.MAX_VALUE, 1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected RepositoryStatusReport generateRepositoryStatusReport() {
|
protected RepositoryStatusReport generateRepositoryStatusReport() {
|
||||||
|
@ -128,10 +129,11 @@ public abstract class AbstractEventAccess implements EventAccess {
|
||||||
* @param isAuthorized is authorized check
|
* @param isAuthorized is authorized check
|
||||||
* @param recursiveStatusDepth the number of levels deep we should recurse and still include the the processors' statuses, the groups' statuses, etc. in the returned ProcessGroupStatus
|
* @param recursiveStatusDepth the number of levels deep we should recurse and still include the the processors' statuses, the groups' statuses, etc. in the returned ProcessGroupStatus
|
||||||
* @param currentDepth the current number of levels deep that we have recursed
|
* @param currentDepth the current number of levels deep that we have recursed
|
||||||
|
* @param includeConnectionDetails whether or not to include the details of the connections that may be expensive to calculate and/or require locks be obtained
|
||||||
* @return the component status
|
* @return the component status
|
||||||
*/
|
*/
|
||||||
ProcessGroupStatus getGroupStatus(final ProcessGroup group, final RepositoryStatusReport statusReport, final Predicate<Authorizable> isAuthorized,
|
ProcessGroupStatus getGroupStatus(final ProcessGroup group, final RepositoryStatusReport statusReport, final Predicate<Authorizable> isAuthorized,
|
||||||
final int recursiveStatusDepth, final int currentDepth) {
|
final int recursiveStatusDepth, final int currentDepth, final boolean includeConnectionDetails) {
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -186,14 +188,14 @@ public abstract class AbstractEventAccess implements EventAccess {
|
||||||
for (final ProcessGroup childGroup : group.getProcessGroups()) {
|
for (final ProcessGroup childGroup : group.getProcessGroups()) {
|
||||||
final ProcessGroupStatus childGroupStatus;
|
final ProcessGroupStatus childGroupStatus;
|
||||||
if (populateChildStatuses) {
|
if (populateChildStatuses) {
|
||||||
childGroupStatus = getGroupStatus(childGroup, statusReport, isAuthorized, recursiveStatusDepth, currentDepth + 1);
|
childGroupStatus = getGroupStatus(childGroup, statusReport, isAuthorized, recursiveStatusDepth, currentDepth + 1, includeConnectionDetails);
|
||||||
localChildGroupStatusCollection.add(childGroupStatus);
|
localChildGroupStatusCollection.add(childGroupStatus);
|
||||||
} else {
|
} else {
|
||||||
// In this case, we don't want to include any of the recursive components' individual statuses. As a result, we can
|
// In this case, we don't want to include any of the recursive components' individual statuses. As a result, we can
|
||||||
// avoid performing any sort of authorizations. Because we only care about the numbers that come back, we can just indicate
|
// avoid performing any sort of authorizations. Because we only care about the numbers that come back, we can just indicate
|
||||||
// that the user is not authorized. This allows us to avoid the expense of both performing the authorization and calculating
|
// that the user is not authorized. This allows us to avoid the expense of both performing the authorization and calculating
|
||||||
// things that we would otherwise need to calculate if the user were in fact authorized.
|
// things that we would otherwise need to calculate if the user were in fact authorized.
|
||||||
childGroupStatus = getGroupStatus(childGroup, statusReport, authorizable -> false, recursiveStatusDepth, currentDepth + 1);
|
childGroupStatus = getGroupStatus(childGroup, statusReport, authorizable -> false, recursiveStatusDepth, currentDepth + 1, includeConnectionDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
activeGroupThreads += childGroupStatus.getActiveThreadCount();
|
activeGroupThreads += childGroupStatus.getActiveThreadCount();
|
||||||
|
@ -252,9 +254,14 @@ public abstract class AbstractEventAccess implements EventAccess {
|
||||||
connStatus.setDestinationName(isDestinationAuthorized ? conn.getDestination().getName() : conn.getDestination().getIdentifier());
|
connStatus.setDestinationName(isDestinationAuthorized ? conn.getDestination().getName() : conn.getDestination().getIdentifier());
|
||||||
connStatus.setBackPressureDataSizeThreshold(conn.getFlowFileQueue().getBackPressureDataSizeThreshold());
|
connStatus.setBackPressureDataSizeThreshold(conn.getFlowFileQueue().getBackPressureDataSizeThreshold());
|
||||||
connStatus.setBackPressureObjectThreshold(conn.getFlowFileQueue().getBackPressureObjectThreshold());
|
connStatus.setBackPressureObjectThreshold(conn.getFlowFileQueue().getBackPressureObjectThreshold());
|
||||||
|
if (includeConnectionDetails) {
|
||||||
connStatus.setTotalQueuedDuration(conn.getFlowFileQueue().getTotalQueuedDuration(now));
|
connStatus.setTotalQueuedDuration(conn.getFlowFileQueue().getTotalQueuedDuration(now));
|
||||||
long minLastQueueDate = conn.getFlowFileQueue().getMinLastQueueDate();
|
long minLastQueueDate = conn.getFlowFileQueue().getMinLastQueueDate();
|
||||||
connStatus.setMaxQueuedDuration(minLastQueueDate == 0 ? 0 : now - minLastQueueDate);
|
connStatus.setMaxQueuedDuration(minLastQueueDate == 0 ? 0 : now - minLastQueueDate);
|
||||||
|
} else {
|
||||||
|
connStatus.setTotalQueuedDuration(0L);
|
||||||
|
connStatus.setMaxQueuedDuration(0L);
|
||||||
|
}
|
||||||
connStatus.setFlowFileAvailability(conn.getFlowFileQueue().getFlowFileAvailability());
|
connStatus.setFlowFileAvailability(conn.getFlowFileQueue().getFlowFileAvailability());
|
||||||
|
|
||||||
final FlowFileEvent connectionStatusReport = statusReport.getReportEntry(conn.getIdentifier());
|
final FlowFileEvent connectionStatusReport = statusReport.getReportEntry(conn.getIdentifier());
|
||||||
|
@ -665,7 +672,11 @@ public abstract class AbstractEventAccess implements EventAccess {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ProcessGroupStatus getControllerStatus() {
|
public ProcessGroupStatus getControllerStatus() {
|
||||||
return getGroupStatus(flowManager.getRootGroupId());
|
final String rootGroupId = flowManager.getRootGroupId();
|
||||||
|
final ProcessGroup group = flowManager.getGroup(rootGroupId);
|
||||||
|
final RepositoryStatusReport statusReport = generateRepositoryStatusReport();
|
||||||
|
|
||||||
|
return getGroupStatus(group, statusReport, authorizable -> true, Integer.MAX_VALUE, 1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -64,7 +64,7 @@ public class SwappablePriorityQueue {
|
||||||
private final EventReporter eventReporter;
|
private final EventReporter eventReporter;
|
||||||
private final FlowFileQueue flowFileQueue;
|
private final FlowFileQueue flowFileQueue;
|
||||||
private final DropFlowFileAction dropAction;
|
private final DropFlowFileAction dropAction;
|
||||||
private final List<FlowFilePrioritizer> priorities = new ArrayList<>();
|
private volatile List<FlowFilePrioritizer> priorities = new ArrayList<>();
|
||||||
private final String swapPartitionName;
|
private final String swapPartitionName;
|
||||||
|
|
||||||
private final List<String> swapLocations = new ArrayList<>();
|
private final List<String> swapLocations = new ArrayList<>();
|
||||||
|
@ -85,6 +85,7 @@ public class SwappablePriorityQueue {
|
||||||
private PriorityQueue<FlowFileRecord> activeQueue;
|
private PriorityQueue<FlowFileRecord> activeQueue;
|
||||||
private ArrayList<FlowFileRecord> swapQueue;
|
private ArrayList<FlowFileRecord> swapQueue;
|
||||||
private boolean swapMode = false;
|
private boolean swapMode = false;
|
||||||
|
private volatile long topPenaltyExpiration = -1L;
|
||||||
|
|
||||||
// The following members are used to keep metrics in memory for reporting purposes so that we don't have to constantly
|
// The following members are used to keep metrics in memory for reporting purposes so that we don't have to constantly
|
||||||
// read these values from swap files on disk.
|
// read these values from swap files on disk.
|
||||||
|
@ -113,19 +114,13 @@ public class SwappablePriorityQueue {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FlowFilePrioritizer> getPriorities() {
|
public List<FlowFilePrioritizer> getPriorities() {
|
||||||
readLock.lock();
|
|
||||||
try {
|
|
||||||
return Collections.unmodifiableList(priorities);
|
return Collections.unmodifiableList(priorities);
|
||||||
} finally {
|
|
||||||
readLock.unlock("getPriorities");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPriorities(final List<FlowFilePrioritizer> newPriorities) {
|
public void setPriorities(final List<FlowFilePrioritizer> newPriorities) {
|
||||||
writeLock.lock();
|
writeLock.lock();
|
||||||
try {
|
try {
|
||||||
priorities.clear();
|
this.priorities = new ArrayList<>(newPriorities);
|
||||||
priorities.addAll(newPriorities);
|
|
||||||
|
|
||||||
final PriorityQueue<FlowFileRecord> newQueue = new PriorityQueue<>(Math.max(20, activeQueue.size()), new QueuePrioritizer(newPriorities));
|
final PriorityQueue<FlowFileRecord> newQueue = new PriorityQueue<>(Math.max(20, activeQueue.size()), new QueuePrioritizer(newPriorities));
|
||||||
newQueue.addAll(activeQueue);
|
newQueue.addAll(activeQueue);
|
||||||
|
@ -443,52 +438,12 @@ public class SwappablePriorityQueue {
|
||||||
|
|
||||||
public FlowFileAvailability getFlowFileAvailability() {
|
public FlowFileAvailability getFlowFileAvailability() {
|
||||||
// If queue is empty, avoid obtaining a lock.
|
// If queue is empty, avoid obtaining a lock.
|
||||||
final FlowFileQueueSize queueSize = getFlowFileQueueSize();
|
if (isActiveQueueEmpty()) {
|
||||||
if (queueSize.getActiveCount() == 0 && queueSize.getSwappedCount() == 0) {
|
|
||||||
return FlowFileAvailability.ACTIVE_QUEUE_EMPTY;
|
return FlowFileAvailability.ACTIVE_QUEUE_EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean mustMigrateSwapToActive = false;
|
final long expiration = topPenaltyExpiration;
|
||||||
FlowFileRecord top;
|
if (expiration > 0 && expiration > System.currentTimeMillis()) { // compare against 0 to avoid unnecessary System call
|
||||||
readLock.lock();
|
|
||||||
try {
|
|
||||||
top = activeQueue.peek();
|
|
||||||
if (top == null) {
|
|
||||||
if (swapQueue.isEmpty() && queueSize.getSwapFileCount() > 0) {
|
|
||||||
// Nothing available in the active queue or swap queue, but there is data swapped out.
|
|
||||||
// We need to trigger that data to be swapped back in. But to do this, we need to hold the write lock.
|
|
||||||
// Because we cannot obtain the write lock while already holding the read lock, we set a flag so that we
|
|
||||||
// can migrate swap to active queue only after we've released the read lock.
|
|
||||||
mustMigrateSwapToActive = true;
|
|
||||||
} else if (swapQueue.isEmpty()) {
|
|
||||||
return FlowFileAvailability.ACTIVE_QUEUE_EMPTY;
|
|
||||||
} else {
|
|
||||||
top = swapQueue.get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
readLock.unlock("isFlowFileAvailable");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we need to migrate swapped data to the active queue, we can do that now that the read lock has been released.
|
|
||||||
// There may well be multiple threads attempting this concurrently, though, so only use tryLock() and if the lock
|
|
||||||
// is not obtained, the other thread can swap data in, or the next iteration of #getFlowFileAvailability will.
|
|
||||||
if (mustMigrateSwapToActive) {
|
|
||||||
final boolean lockObtained = writeLock.tryLock();
|
|
||||||
if (lockObtained) {
|
|
||||||
try {
|
|
||||||
migrateSwapToActive();
|
|
||||||
} finally {
|
|
||||||
writeLock.unlock("getFlowFileAvailability");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (top == null) {
|
|
||||||
return FlowFileAvailability.ACTIVE_QUEUE_EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (top.isPenalized()) {
|
|
||||||
return FlowFileAvailability.HEAD_OF_QUEUE_PENALIZED;
|
return FlowFileAvailability.HEAD_OF_QUEUE_PENALIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -525,6 +480,7 @@ public class SwappablePriorityQueue {
|
||||||
activeQueue.add(flowFile);
|
activeQueue.add(flowFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTopPenaltyExpiration();
|
||||||
logger.trace("{} put to {}", flowFile, this);
|
logger.trace("{} put to {}", flowFile, this);
|
||||||
} finally {
|
} finally {
|
||||||
writeLock.unlock("put(FlowFileRecord)");
|
writeLock.unlock("put(FlowFileRecord)");
|
||||||
|
@ -550,6 +506,7 @@ public class SwappablePriorityQueue {
|
||||||
activeQueue.addAll(flowFiles);
|
activeQueue.addAll(flowFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTopPenaltyExpiration();
|
||||||
logger.trace("{} put to {}", flowFiles, this);
|
logger.trace("{} put to {}", flowFiles, this);
|
||||||
} finally {
|
} finally {
|
||||||
writeLock.unlock("putAll");
|
writeLock.unlock("putAll");
|
||||||
|
@ -573,6 +530,8 @@ public class SwappablePriorityQueue {
|
||||||
unacknowledge(1, flowFile.getSize());
|
unacknowledge(1, flowFile.getSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTopPenaltyExpiration();
|
||||||
|
|
||||||
return flowFile;
|
return flowFile;
|
||||||
} finally {
|
} finally {
|
||||||
writeLock.unlock("poll(Set)");
|
writeLock.unlock("poll(Set)");
|
||||||
|
@ -624,6 +583,7 @@ public class SwappablePriorityQueue {
|
||||||
writeLock.lock();
|
writeLock.lock();
|
||||||
try {
|
try {
|
||||||
doPoll(records, maxResults, expiredRecords, expirationMillis, pollStrategy);
|
doPoll(records, maxResults, expiredRecords, expirationMillis, pollStrategy);
|
||||||
|
updateTopPenaltyExpiration();
|
||||||
} finally {
|
} finally {
|
||||||
writeLock.unlock("poll(int, Set)");
|
writeLock.unlock("poll(int, Set)");
|
||||||
}
|
}
|
||||||
|
@ -704,12 +664,25 @@ public class SwappablePriorityQueue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTopPenaltyExpiration();
|
||||||
|
|
||||||
return selectedFlowFiles;
|
return selectedFlowFiles;
|
||||||
} finally {
|
} finally {
|
||||||
writeLock.unlock("poll(Filter, Set)");
|
writeLock.unlock("poll(Filter, Set)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MUST be called while holding read lock or write lock
|
||||||
|
private void updateTopPenaltyExpiration() {
|
||||||
|
final FlowFileRecord top = activeQueue.peek();
|
||||||
|
if (top == null) {
|
||||||
|
topPenaltyExpiration = -1L;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
topPenaltyExpiration = top.getPenaltyExpirationMillis();
|
||||||
|
}
|
||||||
|
|
||||||
private void doPoll(final List<FlowFileRecord> records, int maxResults, final Set<FlowFileRecord> expiredRecords, final long expirationMillis, final PollStrategy pollStrategy) {
|
private void doPoll(final List<FlowFileRecord> records, int maxResults, final Set<FlowFileRecord> expiredRecords, final long expirationMillis, final PollStrategy pollStrategy) {
|
||||||
migrateSwapToActive();
|
migrateSwapToActive();
|
||||||
|
|
||||||
|
@ -997,6 +970,7 @@ public class SwappablePriorityQueue {
|
||||||
|
|
||||||
incrementSwapQueueSize(swapFlowFileCount, swapByteCount, swapLocations.size());
|
incrementSwapQueueSize(swapFlowFileCount, swapByteCount, swapLocations.size());
|
||||||
this.swapLocations.addAll(swapLocations);
|
this.swapLocations.addAll(swapLocations);
|
||||||
|
updateTopPenaltyExpiration();
|
||||||
} finally {
|
} finally {
|
||||||
writeLock.unlock("Recover Swap Files");
|
writeLock.unlock("Recover Swap Files");
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,7 @@ public class StandardEventAccess extends AbstractEventAccess implements UserAwar
|
||||||
final ProcessGroup group = flowManager.getGroup(groupId);
|
final ProcessGroup group = flowManager.getGroup(groupId);
|
||||||
|
|
||||||
// on demand status request for a specific user... require authorization per component and filter results as appropriate
|
// on demand status request for a specific user... require authorization per component and filter results as appropriate
|
||||||
return getGroupStatus(group, statusReport, authorizable -> authorizable.isAuthorized(this.authorizer, RequestAction.READ, user), Integer.MAX_VALUE, 1);
|
return getGroupStatus(group, statusReport, authorizable -> authorizable.isAuthorized(this.authorizer, RequestAction.READ, user), Integer.MAX_VALUE, 1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -144,6 +144,6 @@ public class StandardEventAccess extends AbstractEventAccess implements UserAwar
|
||||||
final ProcessGroup group = flowManager.getGroup(groupId);
|
final ProcessGroup group = flowManager.getGroup(groupId);
|
||||||
|
|
||||||
// on demand status request for a specific user... require authorization per component and filter results as appropriate
|
// on demand status request for a specific user... require authorization per component and filter results as appropriate
|
||||||
return getGroupStatus(group, statusReport, authorizable -> authorizable.isAuthorized(this.authorizer, RequestAction.READ, user), recursiveStatusDepth, 1);
|
return getGroupStatus(group, statusReport, authorizable -> authorizable.isAuthorized(this.authorizer, RequestAction.READ, user), recursiveStatusDepth, 1, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,9 +153,28 @@ public class TestSocketLoadBalancedFlowFileQueue {
|
||||||
assertFalse(queue.isEmpty());
|
assertFalse(queue.isEmpty());
|
||||||
assertSame(FlowFileAvailability.HEAD_OF_QUEUE_PENALIZED, queue.getFlowFileAvailability());
|
assertSame(FlowFileAvailability.HEAD_OF_QUEUE_PENALIZED, queue.getFlowFileAvailability());
|
||||||
|
|
||||||
|
// Adjust the penalty expiration so that it's not longer penalized.
|
||||||
|
// This will not change the FlowFile Availability, however, because it has already stored the
|
||||||
|
// Penalty expiration date elsewhere. To trigger that to change, we need to add something to the queue
|
||||||
|
// or remove something. We don't want to remove the data yet, so we add a new FlowFile.
|
||||||
penalizedFlowFile.setPenaltyExpiration(System.currentTimeMillis() - 1);
|
penalizedFlowFile.setPenaltyExpiration(System.currentTimeMillis() - 1);
|
||||||
|
final MockFlowFileRecord readyFlowFile = new MockFlowFileRecord(1);
|
||||||
|
queue.put(readyFlowFile);
|
||||||
|
|
||||||
assertFalse(queue.isEmpty());
|
assertFalse(queue.isEmpty());
|
||||||
assertSame(FlowFileAvailability.FLOWFILE_AVAILABLE, queue.getFlowFileAvailability());
|
assertSame(FlowFileAvailability.FLOWFILE_AVAILABLE, queue.getFlowFileAvailability());
|
||||||
|
assertSame(penalizedFlowFile, queue.poll(Collections.emptySet()));
|
||||||
|
|
||||||
|
assertFalse(queue.isEmpty());
|
||||||
|
assertSame(FlowFileAvailability.FLOWFILE_AVAILABLE, queue.getFlowFileAvailability());
|
||||||
|
assertSame(readyFlowFile, queue.poll(Collections.emptySet()));
|
||||||
|
|
||||||
|
assertTrue(queue.isActiveQueueEmpty());
|
||||||
|
assertSame(FlowFileAvailability.ACTIVE_QUEUE_EMPTY, queue.getFlowFileAvailability());
|
||||||
|
|
||||||
|
queue.acknowledge(penalizedFlowFile);
|
||||||
|
queue.acknowledge(readyFlowFile);
|
||||||
|
assertTrue(queue.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in New Issue