/**
*
*/
package com.thinkbiganalytics.alerts.spi.mem;
/*-
* #%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.thinkbiganalytics.alerts.api.Alert;
import com.thinkbiganalytics.alerts.api.Alert.ID;
import com.thinkbiganalytics.alerts.api.Alert.State;
import com.thinkbiganalytics.alerts.api.AlertChangeEvent;
import com.thinkbiganalytics.alerts.api.AlertCriteria;
import com.thinkbiganalytics.alerts.api.AlertResponse;
import com.thinkbiganalytics.alerts.api.core.BaseAlertCriteria;
import com.thinkbiganalytics.alerts.spi.AlertDescriptor;
import com.thinkbiganalytics.alerts.spi.AlertManager;
import com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver;
import com.thinkbiganalytics.alerts.spi.AlertSource;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.net.URI;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
*/
public class InMemoryAlertManager implements AlertManager {
public static final int MAX_ALERTS = 2000;
private static final Logger LOG = LoggerFactory.getLogger(InMemoryAlertManager.class);
private static AtomicReference<GenericAlert> NULL_REF = new AtomicReference<GenericAlert>(null);
private final Set<AlertDescriptor> descriptors;
private final Set<AlertNotifyReceiver> alertReceivers;
private final Map<Alert.ID, AtomicReference<GenericAlert>> alertsById;
private final NavigableMap<DateTime, AtomicReference<GenericAlert>> alertsByTime;
private final ReadWriteLock alertsLock = new ReentrantReadWriteLock();
private volatile Executor receiversExecutor;
private AtomicInteger changeCount = new AtomicInteger(0);
/**
*
*/
public InMemoryAlertManager() {
this.descriptors = Collections.synchronizedSet(new HashSet<AlertDescriptor>());
this.alertReceivers = Collections.synchronizedSet(new HashSet<AlertNotifyReceiver>());
this.alertsById = new ConcurrentHashMap<>();
this.alertsByTime = new ConcurrentSkipListMap<>();
}
public void setReceiversExecutor(Executor receiversExecutor) {
synchronized (this) {
this.receiversExecutor = receiversExecutor;
}
}
protected Executor getRespondersExecutor() {
if (this.receiversExecutor == null) {
synchronized (this) {
if (this.receiversExecutor == null) {
this.receiversExecutor = Executors.newFixedThreadPool(1);
}
}
}
return receiversExecutor;
}
@Override
public Set<AlertDescriptor> getAlertDescriptors() {
synchronized (this.descriptors) {
return new HashSet<>(this.descriptors);
}
}
public void setAlertDescriptors(Collection<AlertDescriptor> types) {
synchronized (this.descriptors) {
this.descriptors.addAll(types);
}
}
@Override
public boolean addDescriptor(AlertDescriptor descriptor) {
return this.descriptors.add(descriptor);
}
@Override
public void addReceiver(AlertNotifyReceiver receiver) {
this.alertReceivers.add(receiver);
}
@Override
public void removeReceiver(AlertNotifyReceiver receiver) {
this.alertReceivers.remove(receiver);
}
@Override
public Optional<Alert> getAlert(ID id) {
return Optional.ofNullable(this.alertsById.getOrDefault(id, NULL_REF).get());
}
@Override
public AlertCriteria criteria() {
return new BaseAlertCriteria();
}
@Override
public ID resolve(Serializable ser) {
if (ser instanceof String) {
return new AlertID((String) ser);
} else if (ser instanceof UUID) {
return new AlertID((UUID) ser);
} else if (ser instanceof AlertID) {
return (AlertID) ser;
} else {
throw new IllegalArgumentException("Invalid ID source format: " + ser.getClass());
}
}
@Override
public Iterator<Alert> getAlerts(AlertCriteria criteria) {
BaseAlertCriteria predicate = (BaseAlertCriteria) (criteria == null ? criteria() : criteria);
// TODO Grab a partition of the map first based on before/after times of criteria
return this.alertsByTime.values().stream()
.map(ref -> (Alert) ref.get())
.filter(predicate)
.iterator();
}
//
// @Override
// public Iterator<Alert> getAlerts(DateTime since) {
// this.alertsLock.readLock().lock();
// try {
// DateTime higher = this.alertsByTime.higherKey(since);
//
// if (higher != null) {
// SortedMap<DateTime, AtomicReference<GenericAlert>> submap = this.alertsByTime.subMap(higher, DateTime.now());
// return submap.values().stream().map(ref -> (Alert) ref.get()).iterator();
// } else {
// return Collections.<Alert>emptySet().iterator();
// }
// } finally {
// this.alertsLock.readLock().unlock();
// }
// }
//
// @Override
// public Iterator<Alert> getAlerts(ID since) {
// AtomicReference<GenericAlert> ref = this.alertsById.get(since);
//
// if (ref == null) {
// return Collections.<Alert>emptySet().iterator();
// } else {
// GenericAlert alert = ref.get();
// int index = alert.getEvents().size() - 1;
// DateTime createdTime = alert.getEvents().get(index).getChangeTime();
//
// return getAlerts(createdTime);
// }
// }
@Override
public <C extends Serializable> Alert create(URI type, Alert.Level level, String description, C content) {
GenericAlert alert = new GenericAlert(type, level, description, content);
DateTime createdTime = alert.getEvents().get(0).getChangeTime();
addAlert(alert, createdTime);
return alert;
}
private void updated(GenericAlert alert) {
this.alertsById.computeIfPresent(alert.getId(),
(id, ref) -> {
ref.set(alert);
return ref;
});
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.spi.AlertManager#getUpdator(com.thinkbiganalytics.alerts.api.Alert)
*/
@Override
public AlertResponse getResponse(Alert alert) {
return new InternalAlertResponse((GenericAlert) alert);
}
@Override
public Alert remove(ID id) {
this.alertsLock.writeLock().lock();
try {
AtomicReference<GenericAlert> ref = this.alertsById.remove(id);
if (ref != null) {
this.alertsByTime.values().remove(ref);
return ref.get();
} else {
return null;
}
} finally {
this.alertsLock.writeLock().unlock();
}
}
protected void addAlert(GenericAlert alert, DateTime createdTime) {
AtomicReference<GenericAlert> ref = new AtomicReference<>(alert);
int count = 0;
this.alertsLock.writeLock().lock();
try {
this.alertsByTime.put(createdTime, ref);
this.alertsById.put(alert.getId(), ref);
count = this.changeCount.incrementAndGet();
} finally {
this.alertsLock.writeLock().unlock();
}
LOG.info("Alert added - pending notifications: {}", count);
if (count > 0) {
signalReceivers();
}
}
private void signalReceivers() {
Executor exec = getRespondersExecutor();
final Set<AlertNotifyReceiver> receivers;
synchronized (this.alertReceivers) {
receivers = new HashSet<>(this.alertReceivers);
}
exec.execute(() -> {
int count = InMemoryAlertManager.this.changeCount.get();
LOG.info("Notifying receivers: {} about events: {}", receivers.size(), count);
for (AlertNotifyReceiver receiver : receivers) {
receiver.alertsAvailable(count);
}
InMemoryAlertManager.this.changeCount.getAndAdd(-count);
});
}
private static class AlertID implements Alert.ID {
private final UUID uuid;
public AlertID() {
this(UUID.randomUUID());
}
public AlertID(String str) {
this(UUID.fromString(str));
}
public AlertID(UUID id) {
this.uuid = id;
}
@Override
public String toString() {
return this.uuid.toString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!this.getClass().equals(obj.getClass())) {
return false;
}
return Objects.equals(this.uuid, ((AlertID) obj).uuid);
}
@Override
public int hashCode() {
return Objects.hash(getClass(), this.uuid);
}
}
private static class GenericChangeEvent implements AlertChangeEvent {
private final AlertID alertId;
private final DateTime changeTime;
private final State state;
private final Principal user;
private final String description;
private final Object content;
public GenericChangeEvent(AlertID id, State state) {
this(id, state, null, null, null);
}
public GenericChangeEvent(AlertID alertId, State state, Principal user, String descr, Object content) {
super();
this.alertId = alertId;
this.state = state;
this.user = user;
this.description = descr;
this.content = content;
this.changeTime = DateTime.now();
}
@Override
public DateTime getChangeTime() {
return changeTime;
}
@Override
public State getState() {
return state;
}
public AlertID getAlertId() {
return alertId;
}
public Principal getUser() {
return user;
}
@Override
public String getDescription() {
return this.description;
}
@Override
@SuppressWarnings("unchecked")
public <C extends Serializable> C getContent() {
return (C) this.content;
}
}
private class InternalAlertResponse implements AlertResponse {
private GenericAlert current;
public InternalAlertResponse(GenericAlert alert) {
this.current = alert;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.api.#inProgress()
*/
@Override
public Alert inProgress(String description) {
return inProgress(description, null);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.api.#inProgress(java.io.Serializable)
*/
@Override
public <C extends Serializable> Alert inProgress(String description, C content) {
checkCleared();
this.current = new GenericAlert(this.current, State.IN_PROGRESS, content);
updated(current);
return this.current;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.api.#handle()
*/
@Override
public Alert handle(String description) {
return handle(null);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.api.#handle(java.io.Serializable)
*/
@Override
public <C extends Serializable> Alert handle(String description, C content) {
checkCleared();
this.current = new GenericAlert(this.current, State.HANDLED, content);
updated(current);
return this.current;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.api.#unHandle()
*/
@Override
public Alert unhandle(String description) {
return unhandle(description, null);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.api.#unHandle(java.io.Serializable)
*/
@Override
public <C extends Serializable> Alert unhandle(String description, C content) {
checkCleared();
this.current = new GenericAlert(this.current, State.UNHANDLED, content);
updated(current);
return this.current;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.alerts.api.AlertResponse#clear()
*/
@Override
public void clear() {
checkCleared();
this.current = new GenericAlert(this.current, true);
this.current = null;
}
private void checkCleared() {
if (this.current == null) {
throw new IllegalStateException("The alert cannot be updated as it has been already cleared.");
}
}
}
private class AlertByIdMap extends LinkedHashMap<Alert.ID, AtomicReference<Alert>> {
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<ID, AtomicReference<Alert>> eldest) {
if (this.size() > MAX_ALERTS) {
InMemoryAlertManager.this.alertsByTime.values().remove(eldest.getValue());
return true;
} else {
return false;
}
}
}
/**
* Immutable implementation of an Alert.
*/
protected class GenericAlert implements Alert {
private final AlertID id;
private final URI type;
private final Level level;
private final String description;
private final boolean cleared;
private final AlertSource source;
private final Object content;
private final List<AlertChangeEvent> events;
public GenericAlert(URI type, Level level, String description, Object content) {
this.id = new AlertID();
this.type = type;
this.level = level;
this.cleared = false;
this.description = description;
this.content = content;
this.source = InMemoryAlertManager.this;
if (content != null) {
this.events = Collections.unmodifiableList(Collections.singletonList(new GenericChangeEvent(this.id, State.UNHANDLED, null, null, content)));
} else {
this.events = Collections.emptyList();
}
}
public GenericAlert(URI type, Level level, Object content) {
this(type, level, "", content);
}
public GenericAlert(GenericAlert alert, State newState, Object eventContent) {
this.id = alert.id;
this.type = alert.type;
this.level = alert.level;
this.description = alert.description;
this.source = alert.source;
this.content = alert.content;
this.cleared = false;
ArrayList<AlertChangeEvent> evList = new ArrayList<>(alert.events);
evList.add(0, new GenericChangeEvent(this.id, newState, null, null, eventContent));
this.events = Collections.unmodifiableList(evList);
}
public GenericAlert(GenericAlert alert, boolean cleared) {
this.id = alert.id;
this.type = alert.type;
this.level = alert.level;
this.description = alert.description;
this.source = alert.source;
this.content = alert.content;
this.events = Collections.unmodifiableList(alert.events);
this.cleared = cleared;
}
@Override
public ID getId() {
return this.id;
}
@Override
public URI getType() {
return this.type;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public Level getLevel() {
return this.level;
}
@Override
public State getState() {
return this.events.get(0).getState();
}
@Override
public DateTime getCreatedTime() {
return this.events.get(this.events.size() - 1).getChangeTime();
}
@Override
public AlertSource getSource() {
return this.source;
}
@Override
public boolean isActionable() {
return true;
}
@Override
public boolean isCleared() {
return this.cleared;
}
@Override
public List<AlertChangeEvent> getEvents() {
return this.events;
}
@Override
@SuppressWarnings("unchecked")
public <C extends Serializable> C getContent() {
return (C) this.content;
}
}
}