/*
* (C) Copyright 2013-2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Benoit Delbosc
* Florent Guillaume
*/
package org.nuxeo.ecm.core.work;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.nuxeo.ecm.core.work.api.Work;
import org.nuxeo.ecm.core.work.api.WorkQueueMetrics;
/**
* Memory-based {@link BlockingQueue}.
* <p>
* In addition, this implementation also keeps a set of {@link Work} ids in the queue when the queue elements are
* {@link WorkHolder}s.
*/
public class MemoryBlockingQueue extends NuxeoBlockingQueue {
/**
* A {@link LinkedBlockingQueue} that blocks on {@link #offer} and prevents starvation deadlocks on reentrant calls.
*/
private static class ReentrantLinkedBlockingQueue<T> extends LinkedBlockingQueue<T> {
private static final long serialVersionUID = 1L;
private final ReentrantLock limitedPutLock = new ReentrantLock();
private final int limitedCapacity;
/**
* Creates a {@link LinkedBlockingQueue} with a maximum capacity.
* <p>
* If the capacity is -1 then this is treated as a regular unbounded {@link LinkedBlockingQueue}.
*
* @param capacity the capacity, or -1 for unbounded
*/
public ReentrantLinkedBlockingQueue(int capacity) {
// Allocate more space to prevent starvation dead lock
// because a worker can add a new job to the queue.
super(capacity < 0 ? Integer.MAX_VALUE : (2 * capacity));
limitedCapacity = capacity;
}
/**
* Block until there are enough remaining capacity to put the entry.
*/
public void limitedPut(T e) throws InterruptedException {
limitedPutLock.lockInterruptibly();
try {
while (remainingCapacity() < limitedCapacity) {
// TODO replace by wakeup when an element is removed
Thread.sleep(100);
}
put(e);
} finally {
limitedPutLock.unlock();
}
}
@Override
public boolean offer(T e) {
if (limitedCapacity < 0) {
return super.offer(e);
}
// turn non-blocking offer into a blocking put
try {
if (Thread.currentThread()
.getName()
.startsWith(WorkManagerImpl.THREAD_PREFIX)) {
// use the full queue capacity for reentrant call
put(e);
} else {
// put only if there are enough remaining capacity
limitedPut(e);
}
return true;
} catch (InterruptedException ie) {
Thread.currentThread()
.interrupt();
throw new RuntimeException("interrupted", ie);
}
}
}
protected final BlockingQueue<Runnable> queue;
protected final Map<String, Work> works = new HashMap<>();
protected final Set<String> scheduledWorks = new HashSet<>();
protected final Set<String> runningWorks = new HashSet<>();
long scheduledCount;
long runningCount;
long completedCount;
long cancelledCount;
/**
* Creates a {@link BlockingQueue} with a maximum capacity.
* <p>
* If the capacity is -1 then this is treated as a regular unbounded {@link LinkedBlockingQueue}.
*
* @param capacity the capacity, or -1 for unbounded
*/
public MemoryBlockingQueue(String id, MemoryWorkQueuing queuing, int capacity) {
super(id, queuing);
queue = new ReentrantLinkedBlockingQueue<>(capacity);
}
@Override
synchronized protected WorkQueueMetrics metrics() {
return new WorkQueueMetrics(queueId, scheduledCount, runningCount, completedCount, cancelledCount);
}
@Override
public int getQueueSize() {
return queue.size();
}
@Override
public void putElement(Runnable r) throws InterruptedException {
queue.put(r);
}
@Override
public Runnable pollElement() {
Runnable r = queue.poll();
return r;
}
@Override
public Runnable take() throws InterruptedException {
Runnable r = queue.take();
if (anotherWorkIsAlreadyRunning(r)) {
// reschedule the work so it does not run concurrently
offer(r);
// take a break we don't want to take too much CPU looping on the same message.
Thread.sleep(100);
return null;
}
return r;
}
private boolean anotherWorkIsAlreadyRunning(Runnable r) throws InterruptedException {
Work work = WorkHolder.getWork(r);
String id = work.getId();
if (runningWorks.contains(id)) {
return true;
}
return false;
}
@Override
public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
nanos = awaitActivation(nanos);
if (nanos <= 0) {
return null;
}
return queue.poll(nanos, TimeUnit.NANOSECONDS);
}
synchronized WorkQueueMetrics workSchedule(Work work) {
String id = work.getId();
if (scheduledWorks.contains(id)) {
return metrics();
}
if (!offer(new WorkHolder(work))) {
return metrics();
}
works.put(id, work);
scheduledWorks.add(id);
scheduledCount += 1;
return metrics();
}
synchronized WorkQueueMetrics workRunning(Work work) {
String id = work.getId();
scheduledWorks.remove(id);
works.put(id, work); // update state
runningWorks.add(id);
scheduledCount -= 1;
runningCount += 1;
return metrics();
}
synchronized WorkQueueMetrics workCanceled(Work work) {
String id = work.getId();
for (Iterator<Runnable> it = queue.iterator(); it.hasNext();) {
if (id.equals(WorkHolder.getWork(it.next())
.getId())) {
it.remove();
scheduledWorks.remove(id);
works.remove(id);
scheduledCount -= 1;
cancelledCount +=1 ;
break;
}
}
return metrics();
}
synchronized WorkQueueMetrics workCompleted(Work work) {
String id = work.getId();
if (runningWorks.remove(id) && !scheduledWorks.contains(id)) {
works.remove(id);
}
runningCount -= 1;
completedCount += 1;
return metrics();
}
synchronized WorkQueueMetrics workRescheduleRunning(Work work) {
String id = work.getId();
if (!runningWorks.remove(id)) {
return metrics();
}
works.remove(id);
runningCount -= 1;
return workSchedule(work);
}
synchronized Work lookup(String workId) {
return works.get(workId);
}
synchronized List<Work> list() {
return new ArrayList<>(works.values());
}
synchronized List<String> keys() {
return new ArrayList<>(works.keySet());
}
synchronized List<Work> listScheduled() {
return scheduledWorks.stream()
.map(works::get)
.collect(Collectors.toList());
}
synchronized List<String> scheduledKeys() {
return new ArrayList<>(scheduledWorks);
}
synchronized List<Work> listRunning() {
return runningWorks.stream()
.map(works::get)
.collect(Collectors.toList());
}
synchronized List<String> runningKeys() {
return new ArrayList<>(runningWorks);
}
}