/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.controller;
import org.candlepin.audit.QpidConnection;
import org.candlepin.audit.QpidConnection.STATUS;
import org.candlepin.audit.QpidQmf;
import org.candlepin.audit.QpidQmf.QpidStatus;
import org.candlepin.cache.CandlepinCache;
import org.candlepin.common.config.Configuration;
import org.candlepin.config.ConfigProperties;
import org.candlepin.model.CandlepinModeChange;
import org.candlepin.model.CandlepinModeChange.Mode;
import org.candlepin.model.CandlepinModeChange.Reason;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Logic to transition Candlepin between different modes (SUSPEND, NORMAL) based
* on what is the current status of Qpid Broker.
*
* This logic can also be run periodically using startPeriodicExecutions
*
* Using this class, clients can attempt to transition to appropriate mode. The
* attempt may be no-op if no transition is required.
* @author fnguyen
*
*/
public class SuspendModeTransitioner implements Runnable {
private static Logger log = LoggerFactory.getLogger(SuspendModeTransitioner.class);
/**
* Factor by which the delay is going to be increased with each failed attempt to
* reconnect to the Qpid Broker. For example if set to 3 and initialDelay set to
* 5. The following will be the delay interval between the executions of this
* transitioner:
*
* When failedAttempts = 0, then delay will be 5
* When failedAttempts = 1, then the delay will be 10
* When failedAttempts = 2, then the delay will be 15
* etc.
*
*/
private int delayGrowth;
/**
* Delay in seconds. SuspendModeTransitioner will wait this amount
* of seconds between periodic checks for Qpid connectivity.
*/
private int initialDelay;
/**
* Maximum delay that this transitioner can wait between executions
*/
private int maxDelay = 0;
private BigInteger failedAttempts = BigInteger.ZERO;
/**
* Single threaded periodic task.
*/
private ScheduledExecutorService execService;
private ModeManager modeManager;
private QpidQmf qmf;
private QpidConnection qpidConnection;
private CandlepinCache candlepinCache;
@Inject
public SuspendModeTransitioner(Configuration config, ScheduledExecutorService execService,
CandlepinCache cache) {
this.execService = execService;
delayGrowth = config.getInt(ConfigProperties.QPID_MODE_TANSITIONER_DELAY_GROWTH);
initialDelay = config.getInt(ConfigProperties.QPID_MODE_TRANSITIONER_INITIAL_DELAY);
maxDelay = config.getInt(ConfigProperties.QPID_MODE_TRANSITIONER_MAX_DELAY);
this.candlepinCache = cache;
}
/**
* Other dependencies are injected using method injection so
* that Guice can handle circular dependency between SuspendModeTransitioner
* and the QpidConnection
*/
@Inject
public void setModeManager(ModeManager modeManager) {
this.modeManager = modeManager;
}
@Inject
public void setQmf(QpidQmf qmf) {
this.qmf = qmf;
}
@Inject
public void setQpidConnection(QpidConnection qpidConnection) {
this.qpidConnection = qpidConnection;
}
/**
* Enables to run the transitioning logic periodically.
*/
public void startPeriodicExecutions() {
log.info("Starting Periodic Suspend Mode Transitioner " +
"with delay grow factor of {}s and initial delay {}s ",
delayGrowth, initialDelay);
schedule();
}
/**
* Schedules next execution of the Suspend Mode check. The delay, in seconds,
* grows as the failedAttempts grow. The formula is:
*
* delay = initialDelay + (delayGrowth * failedAttempts)
*
* Maximal delay is configurable, -1 means there is no bound
*
*/
private void schedule() {
BigInteger delay =
BigInteger.valueOf(initialDelay)
.add(BigInteger.valueOf(delayGrowth)
.multiply(failedAttempts));
if (maxDelay != -1 &&
delay.compareTo(BigInteger.valueOf(maxDelay)) > 0) {
log.debug("Maximum delay {} reached", maxDelay);
delay = BigInteger.valueOf(maxDelay);
}
log.debug("Next Transitioner check will run after {} seconds", delay);
if (failedAttempts.compareTo(BigInteger.ZERO) > 0) {
log.info("SuspendModeTransitioner failed to reconnect to the Qpid Broker " +
"{} times, backing off with next reconnect {} seconds", failedAttempts,
delay);
}
execService.schedule(this, delay.longValue(), TimeUnit.SECONDS);
}
@Override
public void run() {
log.debug("Executing periodic transition attempt");
try {
transitionAppropriately();
}
finally {
/**
* The Transitioner is periodic task. But we want to have flexibility of setting
* different delays between executions. As the amount of failed atttempts to connect
* rise, we want to also increase the delay. Scheduling in finally seems to be the
* easiest way to achieve that.
*/
schedule();
}
}
/**
* Attempts to transition Candlepin according to current Mode and current status of
* the Qpid Broker. Logs and swallows possible exceptions - theoretically
* there should be none.
*
* Most of the time the transition won't be required and this method will be no-op.
* There is an edge-case when transitioning from SUSPEND to NORMAL mode.
* During that transition, there is a small time window between checking the
* Qpid status and attempt to reconnect. If the Qpid status is reported as
* Qpid up, the transitioner will try to reconnect to the broker. This reconnect
* may fail. In that case the transition to NORMAL mode shouldn't go through
*/
public synchronized void transitionAppropriately() {
log.debug("Attempting to transition to appropriate Mode");
try {
QpidStatus status = qmf.getStatus();
CandlepinModeChange modeChange = modeManager.getLastCandlepinModeChange();
log.debug("Qpid status is {}, the current mode is {}", status, modeChange);
if (status != QpidStatus.CONNECTED) {
qpidConnection.setConnectionStatus(STATUS.JMS_OBJECTS_STALE);
}
if (modeChange.getMode() == Mode.SUSPEND) {
switch (status) {
case CONNECTED:
log.info("Connection to qpid is restored! Reconnecting Qpid and" +
" Entering NORMAL mode");
failedAttempts = BigInteger.ZERO;
modeManager.enterMode(Mode.NORMAL, Reason.QPID_UP);
cleanStatusCache();
break;
case FLOW_STOPPED:
case DOWN:
failedAttempts = failedAttempts.add(BigInteger.ONE);
log.debug("Staying in {} mode. So far {} failed attempts",
status,
failedAttempts);
break;
default:
throw new RuntimeException("Unknown status: " + status);
}
}
else if (modeChange.getMode() == Mode.NORMAL) {
switch (status) {
case FLOW_STOPPED:
log.debug("Will need to transition Candlepin into SUSPEND Mode because " +
"the Qpid connection is flow stopped");
modeManager.enterMode(Mode.SUSPEND, Reason.QPID_FLOW_STOPPED);
cleanStatusCache();
break;
case DOWN:
log.debug("Will need to transition Candlepin into SUSPEND Mode because " +
"the Qpid connection is down");
modeManager.enterMode(Mode.SUSPEND, Reason.QPID_DOWN);
cleanStatusCache();
break;
case CONNECTED:
log.debug("Connection to Qpid is ok and current mode is NORMAL. No-op!");
break;
default:
throw new RuntimeException("Unknown status: " + status);
}
}
}
catch (Throwable t) {
log.error("Error while executing period Suspend Transitioner check", t);
/**
* Nothing more we can do here, since this is scheduled thread. We must
* hope that this error won't infinitely recur with each scheduled execution
*/
}
}
/**
* Cleans Status Cache. We need to do this so that client's don't see
* cached status response in case of a mode change.
*/
private void cleanStatusCache() {
candlepinCache.getStatusCache().clear();
}
}