/*
* 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.Iterator;
import java.util.TreeSet;
import java.util.function.Predicate;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import org.drools.core.event.DebugAgendaEventListener;
import org.drools.core.event.DebugRuleRuntimeEventListener;
import org.hawkular.alerts.api.model.data.Data;
import org.hawkular.alerts.api.model.event.Event;
import org.hawkular.alerts.engine.service.RulesEngine;
import org.jboss.logging.Logger;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.ObjectFilter;
import org.kie.api.runtime.rule.FactHandle;
/**
* An implementation of RulesEngine based on drools framework.
*
* This implementations has an approach of fixed rules based on filesystem.
*
* The RulesEngine is invoked only by the AlertsEngine impl and is not invoked concurrently, so
* single-threading is a fair assumption.
*
* @author Jay Shaughnessy
* @author Lucas Ponce
*/
@Singleton
@TransactionAttribute(value = TransactionAttributeType.NOT_SUPPORTED)
public class DroolsRulesEngineImpl implements RulesEngine {
// private final MsgLogger msgLog = MsgLogger.LOGGER;
private final Logger log = Logger.getLogger(DroolsRulesEngineImpl.class);
private static final String SESSION_NAME = "hawkular-alerts-engine-session";
private static final long PERF_BATCHING_THRESHOLD = 3000L; // 3 seconds
private static final long PERF_FIRING_THRESHOLD = 5000L; // 5 seconds
private int minReportingIntervalData;
private int minReportingIntervalEvents;
private KieServices ks;
private KieContainer kc;
private KieSession kSession;
TreeSet<Data> pendingData = new TreeSet<>();
TreeSet<Event> pendingEvents = new TreeSet<>();
public DroolsRulesEngineImpl() {
log.debug("Creating instance.");
ks = KieServices.Factory.get();
kc = ks.getKieClasspathContainer();
kSession = kc.newKieSession(SESSION_NAME);
if (log.isEnabled(Logger.Level.TRACE)) {
kSession.addEventListener(new DebugAgendaEventListener());
kSession.addEventListener(new DebugRuleRuntimeEventListener());
}
minReportingIntervalData = new Integer(
AlertProperties.getProperty(MIN_REPORTING_INTERVAL_DATA,
MIN_REPORTING_INTERVAL_DATA_ENV,
MIN_REPORTING_INTERVAL_DATA_DEFAULT));
minReportingIntervalEvents = new Integer(
AlertProperties.getProperty(MIN_REPORTING_INTERVAL_EVENTS,
MIN_REPORTING_INTERVAL_EVENTS_ENV,
MIN_REPORTING_INTERVAL_EVENTS_DEFAULT));
}
@Override
public void addFact(Object fact) {
if (fact instanceof Data || fact instanceof Event) {
throw new IllegalArgumentException(fact.toString());
}
kSession.insert(fact);
if (log.isDebugEnabled()) {
log.debugf("addFact( %s )", fact.toString());
log.debug("==> Begin Dump");
for (FactHandle f : kSession.getFactHandles()) {
Object sessionObject = kSession.getObject(f);
log.debugf("Fact: %s", sessionObject.toString());
}
log.debug("==> End Dump");
}
}
@Override
public void addFacts(Collection facts) {
for (Object fact : facts) {
if (fact instanceof Data || fact instanceof Event) {
throw new IllegalArgumentException(fact.toString());
}
}
for (Object fact : facts) {
if (log.isDebugEnabled()) {
log.debugf("Insert %s", fact);
}
kSession.insert(fact);
}
if (log.isDebugEnabled()) {
log.debugf("addFacts( %s )", facts.toString());
log.debug("==> Begin Dump");
for (FactHandle f : kSession.getFactHandles()) {
Object sessionObject = kSession.getObject(f);
log.debugf("Fact: %s", sessionObject.toString());
}
log.debug("==> End Dump");
}
}
@Override
public void addData(TreeSet<Data> data) {
pendingData.addAll(data);
}
@Override
public void addEvents(TreeSet<Event> events) {
pendingEvents.addAll(events);
}
@Override
public void addGlobal(String name, Object global) {
if (log.isDebugEnabled()) {
log.debugf("Add Global %s = %s ", name, global);
}
kSession.setGlobal(name, global);
}
@Override
public void clear() {
for (FactHandle factHandle : kSession.getFactHandles()) {
if (log.isDebugEnabled()) {
log.debugf("Delete %s", factHandle);
}
kSession.delete(factHandle);
}
}
@Override
public void fire() {
// The rules engine requires that for any DataId only the oldest Data instance is processed in one
// execution of the rules. So, if we find multiple Data instances for the same Id, defer all but
// the oldest to a subsequent run. Note that pendingData is already sorted by (id ASC, timestamp ASC) so
// the iterator will present Data with the same id together, and time-ordered.
int initialPendingData = pendingData.size();
int initialPendingEvents = pendingEvents.size();
int fireCycle = 0;
long startFiring = System.currentTimeMillis();
while (!pendingData.isEmpty() || !pendingEvents.isEmpty()) {
log.debugf("Firing rules... PendingData [%s] PendingEvents [%s]", initialPendingData,
initialPendingEvents);
batchData();
batchEvents();
if (log.isTraceEnabled()) {
log.tracef("Firing cycle [%s] - with these facts: ", fireCycle);
for (FactHandle fact : kSession.getFactHandles()) {
Object o = kSession.getObject(fact);
log.tracef("Fact: %s", o);
}
}
kSession.fireAllRules();
fireCycle++;
}
long firingTime = System.currentTimeMillis() - startFiring;
if (log.isDebugEnabled()) {
log.debugf("Firing took [%s] ms", firingTime);
}
if (firingTime > PERF_FIRING_THRESHOLD) {
log.warnf("Firing rules... PendingData [%s] PendingEvents [%s] took [%s] ms exceeding [%s] ms",
initialPendingData, initialPendingEvents, firingTime, PERF_FIRING_THRESHOLD);
}
}
private void batchData() {
long startBatching = System.currentTimeMillis();
TreeSet<Data> batchData = pendingData;
pendingData = new TreeSet<>();
// Keep only the least recent datum for any dataId. Remove minReportingInterval violators, defer the rest
Data previousData = null;
for (Iterator<Data> i = batchData.iterator(); i.hasNext();) {
Data d = i.next();
if (!d.same(previousData)) {
previousData = d;
kSession.insert(d);
} else {
if ((d.getTimestamp() - previousData.getTimestamp()) < minReportingIntervalData) {
log.tracef("MinReportingInterval violation, prev: %s, removed: %s", previousData, d);
} else {
pendingData.add(d);
log.tracef("Deferring data, keep: %s, defer: %s", previousData, d);
}
}
if (!pendingData.isEmpty()) {
log.debugf("Deferring [%d] Datum(s) to next firing !!", pendingData.size());
}
}
long batchingTime = System.currentTimeMillis() - startBatching;
log.debugf("Batching Data [%s] took [%s]", batchData.size(), batchingTime);
if (batchingTime > PERF_BATCHING_THRESHOLD) {
log.warnf("Batching Data [%s] took [%s] ms exceeding [%s] ms",
batchData.size(), batchingTime, PERF_BATCHING_THRESHOLD);
}
}
private void batchEvents() {
long startBatching = System.currentTimeMillis();
TreeSet<Event> batchEvents = pendingEvents;
pendingEvents = new TreeSet<>();
// Keep only the least recent datum for any dataId. Remove minReportingInterval violators, defer the rest
Event previousEvent = null;
for (Iterator<Event> i = batchEvents.iterator(); i.hasNext();) {
Event e = i.next();
if (!e.same(previousEvent)) {
previousEvent = e;
kSession.insert(e);
} else {
if ((e.getCtime() - previousEvent.getCtime()) < minReportingIntervalEvents) {
log.tracef("MinReportingInterval violation, prev: %s, removed: %s", previousEvent, e);
} else {
pendingEvents.add(e);
log.tracef("Deferring event, keep: %s, defer: %s", previousEvent, e);
}
}
}
if (!pendingEvents.isEmpty()) {
log.debugf("Deferring [%d] Event(s) to next firing !!", pendingEvents.size());
}
long batchingTime = System.currentTimeMillis() - startBatching;
log.debugf("Batching Events [%s] took [%s]", batchEvents.size(), batchingTime);
if (batchingTime > PERF_BATCHING_THRESHOLD) {
log.warnf("Batching Events [%s] took [%s] ms exceeding [%s] ms",
batchEvents.size(), batchingTime, PERF_BATCHING_THRESHOLD);
}
}
@Override
public void fireNoData() {
kSession.fireAllRules();
}
@Override
public Object getFact(Object o) {
Object result = null;
FactHandle factHandle = kSession.getFactHandle(o);
if (null != factHandle) {
result = kSession.getObject(factHandle);
}
if (log.isDebugEnabled()) {
log.debugf("getFact( %s )", o.toString());
log.debug("==> Begin Dump");
for (FactHandle fact : kSession.getFactHandles()) {
Object sessionObject = kSession.getObject(fact);
log.debugf("Fact: %s", sessionObject.toString());
}
log.debug("==> End Dump");
}
return result;
}
@Override
public void removeFact(Object fact) {
FactHandle factHandle = kSession.getFactHandle(fact);
if (factHandle != null) {
if (log.isDebugEnabled()) {
log.debugf("Delete %s", factHandle);
}
kSession.delete(factHandle);
}
if (log.isDebugEnabled()) {
log.debugf("removeFact( %s )", fact.toString());
log.debug("==> Begin Dump");
for (FactHandle f : kSession.getFactHandles()) {
Object sessionObject = kSession.getObject(f);
log.debugf("Fact: %s", sessionObject.toString());
}
log.debug("==> End Dump");
}
}
@Override
public void updateFact(Object fact) {
FactHandle factHandle = kSession.getFactHandle(fact);
if (factHandle != null) {
if (log.isDebugEnabled()) {
log.debugf("Update %s", factHandle);
}
kSession.update(factHandle, fact);
}
if (log.isDebugEnabled()) {
log.debugf("updateFact( %s )", fact.toString());
log.debug("==> Begin Dump");
for (FactHandle f : kSession.getFactHandles()) {
Object sessionObject = kSession.getObject(f);
log.debugf("Fact: %s", sessionObject.toString());
}
log.debug("==> End Dump");
}
}
@Override
public void removeFacts(Collection facts) {
for (Object fact : facts) {
removeFact(fact);
}
}
@Override
public void removeFacts(Predicate<Object> factFilter) {
Collection<FactHandle> handles = kSession.getFactHandles(new ObjectFilter() {
@Override
public boolean accept(Object object) {
return factFilter.test(object);
}
});
if (null == handles) {
return;
}
for (FactHandle h : handles) {
removeFact(h);
}
}
@Override
public void removeGlobal(String name) {
if (log.isDebugEnabled()) {
log.debugf("Remove Global %s", name);
}
kSession.setGlobal(name, null);
}
@Override
public void reset() {
log.debug("Reset session");
kSession.dispose();
kSession = kc.newKieSession(SESSION_NAME);
}
}