package io.airlift.airship.coordinator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Inject; import io.airlift.airship.coordinator.AgentFilterBuilder.StatePredicate; import io.airlift.airship.shared.AgentLifecycleState; import io.airlift.airship.shared.AgentStatus; import io.airlift.airship.shared.Assignment; import io.airlift.airship.shared.CoordinatorLifecycleState; import io.airlift.airship.shared.CoordinatorStatus; import io.airlift.airship.shared.ExpectedSlotStatus; import io.airlift.airship.shared.Installation; import io.airlift.airship.shared.InstallationUtils; import io.airlift.airship.shared.Repository; import io.airlift.airship.shared.SlotLifecycleState; import io.airlift.airship.shared.SlotStatus; import io.airlift.airship.shared.UpgradeVersions; import io.airlift.discovery.client.ServiceDescriptor; import io.airlift.http.server.HttpServerInfo; import io.airlift.log.Logger; import io.airlift.node.NodeInfo; import io.airlift.units.Duration; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import static io.airlift.airship.shared.SlotLifecycleState.KILLING; import static io.airlift.airship.shared.SlotLifecycleState.RESTARTING; import static io.airlift.airship.shared.SlotLifecycleState.RUNNING; import static io.airlift.airship.shared.SlotLifecycleState.STOPPED; import static io.airlift.airship.shared.SlotLifecycleState.TERMINATED; import static io.airlift.airship.shared.SlotLifecycleState.UNKNOWN; import static io.airlift.airship.shared.VersionsUtil.checkSlotsVersion; public class Coordinator { private static final Logger log = Logger.get(Coordinator.class); private final ConcurrentMap<String, RemoteCoordinator> coordinators = new ConcurrentHashMap<>(); private final ConcurrentMap<String, RemoteAgent> agents = new ConcurrentHashMap<>(); private final CoordinatorStatus coordinatorStatus; private final Repository repository; private final ScheduledExecutorService timerService; private final Duration statusExpiration; private final Provisioner provisioner; private final RemoteCoordinatorFactory remoteCoordinatorFactory; private final RemoteAgentFactory remoteAgentFactory; private final ServiceInventory serviceInventory; private final StateManager stateManager; private final boolean allowDuplicateInstallationsOnAnAgent; private final ExecutorService executor; @Inject public Coordinator(NodeInfo nodeInfo, HttpServerInfo httpServerInfo, CoordinatorConfig config, RemoteCoordinatorFactory remoteCoordinatorFactory, RemoteAgentFactory remoteAgentFactory, Repository repository, Provisioner provisioner, StateManager stateManager, ServiceInventory serviceInventory) { this( new CoordinatorStatus(nodeInfo.getInstanceId(), CoordinatorLifecycleState.ONLINE, nodeInfo.getInstanceId(), httpServerInfo.getHttpUri(), httpServerInfo.getHttpExternalUri(), nodeInfo.getLocation(), null), remoteCoordinatorFactory, remoteAgentFactory, repository, provisioner, stateManager, serviceInventory, checkNotNull(config, "config is null").getStatusExpiration(), config.isAllowDuplicateInstallationsOnAnAgent()); } public Coordinator(CoordinatorStatus coordinatorStatus, RemoteCoordinatorFactory remoteCoordinatorFactory, RemoteAgentFactory remoteAgentFactory, Repository repository, Provisioner provisioner, StateManager stateManager, ServiceInventory serviceInventory, Duration statusExpiration, boolean allowDuplicateInstallationsOnAnAgent) { Preconditions.checkNotNull(coordinatorStatus, "coordinatorStatus is null"); Preconditions.checkNotNull(remoteCoordinatorFactory, "remoteCoordinatorFactory is null"); Preconditions.checkNotNull(remoteAgentFactory, "remoteAgentFactory is null"); Preconditions.checkNotNull(repository, "repository is null"); Preconditions.checkNotNull(provisioner, "provisioner is null"); Preconditions.checkNotNull(stateManager, "stateManager is null"); Preconditions.checkNotNull(serviceInventory, "serviceInventory is null"); Preconditions.checkNotNull(statusExpiration, "statusExpiration is null"); this.coordinatorStatus = coordinatorStatus; this.remoteCoordinatorFactory = remoteCoordinatorFactory; this.remoteAgentFactory = remoteAgentFactory; this.repository = repository; this.provisioner = provisioner; this.stateManager = stateManager; this.serviceInventory = serviceInventory; this.statusExpiration = statusExpiration; this.allowDuplicateInstallationsOnAnAgent = allowDuplicateInstallationsOnAnAgent; this.executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("coordinator-task").build()); timerService = Executors.newScheduledThreadPool(10, new ThreadFactoryBuilder().setNameFormat("coordinator-agent-monitor").setDaemon(true).build()); updateAllCoordinatorsAndWait(); updateAllAgentsAndWait(); } @PostConstruct public void start() { timerService.scheduleWithFixedDelay(new Runnable() { @Override public void run() { try { updateAllCoordinators(); } catch (Throwable e) { log.error(e, "Unexpected exception updating coordinators"); } } }, 0, (long) statusExpiration.toMillis(), TimeUnit.MILLISECONDS); timerService.scheduleWithFixedDelay(new Runnable() { @Override public void run() { try { updateAllAgents(); } catch (Throwable e) { log.error(e, "Unexpected exception updating agents"); } } }, 0, (long) statusExpiration.toMillis(), TimeUnit.MILLISECONDS); } public CoordinatorStatus status() { return coordinatorStatus; } public CoordinatorStatus getCoordinator(String instanceId) { if (coordinatorStatus.getInstanceId().equals(instanceId)) { return status(); } RemoteCoordinator remoteCoordinator = coordinators.get(instanceId); if (remoteCoordinator == null) { return null; } return remoteCoordinator.status(); } public List<CoordinatorStatus> getCoordinators() { List<CoordinatorStatus> statuses = ImmutableList.copyOf(Iterables.transform(coordinators.values(), new Function<RemoteCoordinator, CoordinatorStatus>() { public CoordinatorStatus apply(RemoteCoordinator agent) { return agent.status(); } })); return ImmutableList.<CoordinatorStatus>builder() .add(coordinatorStatus) .addAll(statuses) .build(); } public List<CoordinatorStatus> getCoordinators(Predicate<CoordinatorStatus> coordinatorFilter) { return ImmutableList.copyOf(filter(getCoordinators(), coordinatorFilter)); } public List<CoordinatorStatus> provisionCoordinators(String coordinatorConfigSpec, int coordinatorCount, String instanceType, String availabilityZone, String ami, String keyPair, String securityGroup, String provisioningScriptsArtifact) { List<Instance> instances = provisioner.provisionCoordinators(coordinatorConfigSpec, coordinatorCount, instanceType, availabilityZone, ami, keyPair, securityGroup, provisioningScriptsArtifact); List<CoordinatorStatus> coordinators = newArrayList(); for (Instance instance : instances) { String instanceId = instance.getInstanceId(); if (instanceId.equals(this.coordinatorStatus.getInstanceId())) { throw new IllegalStateException("Provisioner created a coordinator with the same is as this coordinator"); } RemoteCoordinator remoteCoordinator = remoteCoordinatorFactory.createRemoteCoordinator(instance, CoordinatorLifecycleState.PROVISIONING); this.coordinators.put(instanceId, remoteCoordinator); coordinators.add(remoteCoordinator.status()); } return coordinators; } public List<AgentStatus> getAgents() { return ImmutableList.copyOf(Iterables.transform(agents.values(), new Function<RemoteAgent, AgentStatus>() { public AgentStatus apply(RemoteAgent agent) { return agent.status(); } })); } public List<AgentStatus> getAgents(Predicate<AgentStatus> agentFilter) { Iterable<RemoteAgent> remoteAgents = filter(this.agents.values(), filterAgentsBy(agentFilter)); List<AgentStatus> agentStatuses = ImmutableList.copyOf(transform(remoteAgents, getAgentStatus())); return agentStatuses; } public AgentStatus getAgent(String instanceId) { RemoteAgent remoteAgent = agents.get(instanceId); if (remoteAgent == null) { return null; } return remoteAgent.status(); } public AgentStatus getAgentByAgentId(String agentId) { for (RemoteAgent remoteAgent : agents.values()) { AgentStatus status = remoteAgent.status(); if (agentId.equals(status.getAgentId())) { return status; } } return null; } @VisibleForTesting public void updateAllCoordinatorsAndWait() { waitForFutures(updateAllCoordinators()); } private List<ListenableFuture<?>> updateAllCoordinators() { Set<String> instanceIds = newHashSet(); for (Instance instance : this.provisioner.listCoordinators()) { instanceIds.add(instance.getInstanceId()); // skip this server since it is automatically managed if (instance.getInstanceId().equals(this.coordinatorStatus.getInstanceId())) { continue; } RemoteCoordinator remoteCoordinator = remoteCoordinatorFactory.createRemoteCoordinator(instance, instance.getInternalUri() != null ? CoordinatorLifecycleState.ONLINE : CoordinatorLifecycleState.OFFLINE); RemoteCoordinator existing = coordinators.putIfAbsent(instance.getInstanceId(), remoteCoordinator); if (existing != null) { existing.setInternalUri(instance.getInternalUri()); } } // add provisioning coordinators to provisioner list for (RemoteCoordinator remoteCoordinator : coordinators.values()) { if (remoteCoordinator.status().getState() == CoordinatorLifecycleState.PROVISIONING) { instanceIds.add(coordinatorStatus.getCoordinatorId()); } } // remove any coordinators in the provisioner list coordinators.keySet().retainAll(instanceIds); List<ListenableFuture<?>> futures = new ArrayList<>(); for (RemoteCoordinator remoteCoordinator : coordinators.values()) { futures.add(remoteCoordinator.updateStatus()); } return futures; } @VisibleForTesting public void updateAllAgentsAndWait() { waitForFutures(updateAllAgents()); } private List<ListenableFuture<?>> updateAllAgents() { Set<String> instanceIds = newHashSet(); for (Instance instance : this.provisioner.listAgents()) { instanceIds.add(instance.getInstanceId()); RemoteAgent remoteAgent = remoteAgentFactory.createRemoteAgent(instance, instance.getInternalUri() != null ? AgentLifecycleState.ONLINE : AgentLifecycleState.OFFLINE); RemoteAgent existing = agents.putIfAbsent(instance.getInstanceId(), remoteAgent); if (existing != null) { existing.setInternalUri(instance.getInternalUri()); } } // add provisioning agents to provisioner list for (RemoteAgent remoteAgent : agents.values()) { if (remoteAgent.status().getState() == AgentLifecycleState.PROVISIONING) { instanceIds.add(remoteAgent.status().getAgentId()); } } // remove any agents not in the provisioner list agents.keySet().retainAll(instanceIds); List<ListenableFuture<?>> futures = new ArrayList<>(); List<ServiceDescriptor> serviceDescriptors = serviceInventory.getServiceInventory(transform(getAllSlots(), getSlotStatus())); for (RemoteAgent remoteAgent : agents.values()) { futures.add(remoteAgent.updateStatus()); remoteAgent.setServiceInventory(serviceDescriptors); } return futures; } public List<AgentStatus> provisionAgents(String agentConfigSpec, int agentCount, String instanceType, String availabilityZone, String ami, String keyPair, String securityGroup, String provisioningScriptsArtifact) { List<Instance> instances = provisioner.provisionAgents(agentConfigSpec, agentCount, instanceType, availabilityZone, ami, keyPair, securityGroup, provisioningScriptsArtifact); List<AgentStatus> agents = newArrayList(); for (Instance instance : instances) { String instanceId = instance.getInstanceId(); RemoteAgent remoteAgent = remoteAgentFactory.createRemoteAgent(instance, AgentLifecycleState.PROVISIONING); this.agents.put(instanceId, remoteAgent); agents.add(remoteAgent.status()); } return agents; } public AgentStatus terminateAgent(String agentId) { RemoteAgent agent = null; for (Iterator<Entry<String, RemoteAgent>> iterator = agents.entrySet().iterator(); iterator.hasNext(); ) { Entry<String, RemoteAgent> entry = iterator.next(); if (entry.getValue().status().getAgentId().equals(agentId)) { iterator.remove(); agent = entry.getValue(); break; } } if (agent == null) { return null; } if (!agent.getSlots().isEmpty()) { agents.putIfAbsent(agent.status().getInstanceId(), agent); throw new IllegalStateException("Cannot terminate agent that has slots: " + agentId); } provisioner.terminateAgents(ImmutableList.of(agentId)); return agent.status().changeState(AgentLifecycleState.TERMINATED); } public List<SlotStatus> install(Predicate<AgentStatus> filter, int limit, Assignment assignment) { final Installation installation = InstallationUtils.toInstallation(repository, assignment); List<RemoteAgent> targetAgents = new ArrayList<>(selectAgents(filter, installation)); targetAgents = targetAgents.subList(0, Math.min(targetAgents.size(), limit)); return parallel(targetAgents, new Function<RemoteAgent, SlotStatus>() { @Override public SlotStatus apply(RemoteAgent agent) { SlotStatus slotStatus = agent.install(installation); stateManager.setExpectedState(new ExpectedSlotStatus(slotStatus.getId(), STOPPED, installation.getAssignment())); return slotStatus; } }); } private List<RemoteAgent> selectAgents(Predicate<AgentStatus> filter, Installation installation) { // select only online agents filter = Predicates.and(filter, new StatePredicate(AgentLifecycleState.ONLINE)); List<RemoteAgent> allAgents = newArrayList(filter(this.agents.values(), filterAgentsBy(filter))); if (allAgents.isEmpty()) { throw new IllegalStateException("No online agents match the provided filters."); } if (!allowDuplicateInstallationsOnAnAgent) { allAgents = newArrayList(filter(allAgents, filterAgentsWithAssignment(installation))); if (allAgents.isEmpty()) { throw new IllegalStateException("All agents already have the specified binary and configuration installed."); } } // randomize agents so all processes don't end up on the same node // todo sort agents by number of process already installed on them? Collections.shuffle(allAgents); List<RemoteAgent> targetAgents = newArrayList(); for (RemoteAgent agent : allAgents) { // agents without declared resources are considered to have unlimited resources AgentStatus status = agent.status(); if (!status.getResources().isEmpty()) { // verify that required resources are available Map<String, Integer> availableResources = InstallationUtils.getAvailableResources(status); if (!InstallationUtils.resourcesAreAvailable(availableResources, installation.getResources())) { continue; } } targetAgents.add(agent); } if (targetAgents.isEmpty()) { throw new IllegalStateException("No agents have the available resources to run the specified binary and configuration."); } return targetAgents; } public List<SlotStatus> upgrade(Predicate<SlotStatus> filter, UpgradeVersions upgradeVersions, String expectedSlotsVersion, boolean force) { List<RemoteSlot> filteredSlots = selectRemoteSlots(filter, expectedSlotsVersion); final Map<UUID, Assignment> newAssignments = new HashMap<>(); List<RemoteSlot> slotsToUpgrade = new ArrayList<>(); for (RemoteSlot slot : filteredSlots) { SlotStatus status = slot.status(); SlotLifecycleState state = status.getState(); if (state == TERMINATED) { // should never happen but be safe continue; } if ((!force) && (state == UNKNOWN)) { // slots in unknown state are not safe to upgrade (might still be running) continue; } Assignment assignment; if (force && (status.getAssignment() == null)) { // allow forced upgrading if existing assignment is missing assignment = upgradeVersions.forceAssignment(repository); } else { assignment = upgradeVersions.upgradeAssignment(repository, status.getAssignment()); } newAssignments.put(slot.getId(), assignment); slotsToUpgrade.add(slot); } // no slots to upgrade if (newAssignments.isEmpty()) { return ImmutableList.of(); } // assure that new assignments all have the same binary (ignoring version) if (!sameBinary(newAssignments.values())) { TreeSet<String> binaries = new TreeSet<>(); for (RemoteSlot slot : filteredSlots) { binaries.add(slot.status().getAssignment().getBinary()); } throw new IllegalArgumentException("Expected a target slots for upgrade command to have a single binary, but found: " + Joiner.on(", ").join(binaries)); } return parallelCommand(slotsToUpgrade, new Function<RemoteSlot, SlotStatus>() { @Override public SlotStatus apply(RemoteSlot slot) { boolean expectRestart = slot.status().getState() == RUNNING; Assignment assignment = newAssignments.get(slot.getId()); Preconditions.checkState(assignment != null, "Error no assignment for slot " + slot.getId()); URI configFile = repository.configToHttpUri(assignment.getConfig()); Installation installation = new Installation( repository.configShortName(assignment.getConfig()), assignment, repository.binaryToHttpUri(assignment.getBinary()), configFile, ImmutableMap.<String, Integer>of()); stateManager.setExpectedState(new ExpectedSlotStatus(slot.getId(), expectRestart ? RUNNING : STOPPED, installation.getAssignment())); SlotStatus slotStatus = slot.assign(installation); return slotStatus; } }); } private boolean sameBinary(Collection<Assignment> values) { if (values.size() < 2) { return true; } final Assignment assignment = Iterables.getFirst(values, null); return Iterables.all(values, new Predicate<Assignment>() { @Override public boolean apply(@Nullable Assignment input) { return repository.binaryEqualsIgnoreVersion(input.getBinary(), assignment.getBinary()); } }); } public List<SlotStatus> terminate(Predicate<SlotStatus> filter, String expectedSlotsVersion) { Preconditions.checkNotNull(filter, "filter is null"); // filter the slots List<RemoteSlot> filteredSlots = selectRemoteSlots(filter, expectedSlotsVersion); return parallelCommand(filteredSlots, new Function<RemoteSlot, SlotStatus>() { @Override public SlotStatus apply(RemoteSlot slot) { SlotStatus slotStatus = slot.terminate(); if (slotStatus.getState() == TERMINATED) { stateManager.deleteExpectedState(slotStatus.getId()); } return slotStatus; } }); } public List<SlotStatus> setState(final SlotLifecycleState state, Predicate<SlotStatus> filter, String expectedSlotsVersion) { Preconditions.checkArgument(EnumSet.of(RUNNING, RESTARTING, STOPPED, KILLING).contains(state), "Unsupported lifecycle state: " + state); // filter the slots List<RemoteSlot> filteredSlots = selectRemoteSlots(filter, expectedSlotsVersion); return parallelCommand(filteredSlots, new Function<RemoteSlot, SlotStatus>() { @Override public SlotStatus apply(RemoteSlot slot) { switch (state) { case RUNNING: stateManager.setExpectedState(new ExpectedSlotStatus(slot.getId(), RUNNING, slot.status().getAssignment())); return slot.start(); case RESTARTING: stateManager.setExpectedState(new ExpectedSlotStatus(slot.getId(), RUNNING, slot.status().getAssignment())); return slot.restart(); case STOPPED: stateManager.setExpectedState(new ExpectedSlotStatus(slot.getId(), STOPPED, slot.status().getAssignment())); return slot.stop(); case KILLING: stateManager.setExpectedState(new ExpectedSlotStatus(slot.getId(), KILLING, slot.status().getAssignment())); return slot.kill(); default: throw new IllegalArgumentException("Unexpected state transition " + state); } } }); } public List<SlotStatus> resetExpectedState(Predicate<SlotStatus> filter, String expectedSlotsVersion) { // filter the slots List<SlotStatus> filteredSlots = getAllSlotsStatus(filter); // verify the state of the system hasn't changed checkSlotsVersion(expectedSlotsVersion, filteredSlots); return ImmutableList.copyOf(transform(filteredSlots, new Function<SlotStatus, SlotStatus>() { @Override public SlotStatus apply(SlotStatus slotStatus) { if (slotStatus.getState() != SlotLifecycleState.UNKNOWN) { stateManager.setExpectedState(new ExpectedSlotStatus(slotStatus.getId(), slotStatus.getState(), slotStatus.getAssignment())); } else { stateManager.deleteExpectedState(slotStatus.getId()); } return slotStatus; } })); } private List<RemoteSlot> selectRemoteSlots(Predicate<SlotStatus> filter, String expectedSlotsVersion) { // filter the slots List<RemoteSlot> filteredSlots = ImmutableList.copyOf(filter(getAllSlots(), filterSlotsBy(filter))); // verify the state of the system hasn't changed checkSlotsVersion(expectedSlotsVersion, getAllSlotsStatus(filter, filteredSlots)); return filteredSlots; } public List<SlotStatus> getAllSlotStatus() { return getAllSlotsStatus(Predicates.<SlotStatus>alwaysTrue()); } public List<SlotStatus> getAllSlotsStatus(Predicate<SlotStatus> slotFilter) { return getAllSlotsStatus(slotFilter, getAllSlots()); } private List<SlotStatus> getAllSlotsStatus(Predicate<SlotStatus> slotFilter, List<RemoteSlot> allSlots) { ImmutableMap<UUID, ExpectedSlotStatus> expectedStates = Maps.uniqueIndex(stateManager.getAllExpectedStates(), ExpectedSlotStatus.uuidGetter()); ImmutableMap<UUID, SlotStatus> actualStates = Maps.uniqueIndex(transform(allSlots, getSlotStatus()), SlotStatus.uuidGetter()); ArrayList<SlotStatus> stats = newArrayList(); for (UUID uuid : Sets.union(actualStates.keySet(), expectedStates.keySet())) { final SlotStatus actualState = actualStates.get(uuid); final ExpectedSlotStatus expectedState = expectedStates.get(uuid); SlotStatus fullSlotStatus; if (actualState == null) { // skip terminated slots if (expectedState == null || expectedState.getStatus() == SlotLifecycleState.TERMINATED) { continue; } // missing slot fullSlotStatus = SlotStatus.createSlotStatusWithExpectedState(uuid, null, null, null, "/unknown", UNKNOWN, expectedState.getAssignment(), null, ImmutableMap.<String, Integer>of(), expectedState.getStatus(), expectedState.getAssignment(), "Slot is missing; Expected slot to be " + expectedState.getStatus()); } else if (expectedState == null) { // unexpected slot fullSlotStatus = actualState.changeStatusMessage("Unexpected slot").changeExpectedState(null, null); } else { fullSlotStatus = actualState.changeExpectedState(expectedState.getStatus(), expectedState.getAssignment()); // add error message if actual state doesn't match expected state List<String> messages = newArrayList(); if (!Objects.equal(actualState.getState(), expectedState.getStatus())) { messages.add("Expected state to be " + expectedState.getStatus()); } if (!Objects.equal(actualState.getAssignment(), expectedState.getAssignment())) { Assignment assignment = expectedState.getAssignment(); if (assignment != null) { messages.add("Expected assignment to be " + assignment.getBinary() + " " + assignment.getConfig()); } else { messages.add("Expected no assignment"); } } if (!messages.isEmpty()) { fullSlotStatus = fullSlotStatus.changeStatusMessage(Joiner.on("; ").join(messages)); } } if (slotFilter.apply(fullSlotStatus)) { stats.add(fullSlotStatus); } } return stats; } private Predicate<RemoteSlot> filterSlotsBy(final Predicate<SlotStatus> filter) { return new Predicate<RemoteSlot>() { @Override public boolean apply(RemoteSlot input) { return filter.apply(input.status()); } }; } private List<RemoteSlot> getAllSlots() { return ImmutableList.copyOf(concat(Iterables.transform(agents.values(), new Function<RemoteAgent, List<? extends RemoteSlot>>() { public List<? extends RemoteSlot> apply(RemoteAgent agent) { return agent.getSlots(); } }))); } private Predicate<RemoteAgent> filterAgentsBy(final Predicate<AgentStatus> filter) { return new Predicate<RemoteAgent>() { @Override public boolean apply(RemoteAgent input) { return filter.apply(input.status()); } }; } private Function<RemoteSlot, SlotStatus> getSlotStatus() { return new Function<RemoteSlot, SlotStatus>() { @Override public SlotStatus apply(RemoteSlot slot) { return slot.status(); } }; } private Function<RemoteAgent, AgentStatus> getAgentStatus() { return new Function<RemoteAgent, AgentStatus>() { @Override public AgentStatus apply(RemoteAgent agent) { return agent.status(); } }; } private Predicate<RemoteAgent> filterAgentsWithAssignment(Installation installation) { Preconditions.checkNotNull(installation, "installation is null"); final Assignment assignment = installation.getAssignment(); return new Predicate<RemoteAgent>() { @Override public boolean apply(RemoteAgent agent) { for (RemoteSlot slot : agent.getSlots()) { if ((slot.status().getAssignment() != null) && assignmentEqualsIgnoreVersion(assignment, slot.status().getAssignment())) { return false; } } return true; } }; } private boolean assignmentEqualsIgnoreVersion(Assignment a, Assignment b) { return repository.binaryEqualsIgnoreVersion(a.getBinary(), b.getBinary()) && repository.configEqualsIgnoreVersion(a.getConfig(), b.getConfig()); } private <T> ImmutableList<T> parallelCommand(Iterable<RemoteSlot> items, final Function<RemoteSlot, T> function) { ImmutableCollection<Collection<RemoteSlot>> slotsByInstance = Multimaps.index(items, new Function<RemoteSlot, Object>() { @Override public Object apply(RemoteSlot input) { return input.status().getInstanceId(); } }).asMap().values(); // run commands for different instances in parallel return ImmutableList.copyOf(concat(parallel(slotsByInstance, new Function<Collection<RemoteSlot>, List<T>>() { public List<T> apply(Collection<RemoteSlot> input) { // but run commands for a single instance serially return ImmutableList.copyOf(transform(input, function)); } }))); } private <F, T> ImmutableList<T> parallel(Iterable<F> items, final Function<F, T> function) { List<Callable<T>> callables = ImmutableList.copyOf(transform(items, new Function<F, Callable<T>>() { public Callable<T> apply(@Nullable final F item) { return new CallableFunction<>(item, function); } })); List<Future<T>> futures; try { futures = executor.invokeAll(callables); } catch (InterruptedException e) { throw new RuntimeException("Interrupted while waiting for command to finish", e); } List<Throwable> failures = new ArrayList<>(); ImmutableList.Builder<T> results = ImmutableList.builder(); for (Future<T> future : futures) { try { results.add(future.get()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); failures.add(e); } catch (CancellationException e) { failures.add(e); } catch (ExecutionException e) { if (e.getCause() != null) { failures.add(e.getCause()); } else { failures.add(e); } } } if (!failures.isEmpty()) { Throwable first = failures.get(0); RuntimeException runtimeException = new RuntimeException(first.getMessage()); for (Throwable failure : failures) { runtimeException.addSuppressed(failure); } throw runtimeException; } return results.build(); } private static void waitForFutures(Iterable<ListenableFuture<?>> futures) { try { Futures.allAsList(futures).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException ignored) { } } private static class CallableFunction<F, T> implements Callable<T> { private final F item; private final Function<F, T> function; private CallableFunction(F item, Function<F, T> function) { this.item = item; this.function = function; } @Override public T call() { return function.apply(item); } } }