/*
* Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.hawkular.alerts.engine.impl;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.ejb.AccessTimeout;
import javax.ejb.EJB;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import org.hawkular.alerts.api.model.condition.CompareCondition;
import org.hawkular.alerts.api.model.condition.Condition;
import org.hawkular.alerts.api.model.data.Data;
import org.hawkular.alerts.api.model.trigger.Trigger;
import org.hawkular.alerts.api.model.trigger.TriggerType;
import org.hawkular.alerts.api.services.DefinitionsEvent;
import org.hawkular.alerts.api.services.DefinitionsService;
import org.jboss.logging.Logger;
/**
* A helper class to keep track of DataDrivenGroup
*
* @author Jay Shaughnessy
* @author Lucas Ponce
*/
@Singleton
@TransactionAttribute(value = TransactionAttributeType.NOT_SUPPORTED)
public class DataDrivenGroupCacheManager {
private final Logger log = Logger.getLogger(DataDrivenGroupCacheManager.class);
private static final String DATA_DRIVEN_TRIGGERS_ENABLED = "hawkular-alerts.data-driven-triggers-enabled";
private static final String DATA_DRIVEN_TRIGGERS_ENABLED_DEFAULT = "true";
private boolean dataDrivenTriggersEnabled;
// The sources with member triggers for the dataId.
// - null if dataId is not used in a group trigger condition
// - EmptySet if no source members are yet defined
Map<CacheKey, Set<String>> sourcesMap = new HashMap<>();
// The group triggerIds relevant to the dataId, null if none
Map<CacheKey, Set<String>> triggersMap = new HashMap<>();
private volatile boolean updateRequested = false;
private volatile boolean updating = false;
@EJB
DefinitionsService definitions;
@PostConstruct
public void init() {
dataDrivenTriggersEnabled = new Boolean(
AlertProperties.getProperty(DATA_DRIVEN_TRIGGERS_ENABLED, DATA_DRIVEN_TRIGGERS_ENABLED_DEFAULT));
log.infof("Data-driven Group Triggers enabled: %s", dataDrivenTriggersEnabled);
if (dataDrivenTriggersEnabled) {
requestCacheUpdate();
definitions.registerListener(e -> {
requestCacheUpdate();
}, DefinitionsEvent.Type.TRIGGER_CONDITION_CHANGE);
}
}
// Just run updateCache one time if multiple requests come in while an update is already in progress...
private void requestCacheUpdate() {
log.debug("Cache update requested");
if (updateRequested) {
log.debug("Cache update, redundant request ignored.");
return;
}
updateRequested = true;
if (!updating) {
updateCache();
}
}
// cache update can take some time if the trigger population is large and cross-tenant, avoid
// timeouts by allowing longer waits for pending client calls
@AccessTimeout(value = 5, unit = TimeUnit.MINUTES)
private synchronized void updateCache() {
log.debug("Updating cache...");
try {
updating = true;
while (updateRequested) {
updateRequested = false;
log.debug("Cache update in progress..");
Collection<Trigger> allTriggers = definitions.getAllTriggers();
Set<Trigger> ddGroupTriggers = new HashSet<>();
for (Trigger t : allTriggers) {
if (TriggerType.DATA_DRIVEN_GROUP == t.getType()) {
ddGroupTriggers.add(t);
}
}
log.debugf("Updating [%d] data-driven triggers out of [%d] total triggers...", ddGroupTriggers.size(),
allTriggers.size());
for (Trigger groupTrigger : ddGroupTriggers) {
String tenantId = groupTrigger.getTenantId();
Set<String> sources = new HashSet<>();
for (Trigger memberTrigger : definitions.getMemberTriggers(tenantId, groupTrigger.getId(),
false)) {
sources.add(memberTrigger.getSource());
}
for (Condition c : definitions.getTriggerConditions(tenantId, groupTrigger.getId(), null)) {
CacheKey key = new CacheKey(tenantId, c.getDataId());
sourcesMap.put(key, sources);
Set<String> triggers = triggersMap.get(key);
if (null == triggers) {
triggers = new HashSet<>();
}
triggers.add(groupTrigger.getId());
triggersMap.put(key, triggers);
if (c instanceof CompareCondition) {
key = new CacheKey(tenantId, ((CompareCondition) c).getData2Id());
sourcesMap.put(key, sources);
triggers = triggersMap.get(key);
if (null == triggers) {
triggers = new HashSet<>();
}
triggers.add(groupTrigger.getId());
triggersMap.put(key, triggers);
}
}
}
}
} catch (Exception e) {
log.error("FAILED to updateCache. Unable to generate data-driven member triggers!", e);
sourcesMap = new HashMap<>();
} finally {
log.debugf("Cache updates complete. sourceMap: %s", sourcesMap);
updating = false;
}
}
@Lock(LockType.READ)
public boolean isCacheActive() {
return !sourcesMap.isEmpty();
}
@Lock(LockType.READ)
public Set<String> needsSourceMember(String tenantId, String dataId, String source) {
if (isEmpty(source, dataId, tenantId) || Data.SOURCE_NONE.equals(source)) {
return Collections.emptySet();
}
CacheKey key = new CacheKey(tenantId, dataId);
// if the dataId is not relevant to any group triggers just return empty set
if (null == triggersMap.get(key)) {
return Collections.emptySet();
}
// if the dataId is relevant to group triggers but the source is already known just return empty set
Set<String> sources = sourcesMap.get(key);
if (sources.contains(source)) {
return Collections.emptySet();
}
// otherwise, return the triggers that need a member for this source
return triggersMap.get(key);
}
private boolean isEmpty(String... strings) {
for (String s : strings) {
if (null == s || s.trim().isEmpty()) {
return true;
}
}
return false;
}
private static class CacheKey {
private String tenantId;
private String dataId;
public CacheKey(String tenantId, String dataId) {
super();
this.tenantId = tenantId;
this.dataId = dataId;
}
public String getTenantId() {
return tenantId;
}
public String getDataId() {
return dataId;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((dataId == null) ? 0 : dataId.hashCode());
result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CacheKey other = (CacheKey) obj;
if (dataId == null) {
if (other.dataId != null)
return false;
} else if (!dataId.equals(other.dataId))
return false;
if (tenantId == null) {
if (other.tenantId != null)
return false;
} else if (!tenantId.equals(other.tenantId))
return false;
return true;
}
@Override
public String toString() {
return "CacheKey [" + tenantId + ":" + dataId + "]";
}
}
}