package org.dcache.services.info.base;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import java.util.Date;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import dmg.cells.nucleus.CDC;
import dmg.cells.nucleus.CellAddressCore;
import dmg.cells.nucleus.CellIdentityAware;
import org.dcache.util.NDC;
import org.dcache.util.FireAndForgetTask;
/**
* The StateMaintainer class provides the machinery for processing
* StateUpdate objects independently of whichever Thread created them. It is
* also responsible for purging those metrics that have expired.
*/
public class StateMaintainer implements StateUpdateManager, CellIdentityAware
{
private static final Logger LOGGER = LoggerFactory.getLogger(StateMaintainer.class);
private static final boolean CANCEL_RUNNING_METRIC_EXPUNGE = false;
/** Our scheduler */
private ScheduledExecutorService _scheduler;
/** The number of pending requests, queued up in the scheduler */
private final AtomicInteger _pendingRequestCount = new AtomicInteger();
/** Our link to the business logic for update dCache state */
private volatile StateCaretaker _caretaker;
private CellAddressCore _myAddress = new CellAddressCore("unknown@unknown");
/**
* The Future for the next scheduled metric purge, or null if no such
* activity has been scheduled. Access and updates to this object are
* protected by the this (StateMaintainer) object monitor.
*/
private ScheduledFuture<?> _metricExpiryFuture;
private Date _metricExpiryDate;
@Required
public void setCaretaker(StateCaretaker caretaker)
{
_caretaker = caretaker;
}
@Required
public void setExecutor(ScheduledExecutorService executor)
{
_scheduler = executor;
}
@Override
public void setCellAddress(CellAddressCore address)
{
_myAddress = address;
}
/**
* Alter which StateCaretaker the StateMaintainer will use.
*
* @param caretaker
*/
void setStateCaretaker(final StateCaretaker caretaker)
{
_caretaker = caretaker;
}
@Override
public int countPendingUpdates()
{
return _pendingRequestCount.get();
}
@Override
public void enqueueUpdate(final StateUpdate pendingUpdate)
{
LOGGER.trace("enqueing job to process update {}", pendingUpdate);
final NDC ndc = NDC.cloneNdc();
_pendingRequestCount.incrementAndGet();
_scheduler.execute(new FireAndForgetTask(() -> {
CDC.reset(_myAddress);
NDC.set(ndc);
try {
LOGGER.trace("starting job to process update {}", pendingUpdate);
_caretaker.processUpdate(pendingUpdate);
checkScheduledExpungeActivity();
LOGGER.trace("finished job to process update {}", pendingUpdate);
} finally {
_pendingRequestCount.decrementAndGet();
pendingUpdate.updateComplete();
CDC.clear();
}
}));
}
@Override
public void shutdown()
{
List<Runnable> unprocessed = _scheduler.shutdownNow();
if (!unprocessed.isEmpty()) {
LOGGER.info("Shutting down with {} pending updates", unprocessed.size());
} else {
LOGGER.trace("Shutting down without any pending updates");
}
}
/**
* Check StateCaretaker to obtain the earliest time when a metric will
* expire. If this value has changed then reschedule the metric expiry
* activity.
* <p>
* It is safe to call this method whenever the value of
* {@link StateCaretaker#getEarliestMetricExpiryDate()} could possible
* have changed.
*/
synchronized void checkScheduledExpungeActivity()
{
Date earliestMetricExpiry = _caretaker.getEarliestMetricExpiryDate();
if (earliestMetricExpiry == null && _metricExpiryDate == null) {
return;
}
// If the metric expiry date has changed, we try to cancel the update.
if (_metricExpiryDate != null && !_metricExpiryDate.equals(earliestMetricExpiry)) {
LOGGER.trace("Cancelling existing metric purge, due to take place in {} s",
(_metricExpiryDate.getTime() - System.currentTimeMillis())/1000.0);
/* If the cancel fails (returns false) then the metric expunge is
* currently being processed. When this completes, a new metric
* expiry job will be scheduled automatically, so we don't need to
* do anything.
*/
if (_metricExpiryFuture.cancel(CANCEL_RUNNING_METRIC_EXPUNGE)) {
_metricExpiryDate = null;
}
}
if (_metricExpiryDate == null) {
scheduleMetricExpunge(earliestMetricExpiry);
}
}
/**
* If whenExpunge is not null then schedule a task to call
* {@link StateCaretaker#removeExpiredMetrics()} then call
* {@link #scheduleScheduleMetricExpunge()} to schedule the next metric
* purge activity. If whenExpunge is null then no action is taken.
*
* @param whenExpunge
* some time in the future to schedule a task or null.
*/
private synchronized void scheduleMetricExpunge(final Date whenExpunge)
{
_metricExpiryDate = whenExpunge;
if (whenExpunge == null) {
_metricExpiryFuture = null;
return;
}
long delay = whenExpunge.getTime() - System.currentTimeMillis();
LOGGER.trace("Scheduling next metric purge in {} s", delay/1000.0);
try {
_metricExpiryFuture = _scheduler.schedule(new FireAndForgetTask(() -> {
LOGGER.trace("Starting metric purge");
_caretaker.removeExpiredMetrics();
scheduleMetricExpunge();
LOGGER.trace("Metric purge completed");
expungeCompleted();
}), delay, TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
LOGGER.trace("Failed to enqueue expunge task as queue is not accepting further work.");
}
}
public void expungeCompleted()
{
// Allow discovery of when an expung is completed.
}
/**
* Create a new scheduled task to expunge metrics. If the existing metric
* expunge job is not finished then nothing is done. If it is finished
* then a new task is submitted.
* <p>
* This method should only be called when we know the value from
* {@link StateCaretaker#getEarliestMetricExpiryDate()} has changed to
* avoid creating competing tasks.
*/
protected synchronized void scheduleMetricExpunge()
{
scheduleMetricExpunge(_caretaker.getEarliestMetricExpiryDate());
}
}