package com.jivesoftware.os.amza.client.http; import com.google.common.collect.Maps; import com.google.common.io.ByteStreams; import com.jivesoftware.os.amza.api.FailedToAchieveQuorumException; import com.jivesoftware.os.amza.api.PartitionClient.KeyValueFilter; import com.jivesoftware.os.amza.api.filer.FilerOutputStream; import com.jivesoftware.os.amza.api.filer.UIO; import com.jivesoftware.os.amza.api.partition.Consistency; import com.jivesoftware.os.amza.api.partition.PartitionName; import com.jivesoftware.os.amza.api.ring.RingMember; import com.jivesoftware.os.amza.api.stream.ClientUpdates; import com.jivesoftware.os.amza.api.stream.OffsetUnprefixedWALKeys; import com.jivesoftware.os.amza.api.stream.PrefixedKeyRanges; import com.jivesoftware.os.amza.api.stream.UnprefixedWALKeys; import com.jivesoftware.os.amza.client.http.exceptions.LeaderElectionInProgressException; import com.jivesoftware.os.amza.client.http.exceptions.NoLongerTheLeaderException; import com.jivesoftware.os.mlogger.core.MetricLogger; import com.jivesoftware.os.mlogger.core.MetricLoggerFactory; import com.jivesoftware.os.routing.bird.http.client.HttpClient; import com.jivesoftware.os.routing.bird.http.client.HttpResponse; import com.jivesoftware.os.routing.bird.http.client.HttpStreamResponse; import com.jivesoftware.os.routing.bird.shared.HttpClientException; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.ObjectOutputStream; import java.nio.charset.StandardCharsets; import java.util.Map; import javax.ws.rs.core.Response; import org.apache.commons.codec.digest.DigestUtils; import org.apache.http.HttpStatus; /** * * @author jonathan.colt */ public class HttpRemotePartitionCaller implements RemotePartitionCaller<HttpClient, HttpClientException> { private static final MetricLogger LOG = MetricLoggerFactory.getLogger(); private final RouteInvalidator routeInvalidator; private final PartitionName partitionName; private final String base64PartitionName; private final Map<String, ClassMD5Bytes> filterClassCache = Maps.newConcurrentMap(); public HttpRemotePartitionCaller(RouteInvalidator routeInvalidator, PartitionName partitionName) { this.routeInvalidator = routeInvalidator; this.partitionName = partitionName; this.base64PartitionName = partitionName.toBase64(); } @Override public PartitionResponse<NoOpCloseable> commit(RingMember leader, RingMember ringMember, HttpClient client, Consistency consistency, byte[] prefix, ClientUpdates updates, long abandonSolutionAfterNMillis) throws HttpClientException { byte[] lengthBuffer = new byte[8]; boolean checkLeader = ringMember.equals(leader); HttpResponse got = client.postStreamableRequest("/amza/v1/commit/" + base64PartitionName + "/" + consistency.name() + "/" + checkLeader, (out) -> { try { FilerOutputStream fos = new FilerOutputStream(out); UIO.writeByteArray(fos, prefix, "prefix", lengthBuffer); UIO.writeLong(fos, abandonSolutionAfterNMillis, "timeoutInMillis", lengthBuffer); updates.updates((key, value, valueTimestamp, valueTombstoned) -> { UIO.write(fos, new byte[]{0}, "eos"); UIO.writeByteArray(fos, key, "key", lengthBuffer); UIO.writeByteArray(fos, value, "value", lengthBuffer); UIO.writeLong(fos, valueTimestamp, "valueTimestamp", lengthBuffer); UIO.write(fos, new byte[]{valueTombstoned ? (byte) 1 : (byte) 0}, "valueTombstoned"); return true; }); UIO.write(fos, new byte[]{1}, "eos"); } catch (Exception x) { throw new RuntimeException("Failed while streaming commitable.", x); } finally { out.close(); } }, null); if (got.getStatusCode() == Response.Status.ACCEPTED.getStatusCode()) { throw new FailedToAchieveQuorumException( "The server could NOT achieve " + consistency.name() + " within " + abandonSolutionAfterNMillis + "millis"); } handleLeaderStatusCodes(consistency, got.getStatusCode(), got.getStatusReasonPhrase(), null); return new PartitionResponse<>(new NoOpCloseable(), got.getStatusCode() >= 200 && got.getStatusCode() < 300); } @Override public PartitionResponse<CloseableStreamResponse> get(RingMember leader, RingMember ringMember, HttpClient client, Consistency consistency, byte[] prefix, UnprefixedWALKeys keys) throws HttpClientException { byte[] intLongBuffer = new byte[8]; HttpStreamResponse got = client.streamingPostStreamableRequest( "/amza/v1/get/" + base64PartitionName + "/" + consistency.name() + "/" + ringMember.equals(leader), (out) -> { try { FilerOutputStream fos = new FilerOutputStream(out); UIO.writeByteArray(fos, prefix, "prefix", intLongBuffer); keys.consume((key) -> { UIO.write(fos, new byte[]{0}, "eos"); UIO.writeByteArray(fos, key, "key", intLongBuffer); return true; }); UIO.write(fos, new byte[]{1}, "eos"); } catch (Exception x) { throw new RuntimeException("Failed while streaming keys.", x); } finally { out.close(); } }, null); CloseableHttpStreamResponse closeableHttpStreamResponse = new CloseableHttpStreamResponse(got); handleLeaderStatusCodes(consistency, got.getStatusCode(), got.getStatusReasonPhrase(), closeableHttpStreamResponse); return new PartitionResponse<>(closeableHttpStreamResponse, got.getStatusCode() >= 200 && got.getStatusCode() < 300); } @Override public PartitionResponse<CloseableStreamResponse> getOffset(RingMember leader, RingMember ringMember, HttpClient client, Consistency consistency, byte[] prefix, OffsetUnprefixedWALKeys keys) throws HttpClientException { byte[] intLongBuffer = new byte[8]; HttpStreamResponse got = client.streamingPostStreamableRequest( "/amza/v1/getOffset/" + base64PartitionName + "/" + consistency.name() + "/" + ringMember.equals(leader), (out) -> { try { FilerOutputStream fos = new FilerOutputStream(out); UIO.writeByteArray(fos, prefix, "prefix", intLongBuffer); keys.consume((key, offset, length) -> { UIO.write(fos, new byte[]{0}, "eos"); UIO.writeByteArray(fos, key, "key", intLongBuffer); UIO.writeInt(fos, offset, "offset", intLongBuffer); UIO.writeInt(fos, length, "length", intLongBuffer); return true; }); UIO.write(fos, new byte[]{1}, "eos"); } catch (Exception x) { throw new RuntimeException("Failed while streaming keys.", x); } finally { out.close(); } }, null); CloseableHttpStreamResponse closeableHttpStreamResponse = new CloseableHttpStreamResponse(got); handleLeaderStatusCodes(consistency, got.getStatusCode(), got.getStatusReasonPhrase(), closeableHttpStreamResponse); return new PartitionResponse<>(closeableHttpStreamResponse, got.getStatusCode() >= 200 && got.getStatusCode() < 300); } @Override public PartitionResponse<CloseableLong> getApproximateCount(RingMember leader, RingMember ringMember, HttpClient client) throws HttpClientException { HttpResponse got = client.get("/amza/v1/getApproximateCount/" + base64PartitionName + "/" + Consistency.none + "/" + ringMember.equals(leader), null); if (got.getStatusCode() >= 200 && got.getStatusCode() < 300) { return new PartitionResponse<>(new CloseableLong(Long.parseLong(new String(got.getResponseBody()))), true); } else { return new PartitionResponse<>(new CloseableLong(-1), false); } } @Override public PartitionResponse<CloseableStreamResponse> scan(RingMember leader, RingMember ringMember, HttpClient client, Consistency consistency, boolean compressed, PrefixedKeyRanges ranges, KeyValueFilter filter, boolean hydrateValues) throws HttpClientException { byte[] intLongBuffer = new byte[8]; String pathPrefix = (filter != null && compressed) ? "/amza/v1/multiScanFilteredCompressed/" : compressed ? "/amza/v1/multiScanCompressed/" : filter != null ? "/amza/v1/multiScanFiltered/" : "/amza/v1/multiScan/"; HttpStreamResponse got = client.streamingPostStreamableRequest( pathPrefix + base64PartitionName + "/" + consistency.name() + "/" + ringMember.equals(leader) + "/" + hydrateValues, (out) -> { try { FilerOutputStream fos = new FilerOutputStream(out); if (filter != null) { Class<? extends KeyValueFilter> c = filter.getClass(); String className = c.getName(); ClassMD5Bytes classMD5Bytes = filterClassCache.computeIfAbsent(className, aClass -> { String classAsPath = className.replace('.', '/') + ".class"; try (InputStream stream = c.getClassLoader().getResourceAsStream(classAsPath)) { try (ByteArrayOutputStream classOut = new ByteArrayOutputStream()) { ByteStreams.copy(stream, classOut); byte[] bytes = classOut.toByteArray(); byte[] md5 = DigestUtils.md5(bytes); return new ClassMD5Bytes(md5, bytes); } } catch (Exception e) { throw new RuntimeException(e); } }); UIO.writeByteArray(fos, className.getBytes(StandardCharsets.UTF_8), "className", intLongBuffer); UIO.writeByteArray(fos, classMD5Bytes.md5, "classMD5", intLongBuffer); UIO.writeByteArray(fos, classMD5Bytes.bytes, "classBytes", intLongBuffer); new ObjectOutputStream(out).writeObject(filter); } ranges.consume((fromPrefix, fromKey, toPrefix, toKey) -> { UIO.writeByte(fos, (byte) 1, "eos"); UIO.writeByteArray(fos, fromPrefix, "fromPrefix", intLongBuffer); UIO.writeByteArray(fos, fromKey, "fromKey", intLongBuffer); UIO.writeByteArray(fos, toPrefix, "toPrefix", intLongBuffer); UIO.writeByteArray(fos, toKey, "toKey", intLongBuffer); return true; }); UIO.writeByte(fos, (byte) 0, "eos"); } catch (Exception x) { throw new RuntimeException("Failed while scanning ranges.", x); } finally { out.close(); } }, null); return new PartitionResponse<>(new CloseableHttpStreamResponse(got), got.getStatusCode() >= 200 && got.getStatusCode() < 300); } @Override public PartitionResponse<CloseableStreamResponse> takeFromTransactionId(RingMember leader, RingMember ringMember, HttpClient client, Map<RingMember, Long> membersTxId, int limit) throws HttpClientException { long transactionId = membersTxId.getOrDefault(ringMember, -1L); HttpStreamResponse got = client.streamingPostStreamableRequest( "/amza/v1/takeFromTransactionId/" + base64PartitionName + '/' + limit, (out) -> { try { FilerOutputStream fos = new FilerOutputStream(out); UIO.writeLong(fos, transactionId, "transactionId", new byte[8]); } finally { out.close(); } }, null); return new PartitionResponse<>(new CloseableHttpStreamResponse(got), got.getStatusCode() >= 200 && got.getStatusCode() < 300); } @Override public PartitionResponse<CloseableStreamResponse> takePrefixFromTransactionId(RingMember leader, RingMember ringMember, HttpClient client, byte[] prefix, Map<RingMember, Long> membersTxId, int limit) throws HttpClientException { byte[] intLongBuffer = new byte[8]; long transactionId = membersTxId.getOrDefault(ringMember, -1L); HttpStreamResponse got = client.streamingPostStreamableRequest( "/amza/v1/takePrefixFromTransactionId/" + base64PartitionName + '/' + limit, (out) -> { try { FilerOutputStream fos = new FilerOutputStream(out); UIO.writeByteArray(fos, prefix, "prefix", intLongBuffer); UIO.writeLong(fos, transactionId, "transactionId", intLongBuffer); } finally { out.close(); } }, null); return new PartitionResponse<>(new CloseableHttpStreamResponse(got), got.getStatusCode() >= 200 && got.getStatusCode() < 300); } private void handleLeaderStatusCodes(Consistency consistency, int statusCode, String statusReasonPhrase, Closeable closeable) { if (statusCode == HttpStatus.SC_BAD_REQUEST) { try { if (closeable != null) { closeable.close(); } } catch (Exception e) { LOG.warn("Failed to close {}", closeable); } throw new IllegalArgumentException("Bad request: " + statusReasonPhrase); } else if (consistency.requiresLeader()) { if (statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE) { try { if (closeable != null) { closeable.close(); } } catch (Exception e) { LOG.warn("Failed to close {}", closeable); } routeInvalidator.invalidateRouting(partitionName); throw new LeaderElectionInProgressException(partitionName + " " + consistency); } if (statusCode == HttpStatus.SC_CONFLICT) { try { if (closeable != null) { closeable.close(); } } catch (Exception e) { LOG.warn("Failed to close {}", closeable); } routeInvalidator.invalidateRouting(partitionName); throw new NoLongerTheLeaderException(partitionName + " " + consistency); } } } }