org.apache.kudu.client.AsyncKuduSession Maven / Gradle / Ivy
Show all versions of camel-quarkus-kudu-client
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.apache.kudu.client;
import static org.apache.kudu.client.ExternalConsistencyMode.CLIENT_PROPAGATED;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.NotThreadSafe;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import org.apache.yetus.audience.InterfaceAudience;
import org.apache.yetus.audience.InterfaceStability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.kudu.Schema;
import org.apache.kudu.client.AsyncKuduClient.LookupType;
import org.apache.kudu.util.AsyncUtil;
import org.apache.kudu.util.LogThrottler;
import org.apache.kudu.util.Slice;
/**
* An {@code AsyncKuduSession} belongs to a specific {@link AsyncKuduClient}, and represents a
* context in which all write data access should take place. Within a session,
* multiple operations may be accumulated and batched together for better
* efficiency. Settings like timeouts, priorities, and trace IDs are also set
* per session.
*
* {@code AsyncKuduSession} is separate from {@link AsyncKuduClient} because, in a multi-threaded
* application, different threads may need to concurrently execute
* transactions. Similar to a JDBC "session", transaction boundaries will be
* delineated on a per-session basis -- in between a "BeginTransaction" and
* "Commit" call on a given session, all operations will be part of the same
* transaction. Meanwhile another concurrent session object can safely run
* non-transactional work or other transactions without interfering.
*
*
Therefore, this class is not thread-safe.
*
*
Additionally, there is a guarantee that writes from different sessions do not
* get batched together into the same RPCs -- this means that latency-sensitive
* clients can run through the same {@link AsyncKuduClient} object as throughput-oriented
* clients, perhaps by setting the latency-sensitive session's timeouts low and
* priorities high. Without the separation of batches, a latency-sensitive
* single-row insert might get batched along with 10MB worth of inserts from the
* batch writer, thus delaying the response significantly.
*
*
Timeouts are handled differently depending on the flush mode.
* With {@link SessionConfiguration.FlushMode#AUTO_FLUSH_SYNC AUTO_FLUSH_SYNC}, the timeout is set
* on each {@linkplain #apply apply}()'d operation.
* With {@link SessionConfiguration.FlushMode#AUTO_FLUSH_BACKGROUND AUTO_FLUSH_BACKGROUND} and
* {@link SessionConfiguration.FlushMode#MANUAL_FLUSH MANUAL_FLUSH}, the timeout is assigned to a
* whole batch of operations upon {@linkplain #flush flush}()'ing. It means that in a situation
* with a timeout of 500ms and a flush interval of 1000ms, an operation can be outstanding for up to
* 1500ms before being timed out.
*
*
Warning: a note on out-of-order operations
*
*
When using {@code AsyncKuduSession}, it is not difficult to trigger concurrent flushes on
* the same session. The result is that operations applied in a particular order within a single
* session may be applied in a different order on the server side, even for a single tablet. To
* prevent this behavior, ensure that only one flush is outstanding at a given time (the maximum
* concurrent flushes per {@code AsyncKuduSession} is hard-coded to 2).
*
*
If operation interleaving would be unacceptable for your application, consider using one of
* the following strategies to avoid it:
*
*
* - When using {@link SessionConfiguration.FlushMode#MANUAL_FLUSH MANUAL_FLUSH} mode,
* wait for one {@link #flush flush()} to {@code join()} before triggering another flush.
*
- When using {@link SessionConfiguration.FlushMode#AUTO_FLUSH_SYNC AUTO_FLUSH_SYNC}
* mode, wait for each {@link #apply apply()} to {@code join()} before applying another operation.
*
- Consider not using
* {@link SessionConfiguration.FlushMode#AUTO_FLUSH_BACKGROUND AUTO_FLUSH_BACKGROUND} mode.
*
- Make your application resilient to out-of-order application of writes.
*
- Avoid applying an {@link Operation} on a particular row until any previous write to that
* row has been successfully flushed.
*
*
* For more information on per-session operation interleaving, see
* KUDU-1767.
*/
@InterfaceAudience.Public
@InterfaceStability.Unstable
@NotThreadSafe
public class AsyncKuduSession implements SessionConfiguration {
public static final Logger LOG = LoggerFactory.getLogger(AsyncKuduSession.class);
/**
* Instance of LogThrottler isn't static so we can throttle messages per session
*/
private final LogThrottler throttleClosedLog = new LogThrottler(LOG);
private final AsyncKuduClient client;
private final Random randomizer = new Random();
private final ErrorCollector errorCollector;
private int flushIntervalMillis = 1000;
private int mutationBufferMaxOps = 1000; // TODO express this in terms of data size.
private FlushMode flushMode;
private ExternalConsistencyMode consistencyMode;
private long timeoutMillis;
private final long txnId;
/**
* Protects internal state from concurrent access. {@code AsyncKuduSession} is not threadsafe
* from the application's perspective, but because internally async timers and async flushing
* tasks may access the session concurrently with the application, synchronization is still
* needed.
*/
private final Object monitor = new Object();
/**
* Tracks the currently active buffer.
*
* When in mode {@link FlushMode#AUTO_FLUSH_BACKGROUND} or {@link FlushMode#AUTO_FLUSH_SYNC},
* {@code AsyncKuduSession} uses double buffering to improve write throughput. While the
* application is {@link #apply}ing operations to one buffer (the {@code activeBuffer}), the
* second buffer is either being flushed, or if it has already been flushed, it waits in the
* {@link #inactiveBuffers} queue. When the currently active buffer is flushed,
* {@code activeBuffer} is set to {@code null}. On the next call to {@code apply}, an inactive
* buffer is taken from {@code inactiveBuffers} and made the new active buffer. If both
* buffers are still flushing, then the {@code apply} call throws {@link PleaseThrottleException}.
*/
@GuardedBy("monitor")
private Buffer activeBuffer;
/**
* The buffers. May either be active (pointed to by {@link #activeBuffer},
* inactive (in the {@link #inactiveBuffers}) queue, or flushing.
*/
private final Buffer bufferA = new Buffer();
private final Buffer bufferB = new Buffer();
/**
* Queue containing flushed, inactive buffers. May be accessed from callbacks (I/O threads).
* We restrict the session to only two buffers, so {@link BlockingQueue#add} can
* be used without chance of failure.
*/
private final BlockingQueue inactiveBuffers = new ArrayBlockingQueue<>(2, false);
/**
* Deferred used to notify on flush events. Atomically swapped and completed every time a buffer
* is flushed. This can be used to notify handlers of {@link PleaseThrottleException} that more
* capacity may be available in the active buffer.
*/
private final AtomicReference> flushNotification =
new AtomicReference<>(new Deferred<>());
/**
* Tracks whether the session has been closed.
*/
private volatile boolean closed = false;
private boolean ignoreAllDuplicateRows = false;
private boolean ignoreAllNotFoundRows = false;
/**
* Cumulative operation metrics since the beginning of the session.
*/
private final ResourceMetrics writeOpMetrics = new ResourceMetrics();
/**
* Package-private constructor meant to be used via AsyncKuduClient
* @param client client that creates this session
*/
AsyncKuduSession(AsyncKuduClient client) {
this.client = client;
this.txnId = AsyncKuduClient.INVALID_TXN_ID;
flushMode = FlushMode.AUTO_FLUSH_SYNC;
consistencyMode = CLIENT_PROPAGATED;
timeoutMillis = client.getDefaultOperationTimeoutMs();
inactiveBuffers.add(bufferA);
inactiveBuffers.add(bufferB);
errorCollector = new ErrorCollector(mutationBufferMaxOps);
}
/**
* Constructor for a transactional session.
* @param client client that creates this session
* @param txnId transaction identifier for all operations within the session
*/
AsyncKuduSession(AsyncKuduClient client, long txnId) {
assert txnId > AsyncKuduClient.INVALID_TXN_ID;
this.client = client;
this.txnId = txnId;
flushMode = FlushMode.AUTO_FLUSH_SYNC;
consistencyMode = CLIENT_PROPAGATED;
timeoutMillis = client.getDefaultOperationTimeoutMs();
inactiveBuffers.add(bufferA);
inactiveBuffers.add(bufferB);
errorCollector = new ErrorCollector(mutationBufferMaxOps);
}
@Override
public FlushMode getFlushMode() {
return this.flushMode;
}
// TODO(wdberkeley): KUDU-1944. Don't let applications change the flush mode. Use a new session.
@Override
public void setFlushMode(FlushMode flushMode) {
if (hasPendingOperations()) {
throw new IllegalArgumentException("Cannot change flush mode when writes are buffered");
}
this.flushMode = flushMode;
}
@Override
public void setExternalConsistencyMode(ExternalConsistencyMode consistencyMode) {
if (hasPendingOperations()) {
throw new IllegalArgumentException("Cannot change consistency mode " +
"when writes are buffered");
}
this.consistencyMode = consistencyMode;
}
@Override
public void setMutationBufferSpace(int numOps) {
if (hasPendingOperations()) {
throw new IllegalArgumentException("Cannot change the buffer" +
" size when operations are buffered");
}
this.mutationBufferMaxOps = numOps;
}
@Override
public void setErrorCollectorSpace(int size) {
this.errorCollector.resize(size);
}
@Deprecated
@Override
public void setMutationBufferLowWatermark(float mutationBufferLowWatermarkPercentage) {
LOG.warn("setMutationBufferLowWatermark is deprecated");
}
/**
* Lets us set a specific seed for tests
* @param seed the seed to use
*/
@InterfaceAudience.LimitedPrivate("Test")
void setRandomSeed(long seed) {
this.randomizer.setSeed(seed);
}
@Override
public void setFlushInterval(int flushIntervalMillis) {
this.flushIntervalMillis = flushIntervalMillis;
}
@Override
public void setTimeoutMillis(long timeout) {
this.timeoutMillis = timeout;
}
@Override
public long getTimeoutMillis() {
return this.timeoutMillis;
}
@Override
public boolean isClosed() {
return closed;
}
@Override
public boolean isIgnoreAllDuplicateRows() {
return ignoreAllDuplicateRows;
}
@Override
public void setIgnoreAllDuplicateRows(boolean ignoreAllDuplicateRows) {
this.ignoreAllDuplicateRows = ignoreAllDuplicateRows;
}
@Override
public boolean isIgnoreAllNotFoundRows() {
return ignoreAllNotFoundRows;
}
@Override
public void setIgnoreAllNotFoundRows(boolean ignoreAllNotFoundRows) {
this.ignoreAllNotFoundRows = ignoreAllNotFoundRows;
}
@Override
public int countPendingErrors() {
return errorCollector.countErrors();
}
@Override
public RowErrorsAndOverflowStatus getPendingErrors() {
return errorCollector.getErrors();
}
@Override
public ResourceMetrics getWriteOpMetrics() {
return this.writeOpMetrics;
}
/**
* Flushes the buffered operations and marks this session as closed.
* See the javadoc on {@link #flush()} on how to deal with exceptions coming out of this method.
* @return a Deferred whose callback chain will be invoked when.
* everything that was buffered at the time of the call has been flushed.
*/
public Deferred> close() {
if (!closed) {
closed = true;
client.removeSession(this);
}
return flush();
}
/**
* Callback which waits for all tablet location lookups to complete, groups all operations into
* batches by tablet, and dispatches them. When all of the batches are complete, a deferred is
* fired and the buffer is added to the inactive queue.
*/
private final class TabletLookupCB implements Callback {
private final AtomicInteger lookupsOutstanding;
private final Buffer buffer;
private final Deferred> deferred;
public TabletLookupCB(Buffer buffer, Deferred> deferred) {
this.lookupsOutstanding = new AtomicInteger(buffer.getOperations().size());
this.buffer = buffer;
this.deferred = deferred;
}
@Override
public Void call(Object unused) throws Exception {
if (lookupsOutstanding.decrementAndGet() != 0) {
return null;
}
// The final tablet lookup is complete. Batch all of the buffered
// operations into their respective tablet, and then send the batches.
// Group the operations by tablet.
Map batches = new HashMap<>();
List opsFailedInLookup = new ArrayList<>();
List opsFailedIndexesList = new ArrayList<>();
int currentIndex = 0;
for (BufferedOperation bufferedOp : buffer.getOperations()) {
Operation operation = bufferedOp.getOperation();
if (bufferedOp.tabletLookupFailed()) {
Exception failure = bufferedOp.getTabletLookupFailure();
RowError error;
if (failure instanceof NonCoveredRangeException) {
// TODO: this should be something different than NotFound so that
// applications can distinguish from updates on missing rows.
error = new RowError(Status.NotFound(String.format(
"%s: %s", failure.getMessage(), operation.getTable().getName())), operation);
} else {
LOG.warn("unexpected tablet lookup failure for operation {}", operation, failure);
error = new RowError(Status.RuntimeError(failure.getMessage()), operation);
}
OperationResponse response = new OperationResponse(0, null, 0, operation, error);
// Add the row error to the error collector if the session is in background flush mode,
// and complete the operation's deferred with the error response. The ordering between
// adding to the error collector and completing the deferred should not matter since
// applications should be using one or the other method for error handling, not both.
if (flushMode == FlushMode.AUTO_FLUSH_BACKGROUND) {
errorCollector.addError(error);
}
operation.callback(response);
opsFailedInLookup.add(response);
opsFailedIndexesList.add(currentIndex++);
continue;
}
LocatedTablet tablet = bufferedOp.getTablet();
Slice tabletId = new Slice(tablet.getTabletId());
Batch batch = batches.get(tabletId);
if (batch == null) {
batch = new Batch(operation.getTable(), tablet, ignoreAllDuplicateRows,
ignoreAllNotFoundRows, txnId);
batches.put(tabletId, batch);
}
batch.add(operation, currentIndex++);
}
List> batchResponses = new ArrayList<>(batches.size() + 1);
if (!opsFailedInLookup.isEmpty()) {
batchResponses.add(
Deferred.fromResult(new BatchResponse(opsFailedInLookup, opsFailedIndexesList)));
}
for (Batch batch : batches.values()) {
if (timeoutMillis != 0) {
batch.resetTimeoutMillis(client.getTimer(), timeoutMillis);
}
addBatchCallbacks(batch);
batchResponses.add(client.sendRpcToTablet(batch));
}
// On completion of all batches, fire the completion deferred, and add the buffer
// back to the inactive buffers queue. This frees it up for new inserts.
AsyncUtil.addBoth(
Deferred.group(batchResponses),
new Callback() {
@Override
public Void call(Object responses) {
queueBuffer(buffer);
deferred.callback(responses);
return null;
}
});
return null;
}
/**
* Creates callbacks to handle a multi-put and adds them to the request.
* @param request the request for which we must handle the response
*/
private void addBatchCallbacks(final Batch request) {
final class BatchCallback implements Callback {
@Override
public BatchResponse call(final BatchResponse response) {
LOG.trace("Got a Batch response for {} rows", request.operations.size());
AsyncKuduSession.this.client.updateLastPropagatedTimestamp(response.getWriteTimestamp());
// Send individualized responses to all the operations in this batch.
for (OperationResponse operationResponse : response.getIndividualResponses()) {
if (flushMode == FlushMode.AUTO_FLUSH_BACKGROUND && operationResponse.hasRowError()) {
errorCollector.addError(operationResponse.getRowError());
}
// Fire the callback after collecting the errors so that the errors
// are visible should the callback interrogate the error collector.
operationResponse.getOperation().callback(operationResponse);
}
writeOpMetrics.update(response.getWriteOpMetrics());
return response;
}
@Override
public String toString() {
return "apply batch response";
}
}
final class BatchErrCallback implements Callback