package org.corfudb.runtime.clients; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.corfudb.infrastructure.TestServerRouter; import org.corfudb.protocols.wireprotocol.CorfuMsg; import org.corfudb.protocols.wireprotocol.CorfuMsgType; import org.corfudb.runtime.CorfuRuntime; import org.corfudb.runtime.exceptions.WrongEpochException; import org.corfudb.util.CFUtils; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import static org.corfudb.AbstractCorfuTest.PARAMETERS; /** * Created by mwei on 12/13/15. */ @Slf4j // this class does not have access to parameters and is // scheduled to be deprecated anyway. @SuppressWarnings("checkstyle:magicnumber") public class TestClientRouter implements IClientRouter { /** * The clients registered to this router. */ public List<IClient> clientList; /** * The handlers registered to this router. */ public Map<CorfuMsgType, IClient> handlerMap; /** * The outstanding requests on this router. */ public Map<Long, CompletableFuture> outstandingRequests; public volatile AtomicLong requestID; private long epoch; public synchronized long getEpoch() { return epoch; } public synchronized void setEpoch(long epoch) { this.epoch = epoch; } @Getter @Setter public long serverEpoch; @Getter @Setter public UUID clientID; /** * New connection timeout (milliseconds) */ @Getter @Setter public long timeoutConnect = PARAMETERS.TIMEOUT_NORMAL.toMillis(); /** * Sync call response timeout (milliseconds) */ @Getter @Setter public long timeoutResponse = PARAMETERS.TIMEOUT_NORMAL.toMillis(); /** * Retry interval after timeout (milliseconds) */ @Getter @Setter public long timeoutRetry = PARAMETERS.TIMEOUT_SHORT.toMillis(); public List<TestRule> rules; /** The server router endpoint this client should route to. */ TestServerRouter serverRouter; /** A mock channel context for this connection. */ TestChannelContext channelContext; /** * The test host that this router is routing requests for. */ @Getter String host = "testServer"; /** * The test port that this router is routing requests for. */ @Getter Integer port; public TestClientRouter(TestServerRouter serverRouter) { clientList = new ArrayList<>(); handlerMap = new ConcurrentHashMap<>(); outstandingRequests = new ConcurrentHashMap<>(); requestID = new AtomicLong(); clientID = CorfuRuntime.getStreamID("testClient"); rules = new ArrayList<>(); this.serverRouter = serverRouter; channelContext = new TestChannelContext(this::handleMessage); port = serverRouter.getPort(); } private void handleMessage(Object o) { if (o instanceof CorfuMsg) { CorfuMsg m = (CorfuMsg) o; if (validateEpochAndClientID(m, channelContext)) { IClient handler = handlerMap.get(m.getMsgType()); handler.handleMessage(m, null); } } } private void routeMessage(CorfuMsg message) { CorfuMsg m = simulateSerialization(message); serverRouter.sendServerMessage(m, channelContext); } /** * Add a new client to the router. * * @param client The client to add to the router. * @return This IClientRouter, to support chaining and the builder pattern. */ @Override public IClientRouter addClient(IClient client) { // Set the client's router to this instance. client.setRouter(this); // Iterate through all types of CorfuMsgType, registering the handler client.getHandledTypes().stream() .forEach(x -> { handlerMap.put(x, client); log.trace("Registered {} to handle messages of type {}", client, x); }); // Register this type clientList.add(client); return this; } /** * Gets a client that matches a particular type. * * @param clientType The class of the client to match. * @return The first client that matches that type. * @throws NoSuchElementException If there are no clients matching that type. */ @Override @SuppressWarnings("unchecked") public <T extends IClient> T getClient(Class<T> clientType) { return (T) clientList.stream() .filter(clientType::isInstance) .findFirst().get(); } /** * Send a message and get a completable future to be fulfilled by the reply. * * @param ctx The channel handler context to send the message under. * @param message The message to send. * @return A completable future which will be fulfilled by the reply, * or a timeout in the case there is no response. */ @Override public <T> CompletableFuture<T> sendMessageAndGetCompletable(ChannelHandlerContext ctx, CorfuMsg message) { // Get the next request ID. final long thisRequest = requestID.getAndIncrement(); // Set the message fields. message.setClientID(clientID); message.setRequestID(thisRequest); message.setEpoch(getEpoch()); // Generate a future and put it in the completion table. final CompletableFuture<T> cf = new CompletableFuture<>(); outstandingRequests.put(thisRequest, cf); // Evaluate rules. if (rules.stream() .map(x -> x.evaluate(message, this)) .allMatch(x -> x)) { // Write the message out to the channel log.trace(Thread.currentThread().getId() + ":Sent message: {}", message); routeMessage(message); } // Generate a timeout future, which will complete exceptionally if the main future is not completed. final CompletableFuture<T> cfTimeout = CFUtils.within(cf, Duration.ofMillis(timeoutResponse)); cfTimeout.exceptionally(e -> { outstandingRequests.remove(thisRequest); log.debug("Remove request {} due to timeout!", thisRequest); return null; }); return cfTimeout; } /** * Send a one way message, without adding a completable future. * * @param ctx The context to send the message under. * @param message The message to send. */ @Override public void sendMessage(ChannelHandlerContext ctx, CorfuMsg message) { // Get the next request ID. final long thisRequest = requestID.getAndIncrement(); message.setClientID(clientID); message.setRequestID(thisRequest); message.setEpoch(getEpoch()); // Evaluate rules. if (rules.stream() .map(x -> x.evaluate(message, this)) .allMatch(x -> x)) { // Write the message out to the channel. routeMessage(message); } } /** * Send a netty message through this router, setting the fields in the outgoing message. * * @param ctx Channel handler context to use. * @param inMsg Incoming message to respond to. * @param outMsg Outgoing message. */ @Override public void sendResponseToServer(ChannelHandlerContext ctx, CorfuMsg inMsg, CorfuMsg outMsg) { outMsg.copyBaseFields(inMsg); if (rules.stream() .map(x -> x.evaluate(outMsg, this)) .allMatch(x -> x)) { // Write the message out to the channel. ctx.writeAndFlush(outMsg); log.trace("Sent response: {}", outMsg); } } /** * Validate the epoch of a CorfuMsg, and send a WRONG_EPOCH response if * the server is in the wrong epoch. Ignored if the message type is reset (which * is valid in any epoch). * * @param msg The incoming message to validate. * @param ctx The context of the channel handler. * @return True, if the epoch is correct, but false otherwise. */ public boolean validateEpochAndClientID(CorfuMsg msg, ChannelHandlerContext ctx) { // Check if the message is intended for us. If not, drop the message. if (!msg.getClientID().equals(clientID)) { log.warn("Incoming message intended for client {}, our id is {}, dropping!", msg.getClientID(), clientID); return false; } // Check if the message is in the right epoch. if (!msg.getMsgType().ignoreEpoch && msg.getEpoch() != getEpoch()) { CorfuMsg m = new CorfuMsg(); log.trace("Incoming message with wrong epoch, got {}, expected {}, message was: {}", msg.getEpoch(), getEpoch(), msg); /* If this message was pending a completion, complete it with an error. */ completeExceptionally(msg.getRequestID(), new WrongEpochException(getEpoch())); return false; } return true; } /** * Complete a given outstanding request with a completion value. * * @param requestID The request to complete. * @param completion The value to complete the request with * @param <T> The type of the completion. */ @SuppressWarnings("unchecked") public <T> void completeRequest(long requestID, T completion) { CompletableFuture<T> cf; if ((cf = (CompletableFuture<T>) outstandingRequests.get(requestID)) != null) { cf.complete(completion); outstandingRequests.remove(requestID); } else { log.warn("Attempted to complete request {}, but request not outstanding!", requestID); } } /** * Exceptionally complete a request with a given cause. * * @param requestID The request to complete. * @param cause The cause to give for the exceptional completion. */ public void completeExceptionally(long requestID, Throwable cause) { CompletableFuture cf; if ((cf = outstandingRequests.get(requestID)) != null) { cf.completeExceptionally(cause); outstandingRequests.remove(requestID); } else { log.warn("Attempted to exceptionally complete request {}, but request not outstanding!", requestID); } } /** * Starts routing requests. */ @Override public void start() { } /** * Stops routing requests. */ @Override public void stop() { //TODO - pause pipeline } @Override public void stop(boolean unused) { //TODO - pause pipeline } public CorfuMsg simulateSerialization(CorfuMsg message) { /* simulate serialization/deserialization */ ByteBuf oBuf = Unpooled.buffer(); message.serialize(oBuf); oBuf.resetReaderIndex(); CorfuMsg msg = CorfuMsg.deserialize(oBuf); oBuf.release(); return msg; } }