package io.cattle.platform.configitem.version.impl;
import io.cattle.platform.agent.AgentLocator;
import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.async.utils.AsyncUtils;
import io.cattle.platform.async.utils.TimeoutException;
import io.cattle.platform.configitem.events.ConfigUpdate;
import io.cattle.platform.configitem.exception.ConfigTimeoutException;
import io.cattle.platform.configitem.model.Client;
import io.cattle.platform.configitem.model.ItemVersion;
import io.cattle.platform.configitem.request.ConfigUpdateItem;
import io.cattle.platform.configitem.request.ConfigUpdateRequest;
import io.cattle.platform.configitem.version.ConfigItemStatusManager;
import io.cattle.platform.configitem.version.dao.ConfigItemStatusDao;
import io.cattle.platform.core.model.ConfigItemStatus;
import io.cattle.platform.deferred.util.DeferredUtils;
import io.cattle.platform.eventing.EventService;
import io.cattle.platform.eventing.exception.AgentRemovedException;
import io.cattle.platform.eventing.model.Event;
import io.cattle.platform.eventing.model.EventVO;
import io.cattle.platform.lock.LockCallback;
import io.cattle.platform.lock.LockManager;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.server.context.ServerContext;
import io.cattle.platform.server.context.ServerContext.BaseProtocol;
import io.cattle.platform.util.type.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.netflix.config.DynamicBooleanProperty;
import com.netflix.config.DynamicStringListProperty;
public class ConfigItemStatusManagerImpl implements ConfigItemStatusManager {
private static final DynamicBooleanProperty BLOCK = ArchaiusUtil.getBoolean("item.migration.block.on.failure");
private static final DynamicStringListProperty PRIORITY_ITEMS = ArchaiusUtil.getList("item.priority");
private static final Logger log = LoggerFactory.getLogger(ConfigItemStatusManagerImpl.class);
@Inject
ConfigItemStatusDao configItemStatusDao;
@Inject
ObjectManager objectManager;
@Inject
AgentLocator agentLocator;
@Inject
ConfigUpdatePublisher publisher;
@Inject
LockManager lockManager;
@Inject
EventService eventService;
@Override
public boolean runUpdateForEvent(final String itemName, final ConfigUpdate update, final Client client, final Runnable run) {
boolean found = false;
for (ConfigUpdateItem item : update.getData().getItems()) {
if (itemName.equals(item.getName())) {
found = true;
}
}
if (!found) {
return false;
}
return lockManager.tryLock(new ConfigItemProcessLock(itemName, client), new LockCallback<Object>() {
@Override
public Object doWithLock() {
ItemVersion itemVersion = getRequestedVersion(client, itemName);
if (itemVersion == null) {
return null;
}
run.run();
setApplied(client, itemName, itemVersion);
eventService.publish(EventVO.reply(update));
return new Object();
}
}) != null;
}
protected Map<String, ConfigItemStatus> getStatus(ConfigUpdateRequest request) {
Map<String, ConfigItemStatus> statuses = new HashMap<String, ConfigItemStatus>();
for (ConfigItemStatus status : configItemStatusDao.listItems(request)) {
statuses.put(status.getName(), status);
}
return statuses;
}
@Override
public void updateConfig(ConfigUpdateRequest request) {
if (request.getClient() == null) {
throw new IllegalArgumentException("Client is null on request [" + request + "]");
}
log.trace("ITEM UPDATE: for [{}]", request.getClient());
Client client = request.getClient();
Map<String, ConfigItemStatus> statuses = getStatus(request);
List<ConfigUpdateItem> toTrigger = new ArrayList<ConfigUpdateItem>();
for (ConfigUpdateItem item : request.getItems()) {
String name = item.getName();
ConfigItemStatus status = statuses.get(name);
Long requestedVersion = item.getRequestedVersion();
if (status == null) {
if (item.isApply()) {
log.trace("ITEM UPDATE: incrementOrApply [{}]", request.getClient());
configItemStatusDao.incrementOrApply(client, name);
log.trace("ITEM UPDATE: done incrementOrApply [{}]", request.getClient());
} else {
log.info("ITEM UPDATE: ignore [{}] [{}]", name, request.getClient());
continue;
}
log.trace("ITEM UPDATE: get requested [{}]", request.getClient());
requestedVersion = configItemStatusDao.getRequestedVersion(client, name);
log.trace("ITEM UPDATE: done get requested [{}]", request.getClient());
} else if (requestedVersion == null && item.getSetVersion() != null) {
log.trace("ITEM UPDATE: setVersion [{}]", request.getClient());
configItemStatusDao.setIfOlder(client, name, item.getSetVersion());
log.trace("ITEM UPDATE: done setVersion [{}]", request.getClient());
requestedVersion = item.getSetVersion();
} else if (requestedVersion == null && item.isIncrement()) {
log.trace("ITEM UPDATE: incrementOrApply [{}]", request.getClient());
configItemStatusDao.incrementOrApply(client, name);
log.trace("ITEM UPDATE: done incrementOrApply [{}]", request.getClient());
requestedVersion = status.getRequestedVersion() + 1;
} else if (requestedVersion == null) {
requestedVersion = status.getRequestedVersion();
}
item.setRequestedVersion(requestedVersion);
toTrigger.add(item);
}
triggerUpdate(request, toTrigger);
}
protected void triggerUpdate(final ConfigUpdateRequest request, final List<ConfigUpdateItem> items) {
final ConfigUpdate event = getEvent(request, items);
if (event == null) {
return;
}
Runnable run = new Runnable() {
@Override
public void run() {
request.setUpdateFuture(call(request.getClient(), event));
}
};
if (request.isDeferredTrigger()) {
DeferredUtils.defer(run);
} else {
run.run();
}
}
private ListenableFuture<? extends Event> call(Client client, ConfigUpdate event) {
return publisher.publish(client, event);
}
protected ConfigUpdate getEvent(ConfigUpdateRequest request, List<ConfigUpdateItem> items) {
Client client = request.getClient();
String url = ServerContext.getHostApiBaseUrl(BaseProtocol.HTTP);
if (items.size() == 0) {
return new ConfigUpdate(client.getEventName(), url, Collections.<ConfigUpdateItem> emptyList());
}
ConfigUpdate event = new ConfigUpdate(client.getEventName(), url, items);
event.withResourceType(objectManager.getType(client.getResourceType())).withResourceId(Long.toString(client.getResourceId()));
return event;
}
protected ConfigUpdate getEvent(ConfigUpdateRequest request) {
List<ConfigUpdateItem> toTrigger = getNeedsUpdating(request, !request.isMigration());
return getEvent(request, toTrigger);
}
@Override
public ListenableFuture<?> whenReady(final ConfigUpdateRequest request) {
ConfigUpdate event = getEvent(request);
if (event.getData().getItems().size() == 0) {
return AsyncUtils.done();
}
ListenableFuture<? extends Event> future = request.getUpdateFuture();
if (future == null) {
future = call(request.getClient(), event);
}
return Futures.transform(future, new Function<Event, Object>() {
@Override
public Object apply(Event input) {
logResponse(request, input);
List<ConfigUpdateItem> toTrigger = getNeedsUpdating(request, true);
if (toTrigger.size() > 0) {
throw new ConfigTimeoutException(request, toTrigger);
}
return Boolean.TRUE;
}
});
}
protected List<ConfigUpdateItem> getNeedsUpdating(ConfigUpdateRequest request, boolean checkVersions) {
Client client = request.getClient();
Map<String, ConfigItemStatus> statuses = getStatus(request);
List<ConfigUpdateItem> toTrigger = new ArrayList<ConfigUpdateItem>();
for (ConfigUpdateItem item : request.getItems()) {
String name = item.getName();
ConfigItemStatus status = statuses.get(item.getName());
if (status == null) {
log.error("Waiting on config item [{}] on client [{}] but it is not applied", name, client);
continue;
}
if (item.isCheckInSyncOnly()) {
if (!checkVersions || !ObjectUtils.equals(status.getRequestedVersion(), status.getAppliedVersion())) {
if (request.isMigration()) {
log.info("Waiting on [{}] on [{}], for migration", client, name);
} else {
log.debug("Waiting on [{}] on [{}], not in sync requested [{}] != applied [{}]", client, name, status.getRequestedVersion(), status
.getAppliedVersion());
}
addToList(toTrigger, item);
}
} else if (item.getRequestedVersion() != null) {
Long applied = status.getAppliedVersion();
if (applied == null || item.getRequestedVersion() > applied) {
log.debug("Waiting on [{}] on [{}], not applied requested [{}] > applied [{}]", client, name, item.getRequestedVersion(), applied);
addToList(toTrigger, item);
}
}
}
return toTrigger;
}
protected static void addToList(List<ConfigUpdateItem> list, ConfigUpdateItem item) {
if (PRIORITY_ITEMS.get().contains(item.getName())) {
list.add(0, item);
} else {
list.add(item);
}
}
@Override
public void waitFor(ConfigUpdateRequest request) {
AsyncUtils.get(whenReady(request));
}
@Override
public void sync(final boolean migration) {
Map<Client, List<String>> items = configItemStatusDao.findOutOfSync(migration);
boolean first = true;
for (final Map.Entry<Client, List<String>> entry : items.entrySet()) {
final Client client = entry.getKey();
final ConfigUpdateRequest request = new ConfigUpdateRequest(client).withMigration(migration);
for (String item : entry.getValue()) {
request.addItem(item).withApply(false).withIncrement(false).withCheckInSyncOnly(true);
}
log.info("Requesting {} of item(s) {} on [{}]", migration ? "migration" : "update", entry.getValue(), client);
if (first && migration && BLOCK.get()) {
waitFor(request);
} else {
ConfigUpdate event = getEvent(request);
ListenableFuture<? extends Event> future = call(client, event);
Futures.addCallback(future, new FutureCallback<Event>() {
@Override
public void onSuccess(Event result) {
logResponse(request, result);
}
@Override
public void onFailure(Throwable t) {
if (t instanceof TimeoutException) {
log.info("Timeout {} item(s) {} on [{}]", migration ? "migrating" : "updating", entry.getValue(), client);
} else if (t instanceof AgentRemovedException) {
log.info("Agent removed {} item(s) {} on [{}]", migration ? "migrating" : "updating", entry.getValue(), client);
} else {
log.error("Error {} item(s) {} on [{}]", migration ? "migrating" : "updating", entry.getValue(), client, t);
}
}
});
}
first = false;
}
}
protected static void logResponse(ConfigUpdateRequest request, Event event) {
Map<String, Object> data = CollectionUtils.toMap(event.getData());
Object exitCode = data.get("exitCode");
Object output = data.get("output");
if (exitCode != null) {
long exit = Long.parseLong(exitCode.toString());
if (exit == 0) {
log.debug("Success {}", request);
} else if (exit == 122 && "Lock failed".equals(output)) {
/*
* This happens when the lock fails to apply. Really we should
* upgrade to newer util-linux that supports -E and then set a
* special exit code. That will be slightly better
*/
log.info("Failed {}, exit code [{}] output [{}]", request, exitCode, output);
} else {
log.error("Failed {}, exit code [{}] output [{}]", request, exitCode, output);
}
}
}
@Override
public boolean setApplied(Client client, String itemName, ItemVersion version) {
return configItemStatusDao.setApplied(client, itemName, version);
}
@Override
public void setLatest(Client client, String itemName, String sourceRevision) {
configItemStatusDao.setLatest(client, itemName, sourceRevision);
}
@Override
public boolean isAssigned(Client client, String itemName) {
return configItemStatusDao.isAssigned(client, itemName);
}
@Override
public void setItemSourceVersion(String name, String sourceRevision) {
configItemStatusDao.setItemSourceVersion(name, sourceRevision);
}
@Override
public ItemVersion getRequestedVersion(Client client, String itemName) {
return configItemStatusDao.getRequestedItemVersion(client, itemName);
}
}