/**
*
*/
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;
}
}
}