package io.cattle.platform.servicediscovery.upgrade.impl;
import static io.cattle.platform.core.model.tables.ServiceExposeMapTable.*;
import io.cattle.platform.activity.ActivityLog;
import io.cattle.platform.activity.ActivityService;
import io.cattle.platform.core.addon.InServiceUpgradeStrategy;
import io.cattle.platform.core.addon.RollingRestartStrategy;
import io.cattle.platform.core.addon.ServiceRestart;
import io.cattle.platform.core.addon.ServiceUpgradeStrategy;
import io.cattle.platform.core.addon.ToServiceUpgradeStrategy;
import io.cattle.platform.core.constants.CommonStatesConstants;
import io.cattle.platform.core.constants.HealthcheckConstants;
import io.cattle.platform.core.constants.InstanceConstants;
import io.cattle.platform.core.constants.ServiceConstants;
import io.cattle.platform.core.model.Instance;
import io.cattle.platform.core.model.Service;
import io.cattle.platform.core.model.ServiceExposeMap;
import io.cattle.platform.engine.process.ExitReason;
import io.cattle.platform.engine.process.impl.ProcessCancelException;
import io.cattle.platform.engine.process.impl.ProcessExecutionExitException;
import io.cattle.platform.json.JsonMapper;
import io.cattle.platform.lock.LockCallbackNoReturn;
import io.cattle.platform.lock.LockManager;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.object.process.ObjectProcessManager;
import io.cattle.platform.object.resource.ResourceMonitor;
import io.cattle.platform.object.resource.ResourcePredicate;
import io.cattle.platform.object.util.DataAccessor;
import io.cattle.platform.process.common.util.ProcessUtils;
import io.cattle.platform.servicediscovery.api.dao.ServiceExposeMapDao;
import io.cattle.platform.servicediscovery.deployment.DeploymentManager;
import io.cattle.platform.servicediscovery.deployment.impl.lock.ServiceLock;
import io.cattle.platform.servicediscovery.service.ServiceDiscoveryService;
import io.cattle.platform.servicediscovery.upgrade.UpgradeManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
public class UpgradeManagerImpl implements UpgradeManager {
private enum Type {
ToUpgrade,
ToCleanup,
UpgradedUnmanaged,
}
@Inject
ServiceExposeMapDao exposeMapDao;
@Inject
ObjectManager objectManager;
@Inject
DeploymentManager deploymentMgr;
@Inject
LockManager lockManager;
@Inject
ObjectProcessManager objectProcessMgr;
@Inject
ResourceMonitor resourceMntr;
@Inject
ServiceDiscoveryService serviceDiscoveryService;
@Inject
JsonMapper jsonMapper;
@Inject
ActivityService activityService;
private static final long SLEEP = 1000L;
protected void setUpgrade(ServiceExposeMap map, boolean upgrade) {
if (upgrade) {
map.setUpgrade(true);
map.setManaged(false);
} else {
map.setUpgrade(false);
map.setManaged(true);
}
objectManager.persist(map);
}
public boolean doInServiceUpgrade(Service service, InServiceUpgradeStrategy strategy, boolean isUpgrade, String currentProcess) {
long batchSize = strategy.getBatchSize();
boolean startFirst = strategy.getStartFirst();
Map<String, List<Instance>> deploymentUnitInstancesToUpgrade = formDeploymentUnitsForUpgrade(service,
Type.ToUpgrade, isUpgrade, strategy);
Map<String, List<Instance>> deploymentUnitInstancesUpgradedUnmanaged = formDeploymentUnitsForUpgrade(
service,
Type.UpgradedUnmanaged, isUpgrade, strategy);
Map<String, List<Instance>> deploymentUnitInstancesToCleanup = formDeploymentUnitsForUpgrade(service,
Type.ToCleanup, isUpgrade, strategy);
// upgrade deployment units
upgradeDeploymentUnits(service, deploymentUnitInstancesToUpgrade, deploymentUnitInstancesUpgradedUnmanaged,
deploymentUnitInstancesToCleanup,
batchSize, startFirst, preseveDeploymentUnit(service, strategy), isUpgrade, currentProcess, strategy);
// check if empty
if (deploymentUnitInstancesToUpgrade.isEmpty()) {
deploymentMgr.activate(service);
return true;
}
return false;
}
protected boolean preseveDeploymentUnit(Service service, InServiceUpgradeStrategy strategy) {
boolean isServiceIndexDUStrategy = StringUtils.equalsIgnoreCase(
ServiceConstants.SERVICE_INDEX_DU_STRATEGY,
DataAccessor.fieldString(service, ServiceConstants.FIELD_SERVICE_INDEX_STRATEGY));
return isServiceIndexDUStrategy || !strategy.isFullUpgrade();
}
protected void upgradeDeploymentUnits(final Service service,
final Map<String, List<Instance>> deploymentUnitInstancesToUpgrade,
final Map<String, List<Instance>> deploymentUnitInstancesUpgradedUnmanaged,
final Map<String, List<Instance>> deploymentUnitInstancesToCleanup,
final long batchSize,
final boolean startFirst, final boolean preseveDeploymentUnit, final boolean isUpgrade,
final String currentProcess, final InServiceUpgradeStrategy strategy) {
// hold the lock so service.reconcile triggered by config.update
// (in turn triggered by instance.remove) won't interfere
lockManager.lock(new ServiceLock(service), new LockCallbackNoReturn() {
@Override
public void doWithLockNoResult() {
// wait for healthy only for upgrade
// should be skipped for rollback
if (isUpgrade) {
deploymentMgr.activate(service);
waitForHealthyState(service, currentProcess, strategy);
}
// mark for upgrade
markForUpgrade(batchSize);
if (startFirst) {
// 1. reconcile to start new instances
activate(service);
if (isUpgrade) {
waitForHealthyState(service, currentProcess, strategy);
}
// 2. stop instances
stopInstances(service, deploymentUnitInstancesToCleanup);
} else {
// reverse order
// 1. stop instances
stopInstances(service, deploymentUnitInstancesToCleanup);
// 2. wait for reconcile (new instances will be started along)
activate(service);
}
}
protected void markForUpgrade(final long batchSize) {
markForCleanup(batchSize, preseveDeploymentUnit);
}
protected void markForCleanup(final long batchSize,
boolean preseveDeploymentUnit) {
long i = 0;
Iterator<Map.Entry<String, List<Instance>>> it = deploymentUnitInstancesToUpgrade.entrySet()
.iterator();
while (it.hasNext() && i < batchSize) {
Map.Entry<String, List<Instance>> instances = it.next();
String deploymentUnitUUID = instances.getKey();
markForRollback(deploymentUnitUUID);
for (Instance instance : instances.getValue()) {
activityService.instance(instance, "mark.upgrade", "Mark for upgrade", ActivityLog.INFO);
ServiceExposeMap map = objectManager.findAny(ServiceExposeMap.class,
SERVICE_EXPOSE_MAP.INSTANCE_ID, instance.getId());
setUpgrade(map, true);
}
deploymentUnitInstancesToCleanup.put(deploymentUnitUUID, instances.getValue());
it.remove();
i++;
}
}
protected void markForRollback(String deploymentUnitUUIDToRollback) {
List<Instance> instances = new ArrayList<>();
if (preseveDeploymentUnit) {
instances = deploymentUnitInstancesUpgradedUnmanaged.get(deploymentUnitUUIDToRollback);
} else {
// when preserveDeploymentunit == false, we don't care what deployment unit needs to be rolled back
String toExtract = null;
for (String key : deploymentUnitInstancesUpgradedUnmanaged.keySet()) {
if (toExtract != null) {
break;
}
toExtract = key;
}
instances = deploymentUnitInstancesUpgradedUnmanaged.get(toExtract);
deploymentUnitInstancesUpgradedUnmanaged.remove(toExtract);
}
if (instances != null) {
for (Instance instance : instances) {
ServiceExposeMap map = objectManager.findAny(ServiceExposeMap.class,
SERVICE_EXPOSE_MAP.INSTANCE_ID, instance.getId());
setUpgrade(map, false);
}
}
}
});
}
protected Map<String, List<Instance>> formDeploymentUnitsForUpgrade(Service service, Type type, boolean isUpgrade,
InServiceUpgradeStrategy strategy) {
Map<String, Pair<String, Map<String, Object>>> preUpgradeLaunchConfigNamesToVersion = new HashMap<>();
Map<String, Pair<String, Map<String, Object>>> postUpgradeLaunchConfigNamesToVersion = new HashMap<>();
// getting an original config set (to cover the scenario when config could be removed along with the upgrade)
if (isUpgrade) {
postUpgradeLaunchConfigNamesToVersion.putAll(strategy.getNameToVersionToConfig(service.getName(), false));
preUpgradeLaunchConfigNamesToVersion.putAll(strategy.getNameToVersionToConfig(service.getName(), true));
} else {
postUpgradeLaunchConfigNamesToVersion.putAll(strategy.getNameToVersionToConfig(service.getName(), true));
preUpgradeLaunchConfigNamesToVersion.putAll(strategy.getNameToVersionToConfig(service.getName(), false));
}
Map<String, List<Instance>> deploymentUnitInstances = new HashMap<>();
// iterate over pre-upgraded state
// get desired version from post upgrade state
if (type == Type.UpgradedUnmanaged) {
for (String launchConfigName : postUpgradeLaunchConfigNamesToVersion.keySet()) {
List<Instance> instances = new ArrayList<>();
Pair<String, Map<String, Object>> post = postUpgradeLaunchConfigNamesToVersion.get(launchConfigName);
String toVersion = post.getLeft();
instances.addAll(exposeMapDao.getUpgradedInstances(service,
launchConfigName, toVersion, false));
for (Instance instance : instances) {
addInstanceToDeploymentUnits(deploymentUnitInstances, instance);
}
}
} else {
for (String launchConfigName : preUpgradeLaunchConfigNamesToVersion.keySet()) {
String toVersion = "undefined";
Pair<String, Map<String, Object>> post = postUpgradeLaunchConfigNamesToVersion.get(launchConfigName);
if (post != null) {
toVersion = post.getLeft();
}
List<Instance> instances = new ArrayList<>();
if (type == Type.ToUpgrade) {
instances.addAll(exposeMapDao.getInstancesToUpgrade(service, launchConfigName, toVersion));
} else if (type == Type.ToCleanup) {
instances.addAll(exposeMapDao.getInstancesToCleanup(service, launchConfigName, toVersion));
}
for (Instance instance : instances) {
addInstanceToDeploymentUnits(deploymentUnitInstances, instance);
}
}
}
return deploymentUnitInstances;
}
protected Map<String, List<Instance>> formDeploymentUnitsForRestart(Service service) {
Map<String, List<Instance>> deploymentUnitInstances = new HashMap<>();
List<? extends Instance> instances = getServiceInstancesToRestart(service);
for (Instance instance : instances) {
addInstanceToDeploymentUnits(deploymentUnitInstances, instance);
}
return deploymentUnitInstances;
}
protected List<? extends Instance> getServiceInstancesToRestart(Service service) {
// get all instances of the service
List<? extends Instance> instances = exposeMapDao.listServiceManagedInstances(service);
List<Instance> toRestart = new ArrayList<>();
ServiceRestart svcRestart = DataAccessor.field(service, ServiceConstants.FIELD_RESTART,
jsonMapper, ServiceRestart.class);
RollingRestartStrategy strategy = svcRestart.getRollingRestartStrategy();
Map<Long, Long> instanceToStartCount = strategy.getInstanceToStartCount();
// compare its start_count with one set on the service restart field
for (Instance instance : instances) {
if (instanceToStartCount.containsKey(instance.getId())) {
Long previousStartCount = instanceToStartCount.get(instance.getId());
if (previousStartCount == instance.getStartCount()) {
toRestart.add(instance);
}
}
}
return toRestart;
}
protected void addInstanceToDeploymentUnits(Map<String, List<Instance>> deploymentUnitInstancesToUpgrade,
Instance instance) {
List<Instance> toRemove = deploymentUnitInstancesToUpgrade.get(instance.getDeploymentUnitUuid());
if (toRemove == null) {
toRemove = new ArrayList<Instance>();
}
toRemove.add(instance);
deploymentUnitInstancesToUpgrade.put(instance.getDeploymentUnitUuid(), toRemove);
}
@Override
public void upgrade(Service service, io.cattle.platform.core.addon.ServiceUpgradeStrategy strategy, String currentProcess) {
if (strategy instanceof ToServiceUpgradeStrategy) {
ToServiceUpgradeStrategy toServiceStrategy = (ToServiceUpgradeStrategy) strategy;
Service toService = objectManager.loadResource(Service.class, toServiceStrategy.getToServiceId());
if (toService == null || toService.getRemoved() != null) {
return;
}
updateLinks(service, toServiceStrategy);
}
while (!doUpgrade(service, strategy, currentProcess)) {
sleep(service, strategy, currentProcess);
}
}
@Override
public void rollback(Service service, ServiceUpgradeStrategy strategy) {
if (strategy instanceof ToServiceUpgradeStrategy) {
return;
}
while (!doInServiceUpgrade(service, (InServiceUpgradeStrategy) strategy, false,
ServiceConstants.STATE_ROLLINGBACK)) {
sleep(service, strategy, ServiceConstants.STATE_ROLLINGBACK);
}
}
public boolean doUpgrade(Service service, io.cattle.platform.core.addon.ServiceUpgradeStrategy strategy,
String currentProcess) {
if (strategy instanceof InServiceUpgradeStrategy) {
InServiceUpgradeStrategy inService = (InServiceUpgradeStrategy) strategy;
return doInServiceUpgrade(service, inService, true, currentProcess);
} else {
ToServiceUpgradeStrategy toService = (ToServiceUpgradeStrategy) strategy;
return doToServiceUpgrade(service, toService, currentProcess);
}
}
protected void updateLinks(Service service, ToServiceUpgradeStrategy strategy) {
if (!strategy.isUpdateLinks()) {
return;
}
serviceDiscoveryService.cloneConsumingServices(service, objectManager.loadResource(Service.class,
strategy.getToServiceId()));
}
protected void sleep(final Service service, ServiceUpgradeStrategy strategy, final String currentProcess) {
final long interval = strategy.getIntervalMillis();
activityService.run(service, "sleep", String.format("Sleeping for %d seconds", interval/1000), new Runnable() {
@Override
public void run() {
for (int i = 0;; i++) {
final long sleepTime = Math.max(0, Math.min(SLEEP, interval - i * SLEEP));
if (sleepTime == 0) {
break;
} else {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
stateCheck(service, currentProcess);
}
}
});
}
protected Service stateCheck(Service service, String currentProcess) {
service = objectManager.reload(service);
List<String> states = Arrays.asList(ServiceConstants.STATE_UPGRADING,
ServiceConstants.STATE_ROLLINGBACK, ServiceConstants.STATE_RESTARTING,
ServiceConstants.STATE_FINISHING_UPGRADE);
if (!states.contains(service.getState())) {
throw new ProcessExecutionExitException(ExitReason.STATE_CHANGED);
}
if (StringUtils.equals(currentProcess, ServiceConstants.STATE_RESTARTING)) {
return service;
}
// rollback should cancel upgarde, and vice versa
if (!StringUtils.equals(currentProcess, service.getState())) {
throw new ProcessExecutionExitException(ExitReason.STATE_CHANGED);
}
return service;
}
/**
* @param fromService
* @param strategy
* @return true if the upgrade is done
*/
protected boolean doToServiceUpgrade(Service fromService, ToServiceUpgradeStrategy strategy, String currentProcess) {
Service toService = objectManager.loadResource(Service.class, strategy.getToServiceId());
if (toService == null || toService.getRemoved() != null) {
return true;
}
deploymentMgr.activate(toService);
if (!deploymentMgr.isHealthy(toService)) {
return false;
}
deploymentMgr.activate(fromService);
fromService = objectManager.reload(fromService);
toService = objectManager.reload(toService);
long batchSize = strategy.getBatchSize();
long finalScale = strategy.getFinalScale();
long toScale = getScale(toService);
long totalScale = getScale(fromService) + toScale;
if (totalScale > finalScale) {
fromService = changeScale(fromService, 0 - Math.min(batchSize, totalScale - finalScale));
} else if (toScale < finalScale) {
long max = Math.min(batchSize, finalScale - toScale);
toService = changeScale(toService, Math.min(max, finalScale + batchSize - totalScale));
}
if (getScale(fromService) == 0 && getScale(toService) != finalScale) {
changeScale(toService, finalScale - getScale(toService));
}
return getScale(fromService) == 0 && getScale(toService) == finalScale;
}
protected Service changeScale(Service service, long delta) {
if (delta == 0) {
return service;
}
long newScale = Math.max(0, getScale(service) + delta);
service = objectManager.setFields(service, ServiceConstants.FIELD_SCALE, newScale);
deploymentMgr.activate(service);
return objectManager.reload(service);
}
protected int getScale(Service service) {
Integer i = DataAccessor.fieldInteger(service, ServiceConstants.FIELD_SCALE);
return i == null ? 0 : i;
}
@Override
public void finishUpgrade(Service service, boolean reconcile) {
// cleanup instances set for upgrade
cleanupUpgradedInstances(service);
// reconcile
if (reconcile) {
deploymentMgr.activate(service);
}
}
protected void waitForHealthyState(final Service service, final String currentProcess,
final InServiceUpgradeStrategy strategy) {
activityService.run(service, "wait", "Waiting for all instances to be healthy", new Runnable() {
@Override
public void run() {
final List<String> healthyStates = Arrays.asList(HealthcheckConstants.HEALTH_STATE_HEALTHY,
HealthcheckConstants.HEALTH_STATE_UPDATING_HEALTHY);
List<? extends Instance> instancesToCheck = getInstancesToCheckForHealth(service, strategy);
for (final Instance instance : instancesToCheck) {
if (instance.getState().equalsIgnoreCase(InstanceConstants.STATE_RUNNING)) {
resourceMntr.waitFor(instance,
new ResourcePredicate<Instance>() {
@Override
public boolean evaluate(Instance obj) {
boolean healthy = instance.getHealthState() == null
|| healthyStates.contains(obj.getHealthState());
if (!healthy) {
stateCheck(service, currentProcess);
}
return healthy;
}
@Override
public String getMessage() {
return "healthy";
}
});
}
}
}
});
}
private List<? extends Instance> getInstancesToCheckForHealth(Service service,
InServiceUpgradeStrategy strategy) {
if (strategy == null) {
return exposeMapDao.listServiceManagedInstances(service);
}
Map<String, String> lcToCurrentV = getLaunchConfigToCurrentVersion(service, strategy);
List<Instance> filtered = new ArrayList<>();
// only check upgraded instances for health
for (String lc : lcToCurrentV.keySet()) {
List<? extends Instance> instances = exposeMapDao.listServiceManagedInstances(service, lc);
for (Instance instance : instances) {
if (instance.getVersion() != null &&
instance.getVersion().equalsIgnoreCase(lcToCurrentV.get(lc))) {
filtered.add(instance);
}
}
}
return filtered;
}
private Map<String, String> getLaunchConfigToCurrentVersion(Service service, InServiceUpgradeStrategy strategy) {
Map<String, String> lcToV = new HashMap<>();
Map<String, Pair<String, Map<String, Object>>> vToC = strategy.getNameToVersionToConfig(service.getName(),
false);
for (String lc : vToC.keySet()) {
lcToV.put(lc, vToC.get(lc).getLeft());
}
return lcToV;
}
public void cleanupUpgradedInstances(Service service) {
List<? extends ServiceExposeMap> maps = exposeMapDao.getInstancesSetForUpgrade(service.getId());
List<Instance> waitList = new ArrayList<>();
for (ServiceExposeMap map : maps) {
Instance instance = objectManager.loadResource(Instance.class, map.getInstanceId());
if (instance == null || instance.getRemoved() != null || instance.getState().equals(
CommonStatesConstants.REMOVING)) {
continue;
}
try {
objectProcessMgr.scheduleProcessInstanceAsync(InstanceConstants.PROCESS_REMOVE,
instance, null);
} catch (ProcessCancelException ex) {
// in case instance was manually restarted
objectProcessMgr.scheduleProcessInstanceAsync(InstanceConstants.PROCESS_STOP,
instance, ProcessUtils.chainInData(new HashMap<String, Object>(),
InstanceConstants.PROCESS_STOP, InstanceConstants.PROCESS_REMOVE));
}
}
for (Instance instance : waitList) {
resourceMntr.waitForState(instance, CommonStatesConstants.REMOVED);
}
}
@Override
public void restart(Service service, RollingRestartStrategy strategy) {
Map<String, List<Instance>> toRestart = formDeploymentUnitsForRestart(service);
while (!doRestart(service, strategy, toRestart)) {
sleep(service, strategy, ServiceConstants.STATE_RESTARTING);
}
}
public boolean doRestart(Service service, RollingRestartStrategy strategy,
Map<String, List<Instance>> toRestart) {
long batchSize = strategy.getBatchSize();
final Map<String, List<Instance>> restartBatch = new HashMap<>();
long i = 0;
Iterator<Map.Entry<String, List<Instance>>> it = toRestart.entrySet()
.iterator();
while (it.hasNext() && i < batchSize) {
Map.Entry<String, List<Instance>> instances = it.next();
String deploymentUnitUUID = instances.getKey();
restartBatch.put(deploymentUnitUUID, instances.getValue());
it.remove();
i++;
}
restartDeploymentUnits(service, restartBatch);
if (toRestart.isEmpty()) {
return true;
}
return false;
}
protected void restartDeploymentUnits(final Service service,
final Map<String, List<Instance>> deploymentUnitsToStop) {
// hold the lock so service.reconcile triggered by config.update
// (in turn triggered by instance.remove) won't interfere
lockManager.lock(new ServiceLock(service), new LockCallbackNoReturn() {
@Override
public void doWithLockNoResult() {
// 1. Wait for the service instances to become healthy
waitForHealthyState(service, ServiceConstants.STATE_RESTARTING, null);
// 2. stop instances
stopInstances(service, deploymentUnitsToStop);
// 3. wait for reconcile (instances will be restarted along)
activate(service);
}
});
}
protected void activate(final Service service) {
activityService.run(service, "starting", "Starting new instances", new Runnable() {
@Override
public void run() {
deploymentMgr.activate(service);
}
});
}
protected void stopInstances(Service service, final Map<String, List<Instance>> deploymentUnitInstancesToStop) {
activityService.run(service, "stopping", "Stopping instances", new Runnable() {
@Override
public void run() {
List<Instance> toStop = new ArrayList<>();
List<Instance> toWait = new ArrayList<>();
for (String key : deploymentUnitInstancesToStop.keySet()) {
toStop.addAll(deploymentUnitInstancesToStop.get(key));
}
for (Instance instance : toStop) {
instance = resourceMntr.waitForNotTransitioning(instance);
if (InstanceConstants.STATE_ERROR.equals(instance.getState())) {
objectProcessMgr.scheduleProcessInstanceAsync(InstanceConstants.PROCESS_REMOVE,
instance, null);
} else if (!instance.getState().equalsIgnoreCase(InstanceConstants.STATE_STOPPED)) {
objectProcessMgr.scheduleProcessInstanceAsync(InstanceConstants.PROCESS_STOP,
instance, null);
toWait.add(instance);
}
}
for (Instance instance : toWait) {
resourceMntr.waitForState(instance, InstanceConstants.STATE_STOPPED);
}
}
});
}
}