/** * */ package com.thinkbiganalytics.alerts.api.core; /*- * #%L * thinkbig-alerts-core * %% * Copyright (C) 2017 ThinkBig Analytics * %% * 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. * #L% */ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.thinkbiganalytics.alerts.api.Alert; import com.thinkbiganalytics.alerts.api.Alert.ID; import com.thinkbiganalytics.alerts.api.AlertCriteria; import com.thinkbiganalytics.alerts.api.AlertListener; import com.thinkbiganalytics.alerts.api.AlertProvider; import com.thinkbiganalytics.alerts.api.AlertResponder; import com.thinkbiganalytics.alerts.api.AlertResponse; import com.thinkbiganalytics.alerts.spi.AlertManager; import com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver; import com.thinkbiganalytics.alerts.spi.AlertSource; import com.thinkbiganalytics.alerts.spi.AlertSourceAggregator; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Named; import reactor.bus.Event; import reactor.bus.EventBus; import reactor.bus.registry.Registration; import reactor.bus.selector.MatchAllSelector; import reactor.fn.Consumer; /** * */ public class AggregatingAlertProvider implements AlertProvider, AlertSourceAggregator, AlertNotifyReceiver, Consumer<Event<Alert>> { private static final Logger LOG = LoggerFactory.getLogger(AggregatingAlertProvider.class); private List<AlertResponder> responders; private Registration<?, ?> respondersRegistration; private Map<AlertListener, Registration<?, ?>> listeners; private Map<String, AlertSource> sources; private Map<String, AlertManager> managers; private Executor availableAlertsExecutor; private volatile DateTime lastAlertsTime = DateTime.now(); @Inject @Named("alertsEventBus") private EventBus alertsBus; @Inject @Named("respondableAlertsEventBus") private EventBus respondableAlertsBus; /** * */ public AggregatingAlertProvider() { this.listeners = Collections.synchronizedMap(new HashMap<AlertListener, Registration<?, ?>>()); this.responders = Collections.synchronizedList(new ArrayList<AlertResponder>()); this.sources = Collections.synchronizedMap(new HashMap<String, AlertSource>()); this.managers = Collections.synchronizedMap(new HashMap<String, AlertManager>()); this.availableAlertsExecutor = Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setDaemon(true).build()); } /** * Generates a unique, internal ID for this source */ private static String createAlertSourceId(AlertSource src) { return Integer.toString(src.hashCode()); } private static String getSourceId(Alert decorator) { SourceAlertID srcAlertId = (SourceAlertID) decorator.getId(); return srcAlertId.sourceId; } /** * @param availableAlertsExecutor the availableAlertsExecutor to set */ public void setAvailableAlertsExecutor(Executor availableAlertsExecutor) { this.availableAlertsExecutor = availableAlertsExecutor; } /* (non-Javadoc) * @see reactor.fn.Consumer#accept(java.lang.Object) */ @Override public void accept(Event<Alert> event) { final Alert alert = unwrapAlert(event.getData()); final AlertManager mgr = (AlertManager) alert.getSource(); final List<AlertResponder> responders = snapshotResponderts(); responders.forEach(responder -> { AlertResponse resp = mgr.getResponse(alert); AlertResponseWrapper wrapper = new AlertResponseWrapper(resp); responder.alertChange(alert, wrapper); }); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#criteria() */ @Override public AlertCriteria criteria() { return new Criteria(); } @Override public ID resolve(Serializable value) { if (value instanceof String) { return SourceAlertID.create((String) value, this.sources, this.managers); } else if (value instanceof SourceAlertID) { return (SourceAlertID) value; } else { throw new IllegalArgumentException("Unrecognized alert ID format: " + value); } } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#addListener(com.thinkbiganalytics.alerts.api.AlertListener) */ @Override public void addListener(AlertListener listener) { // TODO matching all alerts for every listener. Allow filtering at this level, such as by type? Registration<?, ?> reg = this.alertsBus.on(new MatchAllSelector(), new ListenerConsumer(listener)); this.listeners.put(listener, reg); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#addResponder(com.thinkbiganalytics.alerts.api.AlertResponder) */ @Override public void addResponder(AlertResponder responder) { this.responders.add(responder); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSourceAggregator#addAlertSource(com.thinkbiganalytics.alerts.spi.AlertSource) */ @Override public boolean addAlertSource(AlertSource src) { return this.sources.put(createAlertSourceId(src), src) == null; } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSourceAggregator#removeAlertSource(com.thinkbiganalytics.alerts.spi.AlertSource) */ @Override public boolean removeAlertSource(AlertSource src) { return this.sources.remove(createAlertSourceId(src)) != null; } @Override public boolean addAlertManager(AlertManager mgr) { if (this.managers.put(createAlertSourceId(mgr), mgr) == null) { mgr.addReceiver(this); return true; } else { return false; } } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.spi.AlertSourceAggregator#removeAlertManager(com.thinkbiganalytics.alerts.spi.AlertManager) */ @Override public boolean removeAlertManager(AlertManager mgr) { if (this.managers.remove(createAlertSourceId(mgr)) != null) { mgr.removeReceiver(this); return true; } else { return false; } } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#getAlert(com.thinkbiganalytics.alerts.api.Alert.ID) */ @Override public Optional<Alert> getAlert(ID id) { SourceAlertID alertId = asSourceAlertId(id); AlertSource src = getSource(alertId.sourceId); if (src != null) { return getAlert(alertId.alertId, src); } else { return null; } } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#getAlerts(com.thinkbiganalytics.alerts.api.AlertCriteria) */ @Override public Iterator<? extends Alert> getAlerts(AlertCriteria criteria) { Map<String, AlertSource> srcs = snapshotAllSources(); return combineAlerts(criteria, srcs).iterator(); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#getAlerts(org.joda.time.DateTime) */ @Override public Iterator<? extends Alert> getAlertsAfter(DateTime time) { return getAlerts(criteria().after(time)); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#getAlertsBefore(org.joda.time.DateTime) */ @Override public Iterator<? extends Alert> getAlertsBefore(DateTime time) { return getAlerts(criteria().before(time)); } /* (non-Javadoc) * @see com.thinkbiganalytics.alerts.api.AlertProvider#respondTo(com.thinkbiganalytics.alerts.api.Alert.ID, com.thinkbiganalytics.alerts.api.AlertResponder) */ @Override public void respondTo(ID id, AlertResponder responder) { SimpleEntry<Alert, AlertManager> found = findActionableAlert(id); if (found != null) { alertChange(found.getKey(), responder, found.getValue()); } } @Override public void alertsAvailable(int count) { LOG.debug("Alerts available: {}", count); this.availableAlertsExecutor.execute(() -> { final AtomicReference<DateTime> sinceTime = new AtomicReference<>(AggregatingAlertProvider.this.lastAlertsTime); Map<String, AlertSource> sources = snapshotAllSources(); combineAlerts(criteria().after(sinceTime.get()), sources).forEach(alert -> { LOG.debug("Alert {} received from {}", alert.getId(), alert.getSource()); notifyListeners(alert); if (alert.isActionable()) { notifyResponders(alert); } sinceTime.set(alert.getCreatedTime()); }); AggregatingAlertProvider.this.lastAlertsTime = sinceTime.get(); }); } @PostConstruct private void createRegistrations() { this.respondersRegistration = this.respondableAlertsBus.on(new MatchAllSelector(), this); } @PreDestroy private void cancelRegistrations() { this.respondersRegistration.cancel(); this.listeners.values().forEach(reg -> reg.cancel()); } private AlertSource getSource(String srcId) { AlertSource src = this.sources.get(srcId); if (src == null) { return this.managers.get(srcId); } else { return src; } } private Optional<Alert> getAlert(Alert.ID id, AlertSource src) { return src.getAlert(id).map(alert -> wrapAlert(alert, src)); } private Stream<Alert> combineAlerts(AlertCriteria criteria, Map<String, AlertSource> srcs) { Criteria critImpl = (Criteria) criteria; return srcs.values().stream() .map(src -> { AlertCriteria srcCrit = src.criteria(); critImpl.transfer(srcCrit); Iterable<Alert> alerts = () -> src.getAlerts(srcCrit); return StreamSupport.stream(alerts.spliterator(), false); }) .flatMap(s -> s) .map(alert -> wrapAlert(alert, alert.getSource())) .sorted((a1, a2) -> a2.getCreatedTime().compareTo(a1.getCreatedTime())); } private void notifyChanged(Alert alert) { notifyListeners(alert); } private void notifyResponders(Alert alert) { Event<Alert> event = Event.wrap(alert); this.respondableAlertsBus.notify(alert.getType(), event); } private void notifyListeners(Alert alert) { Event<Alert> event = Event.wrap(alert); this.alertsBus.notify(alert.getType(), event); } // private void notifyResponders(final Alert.ID id, final AlertManager manager) { // LOG.debug("Notifying responders of change for alert ID: {}", id); // // final List<AlertResponder> respList = snapshotResponderts(); // // getRespondersExecutor().execute(new Runnable() { // @Override // public void run() { // // LOG.debug("Invoking responders for alerts: {}", respList); // // for (AlertResponder responder : respList) { // SourceAlertID srcId = asSourceAlertId(id); // Alert alert = getAlert(srcId.alertId, manager); // // LOG.debug("Alert change: {} from source: {} responder: {}", alert, manager, responder); // // if (alert != null) { // alertChange(alert, responder, manager); // } // } // } // }); // } private Alert alertChange(Alert alert, AlertResponder responder, AlertManager manager) { AlertResponseWrapper response = new AlertResponseWrapper(manager.getResponse(alert)); responder.alertChange(alert, response); return response.latestAlert; } private List<AlertListener> snapshotListeners() { synchronized (AggregatingAlertProvider.this.listeners) { return new ArrayList<>(AggregatingAlertProvider.this.listeners.keySet()); } } private List<AlertResponder> snapshotResponderts() { synchronized (AggregatingAlertProvider.this.responders) { return new ArrayList<>(AggregatingAlertProvider.this.responders); } } private Map<String, AlertSource> snapshotSources() { synchronized (this.sources) { return new HashMap<>(this.sources); } } private Map<String, AlertSource> snapshotManagers() { synchronized (this.managers) { return new HashMap<>(this.managers); } } private Map<String, AlertSource> snapshotAllSources() { Map<String, AlertSource> srcs = snapshotSources(); srcs.putAll(snapshotManagers()); return srcs; } private SimpleEntry<Alert, AlertManager> findActionableAlert(ID id) { SourceAlertID srcId = asSourceAlertId(id); AlertManager mgr = this.managers.get(srcId.sourceId); if (mgr != null) { return getAlert(srcId.alertId, mgr) .filter(alert -> alert.isActionable()) .map(alert -> new SimpleEntry<>(unwrapAlert(alert), mgr)) .orElse(null); } else { return null; } } private SourceAlertID asSourceAlertId(ID id) { if (id instanceof SourceAlertID) { return (SourceAlertID) id; } else { // Can only happen if the client uses a different ID than was supplied by this provider. throw new IllegalArgumentException("Unrecognized sourceAlert ID type: " + id); } } private Alert wrapAlert(final Alert srcAlert, final AlertSource src) { return wrapAlert(new SourceAlertID(srcAlert.getId(), src), srcAlert); } private Alert wrapAlert(final SourceAlertID id, final Alert alert) { if (Proxy.isProxyClass(alert.getClass())) { return alert; } else { InvocationHandler handler = new AlertInvocationHandler(alert, id); return (Alert) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[]{Alert.class}, handler); } } private Alert unwrapAlert(Alert proxy) { if (Proxy.isProxyClass(proxy.getClass())) { AlertInvocationHandler handler = (AlertInvocationHandler) Proxy.getInvocationHandler(proxy); return handler.wrapped; } else { return proxy; // not a proxy } } protected static class AlertInvocationHandler implements InvocationHandler { private final Alert wrapped; private final SourceAlertID proxyId; public AlertInvocationHandler(Alert wrapped, SourceAlertID proxyId) { super(); this.wrapped = wrapped; this.proxyId = proxyId; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("getId")) { return (Alert.ID) this.proxyId; } else { return method.invoke(this.wrapped, args); } } public Alert getWrappedAlert() { return this.wrapped; } } /** * Decorates an alert ID with an internal identifier of its source. */ protected static class SourceAlertID implements Alert.ID { private static final long serialVersionUID = -3799345314250454959L; private final Alert.ID alertId; private final String sourceId; public SourceAlertID(ID alertId, AlertSource src) { super(); this.alertId = alertId; this.sourceId = createAlertSourceId(src); } public static SourceAlertID create(String str, Map<String, AlertSource> sources, Map<String, AlertManager> managers) { int sepIdx = str.lastIndexOf(":"); String alertPart = str.substring(0, sepIdx); String srcId = str.substring(sepIdx + 1); AlertSource src = sources.get(srcId); src = src == null ? managers.get(srcId) : src; if (src != null) { Alert.ID alertId = src.resolve(alertPart); return new SourceAlertID(alertId, src); } else { throw new IllegalArgumentException("Unrecognized alert ID: " + str); } } @Override public String toString() { return this.alertId.toString() + ":" + this.sourceId; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!this.getClass().equals(obj.getClass())) { return false; } SourceAlertID that = (SourceAlertID) obj; return Objects.equals(this.alertId, that.alertId) && Objects.equals(this.sourceId, that.sourceId); } @Override public int hashCode() { return Objects.hash(getClass(), this.alertId, this.sourceId); } } private static class ListenerConsumer implements Consumer<Event<Alert>> { private final AlertListener listener; public ListenerConsumer(AlertListener listener) { super(); this.listener = listener; } @Override public void accept(Event<Alert> event) { this.listener.alertChange(event.getData()); } } protected static class Criteria extends BaseAlertCriteria { } protected class AlertResponseWrapper implements AlertResponse { private final AlertResponse delegate; private Alert latestAlert; public AlertResponseWrapper(AlertResponse delegate) { this.delegate = delegate; } @Override public Alert inProgress(String description) { return inProgress(description, null); } @Override public <C extends Serializable> Alert inProgress(String description, C content) { return changed(this.delegate.inProgress(description, content)); } @Override public Alert handle(String description) { return handle(description, null); } @Override public <C extends Serializable> Alert handle(String description, C content) { return changed(this.delegate.handle(description, content)); } @Override public Alert unhandle(String description) { return unhandle(description, null); } @Override public <C extends Serializable> Alert unhandle(String description, C content) { return changed(this.delegate.unhandle(description, content)); } @Override public void clear() { this.delegate.clear(); } private Alert changed(Alert alert) { notifyChanged(alert); this.latestAlert = alert; return alert; } } }