/* * Copyright 2013 Jive Software, Inc * * 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.jivesoftware.os.amza.service.replication.http.endpoints; import com.fasterxml.jackson.databind.ObjectMapper; import com.jivesoftware.os.amza.api.AmzaInterner; import com.jivesoftware.os.amza.api.filer.UIO; import com.jivesoftware.os.amza.api.partition.VersionedPartitionName; import com.jivesoftware.os.amza.api.ring.RingHost; import com.jivesoftware.os.amza.api.ring.RingMember; import com.jivesoftware.os.amza.api.ring.TimestampedRingHost; import com.jivesoftware.os.amza.service.AmzaInstance; import com.jivesoftware.os.amza.service.stats.AmzaStats; import com.jivesoftware.os.mlogger.core.MetricLogger; import com.jivesoftware.os.mlogger.core.MetricLoggerFactory; import com.jivesoftware.os.routing.bird.shared.ResponseHelper; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.concurrent.TimeoutException; import javax.inject.Singleton; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import org.glassfish.jersey.server.ChunkedOutput; import org.glassfish.jersey.server.LatchChunkedOutput; import org.nustaq.serialization.FSTConfiguration; import org.xerial.snappy.SnappyOutputStream; @Singleton @Path("/amza") public class AmzaReplicationRestEndpoints { private static final MetricLogger LOG = MetricLoggerFactory.getLogger(); private static final FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration(); private final AmzaStats amzaStats; private final AmzaInstance amzaInstance; private final AmzaInterner amzaInterner; private final ObjectMapper objectMapper; public AmzaReplicationRestEndpoints(@Context AmzaStats amzaStats, @Context AmzaInstance amzaInstance, @Context AmzaInterner amzaInterner, @Context ObjectMapper objectMapper) { this.amzaStats = amzaStats; this.amzaInstance = amzaInstance; this.amzaInterner = amzaInterner; this.objectMapper = objectMapper; } @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("/rows/stream/{ringMemberString}/{versionedPartitionName}/{takeSessionId}/{txId}/{leadershipToken}/{limit}") public Response rowsStream(@PathParam("ringMemberString") String ringMemberString, @PathParam("versionedPartitionName") String versionedPartitionName, @PathParam("takeSessionId") long takeSessionId, @PathParam("txId") long txId, @PathParam("leadershipToken") long leadershipToken, @PathParam("limit") long limit, byte[] takeSharedKey) { try { amzaStats.rowsStream.increment(); StreamingOutput stream = (OutputStream os) -> { os.flush(); BufferedOutputStream bos = new BufferedOutputStream(new SnappyOutputStream(os), 8192); // TODO expose to config final DataOutputStream dos = new DataOutputStream(bos); try { amzaInstance.rowsStream(dos, new RingMember(ringMemberString), VersionedPartitionName.fromBase64(versionedPartitionName, amzaInterner), takeSessionId, objectMapper.readValue(takeSharedKey, Long.class), txId, leadershipToken, limit); } catch (IOException x) { if (x.getCause() instanceof TimeoutException) { LOG.error("Timed out while streaming takes"); } else { LOG.error("Failed to stream takes.", x); } throw x; } catch (Exception x) { LOG.error("Failed to stream takes.", x); throw new IOException("Failed to stream takes.", x); } finally { dos.flush(); amzaStats.rowsStream.decrement(); amzaStats.completedRowsStream.increment(); } }; return Response.ok(stream).build(); } catch (Exception x) { Object[] vals = new Object[] { ringMemberString, versionedPartitionName, txId }; LOG.warn("Failed to rowsStream {} {} {}. ", vals, x); return ResponseHelper.INSTANCE.errorResponse("Failed to rowsStream " + Arrays.toString(vals), x); } } @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("/rows/available/{ringMember}/{ringHost}/{system}/{ringTimestampId}/{takeSessionId}/{timeoutMillis}") public ChunkedOutput<byte[]> availableRowsStream(@PathParam("ringMember") String ringMemberString, @PathParam("ringHost") String ringHost, @PathParam("system") boolean system, @PathParam("ringTimestampId") long ringTimestampId, @PathParam("takeSessionId") long takeSessionId, @PathParam("timeoutMillis") long timeoutMillis, byte[] sharedKey) { LatchChunkedOutput chunkedOutput = new LatchChunkedOutput(10_000); new Thread(() -> { chunkedOutput.await("availableRowsStream", () -> { amzaStats.availableRowsStream.increment(); try { amzaInstance.availableRowsStream( system, chunkedOutput::write, new RingMember(ringMemberString), new TimestampedRingHost(RingHost.fromCanonicalString(ringHost), ringTimestampId), takeSessionId, objectMapper.readValue(sharedKey, Long.class), timeoutMillis); return null; } finally { amzaStats.availableRowsStream.decrement(); } }); }, "available-" + ringMemberString + "-" + (system ? "system" : "striped")).start(); return chunkedOutput; } /*@POST @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_JSON) @Path("/rows/taken/{memberName}/{takeSessionId}/{versionedPartitionName}/{txId}/{leadershipToken}") public Response rowsTaken(@PathParam("memberName") String ringMemberName, @PathParam("takeSessionId") long takeSessionId, @PathParam("versionedPartitionName") String versionedPartitionName, @PathParam("txId") long txId, @PathParam("leadershipToken") long leadershipToken, byte[] sharedKey) { try { amzaStats.rowsTaken.increment(); amzaInstance.rowsTaken(new RingMember(ringMemberName), takeSessionId, new String(sharedKey, StandardCharsets.UTF_8), VersionedPartitionName.fromBase64(versionedPartitionName, amzaInterner), txId, leadershipToken); return ResponseHelper.INSTANCE.jsonResponse(Boolean.TRUE); } catch (Exception x) { LOG.warn("Failed to ack for member:{} partition:{} txId:{}", new Object[] { ringMemberName, versionedPartitionName, txId }, x); return ResponseHelper.INSTANCE.errorResponse("Failed to ack.", x); } finally { amzaStats.rowsTaken.decrement(); amzaStats.completedRowsTake.increment(); } } @POST @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_JSON) @Path("/pong/{memberName}/{takeSessionId}") public Response pong(@PathParam("memberName") String ringMemberName, @PathParam("takeSessionId") long takeSessionId, byte[] sharedKey) { try { amzaInstance.pong(new RingMember(ringMemberName), takeSessionId, new String(sharedKey, StandardCharsets.UTF_8)); return ResponseHelper.INSTANCE.jsonResponse(Boolean.TRUE); } catch (Exception x) { LOG.warn("Failed pong for member:{} session:{}", new Object[] { ringMemberName, takeSessionId }, x); return ResponseHelper.INSTANCE.errorResponse("Failed pong.", x); } finally { amzaStats.pongsReceived.increment(); } }*/ @POST @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("/ackBatch") public Response ackBatch(InputStream is) { try { DataInputStream in = new DataInputStream(is); try { while (in.readByte() == 1) { int length = in.readShort(); byte[] bytes = new byte[length]; in.readFully(bytes); VersionedPartitionName versionedPartitionName = amzaInterner.internVersionedPartitionName(bytes, 0, length); length = in.readShort(); bytes = new byte[length]; in.readFully(bytes); RingMember ringMember = amzaInterner.internRingMember(bytes, 0, length); long takeSessionId = in.readLong(); long takeSharedKey = in.readLong(); long txId = in.readLong(); long leadershipToken = in.readLong(); amzaInstance.rowsTaken(ringMember, takeSessionId, takeSharedKey, versionedPartitionName, txId, leadershipToken); } if (in.readByte() == 1) { int length = in.readShort(); byte[] bytes = new byte[length]; in.readFully(bytes); RingMember ringMember = amzaInterner.internRingMember(bytes, 0, length); long takeSessionId = in.readLong(); long takeSharedKey = in.readLong(); amzaInstance.pong(ringMember, takeSessionId, takeSharedKey); } return Response.ok(conf.asByteArray(Boolean.TRUE)).build(); } finally { try { in.close(); } catch (Exception x) { LOG.error("Failed to close input stream", x); } } } catch (Exception x) { LOG.warn("Failed ackBatch", x); return ResponseHelper.INSTANCE.errorResponse("Failed ackBatch.", x); } finally { amzaStats.pongsReceived.increment(); } } @POST @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_JSON) @Path("/invalidate/{memberName}/{takeSessionId}/{versionedPartitionName}") public Response invalidate(@PathParam("memberName") String ringMemberName, @PathParam("takeSessionId") long takeSessionId, @PathParam("versionedPartitionName") String versionedPartitionName, byte[] sharedKey) { try { amzaInstance.invalidate(new RingMember(ringMemberName), takeSessionId, UIO.bytesLong(sharedKey), VersionedPartitionName.fromBase64(versionedPartitionName, amzaInterner)); return ResponseHelper.INSTANCE.jsonResponse(Boolean.TRUE); } catch (Exception x) { LOG.warn("Failed invalidate for member:{} session:{} partition:{}", new Object[] { ringMemberName, takeSessionId, versionedPartitionName }, x); return ResponseHelper.INSTANCE.errorResponse("Failed pong.", x); } finally { amzaStats.pongsReceived.increment(); } } }