// Copyright 2017 JanusGraph Authors
//
// 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.
package org.janusgraph.graphdb.database.idassigner;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.janusgraph.core.JanusGraphException;
import org.janusgraph.diskstorage.BackendException;
import org.janusgraph.diskstorage.IDBlock;
import org.janusgraph.diskstorage.IDAuthority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Matthias Broecheler (me@matthiasb.com)
*/
public class StandardIDPool implements IDPool {
private static final Logger log =
LoggerFactory.getLogger(StandardIDPool.class);
private static final IDBlock ID_POOL_EXHAUSTION = new IDBlock() {
@Override
public long numIds() {
throw new UnsupportedOperationException();
}
@Override
public long getId(long index) {
throw new UnsupportedOperationException();
}
};
private static final IDBlock UNINITIALIZED_BLOCK = new IDBlock() {
@Override
public long numIds() {
return 0;
}
@Override
public long getId(long index) {
throw new ArrayIndexOutOfBoundsException(0);
}
};
private static final int RENEW_ID_COUNT = 100;
private final IDAuthority idAuthority;
private final long idUpperBound; //exclusive
private final int partition;
private final int idNamespace;
private final Duration renewTimeout;
private final double renewBufferPercentage;
private IDBlock currentBlock;
private long currentIndex;
private long renewBlockIndex;
// private long nextID;
// private long currentMaxID;
// private long renewBufferID;
private volatile IDBlock nextBlock;
private Future<IDBlock> idBlockFuture;
private IDBlockGetter idBlockGetter;
private final ThreadPoolExecutor exec;
private volatile boolean closed;
private final Queue<Future<?>> closeBlockers;
public StandardIDPool(IDAuthority idAuthority, int partition, int idNamespace, long idUpperBound, Duration renewTimeout, double renewBufferPercentage) {
Preconditions.checkArgument(idUpperBound > 0);
this.idAuthority = idAuthority;
Preconditions.checkArgument(partition>=0);
this.partition = partition;
Preconditions.checkArgument(idNamespace>=0);
this.idNamespace = idNamespace;
this.idUpperBound = idUpperBound;
Preconditions.checkArgument(!renewTimeout.isZero(), "Renew-timeout must be positive");
this.renewTimeout = renewTimeout;
Preconditions.checkArgument(renewBufferPercentage>0.0 && renewBufferPercentage<=1.0,"Renew-buffer percentage must be in (0.0,1.0]");
this.renewBufferPercentage = renewBufferPercentage;
currentBlock = UNINITIALIZED_BLOCK;
currentIndex = 0;
renewBlockIndex = 0;
nextBlock = null;
// daemon=true would probably be fine too
exec = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder()
.setDaemon(false)
.setNameFormat("JanusGraphID(" + partition + ")("+idNamespace+")[%d]")
.build());
//exec.allowCoreThreadTimeOut(false);
//exec.prestartCoreThread();
idBlockFuture = null;
closeBlockers = new ArrayDeque<>(4);
closed = false;
}
private synchronized void waitForIDBlockGetter() throws InterruptedException {
Stopwatch sw = Stopwatch.createStarted();
if (null != idBlockFuture) {
try {
nextBlock = idBlockFuture.get(renewTimeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (ExecutionException e) {
String msg = String.format("ID block allocation on partition(%d)-namespace(%d) failed with an exception in %s",
partition, idNamespace, sw.stop());
throw new JanusGraphException(msg, e);
} catch (TimeoutException e) {
String msg = String.format("ID block allocation on partition(%d)-namespace(%d) timed out in %s",
partition, idNamespace, sw.stop());
// Attempt to cancel the renewer
idBlockGetter.stopRequested();
if (idAuthority.supportsInterruption()) {
idBlockFuture.cancel(true);
} else {
// Attempt to clean one dead element out of closeBlockers every time we append to it
if (!closeBlockers.isEmpty()) {
Future<?> f = closeBlockers.peek();
if (null != f && f.isDone())
closeBlockers.remove();
}
closeBlockers.add(idBlockFuture);
}
throw new JanusGraphException(msg, e);
} catch (CancellationException e) {
String msg = String.format("ID block allocation on partition(%d)-namespace(%d) was cancelled after %s",
partition, idNamespace, sw.stop());
throw new JanusGraphException(msg, e);
} finally {
idBlockFuture = null;
}
// Allow InterruptedException to propagate up the stack
}
}
private synchronized void nextBlock() throws InterruptedException {
assert currentIndex == currentBlock.numIds();
Preconditions.checkState(!closed,"ID Pool has been closed for partition(%s)-namespace(%s) - cannot apply for new id block",
partition,idNamespace);
if (null == nextBlock && null == idBlockFuture) {
startIDBlockGetter();
}
if (null == nextBlock) {
waitForIDBlockGetter();
}
if (nextBlock == ID_POOL_EXHAUSTION)
throw new IDPoolExhaustedException("Exhausted ID Pool for partition(" + partition+")-namespace("+idNamespace+")");
currentBlock = nextBlock;
currentIndex = 0;
log.debug("ID partition({})-namespace({}) acquired block: [{}]", partition, idNamespace, currentBlock);
assert currentBlock.numIds()>0;
nextBlock = null;
assert RENEW_ID_COUNT>0;
renewBlockIndex = Math.max(0,currentBlock.numIds()-Math.max(RENEW_ID_COUNT, Math.round(currentBlock.numIds()*renewBufferPercentage)));
assert renewBlockIndex<currentBlock.numIds() && renewBlockIndex>=currentIndex;
}
@Override
public synchronized long nextID() {
assert currentIndex <= currentBlock.numIds();
if (currentIndex == currentBlock.numIds()) {
try {
nextBlock();
} catch (InterruptedException e) {
throw new JanusGraphException("Could not renew id block due to interruption", e);
}
}
if (currentIndex == renewBlockIndex) {
startIDBlockGetter();
}
long returnId = currentBlock.getId(currentIndex);
currentIndex++;
if (returnId >= idUpperBound) throw new IDPoolExhaustedException("Reached id upper bound of " + idUpperBound);
log.trace("partition({})-namespace({}) Returned id: {}", partition, idNamespace, returnId);
return returnId;
}
@Override
public synchronized void close() {
closed=true;
try {
waitForIDBlockGetter();
} catch (InterruptedException e) {
throw new JanusGraphException("Interrupted while waiting for id renewer thread to finish", e);
}
for (Future<?> closeBlocker : closeBlockers) {
try {
closeBlocker.get();
} catch (InterruptedException e) {
throw new JanusGraphException("Interrupted while waiting for runaway ID renewer task " + closeBlocker, e);
} catch (ExecutionException e) {
log.debug("Runaway ID renewer task completed with exception", e);
}
}
exec.shutdownNow();
}
private synchronized void startIDBlockGetter() {
Preconditions.checkArgument(idBlockFuture == null, idBlockFuture);
if (closed) return; //Don't renew anymore if closed
//Renew buffer
log.debug("Starting id block renewal thread upon {}", currentIndex);
idBlockGetter = new IDBlockGetter(idAuthority, partition, idNamespace, renewTimeout);
idBlockFuture = exec.submit(idBlockGetter);
}
private static class IDBlockGetter implements Callable<IDBlock> {
private final Stopwatch alive;
private final IDAuthority idAuthority;
private final int partition;
private final int idNamespace;
private final Duration renewTimeout;
private volatile boolean stopRequested;
public IDBlockGetter(IDAuthority idAuthority, int partition, int idNamespace, Duration renewTimeout) {
this.idAuthority = idAuthority;
this.partition = partition;
this.idNamespace = idNamespace;
this.renewTimeout = renewTimeout;
this.alive = Stopwatch.createStarted();
}
private void stopRequested()
{
this.stopRequested = true;
}
@Override
public IDBlock call() {
Stopwatch running = Stopwatch.createStarted();
try {
if (stopRequested) {
log.debug("Aborting ID block retrieval on partition({})-namespace({}) after " +
"graceful shutdown was requested, exec time {}, exec+q time {}",
partition, idNamespace, running.stop(), alive.stop());
throw new JanusGraphException("ID block retrieval aborted by caller");
}
IDBlock idBlock = idAuthority.getIDBlock(partition, idNamespace, renewTimeout);
log.debug("Retrieved ID block from authority on partition({})-namespace({}), " +
"exec time {}, exec+q time {}",
partition, idNamespace, running.stop(), alive.stop());
Preconditions.checkArgument(idBlock!=null && idBlock.numIds()>0);
return idBlock;
} catch (BackendException e) {
throw new JanusGraphException("Could not acquire new ID block from storage", e);
} catch (IDPoolExhaustedException e) {
return ID_POOL_EXHAUSTION;
}
}
}
}