/* * Copyright 2012-2017 Aerospike, Inc. * * Portions may be licensed to Aerospike, Inc. under one or more contributor * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0. * * 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 com.aerospike.client.async; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicInteger; import com.aerospike.client.AerospikeException; import com.aerospike.client.admin.AdminCommand; import com.aerospike.client.command.Command; import com.aerospike.client.policy.Policy; import com.aerospike.client.util.ThreadLocalData; /** * Asynchronous command handler. */ public abstract class AsyncCommand extends Command implements Runnable { private static final int IN_PROGRESS = 0; private static final int TIMEOUT_DELAY = 1; private static final int COMPLETE = 2; protected AsyncConnection conn; protected ByteBuffer byteBuffer; protected AsyncNode node; protected final AsyncCluster cluster; protected final Policy policy; private final AtomicInteger state = new AtomicInteger(); private long limit; private int iterations; protected boolean inAuthenticate; protected boolean inHeader = true; public AsyncCommand(AsyncCluster cluster, Policy policy) { this.cluster = cluster; this.policy = policy; } public AsyncCommand(AsyncCommand other) { // Retry constructor. this.cluster = other.cluster; this.policy = other.policy; this.byteBuffer = other.byteBuffer; this.limit = other.limit; this.iterations = other.iterations + 1; this.sequence = other.sequence; } public final void execute() { if (policy.timeout > 0) { limit = System.currentTimeMillis() + policy.timeout; } byteBuffer = cluster.getByteBuffer(); executeCommand(); } private void executeCommand() { try { node = (AsyncNode)getNode(); conn = node.getAsyncConnection(byteBuffer); if (conn == null) { conn = new AsyncConnection(node.getAddress(), cluster); if (cluster.getUser() != null) { inAuthenticate = true; dataBuffer = ThreadLocalData.getBuffer(); AdminCommand command = new AdminCommand(dataBuffer); dataOffset = command.setAuthenticate(cluster.getUser(), cluster.getPassword()); byteBuffer.clear(); byteBuffer.put(dataBuffer, 0, dataOffset); byteBuffer.flip(); conn.execute(this); return; } } writeCommand(); conn.execute(this); } catch (AerospikeException.Connection aec) { // Attempt retry on failed connection. if (iterations < policy.maxRetries && (policy.retryOnTimeout || limit == 0 || System.currentTimeMillis() < limit)) { closeConnection(); iterations++; if (policy.timeout > 0 && policy.retryOnTimeout) { limit = System.currentTimeMillis() + policy.timeout; } executeCommand(); // recursive call } else { cleanup(); throw aec; } } catch (RuntimeException re) { cleanup(); throw re; } } protected final void writeCommand() { writeBuffer(); if (dataOffset > byteBuffer.capacity()) { byteBuffer = ByteBuffer.allocateDirect(dataOffset); } byteBuffer.clear(); byteBuffer.put(dataBuffer, 0, dataOffset); byteBuffer.flip(); } protected final void processAuthenticate() { inAuthenticate = false; inHeader = true; int resultCode = byteBuffer.get(1) & 0xFF; if (resultCode != 0) { throw new AerospikeException(resultCode); } writeCommand(); conn.setWriteable(); } protected final void write() throws IOException { conn.write(byteBuffer); } protected final boolean checkTimeout() { int status = state.get(); if (status == COMPLETE) { return false; } if (limit > 0 && System.currentTimeMillis() > limit) { // Check if timeouts are allowed in the current state. // Do not timeout if the command is currently reading data in an offloaded task thread. // This means actual time elapsed can be significantly greater than the timeout because // we won't check again for another selector iteration. if (conn.allowTimeout()) { // Command has timed out in timeout queue thread. // At this point, we know another thread can't be modifying this command's state. if (policy.timeoutDelay > 0) { if (status == IN_PROGRESS) { if (state.compareAndSet(IN_PROGRESS, TIMEOUT_DELAY)) { // Notify user of timeout, but allow transaction to continue // in hope of reusing the socket. Do not perform concurrent retry // because that would require a new byte buffer which could possibly // result in deadlock. limit = System.currentTimeMillis() + policy.timeoutDelay; onFailure(new AerospikeException.Timeout(node, policy.timeout, iterations + 1, 0, 0)); return true; } } else { if (state.compareAndSet(TIMEOUT_DELAY, COMPLETE)) { // Transaction has been delayed long enough. // We know the task has not been offloaded to another thread, // so we can close safely here. cleanup(); } } } else { // Ensure that command succeeds or fails, but not both. if (state.compareAndSet(IN_PROGRESS, COMPLETE)) { // We know the task has not been offloaded to another thread, // so we can close safely here. // Attempt retry. if (iterations < policy.maxRetries && policy.retryOnTimeout) { AsyncCommand command = cloneCommand(); if (command != null) { closeConnection(); command.limit = System.currentTimeMillis() + policy.timeout; try { command.executeCommand(); } catch (Exception e) { // Command has already been cleaned up. // Notify user with original error. onFailure(new AerospikeException.Timeout(node, policy.timeout, iterations + 1, 0, 0)); } return false; } } cleanup(); onFailure(new AerospikeException.Timeout(node, policy.timeout, iterations + 1, 0, 0)); } } return false; // Do not put back on timeout queue. } } return true; } public void run() { try { read(); if (state.get() != COMPLETE) { conn.setReadable(); } } catch (AerospikeException.Connection ac) { onNetworkError(ac); } catch (AerospikeException ae) { // Fail without retry on non-network errors. onApplicationError(ae); } catch (IOException ioe) { onNetworkError(new AerospikeException(ioe)); } catch (Exception e) { // Fail without retry on unknown errors. onApplicationError(new AerospikeException(e)); } } protected final void finish() { // Finish could be called from a separate asyncTaskThreadPool thread. // Make sure SelectorManager thread has not already caused a transaction timeout. if (state.compareAndSet(IN_PROGRESS, COMPLETE)) { conn.unregister(); node.putAsyncConnection(conn); cluster.putByteBuffer(byteBuffer); try { onSuccess(); } catch (AerospikeException ae) { // The user's onSuccess() may have already been called which in turn generates this // exception. It's important to call onFailure() anyhow because the user's code // may be waiting for completion notification which wasn't yet called in // onSuccess(). This is the only case where both onSuccess() and onFailure() // gets called for the same command. onFailure(ae); } catch (Exception e) { onFailure(new AerospikeException(e)); } } else if (state.compareAndSet(TIMEOUT_DELAY, COMPLETE)) { // User has already been notified of timeout. // Put connection back into pool and discard response. conn.unregister(); node.putAsyncConnection(conn); cluster.putByteBuffer(byteBuffer); } } protected final void onNetworkError(AerospikeException ae) { // Ensure that command succeeds or fails, but not both. if (state.compareAndSet(IN_PROGRESS, COMPLETE)) { // Attempt retry. if (iterations < policy.maxRetries && (policy.retryOnTimeout || limit == 0 || System.currentTimeMillis() < limit)) { AsyncCommand command = cloneCommand(); if (command != null) { closeConnection(); if (policy.timeout > 0 && policy.retryOnTimeout) { command.limit = System.currentTimeMillis() + policy.timeout; } try { command.executeCommand(); } catch (Exception e) { // Command has already been cleaned up. // Notify user with original error. onFailure(ae); } return; } } cleanup(); onFailure(ae); } else if (state.compareAndSet(TIMEOUT_DELAY, COMPLETE)) { cleanup(); } } protected final void onApplicationError(AerospikeException ae) { // Ensure that command succeeds or fails exactly once. boolean notify = state.compareAndSet(IN_PROGRESS, COMPLETE); if (notify || state.compareAndSet(TIMEOUT_DELAY, COMPLETE)) { if (ae.keepConnection()) { // Put connection back in pool. conn.unregister(); node.putAsyncConnection(conn); cluster.putByteBuffer(byteBuffer); } else { // Close socket to flush out possible garbage. cleanup(); } if (notify) { onFailure(ae); } } } private void cleanup() { closeConnection(); cluster.putByteBuffer(byteBuffer); } private void closeConnection() { if (conn != null) { conn.close(); conn = null; } } protected abstract AsyncCommand cloneCommand(); protected abstract void read() throws AerospikeException, IOException; protected abstract void onSuccess(); protected abstract void onFailure(AerospikeException ae); }