/*
* RHQ Management Platform
* Copyright (C) 2005-2014 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package org.rhq.enterprise.server.measurement;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.ejb3.annotation.TransactionTimeout;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.common.EntityContext;
import org.rhq.core.domain.criteria.AvailabilityCriteria;
import org.rhq.core.domain.discovery.AvailabilityReport;
import org.rhq.core.domain.discovery.AvailabilityReport.Datum;
import org.rhq.core.domain.measurement.Availability;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.ResourceAvailability;
import org.rhq.core.domain.resource.Agent;
import org.rhq.core.domain.resource.InventoryStatus;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.composite.ResourceIdWithAvailabilityComposite;
import org.rhq.core.domain.resource.group.composite.ResourceGroupAvailability;
import org.rhq.core.domain.resource.group.composite.ResourceGroupComposite;
import org.rhq.core.domain.resource.group.composite.ResourceGroupComposite.GroupAvailabilityType;
import org.rhq.core.domain.server.PersistenceUtility;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.domain.util.PageOrdering;
import org.rhq.core.util.StopWatch;
import org.rhq.enterprise.server.RHQConstants;
import org.rhq.enterprise.server.alert.engine.AlertConditionCacheManagerLocal;
import org.rhq.enterprise.server.alert.engine.AlertConditionCacheStats;
import org.rhq.enterprise.server.alert.engine.model.AvailabilityDurationCacheElement;
import org.rhq.enterprise.server.authz.AuthorizationManagerLocal;
import org.rhq.enterprise.server.authz.PermissionException;
import org.rhq.enterprise.server.core.AgentManagerLocal;
import org.rhq.enterprise.server.measurement.instrumentation.MeasurementMonitor;
import org.rhq.enterprise.server.resource.ResourceAvailabilityManagerLocal;
import org.rhq.enterprise.server.resource.ResourceManagerLocal;
import org.rhq.enterprise.server.resource.group.ResourceGroupManagerLocal;
import org.rhq.enterprise.server.scheduler.jobs.AlertAvailabilityDurationJob;
import org.rhq.enterprise.server.util.CriteriaQueryGenerator;
import org.rhq.enterprise.server.util.CriteriaQueryRunner;
import org.rhq.enterprise.server.util.concurrent.AvailabilityReportSerializer;
/**
* Manager for availability related tasks.
*
* @author Heiko W. Rupp
* @author John Mazzitelli
*/
@Stateless
public class AvailabilityManagerBean implements AvailabilityManagerLocal, AvailabilityManagerRemote {
private final Log log = LogFactory.getLog(AvailabilityManagerBean.class);
static private final int MERGE_BATCH_SIZE;
static {
// The value of 200 has been settled upon after testing several batch sizes. If changed it still must be less
// than 1000 for Oracle IN clause limitation reasons.
int mergeBatchSize = 200;
try {
mergeBatchSize = Integer.parseInt(System.getProperty("rhq.server.availability.merge.batch.size", "200"));
} catch (Throwable t) {
//
}
MERGE_BATCH_SIZE = (mergeBatchSize > 999) ? 999 : mergeBatchSize;
}
@PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME)
private EntityManager entityManager;
@EJB
private AvailabilityManagerLocal availabilityManager;
@EJB
private AgentManagerLocal agentManager;
@EJB
private AuthorizationManagerLocal authorizationManager;
@EJB
private ResourceManagerLocal resourceManager;
@EJB
private ResourceGroupManagerLocal resourceGroupManager;
@EJB
private ResourceAvailabilityManagerLocal resourceAvailabilityManager;
@EJB
private AlertConditionCacheManagerLocal alertConditionCacheManager;
// For Avail Duration Alert Condition Checks
@javax.annotation.Resource
private TimerService timerService;
@Override
public AvailabilityType getCurrentAvailabilityTypeForResource(Subject subject, int resourceId) {
return resourceAvailabilityManager.getLatestAvailabilityType(subject, resourceId);
}
@Override
public Availability getCurrentAvailabilityForResource(Subject subject, int resourceId) {
Availability retAvailability;
if (authorizationManager.canViewResource(subject, resourceId) == false) {
throw new PermissionException("User [" + subject
+ "] does not have permission to view current availability for resource[id=" + resourceId + "]");
}
try {
Query q = entityManager.createNamedQuery(Availability.FIND_CURRENT_BY_RESOURCE);
q.setParameter("resourceId", resourceId);
retAvailability = (Availability) q.getSingleResult();
} catch (NoResultException nre) {
// Fall back to searching for the one with the latest start date, but most likely it doesn't exist
Resource resource = resourceManager.getResourceById(subject, resourceId);
List<Availability> availList = resource.getAvailability();
if ((availList != null) && (availList.size() > 0)) {
log.warn("Could not query for latest avail but found one - missing null end time (this should never happen)");
retAvailability = availList.get(availList.size() - 1);
} else {
retAvailability = new Availability(resource, AvailabilityType.UNKNOWN);
}
}
return retAvailability;
}
@Override
public List<Availability> getAvailabilitiesForResource(Subject subject, int resourceId, long startTime, long endTime) {
if (!authorizationManager.canViewResource(subject, resourceId)) {
throw new PermissionException("User [" + subject.getName() + "] does not have permission to view ["
+ resourceId + "]");
}
List<Availability> result;
Date startDate = new Date(startTime);
Date endDate = new Date(endTime);
AvailabilityCriteria c = new AvailabilityCriteria();
c.addFilterResourceId(resourceId);
c.addFilterInterval(startTime, endTime); // reduce by 1 ms to fake exclusive end time on the range.
c.addSortStartTime(PageOrdering.ASC);
result = findAvailabilityByCriteria(subject, c);
// The criteria interval filter is inclusive. But since availN(endTime) == availN+1(startTime) we can get
// unwanted avails in the query result, when the range falls on an avail border.
// For example, assume the following three Availability records exist:
// AV-1 [0..100]
// AV-2 [100..200]
// AV-3 [200..300]
//
// If we happen to query for startTime=100 and endTime=200 we'll end up with 3 avails in the result:
// Result = { AV-1 [0..100], AV-2 [100..200], AV-3 [200..300] }. We really only want AV-2 [100..200] because
// the other two end up having 0 length, |100..100| and |200..200|.
//
// Remove any unwanted entries.
for (Iterator<Availability> i = result.iterator(); i.hasNext();) {
Availability av = i.next();
if ((null != av.getEndTime() && av.getEndTime().equals(startTime) || av.getStartTime().equals(endTime))) {
i.remove();
}
}
// Check if the availabilities obtained cover the startTime of the range.
// If not, we need to provide a "surrogate" for the beginning interval. The availabilities
// obtained from the DB are sorted in ascending order of time. So we can insert one
// pseudo-availability in front of the list if needed. Note that due to avail purging
// we can end up with periods without avail data. The surrogate will be for type UNKNOWN.
if (result.size() > 0) {
Availability firstAvailability = result.get(0);
if (firstAvailability.getStartTime() > startDate.getTime()) {
Availability surrogateAvailability = new Availability(firstAvailability.getResource(),
startDate.getTime(), AvailabilityType.UNKNOWN);
surrogateAvailability.setEndTime(firstAvailability.getStartTime());
result.add(0, surrogateAvailability); // add at the head of the list
}
} else {
Resource surrogateResource = entityManager.find(Resource.class, resourceId);
Availability surrogateAvailability = new Availability(surrogateResource, startDate.getTime(),
AvailabilityType.UNKNOWN);
surrogateAvailability.setEndTime(endDate.getTime());
result.add(surrogateAvailability); // add as the only element, covering the entire interval
}
// Now, limit the Availability ranges to the desired range. Detach the entities prior to to the update so
// the value changes are not propagated to the DB.
entityManager.detach(result.get(0));
if (result.size() > 1) {
entityManager.detach(result.get(result.size() - 1));
}
result.get(0).setStartTime(startDate.getTime());
result.get(result.size() - 1).setEndTime(endDate.getTime());
return result;
}
@Override
public List<ResourceGroupAvailability> getAvailabilitiesForResourceGroup(Subject subject, int groupId,
long startTime, long endTime) {
if (!authorizationManager.canViewGroup(subject, groupId)) {
throw new PermissionException("User [" + subject.getName() + "] does not have permission to view ["
+ groupId + "]");
}
List<ResourceGroupAvailability> result = new ArrayList<ResourceGroupAvailability>();
Date startDate = new Date(startTime);
Date endDate = new Date(endTime);
// all avails for all explicit resources for the time range, ordered by asc startTime
List<Availability> allAvailabilities = findResourceGroupAvailabilityWithinInterval(groupId, startDate, endDate);
// If we have no availabilities we need to return a single group avail, either EMPTY or WARN (all UNKNOWN)
if (allAvailabilities.isEmpty()) {
ResourceGroupAvailability groupAvail = new ResourceGroupAvailability(groupId);
groupAvail.setStartTime(startTime);
groupAvail.setEndTime(endTime);
int explicitMemberCount = resourceGroupManager.getExplicitGroupMemberCount(groupId);
groupAvail.setGroupAvailabilityType((0 == explicitMemberCount) ? GroupAvailabilityType.EMPTY
: GroupAvailabilityType.WARN);
result.add(groupAvail);
return result;
}
// OK, let's try and explain explain what we are doing here. The goal is to have a continuous set of intervals
// extending from startTime to endTime showing all changes in group avail. Each avail change for any member
// could signify a change in the overall group avail. We must first establish the initial group avail and
// then walk forward, checking for changes at each resource avail startTime for a group member.
// One subtlety is that group membership can change over time. We don't track when resources come and go, we
// are dealing with the avails for the *current* explicit members. So, we'll only work with the avails we have
// and not worry about missing avails at any given time. In other words, no "surrogate" UNKNOWN avail insertion.
// OK, calculate the initial group avail
Long atTime = startTime;
int atTimeIndex = 0;
ResourceGroupAvailability currentGroupAvail = null;
int size = allAvailabilities.size();
do {
GroupAvailabilityType groupAvailTypeAtTime = getGroupAvailabilityType(atTime, allAvailabilities);
// if this is a change in group avail type then add it to the result
if (null == currentGroupAvail || currentGroupAvail.getGroupAvailabilityType() != groupAvailTypeAtTime) {
if (null != currentGroupAvail) {
currentGroupAvail.setEndTime(atTime);
}
// leave endTime unset, we don't know endTime until we know the next startTime, or are done
currentGroupAvail = new ResourceGroupAvailability(groupId);
currentGroupAvail.setStartTime(atTime);
currentGroupAvail.setGroupAvailabilityType(groupAvailTypeAtTime);
result.add(currentGroupAvail);
}
// move atTime to the next possible startTime
while (atTimeIndex < size && allAvailabilities.get(atTimeIndex).getStartTime() <= atTime) {
++atTimeIndex;
}
if (atTimeIndex < size) {
atTime = allAvailabilities.get(atTimeIndex).getStartTime();
}
} while (atTimeIndex < size);
currentGroupAvail.setEndTime(endTime);
return result;
}
private GroupAvailabilityType getGroupAvailabilityType(long atTime, List<Availability> allAvailabilities) {
long count = 0;
long disabled = 0;
long down = 0;
long unknown = 0;
long up = 0;
for (Availability av : allAvailabilities) {
// if the Avail straddles startTime (startTime inclusive, endTime exclusive) then it is relevant.
// EndTime is exclusive because Avail(N).endTime=Avail(N+1).startTime and we don't want to consider
// two records. The relevant availType is the one starting, not ending at atTime.
Long startTime = av.getStartTime();
Long endTime = av.getEndTime();
if (startTime <= atTime && (null == endTime || endTime > atTime)) {
++count;
switch (av.getAvailabilityType()) {
case UP:
++up;
break;
case DOWN:
++down;
break;
case UNKNOWN:
++unknown;
break;
case DISABLED:
++disabled;
break;
default:
// Only stored avail types are relevant, MISSING, for example, is never stored
break;
}
}
}
if (0 == count) {
return GroupAvailabilityType.EMPTY;
}
if (down == count) {
return GroupAvailabilityType.DOWN;
}
if (down > 0 || unknown > 0) {
return GroupAvailabilityType.WARN;
}
if (disabled > 0) {
return GroupAvailabilityType.DISABLED;
}
return GroupAvailabilityType.UP;
}
@Override
public List<AvailabilityPoint> findAvailabilitiesForResource(Subject subject, int resourceId,
long fullRangeBeginTime, long fullRangeEndTime, int numberOfPoints, boolean withCurrentAvailability) {
EntityContext context = new EntityContext(resourceId, -1, -1, -1);
return getAvailabilitiesForContext(subject, context, fullRangeBeginTime, fullRangeEndTime, numberOfPoints,
withCurrentAvailability);
}
@Override
public List<AvailabilityPoint> findAvailabilitiesForResourceGroup(Subject subject, int groupId,
long fullRangeBeginTime, long fullRangeEndTime, int numberOfPoints, boolean withCurrentAvailability) {
EntityContext context = new EntityContext(-1, groupId, -1, -1);
return getAvailabilitiesForContext(subject, context, fullRangeBeginTime, fullRangeEndTime, numberOfPoints,
withCurrentAvailability);
}
@Override
public List<AvailabilityPoint> findAvailabilitiesForAutoGroup(Subject subject, int parentResourceId,
int resourceTypeId, long fullRangeBeginTime, long fullRangeEndTime, int numberOfPoints,
boolean withCurrentAvailability) {
EntityContext context = new EntityContext(-1, -1, parentResourceId, resourceTypeId);
return getAvailabilitiesForContext(subject, context, fullRangeBeginTime, fullRangeEndTime, numberOfPoints,
withCurrentAvailability);
}
private List<AvailabilityPoint> getAvailabilitiesForContext(Subject subject, EntityContext context,
long fullRangeBeginTime, long fullRangeEndTime, int numberOfPoints, boolean withCurrentAvailability) {
if (context.type == EntityContext.Type.Resource) {
if (!authorizationManager.canViewResource(subject, context.resourceId)) {
throw new PermissionException("User [" + subject.getName() + "] does not have permission to view "
+ context.toShortString());
}
} else if (context.type == EntityContext.Type.ResourceGroup) {
if (!authorizationManager.canViewGroup(subject, context.groupId)) {
throw new PermissionException("User [" + subject.getName() + "] does not have permission to view "
+ context.toShortString());
}
} else {
}
if ((numberOfPoints <= 0) || (fullRangeBeginTime >= fullRangeEndTime)) {
return new ArrayList<AvailabilityPoint>();
}
List<Availability> availabilities;
Date fullRangeBeginDate = new Date(fullRangeBeginTime);
Date fullRangeEndDate = new Date(fullRangeEndTime);
try {
if (context.type == EntityContext.Type.Resource) {
AvailabilityCriteria c = new AvailabilityCriteria();
c.addFilterResourceId(context.resourceId);
c.addFilterInterval(fullRangeBeginTime, fullRangeEndTime);
c.addSortStartTime(PageOrdering.ASC);
availabilities = findAvailabilityByCriteria(subject, c);
} else if (context.type == EntityContext.Type.ResourceGroup) {
availabilities = findResourceGroupAvailabilityWithinInterval(context.groupId, fullRangeBeginDate,
fullRangeEndDate);
} else if (context.type == EntityContext.Type.AutoGroup) {
availabilities = findAutoGroupAvailabilityWithinInterval(context.parentResourceId,
context.resourceTypeId, fullRangeBeginDate, fullRangeEndDate);
} else {
throw new IllegalArgumentException("Do not yet support retrieving availability history for Context["
+ context.toShortString() + "]");
}
} catch (Exception e) {
log.warn("Can't obtain Availability for " + context.toShortString(), e);
// create a full list of unknown points
// the for loop goes backwards so the times are calculated in the same way as the rest of this method
List<AvailabilityPoint> availabilityPoints = new ArrayList<AvailabilityPoint>(numberOfPoints);
long totalMillis = fullRangeEndTime - fullRangeBeginTime;
long perPointMillis = totalMillis / numberOfPoints;
for (int i = numberOfPoints; i >= 0; i--) {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.UNKNOWN, i * perPointMillis));
}
Collections.reverse(availabilityPoints);
return availabilityPoints;
}
// Check if the availabilities obtained cover the beginning of the whole data range.
// If not, we need to provide a "surrogate" for the beginning interval. The availabilities
// obtained from the db are sorted in ascending order of time. So we can insert one
// pseudo-availability in front of the list if needed. Note that due to avail purging
// we can end up with periods without avail data.
if (availabilities.size() > 0) {
Availability earliestAvailability = availabilities.get(0);
if (earliestAvailability.getStartTime() > fullRangeBeginDate.getTime()) {
Availability surrogateAvailability = new SurrogateAvailability(earliestAvailability.getResource(),
fullRangeBeginDate.getTime());
surrogateAvailability.setEndTime(earliestAvailability.getStartTime());
availabilities.add(0, surrogateAvailability); // add at the head of the list
}
} else {
Resource surrogateResource = context.type == EntityContext.Type.Resource ? entityManager.find(
Resource.class, context.resourceId) : new Resource(-1);
Availability surrogateAvailability = new SurrogateAvailability(surrogateResource,
fullRangeBeginDate.getTime());
surrogateAvailability.setEndTime(fullRangeEndDate.getTime());
availabilities.add(surrogateAvailability); // add as the only element
}
// Now check if the date range passed in by the user extends into the future. If so, finish the last
// availability at now and add a surrogate after it, as we know nothing about the future.
long now = System.currentTimeMillis();
if (fullRangeEndDate.getTime() > now) {
Availability latestAvailability = availabilities.get(availabilities.size() - 1);
latestAvailability.setEndTime(now);
Availability unknownFuture = new SurrogateAvailability(latestAvailability.getResource(), now);
availabilities.add(unknownFuture);
}
// Now calculate the individual data points. We start at the end time of the range
// and move a current time pointer backwards in time, stopping at each barrier along the way, where a barrier
// is either the start of a data point or the start of an availability record. We move backwards
// in time because the full range may not be neatly divisible by the number of points so we want
// any "leftover" data that we can't account for in the returned list to be the oldest data possible.
long totalMillis = fullRangeEndTime - fullRangeBeginTime;
long perPointMillis = totalMillis / numberOfPoints;
List<AvailabilityPoint> availabilityPoints = new ArrayList<AvailabilityPoint>(numberOfPoints);
long currentTime = fullRangeEndTime;
int currentAvailabilityIndex = availabilities.size() - 1;
long timeUpInDataPoint = 0;
long timeDisabledInDataPoint = 0;
boolean hasDownPeriods = false;
boolean hasDisabledPeriods = false;
boolean hasUnknownPeriods = false;
long dataPointStartBarrier = fullRangeEndTime - perPointMillis;
while (currentTime > fullRangeBeginTime) {
if (currentAvailabilityIndex <= -1) {
// no more availability data, the rest of the data points are unknown
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.UNKNOWN, currentTime));
currentTime -= perPointMillis;
continue;
}
Availability currentAvailability = availabilities.get(currentAvailabilityIndex);
long availabilityStartBarrier = currentAvailability.getStartTime();
// the start of the data point comes first or at same time as availability record (remember, we are going
// backwards in time)
if (dataPointStartBarrier >= availabilityStartBarrier) {
// end the data point
if (currentAvailability instanceof SurrogateAvailability) {
// we are on the edge of the range with a surrogate for this data point. Be pessimistic,
// if we have had any down time, set to down, then disabled, then up, and finally unknown.
if (hasDownPeriods) {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.DOWN, currentTime));
} else if (hasDisabledPeriods) {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.DISABLED, currentTime));
} else if (timeUpInDataPoint > 0) {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.UP, currentTime));
} else {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.UNKNOWN, currentTime));
}
} else {
// bump up the proper counter or set the proper flag for the current time frame
switch (currentAvailability.getAvailabilityType()) {
case UP:
timeUpInDataPoint += currentTime - dataPointStartBarrier;
break;
case DOWN:
hasDownPeriods = true;
break;
case DISABLED:
hasDisabledPeriods = true;
break;
case UNKNOWN:
hasUnknownPeriods = true;
break;
default:
// Only stored avail types are relevant, MISSING, for example, is never stored
break;
}
// if the period has been all green, then set it to UP, otherwise, be pessimistic if there is any
// mix of avail types
if (timeUpInDataPoint == perPointMillis) {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.UP, currentTime));
} else if (hasDownPeriods) {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.DOWN, currentTime));
} else if (hasDisabledPeriods) {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.DISABLED, currentTime));
} else {
availabilityPoints.add(new AvailabilityPoint(AvailabilityType.UNKNOWN, currentTime));
}
}
timeUpInDataPoint = 0;
hasDownPeriods = false;
hasDisabledPeriods = false;
hasUnknownPeriods = false;
// if we reached the start of the current availability record, move to the previous one (going back in time, remember)
if (dataPointStartBarrier == availabilityStartBarrier) {
currentAvailabilityIndex--;
}
// move the current time pointer to the next data point and move back to the next data point start time
currentTime = dataPointStartBarrier;
dataPointStartBarrier -= perPointMillis;
// the division determing perPointMillis drops the remainder, which may leave us slightly short.
// if we go negative, we're done.
if (dataPointStartBarrier < 0) {
break;
}
} else { // the end of the availability record comes first, in the middle of a data point
switch (currentAvailability.getAvailabilityType()) {
case UP:
// if the resource has been up in the current time frame, bump up the counter
timeUpInDataPoint += currentTime - availabilityStartBarrier;
break;
case DOWN:
hasDownPeriods = true;
break;
case DISABLED:
hasDisabledPeriods = true;
break;
case UNKNOWN:
default:
hasUnknownPeriods = true;
// Only stored avail types are relevant, MISSING, for example, is never stored
}
// move to the previous availability record
currentAvailabilityIndex--;
// move the current time pointer to the start of the next
currentTime = availabilityStartBarrier;
}
}
// remember we went backwards in time, but we want the returned data to be ascending, so reverse the order
Collections.reverse(availabilityPoints);
/*
* RHQ-1631, make the latest availability dot match the current availability IF desired by the user
* note: this must occur AFTER reversing the collection so the last dot refers to the most recent time slice
*/
if (withCurrentAvailability) {
AvailabilityPoint oldFirstAvailabilityPoint = availabilityPoints.remove(availabilityPoints.size() - 1);
AvailabilityType newFirstAvailabilityType = oldFirstAvailabilityPoint.getAvailabilityType();
if (context.type == EntityContext.Type.Resource) {
newFirstAvailabilityType = getCurrentAvailabilityTypeForResource(subject, context.resourceId);
} else if (context.type == EntityContext.Type.ResourceGroup) {
ResourceGroupComposite composite = resourceGroupManager.getResourceGroupComposite(subject,
context.groupId);
switch (composite.getExplicitAvailabilityType()) {
case EMPTY:
newFirstAvailabilityType = null;
break;
case DOWN:
case WARN:
newFirstAvailabilityType = AvailabilityType.DOWN;
break;
case DISABLED:
newFirstAvailabilityType = AvailabilityType.DISABLED;
break;
default:
newFirstAvailabilityType = AvailabilityType.UP;
}
} else {
// March 20, 2009: we only support the "summary area" for resources and resourceGroups to date
// as a result, newFirstAvailabilityType will be a pass-through of the type in oldFirstAvailabilityPoint
}
availabilityPoints.add(new AvailabilityPoint(newFirstAvailabilityType, oldFirstAvailabilityPoint
.getTimestamp()));
}
// This should never happen, but add a check just to be safe.
if (availabilityPoints.size() != numberOfPoints) {
String errorMsg = "Calculation of availability did not produce the proper number of data points! "
+ context.toShortString() + "; begin=[" + fullRangeBeginTime + "(" + new Date(fullRangeBeginTime) + ")"
+ "]; end=[" + fullRangeEndTime + "(" + new Date(fullRangeEndTime) + ")" + "]; numberOfPoints=["
+ numberOfPoints + "]; actual-number=[" + availabilityPoints.size() + "]";
log.warn(errorMsg);
}
return availabilityPoints;
}
// This class does nothing more than give us a way to identify that this is not a real Availability, it's
// a surrogate used in the AvailabilityPoint logic above. We used to use a null availType but to flag the
// surrogate but that is no longer allowed in Availability, and this is more explicit anyway.
private static class SurrogateAvailability extends Availability {
private static final long serialVersionUID = 1L;
public SurrogateAvailability(Resource resource, Long startTime) {
super(resource, startTime, null);
}
}
@Override
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public void setResourceAvailabilities(Map<Agent, int[]> map, AvailabilityType avail) {
long now = System.currentTimeMillis();
for (Agent agent : map.keySet()) {
AvailabilityReport report = new AvailabilityReport(true, null);
report.setServerSideReport(true);
for (int resourceId : map.get(agent)) {
report.addAvailability(new Datum(resourceId, avail, now));
}
AvailabilityReportSerializer.getSingleton().lock(agent.getName());
try {
this.availabilityManager.mergeAvailabilityReport(report);
} finally {
AvailabilityReportSerializer.getSingleton().unlock(agent.getName());
}
}
return;
}
@Override
@TransactionAttribute(TransactionAttributeType.NEVER)
public boolean mergeAvailabilityReport(AvailabilityReport report) {
int reportSize = report.getResourceAvailability().size();
String agentName = report.getAgentName();
StopWatch watch = new StopWatch();
if (reportSize == 0) {
log.error("Agent [" + agentName + "] sent an empty availability report. This is a bug, please report it");
return true; // even though this report is bogus, do not ask for an immediate full report to avoid unusual infinite recursion due to this error condition
} else if (log.isDebugEnabled()) {
log.debug("Agent [" + agentName + "]: processing availability report of size: " + reportSize);
}
// translate data into Availability objects for downstream processing
List<Availability> availabilities = new ArrayList<Availability>(report.getResourceAvailability().size());
for (AvailabilityReport.Datum datum : report.getResourceAvailability()) {
availabilities.add(new Availability(new Resource(datum.getResourceId()), datum.getStartTime(), datum
.getAvailabilityType()));
}
Integer agentToUpdate = agentManager.getAgentIdByName(agentName);
MergeInfo mergeInfo = new MergeInfo(report);
// For agent reports (not a server-side report)
if (!report.isServerSideReport() && agentToUpdate != null) {
// if this is a changes-only report, and the agent appears backfilled, then immediately request and process
// a full report because, obviously, the agent is no longer down but the server thinks it still is down -
// we need to know the availabilities for all the resources on that agent
if (report.isChangesOnlyReport() && agentManager.isAgentBackfilled(agentToUpdate.intValue())) {
mergeInfo.setAskForFullReport(true);
}
// update the lastAvailReport time and unset the backfill flag if it is set.
availabilityManager.updateLastAvailabilityReportInNewTransaction(agentToUpdate.intValue());
}
// process the report in batches to avoid an overly long transaction and to potentially increase the
// speed in which an avail change becomes visible.
while (!availabilities.isEmpty()) {
int size = availabilities.size();
int end = (MERGE_BATCH_SIZE < size) ? MERGE_BATCH_SIZE : size;
List<Availability> availBatch = availabilities.subList(0, end);
availabilityManager.mergeAvailabilitiesInNewTransaction(availBatch, mergeInfo);
// Advance our progress and possibly help GC. This will remove the processed avails from the backing list
availBatch.clear();
}
MeasurementMonitor.getMBean().incrementAvailabilityReports(report.isChangesOnlyReport());
MeasurementMonitor.getMBean().incrementAvailabilitiesInserted(mergeInfo.getNumInserted());
MeasurementMonitor.getMBean().incrementAvailabilityInsertTime(watch.getElapsed());
watch.reset();
if (!report.isServerSideReport()) {
if (agentToUpdate != null) {
// don't bother asking for a full report if the one we are currently processing is already full
if (mergeInfo.isAskForFullReport() && report.isChangesOnlyReport()) {
log.debug("The server is unsure that it has up-to-date availabilities for agent [" + agentName
+ "]; asking for a full report to be sent");
return false;
}
} else {
log.error("Could not figure out which agent sent availability report. "
+ "This error is harmless and should stop appearing after a short while if the platform of the agent ["
+ agentName + "] was recently removed. In any other case this is a bug." + report);
}
}
return true; // everything is OK and things look to be in sync
}
static class MergeInfo {
private AvailabilityReport report;
private int numInserted = 0;
private boolean askForFullReport = false;
public MergeInfo(AvailabilityReport report) {
super();
this.report = report;
}
public int getNumInserted() {
return numInserted;
}
public void incrementNumInserted() {
++this.numInserted;
}
public boolean isAskForFullReport() {
return askForFullReport;
}
public void setAskForFullReport(boolean askForFullReport) {
this.askForFullReport = askForFullReport;
}
public boolean isEnablementReport() {
return report.isEnablementReport();
}
public boolean isServerSideReport() {
return report.isServerSideReport();
}
public String toString(boolean includeAll) {
return report.toString(includeAll);
}
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void mergeAvailabilitiesInNewTransaction(List<Availability> availabilities, MergeInfo mergeInfo) {
// We will alert only on the avails for enabled resources. Keep track of any that are disabled.
List<Availability> disabledAvailabilities = new ArrayList<Availability>();
Query q = entityManager.createNamedQuery(Availability.FIND_LATEST_BY_RESOURCE_IDS);
List<Integer> resourceIds = new ArrayList<Integer>(availabilities.size());
for (Availability reported : availabilities) {
resourceIds.add(reported.getResource().getId());
}
q.setParameter("resourceIds", resourceIds);
List<Availability> latestAvailabilitiesList = q.getResultList();
resourceIds.clear(); // done with this, perhaps helps GC
resourceIds = null;
// populate Map of resourceIds to latestAvailability
// there should be a single latest avail per resource. mark any situation where we have multiple
Object nonUniqueMarker = new Object();
Map<Integer, Object> latestAvailabilities = new HashMap(availabilities.size() + 100);
for (Availability latestAvailability : latestAvailabilitiesList) {
Integer resourceId = latestAvailability.getResource().getId();
if (latestAvailabilities.containsKey(resourceId)) {
latestAvailabilities.put(resourceId, nonUniqueMarker);
} else {
latestAvailabilities.put(resourceId, latestAvailability);
}
}
// keep track of the changes in availability so we can update the relevant ResourceAvailabilities in a batch
List<Availability> changedAvailabilities = new ArrayList<Availability>(availabilities.size());
for (Availability reported : availabilities) {
// availability reports only tell us the current state at the start time; end time is ignored/must be null
reported.setEndTime(null);
// get the latest avail for the reported resource
//q.setParameter("resourceId", reported.getResource().getId());
Integer resourceId = reported.getResource().getId();
Object latestObject = latestAvailabilities.get(resourceId);
Availability latest = null;
if (null == latestObject) { // this is like NoResultException
// This should not happen unless the Resource in the report is stale, which can happen in certain
// sync scenarios. A Resource is given its initial Availability/ResourceAvailability when it is
// persisted so it is guaranteed to have Availability, so, the Resource must not exist. At least
// it must not exist in my utopian view of the world. Let's just make sure...
Resource attachedResource = entityManager.find(Resource.class, reported.getResource().getId());
if ((null == attachedResource) || (InventoryStatus.COMMITTED != attachedResource.getInventoryStatus())) {
// expected case
log.info("Skipping mergeAvailabilityReport() for stale resource [" + reported.getResource()
+ "]. These messages should go away after the next agent synchronization with the server.");
continue;
} else {
// this should not really happen but is possible in rare failure situations, it means the resource
// exists but has no latest Availability record (i.e. sendTime == null). Correct the situation and
// then process the reported avail.
log.warn("Resource [" + reported.getResource()
+ "] has no latest availability record (i.e. no endtime) - will attempt to repair.\n"
+ mergeInfo.toString(false));
try {
List<Availability> attachedAvails = attachedResource.getAvailability();
Availability attachedLastAvail = null;
if (attachedAvails.isEmpty()) {
latest = new Availability(attachedResource, 0L, AvailabilityType.UNKNOWN);
entityManager.persist(latest);
} else {
latest = attachedAvails.get(attachedAvails.size() - 1);
latest.setEndTime(null);
latest = entityManager.merge(latest);
}
// update the Map to reflect the repaired latest avail
latestAvailabilities.put(resourceId, latest);
updateResourceAvailability(latest);
// ask the agent for a full report so as to ensure we are in sync with agent
mergeInfo.setAskForFullReport(true);
} catch (Throwable t) {
log.warn("Unable to repair NoResult latest availablity for Resource [" + reported.getResource()
+ "]", t);
continue;
}
}
} else if (latestObject == nonUniqueMarker) { // this is like NonUniqueResultException
// This condition should never happen. In my world of la-la land, I've done everything
// correctly so this never happens. But, due to the asynchronous nature of things,
// I have to believe that this still might happen (albeit rarely). If it does happen,
// and we do nothing about it - bad things arise. So, if we find that a resource
// has 2 or more availabilities with endTime of null, we need to delete all but the
// latest one (the one whose start time is the latest). This should correct the
// problem and allow us to continue processing availability reports for that resource
// This problem happens and can be helped to occur by for example forcibly killing an agent
// While it is a condition that shouldn't happen under normal operation, it is recoverable,
// so we only log on the debug level.
log.debug("Resource [" + reported.getResource()
+ "] has multiple availabilities without an endtime - will attempt to remove the extra ones\n"
+ mergeInfo.toString(false));
try {
q = entityManager.createNamedQuery(Availability.FIND_CURRENT_BY_RESOURCE);
q.setParameter("resourceId", resourceId);
List<Availability> latestList = q.getResultList();
// delete all but the last one (our query sorts in ASC start time order)
int latestCount = latestList.size();
for (int i = 0; i < (latestCount - 1); i++) {
entityManager.remove(latestList.get(i));
}
latest = latestList.get(latestCount - 1);
updateResourceAvailability(latest);
// update the Map to reflect the repaired latest avail
latestAvailabilities.put(resourceId, latest);
// this is an unusual report - ask the agent for a full report so as to ensure we are in sync with agent
mergeInfo.setAskForFullReport(true);
} catch (Throwable t) {
log.warn(
"Unable to repair NonUnique Result latest availablity for Resource [" + reported.getResource()
+ "]", t);
continue;
}
} else {
latest = (Availability) latestObject;
}
AvailabilityType latestType = latest.getAvailabilityType();
AvailabilityType reportedType = reported.getAvailabilityType();
// If the reported type is MISSING and this type is enabled for automatic uninventory, then
// uninventory the resource and continue with the next reported avail. Otherwise, convert to
// DOWN and process as usual.
if (AvailabilityType.MISSING == reportedType) {
// the reported.getResource() gives us only a resource with an id. Nothing else, so we call a
// dedicated SLSB method to do this work.
boolean uninventoried = resourceManager.handleMissingResourceInNewTransaction(resourceId);
if (uninventoried) {
continue;
} else {
if (log.isDebugEnabled()) {
log.debug("Type not enabled for automatic uninventory of MISSING resources. Converting MISSING to DOWN AvailabilityType for resource: "
+ reported.getResource());
}
reported.setAvailabilityType(AvailabilityType.DOWN);
reportedType = AvailabilityType.DOWN;
}
}
// If the current avail is DISABLED, and this report is not trying to re-enable the resource,
// Then ignore the reported avail.
if (AvailabilityType.DISABLED == latestType) {
if (!(mergeInfo.isEnablementReport() && (AvailabilityType.UNKNOWN == reportedType))) {
disabledAvailabilities.add(reported);
continue;
}
}
if (reported.getStartTime() >= latest.getStartTime()) {
//log.info( "new avail (latest/reported)-->" + latest + "/" + reported );
// the new availability data is for a time after our last known state change
// we are run-length encoded, so only persist data if the availability changed
if (latest.getAvailabilityType() != reported.getAvailabilityType()) {
entityManager.persist(reported);
// the reported avail is the new latest avail, update the Map in case we have multiple reported
// changes for the same resource in this report
latestAvailabilities.put(resourceId, reported);
mergeInfo.incrementNumInserted();
latest.setEndTime(reported.getStartTime());
latest = entityManager.merge(latest);
changedAvailabilities.add(reported);
}
// our last known state was unknown, ask for a full report to ensure we are in sync with agent
if (latest.getAvailabilityType() == AvailabilityType.UNKNOWN) {
mergeInfo.setAskForFullReport(true);
}
} else {
//log.info( "past avail (latest/reported)==>" + latest + "/" + reported );
// The new data is for a time in the past, probably an agent sending a report after
// a network outage has been corrected but after we have already backfilled.
// We need to insert it into our past timeline.
insertAvailability(reported);
mergeInfo.incrementNumInserted();
// this is an unusual report - ask the agent for a full report so as to ensure we are in sync with agent
mergeInfo.setAskForFullReport(true);
}
}
// update the affected ResourceAvailabilities
updateResourceAvailabilities(changedAvailabilities);
latestAvailabilities.clear(); // done with these, perhaps helps GC
latestAvailabilities = null;
changedAvailabilities.clear();
changedAvailabilities = null;
// notify alert condition cache manager for all reported avails for for enabled resources
availabilities.removeAll(disabledAvailabilities);
notifyAlertConditionCacheManager("mergeAvailabilityReport",
availabilities.toArray(new Availability[availabilities.size()]));
return;
}
private void updateResourceAvailability(Availability reported) {
ResourceAvailability currentAvailability = resourceAvailabilityManager.getLatestAvailability(reported
.getResource().getId());
updateResourceAvailabilityIfNecessary(reported, currentAvailability);
}
// update the current availability data for this resource but only if necessary (actually changed)
private void updateResourceAvailabilityIfNecessary(Availability reported, ResourceAvailability currentAvailability) {
if (currentAvailability != null && currentAvailability.getAvailabilityType() != reported.getAvailabilityType()) {
currentAvailability.setAvailabilityType(reported.getAvailabilityType());
entityManager.merge(currentAvailability);
} else if (currentAvailability == null) {
// This should not happen unless the Resource in the report is stale, which can happen in certain
// sync scenarios. A Resource is given its initial ResourceAvailability when it is persisted so it
// is guaranteed to have currentAvailability, so, the Resource must not exist.
log.info("Skipping updateResourceAvailabilityIfNecessary() for stale resource [" + reported.getResource()
+ "]. These messages should go away after the next agent synchronization with the server.");
}
}
// pulls all the ResourceAvailabilities in one query to reduce DB round trips
private void updateResourceAvailabilities(List<Availability> reportedChanges) {
if (null == reportedChanges || reportedChanges.isEmpty()) {
return;
}
Query q = entityManager.createNamedQuery(ResourceAvailability.QUERY_FIND_BY_RESOURCE_IDS);
List<Integer> resourceIds = new ArrayList<Integer>(reportedChanges.size());
for (Availability reported : reportedChanges) {
resourceIds.add(reported.getResource().getId());
}
q.setParameter("resourceIds", resourceIds);
List<ResourceAvailability> resourceAvailabilityList = q.getResultList();
resourceIds.clear(); // done with this, perhaps helps GC
resourceIds = null;
// populate Map of resourceIds to resourceAvailability
Map<Integer, ResourceAvailability> resourceAvailabilities = new HashMap(reportedChanges.size());
for (ResourceAvailability resourceAvailability : resourceAvailabilityList) {
resourceAvailabilities.put(resourceAvailability.getResourceId(), resourceAvailability);
}
for (Availability reported : reportedChanges) {
ResourceAvailability currentAvailability = resourceAvailabilities.get(reported.getResource().getId());
// update the last known availability data for this resource but only if necessary (actually changed)
updateResourceAvailabilityIfNecessary(reported, currentAvailability);
}
resourceAvailabilities.clear(); // done with these, perhaps helps GC
resourceAvailabilities = null;
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void updateLastAvailabilityReportInNewTransaction(int agentId) {
// should we catch exceptions here, or allow them to bubble up and be caught?
/*
* since we already know we have to update the agent row with the last avail report time, might as well
* set the backfilled to false here (as opposed to called agentManager.setBackfilled(agentId, false)
*/
Query query = entityManager.createNamedQuery(Agent.QUERY_UPDATE_LAST_AVAIL_REPORT);
query.setParameter("reportTime", System.currentTimeMillis());
query.setParameter("agentId", agentId);
query.executeUpdate();
}
@Override
@SuppressWarnings("unchecked")
public void updateAgentResourceAvailabilities(int agentId, AvailabilityType platformAvailType,
AvailabilityType childAvailType) {
platformAvailType = (null == platformAvailType) ? AvailabilityType.DOWN : platformAvailType;
childAvailType = (null == childAvailType) ? AvailabilityType.UNKNOWN : childAvailType;
// get the platform resource if not already at platformAvailType (since this is the one
// we need to change)
Query query = entityManager
.createNamedQuery(Availability.FIND_PLATFORM_COMPOSITE_BY_AGENT_AND_NONMATCHING_TYPE);
query.setParameter("agentId", agentId);
query.setParameter("availabilityType", platformAvailType);
// should be 0 or 1 entry
List<ResourceIdWithAvailabilityComposite> platformResourcesWithStatus = query.getResultList();
// get the child resources not disabled and not already at childAvailType
// (since these are the ones we need to change)
query = entityManager.createNamedQuery(Availability.FIND_CHILD_COMPOSITE_BY_AGENT_AND_NONMATCHING_TYPE);
query.setParameter("agentId", agentId);
query.setParameter("availabilityType", childAvailType);
query.setParameter("disabled", AvailabilityType.DISABLED);
List<ResourceIdWithAvailabilityComposite> resourcesWithStatus = query.getResultList();
// The above queries only return resources if they have at least one row in Availability. This should
// not be a problem since a new Resource gets an initial UNKNOWN Availability record at persist-time.
if (log.isDebugEnabled()) {
log.debug("Agent #[" + agentId + "] is going to have [" + resourcesWithStatus.size()
+ "] resources backfilled with [" + childAvailType.getName() + "]");
}
Date now = new Date();
int newAvailsSize = platformResourcesWithStatus.size() + resourcesWithStatus.size();
List<Availability> newAvailabilities = new ArrayList<Availability>(newAvailsSize);
// if the platform is being set to a new status handle it now
if (!platformResourcesWithStatus.isEmpty()) {
Availability newAvailabilityInterval = getNewInterval(platformResourcesWithStatus.get(0), now,
platformAvailType);
if (newAvailabilityInterval != null) {
newAvailabilities.add(newAvailabilityInterval);
}
resourceAvailabilityManager.updateAgentResourcesLatestAvailability(agentId, platformAvailType, true);
}
// for those resources that have a current availability status that is different, change them
for (ResourceIdWithAvailabilityComposite record : resourcesWithStatus) {
Availability newAvailabilityInterval = getNewInterval(record, now, childAvailType);
if (newAvailabilityInterval != null) {
newAvailabilities.add(newAvailabilityInterval);
}
}
resourceAvailabilityManager.updateAgentResourcesLatestAvailability(agentId, childAvailType, false);
// To handle backfilling process, which will mark them unknown
notifyAlertConditionCacheManager("setAllAgentResourceAvailabilities",
newAvailabilities.toArray(new Availability[newAvailabilities.size()]));
if (log.isDebugEnabled()) {
log.debug("Resources for agent #[" + agentId + "] have been fully backfilled.");
}
return;
}
/**
* Starts a new availability interval for a given resource. If the new interval is of the same type as the previous,
* then the previous will be extended. Otherwise the previous will be terminated and a new one will be started. The
* Availability objects in the given record will be modified; make sure they are managed by an entity manager if you
* want the changes to be persisted.
*
* @param record identifies the resource and its current availability
* @param startDate Start date of the new interval (which must be after the current availability interval)
* @param aType the new type of availability (UP, DOWN) that the resource will now have
*
* @return the new availability interval for a given resource, or null if there is already an existing availability
*/
private Availability getNewInterval(ResourceIdWithAvailabilityComposite record, Date startDate,
AvailabilityType aType) {
// if there is already an existing availability, update it
Availability old = record.getAvailability();
if (old != null) {
if (old.getAvailabilityType() == aType) {
// existing availability is the same type, just extend it without creating a new entity
old.setEndTime(null); // don't really need to do this; just enforces the fact that we extend the last interval
return null;
}
old.setEndTime(startDate.getTime());
}
Resource resource = new Resource();
resource.setId(record.getResourceId());
Availability newAvail = new Availability(resource, startDate.getTime(), aType);
entityManager.persist(newAvail);
return newAvail;
}
/**
* Try to insert <code>toInsert</code> into the resource's availability timeline. It is expected that:
*
* <ul>
* <li>only the start time in <code>toInsert</code> is valid.</li>
* <li><code>toInsert</code> is not to be inserted at the end (that is, it is not the latest availability - it is
* something that occurred in the past).</li>
* <li>there is at least 1 availability record for the resource</li>
* </ul>
*
* @param toInsert new interval, probably being backfilled from a re-appeared agent
*/
@SuppressWarnings("unchecked")
private void insertAvailability(Availability toInsert) {
// get the existing availability interval where the new availability will be shoe-horned in
Query query = entityManager.createNamedQuery(Availability.FIND_BY_RESOURCE_AND_DATE);
query.setParameter("resourceId", toInsert.getResource().getId());
query.setParameter("aTime", toInsert.getStartTime());
Availability existing;
try {
existing = (Availability) query.getSingleResult();
} catch (NoResultException nre) {
// this should never happen since we create an initial Availability when the resource is persisted.
log.warn("Resource [" + toInsert.getResource()
+ "] has no Availabilities, this should not happen. Correcting situation by adding an Availability.");
// we are inserting this as the very first interval
query = entityManager.createNamedQuery(Availability.FIND_BY_RESOURCE);
query.setParameter("resourceId", toInsert.getResource().getId());
query.setMaxResults(1); // we only need the very first one
Availability firstAvail = ((List<Availability>) query.getResultList()).get(0);
// only add a new row if its a different status; otherwise, just move the first interval back further
if (firstAvail.getAvailabilityType() != toInsert.getAvailabilityType()) {
toInsert.setEndTime(firstAvail.getStartTime());
entityManager.persist(toInsert);
} else {
firstAvail.setStartTime(toInsert.getStartTime());
}
return;
} catch (NonUniqueResultException nure) {
// This should not happen but can happen if the startTime exactly matches an existing start time. In
// this case assume we have somehow been passed a duplicate report, and ignore the entry.
log.warn("Resource [" + toInsert.getResource()
+ "] received a duplicate Availability. It is being ignored: " + toInsert);
return;
}
// If we are inserting the same availability type, the first one can just continue
// and there is nothing to do!
if (existing.getAvailabilityType() != toInsert.getAvailabilityType()) {
// get the afterExisting availability. note: we are assured this query will return something;
// semantics of this method is that it is never called if we are inserting in the last interval
query = entityManager.createNamedQuery(Availability.FIND_BY_RESOURCE_AND_DATE);
query.setParameter("resourceId", toInsert.getResource().getId());
query.setParameter("aTime", existing.getEndTime() + 1);
Availability afterExisting = (Availability) query.getSingleResult();
if (toInsert.getAvailabilityType() == afterExisting.getAvailabilityType()) {
// the inserted avail type is the same as the following avail type, we don't need to
// insert a new avail record, just adjust the start/end times of the existing records.
if (existing.getStartTime() == toInsert.getStartTime()) {
// Edge Case: If the insertTo start time equals the existing start time
// just remove the existing record and let afterExisting cover the interval.
entityManager.remove(existing);
} else {
existing.setEndTime(toInsert.getStartTime());
}
// stretch next interval to cover the inserted interval
afterExisting.setStartTime(toInsert.getStartTime());
} else {
// the inserted avail type is NOT the same as the following avail type, we likely need to
// insert a new avail record.
if (existing.getStartTime() == toInsert.getStartTime()) {
// Edge Case: If the insertTo start time equals the existing end time
// just update the existing avail type to be the new avail type and keep the same boundary.
existing.setAvailabilityType(toInsert.getAvailabilityType());
} else {
// insert the new avail type interval, witch is different than existing and afterExisting.
existing.setEndTime(toInsert.getStartTime());
toInsert.setEndTime(afterExisting.getStartTime());
entityManager.persist(toInsert);
}
}
}
return;
}
/**
* Find all availability records for a given Resource that match the given interval [startDate, endDate]. The
* returned objects will probably cover a larger interval than the required one.
*
* @param resourceId identifies the resource for which we want the values
* @param startDate start date of the desired interval
* @param endDate end date of the desired interval
*
* @return A list of availabilities that cover at least the given date range
* @Deprecated used in portal war EventsView.jsp. Use {@link #findAvailabilityByCriteria(Subject, AvailabilityCriteria)}
*/
@Override
@SuppressWarnings("unchecked")
@Deprecated
public List<Availability> findAvailabilityWithinInterval(int resourceId, Date startDate, Date endDate) {
Query q = entityManager.createNamedQuery(Availability.FIND_FOR_RESOURCE_WITHIN_INTERVAL);
q.setParameter("resourceId", resourceId);
q.setParameter("start", startDate.getTime());
q.setParameter("end", endDate.getTime());
List<Availability> results = q.getResultList();
return results;
}
@Override
@SuppressWarnings("unchecked")
public PageList<Availability> findAvailabilityByCriteria(Subject subject, AvailabilityCriteria criteria) {
CriteriaQueryGenerator generator = new CriteriaQueryGenerator(subject, criteria);
if (authorizationManager.isInventoryManager(subject) == false) {
generator.setAuthorizationResourceFragment(CriteriaQueryGenerator.AuthorizationTokenType.RESOURCE,
"resource", subject.getId());
}
CriteriaQueryRunner<Availability> queryRunner = new CriteriaQueryRunner(criteria, generator, entityManager);
PageList<Availability> result = queryRunner.execute();
return result;
}
/**
* @return all avails for all member resources for the specified interval, ordered by asc startTime
*/
@SuppressWarnings("unchecked")
private List<Availability> findResourceGroupAvailabilityWithinInterval(int groupId, Date startDate, Date endDate) {
Query q = entityManager.createNamedQuery(Availability.FIND_FOR_RESOURCE_GROUP_WITHIN_INTERVAL);
q.setParameter("groupId", groupId);
q.setParameter("start", startDate.getTime());
q.setParameter("end", endDate.getTime());
List<Availability> results = q.getResultList();
return results;
}
/**
* @Deprecated used in portal war, should probably go away when portal war goes away
*/
@SuppressWarnings("unchecked")
private List<Availability> findAutoGroupAvailabilityWithinInterval(int parentResourceId, int resourceTypeId,
Date startDate, Date endDate) {
Query q = entityManager.createNamedQuery(Availability.FIND_FOR_AUTO_GROUP_WITHIN_INTERVAL);
q.setParameter("parentId", parentResourceId);
q.setParameter("typeId", resourceTypeId);
q.setParameter("start", startDate.getTime());
q.setParameter("end", endDate.getTime());
List<Availability> results = q.getResultList();
return results;
}
/**
* @Deprecated used in portal war ListAvailabilityHistoryUIBEan. Use {@link #findAvailabilityByCriteria(Subject, AvailabilityCriteria)}
* Note that this methods uses startTime DESC sorting, which must be explicitly set in AvailabilityCriteria.
*/
@Override
@Deprecated
public PageList<Availability> findAvailabilityForResource(Subject subject, int resourceId, PageControl pageControl) {
if (authorizationManager.canViewResource(subject, resourceId) == false) {
throw new PermissionException("User [" + subject
+ "] does not have permission to view Availability history for resource[id=" + resourceId + "]");
}
pageControl.initDefaultOrderingField("av.startTime", PageOrdering.DESC);
Query countQuery = PersistenceUtility.createCountQuery(entityManager, Availability.FIND_BY_RESOURCE_NO_SORT);
Query query = PersistenceUtility.createQueryWithOrderBy(entityManager, Availability.FIND_BY_RESOURCE_NO_SORT,
pageControl);
countQuery.setParameter("resourceId", resourceId);
query.setParameter("resourceId", resourceId);
long count = (Long) countQuery.getSingleResult();
List<Availability> availabilities = query.getResultList();
return new PageList<Availability>(availabilities, (int) count, pageControl);
}
private void notifyAlertConditionCacheManager(String callingMethod, Availability... availabilities) {
AlertConditionCacheStats stats = alertConditionCacheManager.checkConditions(availabilities);
if (log.isDebugEnabled()) {
log.debug(callingMethod + ": " + stats.toString());
}
}
@Override
@TransactionAttribute(TransactionAttributeType.NEVER)
public void scheduleAvailabilityDurationCheck(AvailabilityDurationCacheElement cacheElement, Resource resource,
long startTime) {
String operator = cacheElement.getAlertConditionOperator().name();
String durationString = (String) cacheElement.getAlertConditionOperatorOption();
long duration = Long.valueOf(durationString).longValue() * 1000;
if (log.isDebugEnabled()) {
Date jobTime = new Date(System.currentTimeMillis() + duration);
log.debug("Scheduling availability duration job for [" + DateFormat.getDateTimeInstance().format(jobTime)
+ "]");
}
HashMap<String, String> infoMap = new HashMap<String, String>();
// the condition id is needed to ensure we limit the future avail checking to the one relevant alert condition
infoMap.put(AlertAvailabilityDurationJob.DATAMAP_CONDITION_ID,
String.valueOf(cacheElement.getAlertConditionTriggerId()));
infoMap.put(AlertAvailabilityDurationJob.DATAMAP_RESOURCE_ID, String.valueOf(resource.getId()));
infoMap.put(AlertAvailabilityDurationJob.DATAMAP_OPERATOR, operator);
infoMap.put(AlertAvailabilityDurationJob.DATAMAP_DURATION, durationString); // in seconds
infoMap.put(AlertAvailabilityDurationJob.DATAMAP_START_TIME, String.valueOf(startTime)); // in milliseconds
timerService.createSingleActionTimer(duration, new TimerConfig(infoMap, false));
}
@Timeout
public void handleAvailabilityDurationCheck(Timer timer) {
try {
AlertAvailabilityDurationJob.execute((HashMap<String, String>) timer.getInfo());
} catch (Throwable t) {
log.error("Failed to handle availability duration timer - will try again later. Cause: " + t);
}
}
}