/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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.apache.geode.internal.cache;
/**
* ExpiryTask represents a timeout event for expiration
*/
import org.apache.geode.CancelException;
import org.apache.geode.InternalGemFireError;
import org.apache.geode.SystemFailure;
import org.apache.geode.cache.*;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.PooledExecutorWithDMStats;
import org.apache.geode.internal.SystemTimer;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.LoggingThreadGroup;
import org.apache.geode.internal.logging.log4j.LocalizedMessage;
import org.apache.geode.internal.tcp.ConnectionTable;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
public abstract class ExpiryTask extends SystemTimer.SystemTimerTask {
private static final Logger logger = LogService.getLogger();
private LocalRegion region; // no longer final so cancel can null it out see bug 37574
private static final ThreadPoolExecutor executor;
static {
// default to inline expiry to fix bug 37115
int nThreads =
Integer.getInteger(DistributionConfig.GEMFIRE_PREFIX + "EXPIRY_THREADS", 0).intValue();
if (nThreads > 0) {
ThreadFactory tf = new ThreadFactory() {
private int nextId = 0;
public Thread newThread(final Runnable command) {
String name = "Expiration threads";
final ThreadGroup group = LoggingThreadGroup.createThreadGroup(name);
final Runnable r = new Runnable() {
public void run() {
ConnectionTable.threadWantsSharedResources();
try {
command.run();
} finally {
ConnectionTable.releaseThreadsSockets();
}
}
};
Thread thread = new Thread(group, r, "Expiry " + nextId++);
thread.setDaemon(true);
return thread;
}
};
// LinkedBlockingQueue q = new LinkedBlockingQueue();
SynchronousQueue q = new SynchronousQueue();
executor = new PooledExecutorWithDMStats(q, nThreads, tf);
} else {
executor = null;
}
}
protected ExpiryTask(LocalRegion region) {
this.region = region;
}
protected abstract ExpirationAttributes getIdleAttributes();
protected abstract ExpirationAttributes getTTLAttributes();
/**
* @return the absolute time (ms since Jan 1, 1970) at which this region expires, due to either
* time-to-live or idle-timeout (whichever will occur first), or 0 if neither are used.
*/
public long getExpirationTime() throws EntryNotFoundException {
long ttl = getTTLExpirationTime();
long idle = getIdleExpirationTime();
if (ttl == 0) {
return idle;
} else if (idle == 0) {
return ttl;
}
return Math.min(ttl, idle);
}
/** Return the absolute time when TTL expiration occurs, or 0 if not used */
public final long getTTLExpirationTime() throws EntryNotFoundException {
long ttl = getTTLAttributes().getTimeout();
long tilt = 0;
if (ttl > 0) {
if (getLocalRegion() != null && !getLocalRegion().EXPIRY_UNITS_MS) {
ttl *= 1000;
}
tilt = getLastModifiedTime() + ttl;
}
return tilt;
}
/** Return the absolute time when idle expiration occurs, or 0 if not used */
public final long getIdleExpirationTime() throws EntryNotFoundException {
long idle = getIdleAttributes().getTimeout();
long tilt = 0;
if (idle > 0) {
if (getLocalRegion() != null && !getLocalRegion().EXPIRY_UNITS_MS) {
idle *= 1000;
}
tilt = getLastAccessedTime() + idle;
}
return tilt;
}
/**
* Returns the number of milliseconds until this task should expire. The return value will never
* be negative.
*/
final long getExpiryMillis() throws EntryNotFoundException {
long extm = getExpirationTime() - getNow();
if (extm < 0L)
return 0L;
else
return extm;
}
/**
* Return true if current task could have expired. Return false if expiration is impossible.
*/
protected boolean isExpirationPossible() throws EntryNotFoundException {
long expTime = getExpirationTime();
if (expTime > 0L && getNow() >= expTime) {
return true;
}
return false;
}
/**
* Returns false if the region reliability state does not allow this expiry task to fire.
*/
protected boolean isExpirationAllowed() {
return getLocalRegion().isExpirationAllowed(this);
}
protected void performTimeout() throws CacheException {
if (logger.isDebugEnabled()) {
logger.debug("{}.performTimeout(): getExpirationTime() returns {}", this.toString(),
getExpirationTime());
}
getLocalRegion().performExpiryTimeout(this);
}
protected abstract void basicPerformTimeout(boolean isPending) throws CacheException;
/**
* @guarded.By suspendLock
*/
private static boolean expirationSuspended = false;
private static final Object suspendLock = new Object();
/**
* Test method that causes expiration to be suspended until permitExpiration is called.
*
* @since GemFire 5.0
*/
public final static void suspendExpiration() {
synchronized (suspendLock) {
expirationSuspended = true;
}
}
public final static void permitExpiration() {
synchronized (suspendLock) {
expirationSuspended = false;
suspendLock.notifyAll();
}
}
/**
* Wait until permission is given for expiration to be done. Tests are allowed to suspend
* expiration.
*
* @since GemFire 5.0
*/
private final void waitOnExpirationSuspension() {
for (;;) {
getLocalRegion().getCancelCriterion().checkCancelInProgress(null);
synchronized (suspendLock) {
boolean interrupted = Thread.interrupted();
try {
while (expirationSuspended) {
suspendLock.wait();
}
break;
} catch (InterruptedException ex) {
interrupted = true;
getLocalRegion().getCancelCriterion().checkCancelInProgress(null);
// keep going, we can't cancel
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
} // synchronized
} // for
}
protected final boolean expire(boolean isPending) throws CacheException {
ExpirationAction action = getAction();
if (action == null)
return false;
boolean result = expire(action, isPending);
if (result && expiryTaskListener != null) {
expiryTaskListener.afterExpire(this);
}
return result;
}
/**
* Why did this expire?
*
* @return the action to perform or null if NONE
*/
protected ExpirationAction getAction() {
long ttl = getTTLExpirationTime();
long idle = getIdleExpirationTime();
if (ttl == 0) {
if (idle == 0)
return null;
return getIdleAttributes().getAction();
}
if (idle == 0) {
// we know ttl != 0
return getTTLAttributes().getAction();
}
// Neither is 0
if (idle < ttl) {
return getIdleAttributes().getAction();
}
return getTTLAttributes().getAction();
}
/** Returns true if the ExpirationAction is a distributed action. */
protected boolean isDistributedAction() {
ExpirationAction action = getAction();
return action != null && (action.isInvalidate() || action.isDestroy());
}
final LocalRegion getLocalRegion() {
return this.region;
}
protected final boolean expire(ExpirationAction action, boolean isPending) throws CacheException {
if (action.isInvalidate())
return invalidate();
if (action.isDestroy())
return destroy(isPending);
if (action.isLocalInvalidate())
return localInvalidate();
if (action.isLocalDestroy())
return localDestroy();
throw new InternalGemFireError(
LocalizedStrings.ExpiryTask_UNRECOGNIZED_EXPIRATION_ACTION_0.toLocalizedString(action));
}
/**
* Cancel this task
*/
@Override
public boolean cancel() {
boolean superCancel = super.cancel();
LocalRegion lr = getLocalRegion();
if (lr != null) {
if (superCancel) {
this.region = null; // this is the only place it is nulled
}
}
return superCancel;
}
/**
* An ExpiryTask is sent run() to perform its task. Note that this run() method should never throw
* an exception - otherwise, it takes out the java.util.Timer thread, causing an exception
* whenever we try to schedule more expiration tasks.
*/
@Override
public final void run2() {
try {
if (executor != null) {
executor.execute(new Runnable() {
public void run() {
runInThreadPool();
}
});
} else {
// inline
runInThreadPool();
}
} catch (RejectedExecutionException ex) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Rejected execution in expiration task", ex);
}
} catch (VirtualMachineError err) {
SystemFailure.initiateFailure(err);
// If this ever returns, rethrow the error. We're poisoned
// now, so don't let this thread continue.
throw err;
} catch (Throwable t) {
// Whenever you catch Error or Throwable, you must also
// catch VirtualMachineError (see above). However, there is
// _still_ a possibility that you are dealing with a cascading
// error condition, so you also need to check to see if the JVM
// is still usable:
SystemFailure.checkFailure();
// for surviving and debugging exceptions getting the logger
t.printStackTrace();
}
} catch (CancelException e) {
return; // just bail
} catch (VirtualMachineError err) {
SystemFailure.initiateFailure(err);
// If this ever returns, rethrow the error. We're poisoned
// now, so don't let this thread continue.
throw err;
} catch (Throwable ex) {
// Whenever you catch Error or Throwable, you must also
// catch VirtualMachineError (see above). However, there is
// _still_ a possibility that you are dealing with a cascading
// error condition, so you also need to check to see if the JVM
// is still usable:
SystemFailure.checkFailure();
logger.fatal(
LocalizedMessage.create(LocalizedStrings.ExpiryTask_EXCEPTION_IN_EXPIRATION_TASK), ex);
}
}
protected void runInThreadPool() {
try {
if (isCacheClosing() || getLocalRegion().isClosed() || getLocalRegion().isDestroyed()) {
return;
}
waitOnExpirationSuspension();
if (logger.isTraceEnabled()) {
logger.trace("{} is fired", this);
}
// do our work...
performTimeout();
} catch (RegionDestroyedException re) {
// Ignore - our job is done
} catch (EntryNotFoundException ex) {
// Ignore
} catch (CancelException ex) {
// ignore
} catch (VirtualMachineError err) {
SystemFailure.initiateFailure(err);
// If this ever returns, rethrow the error. We're poisoned
// now, so don't let this thread continue.
throw err;
} catch (Throwable ex) {
// Whenever you catch Error or Throwable, you must also
// catch VirtualMachineError (see above). However, there is
// _still_ a possibility that you are dealing with a cascading
// error condition, so you also need to check to see if the JVM
// is still usable:
SystemFailure.checkFailure();
logger.fatal(
LocalizedMessage.create(LocalizedStrings.ExpiryTask_EXCEPTION_IN_EXPIRATION_TASK), ex);
} finally {
if (expiryTaskListener != null) {
expiryTaskListener.afterTaskRan(this);
}
}
}
protected boolean isCacheClosing() {
return ((GemFireCacheImpl) getLocalRegion().getCache()).isClosed();
}
/**
* Reschedule (or not) this task for later consideration
*/
abstract protected void reschedule() throws CacheException;
@Override
public String toString() {
String expTtl = "<unavailable>";
String expIdle = "<unavailable>";
try {
if (getTTLAttributes() != null) {
expTtl = String.valueOf(getTTLExpirationTime());
}
if (getIdleAttributes() != null) {
expIdle = String.valueOf(getIdleExpirationTime());
}
} catch (VirtualMachineError err) {
SystemFailure.initiateFailure(err);
// If this ever returns, rethrow the error. We're poisoned
// now, so don't let this thread continue.
throw err;
} catch (Throwable t) {
// Whenever you catch Error or Throwable, you must also
// catch VirtualMachineError (see above). However, there is
// _still_ a possibility that you are dealing with a cascading
// error condition, so you also need to check to see if the JVM
// is still usable:
SystemFailure.checkFailure();
}
return super.toString() + " for " + getLocalRegion() + ", ttl expiration time: " + expTtl
+ ", idle expiration time: " + expIdle + ("[now:" + calculateNow() + "]");
}
////// Abstract methods ///////
protected abstract long getLastModifiedTime() throws EntryNotFoundException;
protected abstract long getLastAccessedTime() throws EntryNotFoundException;
protected abstract boolean invalidate() throws CacheException;
protected abstract boolean destroy(boolean isPending) throws CacheException;
protected abstract boolean localInvalidate() throws EntryNotFoundException;
protected abstract boolean localDestroy() throws CacheException;
protected abstract void addExpiryTask() throws EntryNotFoundException;
public abstract boolean isPending();
public abstract Object getKey();
private static final ThreadLocal<Long> now = new ThreadLocal<Long>();
/**
* To reduce the number of times we need to call System.currentTimeMillis you can call this method
* to set a thread local. Make sure and call {@link #clearNow()} in a finally block after calling
* this method.
*/
public static void setNow() {
now.set(calculateNow());
}
private static long calculateNow() {
GemFireCacheImpl cache = GemFireCacheImpl.getInstance();
if (cache != null) {
// Use cache.cacheTimeMillis here. See bug 52267.
InternalDistributedSystem ids = cache.getDistributedSystem();
if (ids != null) {
return ids.getClock().cacheTimeMillis();
}
}
return 0L;
}
/**
* Call this method after a thread has called {@link #setNow()} once you are done calling code
* that may call {@link #getNow()}.
*/
public static void clearNow() {
now.remove();
}
/**
* Returns the current time in milliseconds. If the current thread has called {@link #setNow()}
* then that time is return.
*
* @return the current time in milliseconds
*/
public static long getNow() {
long result;
Long tl = now.get();
if (tl != null) {
result = tl.longValue();
} else {
result = calculateNow();
}
return result;
}
// Should only be set by unit tests
public static ExpiryTaskListener expiryTaskListener;
/**
* Used by tests to determine if events related to an ExpiryTask have happened.
*/
public interface ExpiryTaskListener {
/**
* Called after entry is schedule for expiration.
*/
public void afterSchedule(ExpiryTask et);
/**
* Called after the given expiry task has run. This means that the time it was originally
* scheduled to run has elapsed and the scheduler has run the task. While running the task it
* may decide to expire it or reschedule it.
*/
public void afterTaskRan(ExpiryTask et);
/**
* Called after the given expiry task has been rescheduled. afterTaskRan can still be called on
* the same task. In some cases a task is rescheduled without expiring it. In others it is
* expired and rescheduled.
*/
public void afterReschedule(ExpiryTask et);
/**
* Called after the given expiry task has expired.
*/
public void afterExpire(ExpiryTask et);
/**
* Called when task has been canceled
*/
public void afterCancel(ExpiryTask et);
}
}