package io.cattle.platform.eventing.impl; import io.cattle.platform.archaius.util.ArchaiusUtil; import io.cattle.platform.async.retry.Retry; import io.cattle.platform.async.retry.RetryTimeoutService; import io.cattle.platform.async.utils.AsyncUtils; import io.cattle.platform.async.utils.TimeoutException; import io.cattle.platform.eventing.EventCallOptions; import io.cattle.platform.eventing.EventListener; import io.cattle.platform.eventing.EventService; import io.cattle.platform.eventing.RetryCallback; import io.cattle.platform.eventing.exception.EventExecutionException; import io.cattle.platform.eventing.model.Event; import io.cattle.platform.eventing.model.EventVO; import io.cattle.platform.json.JsonMapper; import io.cattle.platform.metrics.util.MetricsUtil; import io.cattle.platform.pool.PoolConfig; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.apache.commons.pool2.ObjectPool; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Counter; import com.codahale.metrics.Timer; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicLongProperty; public abstract class AbstractEventService implements EventService { private static final Logger log = LoggerFactory.getLogger(AbstractEventService.class); private static final Logger EVENT_LOG_IN = LoggerFactory.getLogger("EventLogIn"); private static final Logger EVENT_LOG_OUT = LoggerFactory.getLogger("EventLogOut"); public static final DynamicIntProperty DEFAULT_RETRIES = ArchaiusUtil.getInt("eventing.retry"); public static final DynamicLongProperty DEFAULT_TIMEOUT = ArchaiusUtil.getLong("eventing.timeout.millis"); private static final Object SUBSCRIPTION_LOCK = new Object(); RetryTimeoutService timeoutService; private ExecutorService executorService; Map<String, List<EventListener>> eventToListeners = new HashMap<String, List<EventListener>>(); Map<EventListener, Set<String>> listenerToEvents = new HashMap<EventListener, Set<String>>(); JsonMapper jsonMapper; ObjectPool<FutureEventListener> listenerPool; Map<String, Counter> request = new ConcurrentHashMap<String, Counter>(); Map<String, Counter> publish = new ConcurrentHashMap<String, Counter>(); Map<String, Counter> failed = new ConcurrentHashMap<String, Counter>(); Map<String, Timer> timers = new ConcurrentHashMap<String, Timer>(); @Override public boolean publish(Event event) { return publish(event, false); } protected boolean publish(Event event, boolean request) { if (event == null) { return false; } String eventString = null; try { eventString = jsonMapper.writeValueAsString(event); } catch (IOException e) { throw new IllegalStateException("Failed to marshall event [" + event + "] to string", e); } if (event.getName() == null) { log.error("Can not publish an event with a null name : {}", eventString); return false; } try { getEventLogOut().debug(eventString); increment(event, request); return doPublish(event.getName(), event, eventString); } catch (Throwable e) { log.warn("Failed to publish event [" + eventString + "]", e); return false; } } protected abstract boolean doPublish(String name, Event event, String eventString) throws IOException; protected List<EventListener> getEventListeners(Event event) { String eventName = event.getName(); List<EventListener> result = eventToListeners.get(eventName); if (event instanceof EventVO) { String listenerKey = ((EventVO<?>) event).getListenerKey(); if (listenerKey != null && !listenerKey.equals(eventName)) { List<EventListener> additional = eventToListeners.get(((EventVO<?>) event).getListenerKey()); if (additional != null) { if (result == null) { return additional; } else { result = new ArrayList<EventListener>(result); result.addAll(additional); } } } } return result; } protected Logger getEventLogIn() { return EVENT_LOG_IN; } protected Logger getEventLogOut() { return EVENT_LOG_OUT; } protected boolean register(String eventName, EventListener listener) { synchronized (SUBSCRIPTION_LOCK) { boolean doSubscribe = false; Set<String> events = listenerToEvents.get(listener); if (events == null) { events = new HashSet<String>(); listenerToEvents.put(listener, events); } List<EventListener> listeners = eventToListeners.get(eventName); if (listeners == null) { listeners = new CopyOnWriteArrayList<EventListener>(); eventToListeners.put(eventName, listeners); doSubscribe = true; } listeners.add(listener); events.add(eventName); return doSubscribe; } } protected boolean unregister(String eventName, EventListener listener) { synchronized (SUBSCRIPTION_LOCK) { boolean doUnsubscribe = false; Set<String> events = listenerToEvents.get(listener); if (events != null) { events.remove(eventName); if (events.size() == 0) { listenerToEvents.remove(listener); } } List<EventListener> listeners = eventToListeners.get(eventName); if (listeners != null) { listeners.remove(listener); if (listeners.size() == 0) { eventToListeners.remove(eventName); doUnsubscribe = true; } } return doUnsubscribe; } } protected Set<String> getSubscriptions(EventListener eventListener) { synchronized (SUBSCRIPTION_LOCK) { Set<String> result = new HashSet<String>(); Set<String> current = listenerToEvents.get(eventListener); if (current != null) { result.addAll(current); } return result; } } @Override public ListenableFuture<?> subscribe(final String eventName, final EventListener listener) { final SettableFuture<?> future = SettableFuture.create(); boolean doSubscribe = register(eventName, listener); if (doSubscribe) { doSubscribe(eventName, future); } else { future.set(null); } future.addListener(new Runnable() { @Override public void run() { try { future.get(); } catch (Exception e) { unsubscribe(eventName, listener); disconnect(); } } }, executorService); return future; } protected abstract void doSubscribe(String eventName, SettableFuture<?> future); @Override public void unsubscribe(String eventName, EventListener listener) { boolean doUnsubscribe = unregister(eventName, listener); if (doUnsubscribe) { doUnsubscribe(eventName); } } protected abstract void doUnsubscribe(String eventName); @Override public void unsubscribe(EventListener listener) { for (String eventName : getSubscriptions(listener)) { unsubscribe(eventName, listener); } } protected EventCallOptions defaultCallOptions() { return new EventCallOptions().withRetry(DEFAULT_RETRIES.get()).withTimeoutMillis(DEFAULT_TIMEOUT.get()); } @Override public Event callSync(Event event, EventCallOptions options) { return AsyncUtils.get(call(event, options)); } @Override public ListenableFuture<Event> call(final Event event, EventCallOptions options) { final long start = System.currentTimeMillis(); Integer retries = options.getRetry(); Long timeoutMillis = options.getTimeoutMillis(); final RetryCallback retryCallback = options.getRetryCallback(); if (event.getTimeoutMillis() != null) { timeoutMillis = event.getTimeoutMillis(); } if (retries == null) { retries = DEFAULT_RETRIES.get(); } if (timeoutMillis == null) { timeoutMillis = DEFAULT_TIMEOUT.get(); } final SettableFuture<Event> future = SettableFuture.create(); final FutureEventListener listener; try { listener = listenerPool.borrowObject(); } catch (Exception e) { future.setException(e); return future; } final EventVO<Object> request = new EventVO<Object>(event, listener.getReplyTo()); request.setTimeoutMillis(timeoutMillis); Retry retry = new Retry(retries, timeoutMillis, future, new Runnable() { @Override public void run() { Event requestToSend = null; if (retryCallback == null) { requestToSend = request; } else { requestToSend = retryCallback.beforeRetry(request); if (requestToSend == null) { requestToSend = request; } } publish(requestToSend, true); } }); final Object cancel = timeoutService.submit(retry); listener.setProgress(options.getProgress()); listener.setFuture(future); listener.setEvent(request); if (options.isProgressIsKeepAlive() && options.getProgress() != null) { listener.setRetry(retry); } future.addListener(new Runnable() { @Override public void run() { try { timeoutService.completed(cancel); future.get(); time(event, start); } catch (ExecutionException t) { error(event); if (t.getCause() instanceof TimeoutException) { // Ignore don't treat as a bad listener } else if (t.getCause() instanceof EventExecutionException) { // Ignore event errors } else { listener.setFailed(true); } } catch (Throwable t) { error(event); listener.setFailed(true); } finally { try { listenerPool.returnObject(listener); } catch (Exception e) { log.error("Failed to return object to pool [" + listener + "]", e); } } } }, executorService); publish(request, true); return future; } @PostConstruct public void init() { if (listenerPool == null) { GenericObjectPoolConfig config = new GenericObjectPoolConfig(); PoolConfig.setConfig(config, "eventing.reply.pool", "eventing.reply.pool.", "global.pool."); listenerPool = new GenericObjectPool<FutureEventListener>(new ListenerPoolObjectFactory(this), config); } } protected void increment(Event event, boolean request) { String metricName = metricName(event, request ? "request" : "publish"); if (metricName == null) { return; } Map<String, Counter> counters = request ? this.request : publish; Counter counter = counters.get(metricName); if (counter == null) { counter = MetricsUtil.getRegistry().counter(metricName); counters.put(metricName, counter); } counter.inc(); } protected void error(Event event) { String metricName = metricName(event, "failed"); if (metricName == null) { return; } Counter counter = failed.get(metricName); if (counter == null) { counter = MetricsUtil.getRegistry().counter(metricName); failed.put(metricName, counter); } counter.inc(); } protected void time(Event event, long start) { long duration = System.currentTimeMillis() - start; String metricName = metricName(event, "time"); if (metricName == null) { return; } Timer timer = timers.get(metricName); if (timer == null) { timer = MetricsUtil.getRegistry().timer(metricName); timers.put(metricName, timer); } timer.update(duration, TimeUnit.MILLISECONDS); } protected String metricName(Event event, String prefix) { String name = event.getName(); if (name.startsWith(REPLY_PREFIX)) { return null; } return "event." + prefix + "." + StringUtils.substringBefore(name, EVENT_SEP).replace('.', '_'); } boolean isSubscribed(String eventName) { return eventToListeners.containsKey(eventName); } protected abstract void disconnect(); public JsonMapper getJsonMapper() { return jsonMapper; } protected Object getSubscriptionLock() { return SUBSCRIPTION_LOCK; } @Inject public void setJsonMapper(JsonMapper jsonMapper) { this.jsonMapper = jsonMapper; } public ObjectPool<FutureEventListener> getListenerPool() { return listenerPool; } public void setListenerPool(ObjectPool<FutureEventListener> listenerPool) { this.listenerPool = listenerPool; } public ExecutorService getExecutorService() { return executorService; } public void setExecutorService(ExecutorService executorService) { this.executorService = executorService; } public RetryTimeoutService getTimeoutService() { return timeoutService; } @Inject public void setTimeoutService(RetryTimeoutService timeoutService) { this.timeoutService = timeoutService; } }