package com.zillabyte.motherbrain.coordination; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.log4j.Logger; import com.zillabyte.motherbrain.coordination.redis.RedisException; import com.zillabyte.motherbrain.coordination.redis.TransactionalMessageWrapper; /**** * This class exists because we want the CoordinationService interface to expose the `ask...` methods, * but we don't want to force all implementations to reinvent the wheel if they dont want to. That * is, we can build the ask-subsystem with the sendMessage(..) and watchForMessage(..) primitives. * * Future implementaitons of CoordinationService can simply use the AskWrapper by exposing those * primitives and avoid making their own aks(..) implementations, if they don't want to. * * @author jake * */ public class AskWrapper implements Serializable { /** * */ private static final long serialVersionUID = -1721540097899692732L; /*** * This interface forces you to delegate handleNewMessage(..) back to AskWrapper, otherwise * transactional messages won't work. */ public static interface AskableService extends CoordinationService { public void handleNewMessage(String key, Object message, MessageHandler handler) throws CoordinationException; } public static final String ASK_PREFIX = "__ask/"; private static Logger _log = Logger.getLogger(AskWrapper.class); private CoordinationService _service; /*** * * @param service */ public AskWrapper(CoordinationService service) { _service = service; } /**** * This should always be called after the universe is instantiated with this state service. * * @param stream * @param message * @param timeout * @throws TimeoutException * @throws InterruptedException * @throws CoordinationException */ public final boolean sendTransactionalMessage(ExecutorService exec, final String stream, final Object message, long timeout) throws CoordinationException, TimeoutException { // Init final UUID returnMessage = UUID.randomUUID(); final String returnStream = UUID.randomUUID().toString(); final TransactionalMessageWrapper wrapper = new TransactionalMessageWrapper(message, returnStream, returnMessage); final SynchronousQueue<Object> response = new SynchronousQueue<>(); // _log.info("Sending message "+message +" on stream "+stream+". Watching for response on "+returnStream); // Watch for reply... Watcher watcher = _service.watchForMessage(exec, wrapper.returnStream, new MessageHandler() { @Override public void handleNewMessage(String key, Object raw) throws InterruptedException { // _log.info("transactional response on "+returnStream+": " + key + " object: " + raw); response.put(raw); } }); try { // Send the initial question... _service.sendMessage(stream, wrapper); // Wait for the reply... debug("waiting for transactional reply for message " + message + " on stream " + stream + " wrapper: " + wrapper); Object o = response.poll(timeout, TimeUnit.MILLISECONDS); // Sanity if (o == null ) throw new TimeoutException("Timeout waiting for response on stream: " + stream); if (o instanceof Exception) throw (RemoteCoordinationException) new RemoteCoordinationException(((Exception)o)).adviseRetry(); debug("response received "+stream); } catch(InterruptedException e) { throw (CoordinationException) new CoordinationException(e).adviseRetry(); } finally { debug("unsubscribing"); watcher.unsubscribe(); } // Done return true; } private void debug(String string) { //_log.debug(string); //System.err.println(string); } /*** * A wrapper around sendMessage(..) that synchronously waits for a response to come back from the listeners. * * @param rawStream * @param value * @param timeout * @throws CoordinationException * @throws InterruptedException * @throws CoordinationException */ public final Object ask(ExecutorService exec, final String rawStream, final Object value, final long timeout) throws TimeoutException, CoordinationException { // INIT // _log.info("ASK: asking on " + rawStream + " with param " + value + " and timeout: " + timeout); final String stream = ASK_PREFIX + rawStream; final String responseStream = UUID.randomUUID().toString(); final SynchronousQueue<Object> response = new SynchronousQueue<>(); Object reply; // Init the question HashMap<String, Object> map = new HashMap<>(); map.put("response_stream", responseStream); map.put("value", value); // Watch for response final Watcher watcher = _service.watchForMessage(exec, responseStream, new MessageHandler() { @Override public void handleNewMessage(String key, Object raw) throws InterruptedException { // _log.info("Recieved ask response at " + key + ": " + raw); response.put(raw); } }); // Send the message _service.sendMessage(stream, map); // Get the response, possible timeout try { reply = response.poll(timeout, TimeUnit.MILLISECONDS); if (reply == null) throw new TimeoutException("ask timeout: " + rawStream); } catch (InterruptedException e) { throw new TimeoutException("interrupted"); } finally { watcher.unsubscribe(); } // Errors if (reply instanceof Exception) { throw (RemoteCoordinationException) new RemoteCoordinationException((Exception)reply).adviseRetry(); } // Done return reply; } /*** * Responds to ASKs * * @param rawStream * @param askHandler * @param exec */ public final Watcher watchForAsk(ExecutorService executor, final String rawStream, final AskHandler askHandler) throws CoordinationException { // INIT final String stream = ASK_PREFIX + rawStream; // _log.info("ask watching on: " + stream + " with handler: " + askHandler.toString()); // Start watching... Watcher watcher = _service.watchForMessage(executor, stream, new MessageHandler() { @Override public void handleNewMessage(String key, Object payload) throws Exception { // Init Map<?, ?> map = (Map<?, ?>) payload; String responseStream = (String) map.get("response_stream"); Object value = map.get("value"); // Sanity checks if (value == null) throw (RedisException) new RedisException("value should not be null!").adviseRetry(); if (responseStream == null) throw (RedisException) new RedisException("responseStream should not be null!").adviseRetry(); // Call the handler final Object result = askHandler.handleAsk(key, value); // Return sanity if (result instanceof Exception) { throw new IllegalArgumentException("cannot return type Exception: " + result); } if (result == null) { throw new IllegalArgumentException("cannot return null"); } // Send the reply // _log.info("ask from: " + stream + " returning to stream: " + responseStream.toString() + " with result: " + result); _service.sendMessage(responseStream, result); } }); // DONE return watcher; } /*** * * @param key * @param message * @param handler * @throws CoordinationException */ public void handleNewMessage(String key, Object message, MessageHandler handler) throws CoordinationException { // Init TransactionalMessageWrapper tWrapper = null; Object ret = null; if (message instanceof TransactionalMessageWrapper) { // Transactional message // _log.info("received transactional message: " + key + " msg: " + message); tWrapper = (TransactionalMessageWrapper) message; message = tWrapper.getMessage(); ret = tWrapper.getReturnMessage(); } else { // Normal message // _log.info("received normal message: " + key + " msg: " + message); } // Let the handler take care of this... try { handler.handleNewMessage(key, message); } catch(Exception ex) { ex.printStackTrace(); _log.error("message handler threw an error: " + ex.getMessage()); ret = ex; } // Compelte transaction if (tWrapper != null) { // _log.info("completing transactional message: " + key); _service.sendMessage(tWrapper.getReturnStream(), ret); } } }