package io.cattle.platform.configitem.version.impl;
import io.cattle.platform.agent.AgentLocator;
import io.cattle.platform.agent.RemoteAgent;
import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.async.utils.TimeoutException;
import io.cattle.platform.configitem.events.ConfigUpdate;
import io.cattle.platform.configitem.events.ConfigUpdateData;
import io.cattle.platform.configitem.events.ConfigUpdated;
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.version.dao.ConfigItemStatusDao;
import io.cattle.platform.core.model.Agent;
import io.cattle.platform.eventing.EventCallOptions;
import io.cattle.platform.eventing.EventProgress;
import io.cattle.platform.eventing.EventService;
import io.cattle.platform.eventing.annotation.AnnotatedEventListener;
import io.cattle.platform.eventing.annotation.EventHandler;
import io.cattle.platform.eventing.model.Event;
import io.cattle.platform.eventing.model.EventVO;
import io.cattle.platform.eventing.util.EventUtils;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.util.type.CollectionUtils;
import io.cattle.platform.util.type.InitializationTask;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.apache.cloudstack.managed.context.NoExceptionRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicLongProperty;
public class ConfigUpdatePublisher extends NoExceptionRunnable implements InitializationTask, AnnotatedEventListener {
private static final DynamicIntProperty RETRY = ArchaiusUtil.getInt("item.wait.for.event.tries");
private static final DynamicLongProperty TIMEOUT = ArchaiusUtil.getLong("item.wait.for.event.timeout.millis");
private static final Logger log = LoggerFactory.getLogger(ConfigUpdatePublisher.class);
@Inject
ScheduledExecutorService executorService;
@Inject
EventService eventService;
@Inject
AgentLocator agentLocator;
@Inject
ConfigItemStatusDao configItemStatusDao;
@Inject
ObjectManager objectManager;
BlockingQueue<WorkItem> requests = new LinkedBlockingQueue<>();
Map<String, List<WorkItem>> waiters = new HashMap<>();
Set<String> inFlight = new HashSet<>();
volatile boolean running = true;
public ListenableFuture<Event> publish(Client client, ConfigUpdate update) {
WorkItem item = new WorkItem(client, update);
if (update.getData() == null || update.getData().getItems().size() == 0) {
reply(item, null, null);
} else {
log.debug("\t\tStaring work item {} [{}]", item.key, item.hashCode());
requests.add(item);
}
return item.future;
}
@Override
protected void doRun() throws Exception {
while (running) {
WorkItem item = requests.take();
if (item.exit) {
break;
}
loopBody(item);
}
}
protected void loopBody(WorkItem item) throws Exception {
if (log.isTraceEnabled()) {
log.info("**************** Loop Start ****************");
}
boolean request = item.response == null && item.t == null;
if (item.check) {
if (log.isTraceEnabled()) {
log.info("\t=== Processing Check [{}] ===", item.key);
}
processCheck(item);
} else if (request) {
if (log.isTraceEnabled()) {
log.info("\t=== Processing Request [{}] ===", item.key);
}
processRequest(item);
} else {
if (log.isTraceEnabled()) {
log.info("\t=== Processing Response [{}] ===", item.key);
}
processDone(item);
}
publish();
if (log.isTraceEnabled()) {
log.info("\t=== Status ===");
log.info("\t\tConfig Updates, requests=[{}], waiters=[{}] in flight: {}", requests.size(), waiters.size(), inFlight);
if (this.waiters.size() > 0 && !request) {
for (Map.Entry<String, List<WorkItem>> entry : this.waiters.entrySet()) {
for (WorkItem workItem : entry.getValue()) {
for ( ConfigUpdateItem updateItem: workItem.request.getData().getItems()) {
log.info("\t\t\tWaiter resource [{}] {} : {}", entry.getKey(), workItem.hashCode(), updateItem);
}
}
}
}
log.info("\t=== Done ===");
log.info("**************** Loop Done ****************");
}
}
protected void publish() {
Map<String, List<WorkItem>> newWaiters = new HashMap<>();
for (Map.Entry<String, List<WorkItem>> entry : waiters.entrySet()) {
List<WorkItem> remaining = new ArrayList<>();
if (inFlight.contains(entry.getKey())) {
newWaiters.put(entry.getKey(), entry.getValue());
continue;
}
ConfigUpdate update = null;
WorkItem lastItem = null;
Client client = null;
Map<String, ItemVersion> applied = null;
Set<String> items = new HashSet<>();
for (WorkItem item : entry.getValue()) {
if (item.isExpired()) {
reply(item, null, new TimeoutException());
continue;
}
if (applied == null) {
applied = getApplied(item.client);
}
if (satisfies(true, item.request, applied)) {
reply(item, null, null);
continue;
}
if (update == null) {
ConfigUpdateData data = item.request.getData();
update = new ConfigUpdate(item.request.getName(), data.getConfigUrl(),
new ArrayList<ConfigUpdateItem>());
update.setResourceId(item.request.getResourceId());
update.setResourceType(item.request.getResourceType());
client = item.client;
}
for (ConfigUpdateItem itemUpdate : item.request.getData().getItems()) {
if (items.contains(itemUpdate.getName())) {
continue;
}
items.add(itemUpdate.getName());
ConfigItemStatusManagerImpl.addToList(update.getData().getItems(), itemUpdate);
}
remaining.add(item);
lastItem = item;
}
if (update != null) {
inFlight.add(lastItem.key);
publishUpdate(lastItem, client, update);
}
if (remaining.size() > 0) {
newWaiters.put(entry.getKey(), remaining);
}
}
this.waiters = newWaiters;
}
protected void publishUpdate(final WorkItem item, Client client, ConfigUpdate update) {
Map<String, ItemVersion> applied = getApplied(item.client);
List<ConfigUpdateItem> updateItems = new ArrayList<>();
for (ConfigUpdateItem updateItem : update.getData().getItems()) {
if (!itemDone(updateItem, applied)) {
updateItems.add(updateItem);
}
}
if (log.isTraceEnabled()) {
log.info("\t=== Publish ===");
List<String> items = new ArrayList<>();
for (ConfigUpdateItem updateItem : updateItems) {
items.add(updateItem.getName());
}
log.info("\t\tUpdate [{}:{}] {}", update.getResourceType(), update.getResourceId(), items);
}
try {
ListenableFuture<? extends Event> future = call(client, new ConfigUpdate(update, updateItems));
Futures.addCallback(future, new FutureCallback<Event>() {
@Override
public void onSuccess(Event result) {
item.response = result;
requests.add(item);
}
@Override
public void onFailure(Throwable t) {
item.t = t;
requests.add(item);
}
});
} catch (Throwable t) {
item.t = t;
requests.add(item);
}
if (log.isTraceEnabled()) {
log.info("\t=== Publish Sent ===");
}
}
protected void processCheck(WorkItem item) {
List<WorkItem> requests = this.waiters.get(item.key);
if (requests == null || requests.size() == 0) {
return;
}
List<WorkItem> unsatifiedRequests = new ArrayList<>();
Map<String, ItemVersion> applied = getApplied(item.client);
for (WorkItem requestItem : requests) {
if (satisfies(false, requestItem.request, applied)) {
reply(requestItem, item.response, null);
} else {
unsatifiedRequests.add(requestItem);
}
}
if (log.isTraceEnabled()) {
log.info("\t\tKey[{}] is not done, unsatisified [{}]", item.key, unsatifiedRequests.size());
}
waiters.put(item.key, unsatifiedRequests);
}
protected void processDone(WorkItem item) {
inFlight.remove(item.key);
List<WorkItem> requests = this.waiters.get(item.key);
List<WorkItem> unsatifiedRequests = new ArrayList<>();
Map<String, ItemVersion> applied = getApplied(item.client);
for (WorkItem requestItem : requests) {
if (item.t != null || Event.TRANSITIONING_ERROR.equals(item.response.getTransitioning()) ) {
reply(requestItem, item.response, item.t);
} else if (satisfies(false, requestItem.request, applied)) {
reply(requestItem, item.response, null);
} else {
unsatifiedRequests.add(requestItem);
}
}
if (unsatifiedRequests.size() == 0) {
if (log.isTraceEnabled()) {
log.info("\t\tKey [{}] is done", item.key);
}
waiters.remove(item.key);
} else {
if (log.isTraceEnabled()) {
log.info("\t\tKey[{}] is not done, unsatisified [{}]", item.key, unsatifiedRequests.size());
}
waiters.put(item.key, unsatifiedRequests);
}
}
private void processRequest(WorkItem item) {
CollectionUtils.addToMap(this.waiters, item.key, item, ArrayList.class);
}
protected EventCallOptions defaultOptions() {
EventCallOptions options = new EventCallOptions(RETRY.get(), TIMEOUT.get()).withProgress(new EventProgress() {
@Override
public void progress(Event event) {
ConfigItemStatusManagerImpl.logResponse(null, event);
}
});
return options;
}
protected ListenableFuture<? extends Event> call(Client client, ConfigUpdate event) {
EventCallOptions options = defaultOptions();
if (client.getResourceType() == Agent.class) {
RemoteAgent agent = agentLocator.lookupAgent(client.getResourceId());
return agent.call(event, options);
}
return eventService.call(event, options);
}
protected Map<String, ItemVersion> getApplied(Client client) {
return configItemStatusDao.getApplied(client);
}
protected boolean itemDone(ConfigUpdateItem item, Map<String, ItemVersion> applied) {
ItemVersion version = applied.get(item.getName());
if (version == null) {
return false;
}
if (item.getRequestedVersion() == null) {
return false;
}
if (version.getRevision() < item.getRequestedVersion()) {
return false;
}
return true;
}
protected boolean satisfies(boolean isRequest, ConfigUpdate request, Map<String, ItemVersion> applied) {
if (request.getData() == null) {
return true;
}
for (ConfigUpdateItem item : request.getData().getItems()) {
ItemVersion version = applied.get(item.getName());
if (version == null) {
if (log.isTraceEnabled() && !isRequest) {
log.info("\t\tUnsatified item {} [{}] is not assigned", item.hashCode(), item.getName());
}
return false;
}
if (item.getRequestedVersion() == null) {
if (isRequest) {
return false;
} else {
continue;
}
}
if (version.getRevision() < item.getRequestedVersion()) {
if (log.isTraceEnabled() && !isRequest) {
log.info("\t\tUnsatified item {} [{}] [{}<{}]", item.hashCode(), item.getName(), version.getRevision(), item.getRequestedVersion());
}
return false;
}
}
return true;
}
protected void reply(WorkItem item, Event response, Throwable t) {
if (t != null) {
log.info("\t\tFinished work item {} [{}] with exception [{}:{}]", item.key, item.hashCode(), t.getClass(), t.getMessage());
item.future.setException(t);
return;
}
EventVO<Object> event = EventVO.reply(item.request);
if (response != null) {
EventUtils.copyTransitioning(response, event);
event.setData(response.getData());
}
log.debug("\t\tFinished work item {} [{}]", item.key, item.hashCode());
item.future.set(event);
}
@EventHandler
public void configUpdated(ConfigUpdated update) {
if (update.getData() == null) {
return;
}
Client client = new Client(update.getData().getClazz(), update.getData().getResourceId());
String type = objectManager.getType(client.getResourceType());
WorkItem item = new WorkItem(client, true, type, Long.toString(client.getResourceId()));
requests.add(item);
}
@Override
public void start() {
running = true;
executorService.scheduleWithFixedDelay(this, 0, 2, TimeUnit.SECONDS);
}
public void stop() {
running = false;
requests.add(new WorkItem(true));
}
private static final class WorkItem {
Client client;
ConfigUpdate request;
Event response;
String key;
SettableFuture<Event> future;
Throwable t;
boolean exit;
boolean check;
Date time;
public WorkItem(Client client, boolean check, String resourceType, String resourceId) {
this.client = client;
this.check = check;
this.key = String.format("%s:%s", resourceType, resourceId);
}
public WorkItem(boolean exit) {
this.exit = exit;
}
public WorkItem(Client client, ConfigUpdate request) {
this(client, false, request.getResourceType(), request.getResourceId());
this.time = new Date();
this.request = request;
this.future = SettableFuture.create();
}
public boolean isExpired() {
return System.currentTimeMillis() > (time.getTime() + (TIMEOUT.get() * 2));
}
}
}