package com.jivesoftware.os.amza.client.http;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jivesoftware.os.amza.api.RingPartitionProperties;
import com.jivesoftware.os.amza.api.filer.FilerInputStream;
import com.jivesoftware.os.amza.api.filer.UIO;
import com.jivesoftware.os.amza.api.partition.PartitionName;
import com.jivesoftware.os.amza.api.partition.PartitionProperties;
import com.jivesoftware.os.amza.api.ring.RingHost;
import com.jivesoftware.os.amza.api.ring.RingMember;
import com.jivesoftware.os.amza.api.ring.RingMemberAndHost;
import com.jivesoftware.os.mlogger.core.MetricLogger;
import com.jivesoftware.os.mlogger.core.MetricLoggerFactory;
import com.jivesoftware.os.routing.bird.http.client.ConnectionDescriptorSelectiveStrategy;
import com.jivesoftware.os.routing.bird.http.client.HttpResponse;
import com.jivesoftware.os.routing.bird.http.client.HttpStreamResponse;
import com.jivesoftware.os.routing.bird.http.client.RoundRobinStrategy;
import com.jivesoftware.os.routing.bird.http.client.TailAtScaleStrategy;
import com.jivesoftware.os.routing.bird.http.client.TenantAwareHttpClient;
import com.jivesoftware.os.routing.bird.shared.ClientCall;
import com.jivesoftware.os.routing.bird.shared.ClientCall.ClientResponse;
import com.jivesoftware.os.routing.bird.shared.HostPort;
import com.jivesoftware.os.routing.bird.shared.HttpClientException;
import com.jivesoftware.os.routing.bird.shared.NextClientStrategy;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
import org.apache.http.HttpStatus;
/**
* @author jonathan.colt
*/
public class HttpPartitionHostsProvider implements PartitionHostsProvider {
private static final MetricLogger LOG = MetricLoggerFactory.getLogger();
private final TenantAwareHttpClient<String> tenantAwareHttpClient;
private final ObjectMapper mapper;
private final RoundRobinStrategy roundRobinStrategy = new RoundRobinStrategy();
private final TailAtScaleStrategy tailAtScaleStrategy;
public HttpPartitionHostsProvider(TenantAwareHttpClient<String> tenantAwareHttpClient, TailAtScaleStrategy tailAtScaleStrategy, ObjectMapper mapper) {
this.tenantAwareHttpClient = tenantAwareHttpClient;
this.tailAtScaleStrategy = tailAtScaleStrategy;
this.mapper = mapper;
}
private static class RingMembersChangedException extends RuntimeException {
public RingMembersChangedException(String message) {
super(message);
}
}
@Override
public RingPartitionProperties getRingPartitionProperties(PartitionName partitionName) throws Exception {
String base64PartitionName = partitionName.toBase64();
return tenantAwareHttpClient.call("", tailAtScaleStrategy, "configPartition", (client) -> {
HttpResponse got = client.get("/amza/v1/properties/" + base64PartitionName, null);
if (got.getStatusCode() >= 200 && got.getStatusCode() < 300) {
try {
return new ClientResponse<>(mapper.readValue(got.getResponseBody(), RingPartitionProperties.class), true);
} catch (IOException x) {
throw new RuntimeException("Failed getting properties for " + partitionName, x);
}
}
throw new RuntimeException("Failed to get properties:" + partitionName + " statusCode:" + got.getStatusCode());
});
}
@Override
public void ensurePartition(PartitionName partitionName, int desiredRingSize, PartitionProperties partitionProperties) throws Exception {
long timeoutAfterTimestamp = System.currentTimeMillis() + 30_000; //TODO config
while (System.currentTimeMillis() < timeoutAfterTimestamp) {
try {
ensure(partitionName, desiredRingSize, partitionProperties);
return;
} catch (RingMembersChangedException e) {
LOG.warn("Ring membership for {} has changed, retrying...", partitionName);
Thread.sleep(10); //TODO config
}
}
throw new TimeoutException("Failed to ensure partition: " + partitionName);
}
private void ensure(PartitionName partitionName, int desiredRingSize, PartitionProperties partitionProperties) throws Exception {
String base64PartitionName = partitionName.toBase64();
String partitionPropertiesString = mapper.writeValueAsString(partitionProperties);
byte[] intBuffer = new byte[4];
Ring partitionsRing = tenantAwareHttpClient.call("", roundRobinStrategy, "configPartition", (client) -> { // maybe switch to tailAtScale
HttpStreamResponse got = client.streamingPost("/amza/v1/configPartition/" + base64PartitionName + "/" + desiredRingSize,
partitionPropertiesString, null);
try {
if (got.getStatusCode() >= 200 && got.getStatusCode() < 300) {
try {
FilerInputStream fis = new FilerInputStream(got.getInputStream());
int ringSize = UIO.readInt(fis, "ringSize", intBuffer);
int leaderIndex = -1;
RingMemberAndHost[] ring = new RingMemberAndHost[ringSize];
for (int i = 0; i < ringSize; i++) {
byte[] ringMemberBytes = UIO.readByteArray(fis, "ringMember", intBuffer);
RingMember ringMember = new RingMember(ringMemberBytes);
RingHost ringHost = RingHost.fromBytes(UIO.readByteArray(fis, "ringHost", intBuffer));
ring[i] = new RingMemberAndHost(ringMember, ringHost);
if (UIO.readBoolean(fis, "leader")) {
if (leaderIndex == -1) {
leaderIndex = i;
} else {
throw new RuntimeException("We suck! Gave back more than one leader!");
}
}
}
return new ClientCall.ClientResponse<>(new Ring(leaderIndex, ring), true);
} catch (Exception x) {
throw new RuntimeException("Failed loading routes for " + partitionName, x);
}
}
} finally {
got.close();
}
throw new RuntimeException("Failed to config partition:" + partitionName + " statusCode:" + got.getStatusCode());
});
HostPort[] orderHostPorts = new HostPort[partitionsRing.members.length];
for (int i = 0; i < orderHostPorts.length; i++) {
RingHost ringHost = partitionsRing.members[i].ringHost;
orderHostPorts[i] = new HostPort(ringHost.getHost(), ringHost.getPort());
}
NextClientStrategy strategy = new ConnectionDescriptorSelectiveStrategy(orderHostPorts);
tenantAwareHttpClient.call("", strategy, "ensurePartition", (client) -> {
HttpResponse got = client.postJson("/amza/v1/ensurePartition/" + base64PartitionName + "/" + 30_000, // TODO config
partitionPropertiesString, null);
if (got.getStatusCode() >= 200 && got.getStatusCode() < 300) {
return new ClientResponse<>(null, true);
} else if (got.getStatusCode() == HttpStatus.SC_CONFLICT) {
throw new RingMembersChangedException("Ring members have changed");
} else {
throw new RuntimeException("Failed to ensure partition: " + partitionName + " statusCode: " + got.getStatusCode());
}
});
}
@Override
public Ring getPartitionHosts(PartitionName partitionName, Optional<RingMemberAndHost> useHost, long waitForLeaderElection) throws HttpClientException {
NextClientStrategy strategy = useHost.map((ringMemberAndHost) -> {
HostPort[] hostPorts = { new HostPort(ringMemberAndHost.ringHost.getHost(), ringMemberAndHost.ringHost.getPort()) };
return (NextClientStrategy) new ConnectionDescriptorSelectiveStrategy(hostPorts);
}).orElse(tailAtScaleStrategy);
byte[] intBuffer = new byte[4];
Ring leaderlessRing = tenantAwareHttpClient.call("", strategy, "getPartitionHosts", (client) -> {
HttpStreamResponse got = client.streamingPost("/amza/v1/ring/"
+ partitionName.toBase64(), "", null);
Ring ring = consumeRing(partitionName, got, intBuffer);
return new ClientCall.ClientResponse<>(ring, true);
});
if (waitForLeaderElection > 0) {
RingMemberAndHost[] actualRing = leaderlessRing.actualRing();
HostPort[] chooseFrom = new HostPort[actualRing.length];
for (int i = 0; i < chooseFrom.length; i++) {
RingHost ringHost = actualRing[i].ringHost;
chooseFrom[i] = new HostPort(ringHost.getHost(), ringHost.getPort());
}
strategy = new ConnectionDescriptorSelectiveStrategy(chooseFrom);
return tenantAwareHttpClient.call("", strategy, "ringLeader", (client) -> {
HttpStreamResponse got = client.streamingPost("/amza/v1/ringLeader/"
+ partitionName.toBase64() + "/" + waitForLeaderElection, "", null);
Ring ring = consumeRing(partitionName, got, intBuffer);
return new ClientCall.ClientResponse<>(ring, true);
});
} else {
return leaderlessRing;
}
}
private Ring consumeRing(PartitionName partitionName, HttpStreamResponse got, byte[] intBuffer) {
try {
if (got.getStatusCode() >= 200 && got.getStatusCode() < 300) {
FilerInputStream fis = null;
try {
fis = new FilerInputStream(got.getInputStream());
int ringSize = UIO.readInt(fis, "ringSize", intBuffer);
int leaderIndex = -1;
RingMemberAndHost[] ring = new RingMemberAndHost[ringSize];
for (int i = 0; i < ringSize; i++) {
byte[] ringMemberBytes = UIO.readByteArray(fis, "ringMember", intBuffer);
RingMember ringMember = new RingMember(ringMemberBytes);
RingHost ringHost = RingHost.fromBytes(UIO.readByteArray(fis, "ringHost", intBuffer));
ring[i] = new RingMemberAndHost(ringMember, ringHost);
if (UIO.readBoolean(fis, "leader")) {
if (leaderIndex == -1) {
leaderIndex = i;
} else {
throw new RuntimeException("We suck! Gave back more than one leader!");
}
}
}
return new Ring(leaderIndex, ring);
} catch (Exception x) {
throw new RuntimeException("Failed loading routes for " + partitionName, x);
}
}
} finally {
got.close();
}
throw new RuntimeException("No routes to partition:" + partitionName + " statusCode:" + got.getStatusCode());
}
}