
org.tarantool.TarantoolClientImpl Maven / Gradle / Ivy
package org.tarantool;
import org.tarantool.logging.Logger;
import org.tarantool.logging.LoggerFactory;
import org.tarantool.protocol.ProtoUtils;
import org.tarantool.protocol.ReadableViaSelectorChannel;
import org.tarantool.protocol.TarantoolGreeting;
import org.tarantool.protocol.TarantoolPacket;
import org.tarantool.util.StringUtils;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TarantoolClientImpl extends TarantoolBase> implements TarantoolClient {
private static final Logger LOGGER = LoggerFactory.getLogger(TarantoolClientImpl.class);
protected TarantoolClientConfig config;
protected long operationTimeout;
/**
* External.
*/
protected SocketChannelProvider socketProvider;
protected SocketChannel channel;
protected ReadableViaSelectorChannel readChannel;
protected volatile Exception thumbstone;
protected Map> futures;
protected AtomicInteger pendingResponsesCount = new AtomicInteger();
/**
* Write properties.
*/
protected ByteBuffer sharedBuffer;
protected ReentrantLock bufferLock = new ReentrantLock(false);
protected Condition bufferNotEmpty = bufferLock.newCondition();
protected Condition bufferEmpty = bufferLock.newCondition();
protected ByteBuffer writerBuffer;
protected ReentrantLock writeLock = new ReentrantLock(true);
/**
* Interfaces.
*/
protected SyncOps syncOps;
protected FireAndForgetOps fireAndForgetOps;
protected ComposableAsyncOps composableAsyncOps;
/**
* Inner.
*/
protected TarantoolClientStats stats;
protected StateHelper state = new StateHelper(StateHelper.RECONNECT);
protected Thread reader;
protected Thread writer;
protected Thread connector = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
reconnect(thumbstone);
try {
state.awaitReconnection();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
});
public TarantoolClientImpl(String address, TarantoolClientConfig config) {
this(new SingleSocketChannelProviderImpl(address), config);
}
public TarantoolClientImpl(SocketChannelProvider socketProvider, TarantoolClientConfig config) {
initClient(socketProvider, config);
if (socketProvider instanceof ConfigurableSocketChannelProvider) {
ConfigurableSocketChannelProvider configurableProvider = (ConfigurableSocketChannelProvider) socketProvider;
configurableProvider.setConnectionTimeout(config.connectionTimeout);
configurableProvider.setRetriesLimit(config.retryCount);
}
startConnector(config.initTimeoutMillis);
}
private void initClient(SocketChannelProvider socketProvider, TarantoolClientConfig config) {
this.config = config;
this.initialRequestSize = config.defaultRequestSize;
this.operationTimeout = config.operationExpiryTimeMillis;
this.socketProvider = socketProvider;
this.stats = new TarantoolClientStats();
this.futures = new ConcurrentHashMap<>(config.predictedFutures);
this.sharedBuffer = ByteBuffer.allocateDirect(config.sharedBufferSize);
this.writerBuffer = ByteBuffer.allocateDirect(sharedBuffer.capacity());
this.connector.setDaemon(true);
this.connector.setName("Tarantool connector");
this.syncOps = new SyncOps();
this.composableAsyncOps = new ComposableAsyncOps();
this.fireAndForgetOps = new FireAndForgetOps();
if (!config.useNewCall) {
setCallCode(Code.OLD_CALL);
this.syncOps.setCallCode(Code.OLD_CALL);
this.fireAndForgetOps.setCallCode(Code.OLD_CALL);
this.composableAsyncOps.setCallCode(Code.OLD_CALL);
}
}
private void startConnector(long initTimeoutMillis) {
connector.start();
try {
if (!waitAlive(initTimeoutMillis, TimeUnit.MILLISECONDS)) {
CommunicationException e = new CommunicationException(
initTimeoutMillis +
"ms is exceeded when waiting for client initialization. " +
"You could configure init timeout in TarantoolConfig",
thumbstone);
close(e);
throw e;
}
} catch (InterruptedException e) {
close(e);
throw new IllegalStateException(e);
}
}
protected void reconnect(Throwable lastError) {
SocketChannel channel = null;
int retryNumber = 0;
while (!Thread.currentThread().isInterrupted()) {
try {
if (lastError != null) {
LOGGER.warn(() -> {
SocketAddress address = socketProvider.getAddress();
return "Attempt to (re-)connect to Tarantool instance " +
(StringUtils.isBlank(config.username) ? "" : config.username + ":*****@") +
(address == null ? "unknown" : address);
}, lastError
);
}
channel = socketProvider.get(retryNumber++, lastError);
} catch (Exception e) {
closeChannel(channel);
lastError = e;
if (!(e instanceof SocketProviderTransientException)) {
close(e);
return;
}
}
try {
if (channel != null) {
connect(channel);
return;
}
} catch (Exception e) {
closeChannel(channel);
lastError = e;
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}
}
protected void connect(final SocketChannel channel) throws Exception {
try {
TarantoolGreeting greeting = ProtoUtils.connect(channel, config.username, config.password, msgPackLite);
this.serverVersion = greeting.getServerVersion();
} catch (IOException e) {
closeChannel(channel);
throw new CommunicationException("Couldn't connect to tarantool", e);
}
channel.configureBlocking(false);
this.channel = channel;
this.readChannel = new ReadableViaSelectorChannel(channel);
bufferLock.lock();
try {
sharedBuffer.clear();
} finally {
bufferLock.unlock();
}
this.thumbstone = null;
startThreads(channel.socket().getRemoteSocketAddress().toString());
}
protected void startThreads(String threadName) throws InterruptedException {
final CountDownLatch ioThreadStarted = new CountDownLatch(2);
final AtomicInteger leftIoThreads = new AtomicInteger(2);
reader = new Thread(() -> {
ioThreadStarted.countDown();
if (state.acquire(StateHelper.READING)) {
try {
readThread();
} finally {
state.release(StateHelper.READING);
// only last of two IO-threads can signal for reconnection
if (leftIoThreads.decrementAndGet() == 0) {
state.trySignalForReconnection();
}
}
}
});
writer = new Thread(() -> {
ioThreadStarted.countDown();
if (state.acquire(StateHelper.WRITING)) {
try {
writeThread();
} finally {
state.release(StateHelper.WRITING);
// only last of two IO-threads can signal for reconnection
if (leftIoThreads.decrementAndGet() == 0) {
state.trySignalForReconnection();
}
}
}
});
state.release(StateHelper.RECONNECT);
configureThreads(threadName);
reader.start();
writer.start();
ioThreadStarted.await();
}
protected void configureThreads(String threadName) {
reader.setName("Tarantool " + threadName + " reader");
writer.setName("Tarantool " + threadName + " writer");
writer.setPriority(config.writerThreadPriority);
reader.setPriority(config.readerThreadPriority);
}
/**
* Executes an operation with default timeout.
*
* @param code operation code
* @param args operation arguments
*
* @return deferred result
*
* @see #setOperationTimeout(long)
*/
protected Future> exec(Code code, Object... args) {
return doExec(operationTimeout, code, args);
}
/**
* Executes an operation with the given timeout.
* {@code timeoutMillis} will override the default
* timeout. 0 means the limitless operation.
*
* @param timeoutMillis operation timeout
* @param code operation code
* @param args operation arguments
*
* @return deferred result
*/
protected Future> exec(long timeoutMillis, Code code, Object... args) {
return doExec(timeoutMillis, code, args);
}
protected TarantoolOp> doExec(long timeoutMillis, Code code, Object[] args) {
validateArgs(args);
long sid = syncId.incrementAndGet();
TarantoolOp> future = makeNewOperation(timeoutMillis, sid, code, args);
if (isDead(future)) {
return future;
}
futures.put(sid, future);
if (isDead(future)) {
futures.remove(sid);
return future;
}
try {
write(code, sid, null, args);
} catch (Exception e) {
futures.remove(sid);
fail(future, e);
}
return future;
}
protected TarantoolOp> makeNewOperation(long timeoutMillis, long sid, Code code, Object[] args) {
return new TarantoolOp<>(sid, code, args)
.orTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
}
protected synchronized void die(String message, Exception cause) {
if (thumbstone != null) {
return;
}
final CommunicationException error = new CommunicationException(message, cause);
this.thumbstone = error;
while (!futures.isEmpty()) {
Iterator>> iterator = futures.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry> elem = iterator.next();
if (elem != null) {
TarantoolOp> future = elem.getValue();
fail(future, error);
}
iterator.remove();
}
}
pendingResponsesCount.set(0);
bufferLock.lock();
try {
sharedBuffer.clear();
bufferEmpty.signalAll();
} finally {
bufferLock.unlock();
}
stopIO();
}
public void ping() {
syncGet(exec(Code.PING));
}
protected void write(Code code, Long syncId, Long schemaId, Object... args)
throws Exception {
ByteBuffer buffer = ProtoUtils.createPacket(msgPackLite, code, syncId, schemaId, args);
if (directWrite(buffer)) {
return;
}
sharedWrite(buffer);
}
protected void sharedWrite(ByteBuffer buffer) throws InterruptedException, TimeoutException {
long start = System.currentTimeMillis();
if (bufferLock.tryLock(config.writeTimeoutMillis, TimeUnit.MILLISECONDS)) {
try {
int rem = buffer.remaining();
stats.sharedMaxPacketSize = Math.max(stats.sharedMaxPacketSize, rem);
if (rem > initialRequestSize) {
stats.sharedPacketSizeGrowth++;
}
while (sharedBuffer.remaining() < buffer.limit()) {
stats.sharedEmptyAwait++;
long remaining = config.writeTimeoutMillis - (System.currentTimeMillis() - start);
try {
if (remaining < 1 || !bufferEmpty.await(remaining, TimeUnit.MILLISECONDS)) {
stats.sharedEmptyAwaitTimeouts++;
throw new TimeoutException(
config.writeTimeoutMillis +
"ms is exceeded while waiting for empty buffer. " +
"You could configure write timeout it in TarantoolConfig"
);
}
} catch (InterruptedException e) {
throw new CommunicationException("Interrupted", e);
}
}
sharedBuffer.put(buffer);
pendingResponsesCount.incrementAndGet();
bufferNotEmpty.signalAll();
stats.buffered++;
} finally {
bufferLock.unlock();
}
} else {
stats.sharedWriteLockTimeouts++;
throw new TimeoutException(
config.writeTimeoutMillis +
"ms is exceeded while waiting for shared buffer lock. " +
"You could configure write timeout in TarantoolConfig"
);
}
}
private boolean directWrite(ByteBuffer buffer) throws InterruptedException, IOException, TimeoutException {
if (sharedBuffer.capacity() * config.directWriteFactor <= buffer.limit()) {
if (writeLock.tryLock(config.writeTimeoutMillis, TimeUnit.MILLISECONDS)) {
try {
int rem = buffer.remaining();
stats.directMaxPacketSize = Math.max(stats.directMaxPacketSize, rem);
if (rem > initialRequestSize) {
stats.directPacketSizeGrowth++;
}
writeFully(channel, buffer);
stats.directWrite++;
pendingResponsesCount.incrementAndGet();
} finally {
writeLock.unlock();
}
return true;
} else {
stats.directWriteLockTimeouts++;
throw new TimeoutException(
config.writeTimeoutMillis +
"ms is exceeded while waiting for channel lock. " +
"You could configure write timeout in TarantoolConfig"
);
}
}
return false;
}
protected void readThread() {
while (!Thread.currentThread().isInterrupted()) {
try {
TarantoolPacket packet = ProtoUtils.readPacket(readChannel, msgPackLite);
Map headers = packet.getHeaders();
Long syncId = (Long) headers.get(Key.SYNC.getId());
TarantoolOp> future = futures.remove(syncId);
stats.received++;
pendingResponsesCount.decrementAndGet();
complete(packet, future);
} catch (Exception e) {
die("Cant read answer", e);
return;
}
}
}
protected void writeThread() {
writerBuffer.clear();
while (!Thread.currentThread().isInterrupted()) {
try {
bufferLock.lock();
try {
while (sharedBuffer.position() == 0) {
bufferNotEmpty.await();
}
sharedBuffer.flip();
writerBuffer.put(sharedBuffer);
sharedBuffer.clear();
bufferEmpty.signalAll();
} finally {
bufferLock.unlock();
}
writerBuffer.flip();
writeLock.lock();
try {
writeFully(channel, writerBuffer);
} finally {
writeLock.unlock();
}
writerBuffer.clear();
stats.sharedWrites++;
} catch (Exception e) {
die("Cant write bytes", e);
return;
}
}
}
protected void fail(TarantoolOp> future, Exception e) {
future.completeExceptionally(e);
}
protected void complete(TarantoolPacket packet, TarantoolOp> future) {
if (future != null) {
long code = packet.getCode();
if (code == 0) {
if (future.getCode() == Code.EXECUTE) {
completeSql(future, packet);
} else {
((TarantoolOp) future).complete(packet.getBody().get(Key.DATA.getId()));
}
} else {
Object error = packet.getBody().get(Key.ERROR.getId());
fail(future, serverError(code, error));
}
}
}
protected void completeSql(TarantoolOp> future, TarantoolPacket pack) {
Long rowCount = SqlProtoUtils.getSQLRowCount(pack);
if (rowCount != null) {
((TarantoolOp) future).complete(rowCount);
} else {
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy