org.hbase.async.MultiAction Maven / Gradle / Ivy
Show all versions of asynchbase Show documentation
/*
* Copyright (C) 2010-2018 The Async HBase Authors. All rights reserved.
* This file is part of Async HBase.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the StumbleUpon nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.hbase.async;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import org.jboss.netty.buffer.ChannelBuffer;
import org.hbase.async.generated.ClientPB.Action;
import org.hbase.async.generated.ClientPB.MultiRequest;
import org.hbase.async.generated.ClientPB.MultiResponse;
import org.hbase.async.generated.ClientPB.RegionAction;
import org.hbase.async.generated.ClientPB.RegionActionResult;
import org.hbase.async.generated.ClientPB.ResultOrException;
import org.hbase.async.generated.HBasePB.NameBytesPair;
/**
* Package-private class to batch multiple RPCs for a same region together.
*
* This RPC is guaranteed to be sent atomically (but HBase doesn't guarantee
* that it will apply it atomically).
*
* Before serializing, the RPCs are sorted by region and (as of 1.8.1)
* by row key (to fix issues with
* https://issues.apache.org/jira/browse/HBASE-17924) so that the
* responses can be matched to the original RPC.
*/
final class MultiAction extends HBaseRpc implements HBaseRpc.IsEdit {
// NOTE: I apologize for the long methods and complex control flow logic,
// with many nested loops and `if's. `multiPut' and `multi' have always
// been a gigantic mess in HBase, and despite my best efforts to provide
// a reasonable implementation, it's obvious that the resulting code is
// unwieldy. I originally wrote support for `multi' as a separate class
// from `multiPut', but this resulted in a lot of code duplication and
// added complexity in other places that had to deal with either kind of
// RPCs depending on the server version. Hence this class supports both
// RPCs, which does add a bit of complexity to the already unnecessarily
// complex logic. This was better than all the code duplication. And
// believe me, what's here is -- in my biased opinion -- significantly
// better than the insane spaghetti mess in HBase's client and server.
/** RPC method name for HBase before 0.92. */
private static final byte[] MULTI_PUT = {
'm', 'u', 'l', 't', 'i', 'P', 'u', 't'
};
/** RPC method name for HBase 0.92 to 0.94. */
private static final byte[] MULTI = { 'm', 'u', 'l', 't', 'i' };
/** RPC method name for HBase 0.95 and above. */
static final byte[] MMULTI = { 'M', 'u', 'l', 't', 'i' };
/** Template for NSREs. */
private static final NotServingRegionException NSRE =
new NotServingRegionException("Region unavailable", null);
/**
* Protocol version from which we can use `multi' instead of `multiPut'.
* Technically we could use `multi' with HBase 0.90.x, but as explained
* in HBASE-5204, because of a change in HBASE-3335 this is harder than
* necessary, so we don't support it.
*/
static final byte USE_MULTI = RegionClient.SERVER_VERSION_092_OR_ABOVE;
/**
* All the RPCs in this batch.
* We'll sort this list before serializing it.
* @see MultiActionComparator
*/
private final ArrayList batch = new ArrayList();
@Override
byte[] method(final byte server_version) {
if (server_version >= RegionClient.SERVER_VERSION_095_OR_ABOVE) {
return MMULTI;
}
return server_version >= USE_MULTI ? MULTI : MULTI_PUT;
}
/** Returns the number of RPCs in this batch. */
public int size() {
return batch.size();
}
/**
* Adds an RPC to this batch.
*
* @param rpc The RPC to add in this batch.
* @throws IllegalArgumentException if the edit specifies an explicit row lock.
* Edits with locks cannot be batched
* @throws IllegalArgumentException if the region was missing from the RPC
*/
public void add(final BatchableRpc rpc) {
if (rpc.lockid != RowLock.NO_LOCK) {
throw new AssertionError("Should never happen! We don't do multi-put"
+ " with RowLocks but we've been given an edit that has one!"
+ " edit=" + rpc + ", this=" + this);
}
if (rpc.region == null || rpc.region.name() == null) {
throw new IllegalArgumentException("RPC " + rpc +
" is missing the region or region name");
}
batch.add(rpc);
}
/** Returns the list of individual RPCs that make up this batch. */
ArrayList batch() {
return batch;
}
/**
* Predicts a lower bound on the serialized size of this RPC.
* This is to avoid using a dynamic buffer, to avoid re-sizing the buffer.
* Since we use a static buffer, if the prediction is wrong and turns out
* to be less than what we need, there will be an exception which will
* prevent the RPC from being serialized. That'd be a severe bug.
* @param server_version What RPC protocol version the server is running.
*/
private int predictSerializedSize(final byte server_version) {
// See the comment in serialize() about the for loop that follows.
int size = 0;
size += 4; // int: Number of parameters.
size += 1; // byte: Type of the 1st parameter.
size += 1; // byte: Type again (see HBASE-2877).
size += 4; // int: How many regions do we want to affect?
// Are we serializing a `multi' RPC, or `multiPut'?
final boolean use_multi = method(server_version) == MULTI;
BatchableRpc prev = PutRequest.EMPTY_PUT;
for (final BatchableRpc rpc : batch) {
final byte[] region_name = rpc.getRegion().name();
final boolean new_region = !Bytes.equals(prev.getRegion().name(),
region_name);
final byte[] family = rpc.family();
final boolean new_key = (new_region
|| prev.code() != rpc.code()
|| !Bytes.equals(prev.key, rpc.key)
|| family == DeleteRequest.WHOLE_ROW);
final boolean new_family = new_key || !Bytes.equals(prev.family(),
family);
if (new_region) {
size += 3; // vint: region name length (3 bytes => max length = 32768).
size += region_name.length; // The region name.
size += 4; // int: How many RPCs for this region.
}
final int key_length = rpc.key.length;
if (new_key) {
if (use_multi) {
size += 4; // int: Number of "attributes" for the last key (none).
size += 4; // Index of this `action'.
size += 3; // 3 bytes to serialize `null'.
size += 1; // byte: Type code for `Action'.
size += 1; // byte: Type code again (see HBASE-2877).
size += 1; // byte: Code for a `Row' object.
size += 1; // byte: Code for a `Put' object.
}
size += 1; // byte: Version of Put.
size += 3; // vint: row key length (3 bytes => max length = 32768).
size += key_length; // The row key.
size += 8; // long: Timestamp.
size += 8; // long: Lock ID.
size += 1; // bool: Whether or not to write to the WAL.
size += 4; // int: Number of families for which we have edits.
}
if (new_family) {
size += 1; // vint: Family length (guaranteed on 1 byte).
size += family.length; // The family.
size += 4; // int: Number of KeyValues that follow.
if (rpc.code() == PutRequest.CODE) {
size += 4; // int: Total number of bytes for all those KeyValues.
}
}
size += rpc.payloadSize();
prev = rpc;
}
return size;
}
/** Serializes this request. */
ChannelBuffer serialize(final byte server_version) {
if (server_version < RegionClient.SERVER_VERSION_095_OR_ABOVE) {
return serializeOld(server_version);
}
// we create a new RegionAction for each region.
Collections.sort(batch, SORT_BY_REGION_AND_KEY);
final MultiRequest.Builder req = MultiRequest.newBuilder();
RegionAction.Builder actions = null;
byte[] prev_region = HBaseClient.EMPTY_ARRAY;
int i = 0;
for (final BatchableRpc rpc : batch) {
final RegionInfo region = rpc.getRegion();
final boolean new_region = !Bytes.equals(prev_region, region.name());
if (new_region) {
if (actions != null) { // If not the first iteration ...
req.addRegionAction(actions.build()); // ... push actions thus far.
}
actions = RegionAction.newBuilder();
actions.setRegion(rpc.getRegion().toProtobuf());
prev_region = region.name();
}
final Action.Builder action = Action.newBuilder().setIndex(i++);
if (rpc instanceof GetRequest) {
action.setGet(((GetRequest)rpc).getPB());
} else {
action.setMutation(rpc.toMutationProto());
}
actions.addAction(action);
}
req.addRegionAction(actions.build());
return toChannelBuffer(MMULTI, req.build());
}
/** Serializes this request for HBase 0.94 and before. */
private ChannelBuffer serializeOld(final byte server_version) {
// Due to the wire format expected by HBase, we need to group all the
// edits by region, then by key, then by family. HBase does this by
// building a crazy map-of-map-of-map-of-list-of-edits, but this is
// memory and time inefficient (lots of unnecessary references and
// O(n log n) operations). The approach we take here is to sort the
// list and iterate on it. Each time we find a different family or
// row key or region, we start a new set of edits. Because the RPC
// format needs to know the number of edits or bytes that follows in
// various places, we store a "0" value and then later monkey-patch it
// once we cross a row key / family / region boundary, because we can't
// efficiently tell ahead of time how many edits or bytes will follow
// until we cross such boundaries.
Collections.sort(batch, MULTI_CMP);
final ChannelBuffer buf = newBuffer(server_version,
predictSerializedSize(server_version));
buf.writeInt(1); // Number of parameters.
// Are we serializing a `multi' RPC, or `multiPut'?
final boolean use_multi = method(server_version) == MULTI;
{ // 1st and only param.
final int code = use_multi ? 66 : 57; // `MultiAction' or `MultiPut'.
buf.writeByte(code); // Type code for the parameter.
buf.writeByte(code); // Type code again (see HBASE-2877).
}
buf.writeInt(0); // How many regions do we want to affect?
// ^------ We'll monkey patch this at the end.
int nregions = 0;
int nkeys_index = -1;
int nkeys = 0;
int nfamilies_index = -1;
int nfamilies = 0;
int nkeys_per_family_index = -1;
int nkeys_per_family = 0;
int nbytes_per_family = 0;
int nrpcs_per_key = 0;
BatchableRpc prev = PutRequest.EMPTY_PUT;
for (final BatchableRpc rpc : batch) {
final byte[] region_name = rpc.getRegion().name();
final boolean new_region = !Bytes.equals(prev.getRegion().name(),
region_name);
final byte[] family = rpc.family();
final boolean new_key = (new_region
|| prev.code() != rpc.code()
|| !Bytes.equals(prev.key, rpc.key)
|| family == DeleteRequest.WHOLE_ROW);
final boolean new_family = new_key || !Bytes.equals(prev.family(),
family);
if (new_key && use_multi && nkeys_index > 0) {
buf.writeInt(0); // Number of "attributes" for the last key (none).
// Trailing useless junk from `Action'. After serializing its
// `action' (which implements an interface awesomely named `Row'),
// HBase adds the index of the `action' as the server will sort
// actions by key. This is pretty useless as if the client was
// sorting things before sending them off to the server, like we
// do, then we wouldn't need to pass an index to be able to match
// up requests and responses. Since we don't need this index, we
// instead use this field for another purpose: remembering how
// many RPCs we sent for this particular key. This will help us
// when de-serializing the response.
buf.writeInt(nrpcs_per_key);
nrpcs_per_key = 0;
// Then, because an `Action' can also contain a result, the code
// also serializes that. Of course for us end-users, this is stupid
// because we never send a result along with our request.
writeHBaseNull(buf);
}
if (new_region) {
// Monkey-patch the number of edits of the previous region.
if (nkeys_index > 0) {
buf.setInt(nkeys_index, nkeys);
nkeys = 0;
}
nregions++;
writeByteArray(buf, region_name); // The region name.
nkeys_index = buf.writerIndex();
// Number of keys for which we have RPCs for this region.
buf.writeInt(0); // We'll monkey patch this later.
}
final byte[] key = rpc.key;
if (new_key) {
// Monkey-patch the number of families of the previous key.
if (nfamilies_index > 0) {
buf.setInt(nfamilies_index, nfamilies);
nfamilies = 0;
}
if (use_multi) {
// Serialize an `Action' for the RPCs on this row.
buf.writeByte(65); // Code for an `Action' object.
buf.writeByte(65); // Code again (see HBASE-2877).
// Inside the action, serialize a `Put' object.
buf.writeByte(64); // Code for a `Row' object.
buf.writeByte(rpc.code()); // Code this object.
}
nkeys++;
// Right now we only support batching puts. In the future this part
// of the code will have to change to also want to allow get/deletes.
// The follow serializes a `Put'.
buf.writeByte(rpc.version(server_version)); // Undocumented versioning.
writeByteArray(buf, key); // The row key.
// This timestamp is unused, only the KeyValue-level timestamp is
// used, except in one case: whole-row deletes. In that case only,
// this timestamp is used by the RegionServer to create per-family
// deletes for that row. See HRegion.prepareDelete() for more info.
buf.writeLong(rpc.timestamp); // Timestamp.
buf.writeLong(RowLock.NO_LOCK); // Lock ID.
buf.writeByte(rpc.durable ? 0x01 : 0x00); // Use the WAL?
nfamilies_index = buf.writerIndex();
// Number of families that follow.
buf.writeInt(0); // We'll monkey patch this later.
}
if (new_family) {
// Monkey-patch the number and size of edits for the previous family.
if (nkeys_per_family_index > 0) {
buf.setInt(nkeys_per_family_index, nkeys_per_family);
if (prev.code() == PutRequest.CODE) {
buf.setInt(nkeys_per_family_index + 4, nbytes_per_family);
}
nkeys_per_family_index = -1;
nkeys_per_family = 0;
nbytes_per_family = 0;
}
if (family == DeleteRequest.WHOLE_ROW) {
prev = rpc; // Short circuit. We have no KeyValue to write.
continue; // So loop again directly.
}
nfamilies++;
writeByteArray(buf, family); // The column family.
nkeys_per_family_index = buf.writerIndex();
// Number of "KeyValues" that follow.
buf.writeInt(0); // We'll monkey patch this later.
if (rpc.code() == PutRequest.CODE) {
// Total number of bytes taken by those "KeyValues".
// This is completely useless and only done for `Put'.
buf.writeInt(0); // We'll monkey patch this later.
}
}
nkeys_per_family += rpc.numKeyValues();
nrpcs_per_key++;
nbytes_per_family += rpc.payloadSize();
rpc.serializePayload(buf);
prev = rpc;
} // Yay, we made it!
if (use_multi) {
buf.writeInt(0); // Number of "attributes" for the last key (none).
// Trailing junk for the last `Action'. See comment above.
buf.writeInt(nrpcs_per_key);
writeHBaseNull(buf); // Useless.
}
// Note: the only case where nkeys_per_family_index remained -1 throughout
// this whole ordeal is where we didn't have any KV to serialize because
// every RPC was a `DeleteRequest.WHOLE_ROW'.
if (nkeys_per_family_index > 0) {
// Monkey-patch everything for the last set of edits.
buf.setInt(nkeys_per_family_index, nkeys_per_family);
if (prev.code() == PutRequest.CODE) {
buf.setInt(nkeys_per_family_index + 4, nbytes_per_family);
}
}
buf.setInt(nfamilies_index, nfamilies);
buf.setInt(nkeys_index, nkeys);
// Monkey-patch the number of regions affected by this RPC.
int header_length = 4 + 4 + 2 + method(server_version).length;
if (server_version >= RegionClient.SERVER_VERSION_092_OR_ABOVE) {
header_length += 1 + 8 + 4;
}
buf.setInt(header_length + 4 + 1 + 1, nregions);
return buf;
}
public String toString() {
// Originally this was simply putting all the batch in the toString
// representation, but that's way too expensive when we have large
// batches. So instead we toString each RPC until we hit some
// hard-coded upper bound on how much data we're willing to put into
// the toString. If we have too many RPCs and hit that constant,
// we skip all the remaining ones until the last one, as it's often
// useful to see the last RPC added when debugging.
final StringBuilder buf = new StringBuilder();
buf.append("MultiAction(batch=[");
final int nrpcs = batch.size();
int i;
for (i = 0; i < nrpcs; i++) {
if (buf.length() >= 1024) {
break;
}
buf.append(batch.get(i)).append(", ");
}
if (i < nrpcs) {
if (i == nrpcs - 1) {
buf.append("... 1 RPC not shown])");
} else {
buf.append("... ").append(nrpcs - 1 - i)
.append(" RPCs not shown ..., ")
.append(batch.get(nrpcs - 1))
.append("])");
}
} else {
buf.setLength(buf.length() - 2); // Remove the last ", "
buf.append("])");
}
return buf.toString();
}
/**
* Sorts {@link BatchableRpc}s appropriately for the multi-put / multi-action RPC.
* We sort by region, row key, column family. No ordering is needed on the
* column qualifier or value.
*/
static final MultiActionComparator MULTI_CMP = new MultiActionComparator();
/**
* Sorts {@link BatchableRpc}s appropriately for the `multi' RPC.
* Used with HBase 0.94 and earlier only.
*/
static final class MultiActionComparator
implements Comparator {
private MultiActionComparator() { // Can't instantiate outside of this class.
}
@Override
/**
* Compares two RPCs.
*/
public int compare(final BatchableRpc a, final BatchableRpc b) {
int d;
if ((d = Bytes.memcmp(a.getRegion().name(), b.getRegion().name())) != 0) {
return d;
} else if ((d = a.code() - b.code()) != 0) { // Sort by code before key.
// Note that DeleteRequest.code() < PutRequest.code() and this matters
// as the RegionServer processes deletes before puts, and we want to
// send RPCs in the same order as they will be processed.
return d;
} else if ((d = Bytes.memcmp(a.key, b.key)) != 0) {
return d;
}
return Bytes.memcmp(a.family(), b.family());
}
}
/**
* Sorts {@link BatchableRpc}s appropriately for HBase 0.95+ multi-action
* as well as 1.3x where the response is also sorted by key.
*/
static final RegionAndKeyComparator SORT_BY_REGION_AND_KEY =
new RegionAndKeyComparator();
/**
* Sorts {@link BatchableRpc}s by region.
* Used with HBase 0.95+ only.
*/
private static final class RegionAndKeyComparator
implements Comparator {
private RegionAndKeyComparator() { // Can't instantiate outside of this class.
}
@Override
/** Compares two RPCs. */
public int compare(final BatchableRpc a, final BatchableRpc b) {
int region_cmp = Bytes.memcmp(a.getRegion().name(), b.getRegion().name());
if (region_cmp != 0) {
return region_cmp;
}
return Bytes.memcmp(a.key, b.key);
}
}
/**
* So this guy is a little messy now that we want to start batching things like
* increments and appends that could return data. If all of the calls are homogeneous
* then it isn't too bad. BUT if we mix things, then it's kinda gross. e.g. if
* we have rpcs like
*
* 1 => append with response
* 2 => append without response
* 3 => put (no response)
* 4 => increment with response
*
* we have to parse those out. To compound matters, HBase will "helpfully"
* return null responses to append requests if you don't ask for a result
* so we have to account for that in our indexing.
*/
@Override
Object deserialize(final ChannelBuffer buf, final int cell_size) {
final MultiResponse resp = readProtobuf(buf, MultiResponse.PARSER);
final int responses = resp.getRegionActionResultCount();
final int nrpcs = batch.size();
final Object[] resps = new Object[nrpcs];
int n = 0; // Index in `batch'.
int r = 0; // Index in `regionActionResult' in the PB.
while (n < nrpcs && r < responses) {
final RegionActionResult results = resp.getRegionActionResult(r++);
final int nresults = results.getResultOrExceptionCount();
if (results.hasException()) {
if (nresults != 0) {
throw new InvalidResponseException("All edits in a batch failed yet"
+ " we found " + nresults
+ " results", results);
}
// All the edits for this region have failed, however the PB doesn't
// tell us how many edits there were for that region, and we don't
// keep track of this information except temporarily during
// serialization. So we need to go back through our list again to
// re-count how many we did put together in this batch, so we can fail
// all those RPCs.
int last_edit = n + 1; // +1 because we have at least 1 edit.
final byte[] region_name = batch.get(n).getRegion().name();
while (last_edit < nrpcs
&& Bytes.equals(region_name,
batch.get(last_edit).getRegion().name())) {
last_edit++;
}
final NameBytesPair pair = results.getException();
for (int j = n; j < last_edit; j++) {
resps[j] = RegionClient.decodeExceptionPair(batch.get(j), pair);
}
n = last_edit;
continue; // This batch failed, move on.
} else if (nresults == 0) {
/* WTF HBase? Why return nulls for appends when we don't ask for a
* response instead of at least returning an empty result like the
* puts? */
if (!(batch.get(n) instanceof AppendRequest)) {
throw new InvalidResponseException("No responses for a multiAction set"
+ " and the first RPC " + batch.get(n)
+ " was not an append request", results);
}
int last_edit = n + 1; // +1 because we have at least 1 edit.
final byte[] region_name = batch.get(n).getRegion().name();
while (last_edit < nrpcs
&& Bytes.equals(region_name,
batch.get(last_edit).getRegion().name())) {
last_edit++;
}
for (int j = n; j < last_edit; j++) {
resps[j] = SUCCESS;
}
n = last_edit;
continue; // All the appends for this batch are successful
} // else: parse out the individual results:
for (int j = 0; j < nresults; j++) {
final ResultOrException roe = results.getResultOrException(j);
final int index = roe.getIndex();
if (index != n) {
// More append fun! If we interleaved appends with puts or appends
// with and without result returns, we need to skip over appends
// without results as they're not counted in {@code nresults}
while (batch.get(n) instanceof AppendRequest &&
!((AppendRequest)batch.get(n)).returnResult()) {
resps[n++] = SUCCESS;
}
if (index != n) {
// if we STILL don't match up on our indices then something is
// foobar so toss an exception
throw new InvalidResponseException("Expected result #" + n
+ " but got result #" + index,
results);
}
}
final Object result;
if (roe.hasException()) {
final NameBytesPair pair = roe.getException();
// This RPC failed, get what the exception was.
result = RegionClient.decodeExceptionPair(batch.get(n), pair);
} else {
// TODO - handle other types of RPCs if we start allowing them in
// a batch
if (batch.get(n) instanceof AppendRequest) {
final AppendRequest append_request = (AppendRequest)(batch.get(n));
ArrayList kvs = GetRequest.convertResultWithAssociatedCells(
roe.getResult(), buf, cell_size);
if (append_request.returnResult()) {
result = kvs.get(0);
} else {
result = SUCCESS;
}
} else if (batch.get(n) instanceof GetRequest) {
result = GetRequest.convertResultWithAssociatedCells(
roe.getResult(), buf, cell_size);
} else {
result = SUCCESS;
}
}
resps[n++] = result;
}
}
if (n != nrpcs) {
// handle trailing appends that don't have a response.
while (n < nrpcs && batch.get(n) instanceof AppendRequest &&
!((AppendRequest)batch.get(n)).returnResult()) {
resps[n++] = SUCCESS;
}
if (n != nrpcs) {
throw new InvalidResponseException("Expected " + nrpcs
+ " results but got " + n, resp);
}
}
return new Response(resps);
}
/**
* Response to a {@link MultiAction} RPC.
*/
final class Response {
/** Response for each Action that was in the batch, in the same order. */
private final Object[] resps;
/** Constructor. */
Response(final Object[] resps) {
assert resps.length == batch.size() : "Got " + resps.length
+ " responses but expected " + batch.size();
this.resps = resps;
}
/** @return The number of objects in the response */
public int size() {
return resps.length;
}
/**
* Returns the result number #i embodied in this response.
* @throws IndexOutOfBoundsException if i is greater than batch.size()
*/
public Object result(final int i) {
return resps[i];
}
public String toString() {
return "MultiAction.Response(" + Arrays.toString(resps) + ')';
}
}
/**
* De-serializes the response to a {@link MultiAction} RPC.
* See HBase's {@code MultiResponse}.
* Only used with HBase 0.94 and earlier.
*/
Response responseFromBuffer(final ChannelBuffer buf) {
switch (buf.readByte()) {
case 58:
return deserializeMultiPutResponse(buf);
case 67:
return deserializeMultiResponse(buf);
}
throw new NonRecoverableException("Couldn't de-serialize "
+ Bytes.pretty(buf));
}
/**
* De-serializes a {@code MultiResponse}.
* This is only used when talking to HBase 0.92.x to 0.94.x.
*/
Response deserializeMultiResponse(final ChannelBuffer buf) {
final int nregions = buf.readInt();
HBaseRpc.checkNonEmptyArrayLength(buf, nregions);
final Object[] resps = new Object[batch.size()];
int n = 0; // Index in `batch'.
for (int i = 0; i < nregions; i++) {
final byte[] region_name = HBaseRpc.readByteArray(buf);
final int nkeys = buf.readInt();
HBaseRpc.checkNonEmptyArrayLength(buf, nkeys);
for (int j = 0; j < nkeys; j++) {
final int nrpcs_per_key = buf.readInt();
boolean error = buf.readByte() != 0x00;
Object resp; // Response for the current region.
if (error) {
final HBaseException e = RegionClient.deserializeException(buf, null);
resp = e;
} else {
resp = RegionClient.deserializeObject(buf, this);
// A successful response to a `Put' will be an empty `Result'
// object, which we de-serialize as an empty `ArrayList'.
// There's no need to waste memory keeping these around.
if (resp instanceof ArrayList && ((ArrayList) resp).isEmpty()) {
resp = SUCCESS;
} else if (resp == null) {
// Awesomely, `null' is used to indicate all RPCs for this region
// have failed, and we're not told why. Most of the time, it will
// be an NSRE, so assume that. What were they thinking when they
// wrote that code in HBase? Seriously WTF.
resp = NSRE;
error = true;
}
}
if (error) {
final HBaseException e = (HBaseException) resp;
for (int k = 0; k < nrpcs_per_key; k++) {
// We need to "clone" the exception for each RPC, as each RPC that
// failed needs to have its own exception with a reference to the
// failed RPC. This makes significantly simplifies the callbacks
// that do error handling, as they can extract the RPC out of the
// exception. The downside is that we don't have a perfect way of
// cloning "e", so instead we just abuse its `make' factory method
// slightly to duplicate it. This mangles the message a bit, but
// that's mostly harmless.
resps[n + k] = e.make(e, batch.get(n + k));
}
} else {
for (int k = 0; k < nrpcs_per_key; k++) {
resps[n + k] = resp;
}
}
n += nrpcs_per_key;
}
}
return new Response(resps);
}
/**
* De-serializes a {@code MultiPutResponse}.
* This is only used when talking to old versions of HBase (pre 0.92).
*/
Response deserializeMultiPutResponse(final ChannelBuffer buf) {
final int nregions = buf.readInt();
HBaseRpc.checkNonEmptyArrayLength(buf, nregions);
final int nrpcs = batch.size();
final Object[] resps = new Object[nrpcs];
int n = 0; // Index in `batch'.
for (int i = 0; i < nregions; i++) {
final byte[] region_name = HBaseRpc.readByteArray(buf);
// Sanity check. Response should give us regions back in same order.
assert Bytes.equals(region_name, batch.get(n).getRegion().name())
: ("WTF? " + Bytes.pretty(region_name) + " != "
+ batch.get(n).getRegion().name());
final int failed = buf.readInt(); // Index of the first failed edit.
// Because of HBASE-2898, we don't know which RPCs exactly failed. We
// only now that for that region, the one at index `failed' wasn't
// successful, so we can only assume that the subsequent ones weren't
// either, and retry them. First we need to find out how many RPCs
// we originally had for this region.
int edits_per_region = n;
while (edits_per_region < nrpcs
&& Bytes.equals(region_name,
batch.get(edits_per_region).getRegion().name())) {
edits_per_region++;
}
edits_per_region -= n;
assert failed < edits_per_region : "WTF? Found more failed RPCs " + failed
+ " than sent " + edits_per_region + " to " + Bytes.pretty(region_name);
// If `failed == -1' then we now that nothing has failed. Otherwise, we
// have that RPCs in [n; n + failed - 1] have succeeded, and RPCs in
// [n + failed; n + edits_per_region] have failed.
if (failed == -1) { // Fast-path for the case with no failures.
for (int j = 0; j < edits_per_region; j++) {
resps[n + j] = SUCCESS;
}
} else { // We had some failures.
assert failed >= 0 : "WTF? Found a negative failure index " + failed
+ " for region " + Bytes.pretty(region_name);
for (int j = 0; j < failed; j++) {
resps[n + j] = SUCCESS;
}
final String msg = "Multi-put failed on RPC #" + failed + "/"
+ edits_per_region + " on region " + Bytes.pretty(region_name);
for (int j = failed; j < edits_per_region; j++) {
resps[n + j] = new MultiPutFailedException(msg, batch.get(n + j));
}
}
n += edits_per_region;
}
return new Response(resps);
}
/**
* Used to represent the partial failure of a multi-put exception.
* This is only used with older versions of HBase (pre 0.92).
*/
private final class MultiPutFailedException extends RecoverableException
implements HasFailedRpcException {
final HBaseRpc failed_rpc;
/**
* Constructor.
* @param msg The message of the exception.
* @param failed_rpc The RPC that caused this exception.
*/
MultiPutFailedException(final String msg, final HBaseRpc failed_rpc) {
super(msg);
this.failed_rpc = failed_rpc;
}
@Override
public String getMessage() {
return super.getMessage() + "\nCaused by RPC: " + failed_rpc;
}
public HBaseRpc getFailedRpc() {
return failed_rpc;
}
@Override
MultiPutFailedException make(final Object msg, final HBaseRpc rpc) {
if (msg == this || msg instanceof MultiPutFailedException) {
final MultiPutFailedException e = (MultiPutFailedException) msg;
return new MultiPutFailedException(e.getMessage(), rpc);
}
return new MultiPutFailedException(msg.toString(), rpc);
}
private static final long serialVersionUID = 1326900942;
}
/** Singleton class returned to indicate success of a multi-put or append. */
static final class MultiActionSuccess {
private MultiActionSuccess() {
}
public String toString() {
return "MultiActionSuccess";
}
}
/** Indicates the results of the mutation were successful */
static final MultiActionSuccess SUCCESS = new MultiActionSuccess();
}