/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.xd.dirt.server.admin.deployment.zk;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.xd.dirt.cluster.Container;
import org.springframework.xd.dirt.cluster.NoContainerException;
import org.springframework.xd.dirt.core.ModuleDeploymentsPath;
import org.springframework.xd.dirt.server.admin.deployment.ModuleDeploymentPropertiesProvider;
import org.springframework.xd.dirt.server.admin.deployment.ModuleDeploymentStatus;
import org.springframework.xd.dirt.zookeeper.Paths;
import org.springframework.xd.dirt.zookeeper.ZooKeeperConnection;
import org.springframework.xd.dirt.zookeeper.ZooKeeperUtils;
import org.springframework.xd.module.ModuleDeploymentProperties;
import org.springframework.xd.module.ModuleDescriptor;
import org.springframework.xd.module.ModuleType;
import org.springframework.xd.module.RuntimeModuleDeploymentProperties;
/**
* Utility class to write module deployment requests under {@code /xd/deployments/modules}.
* There are several {@code writeDeployment} methods that allow for targeting
* a deployment to a specific container or a collection of containers.
* <p/>
* General usage is to invoke {@code writeDeployment} and examine the {@link org.springframework.xd.dirt.server.admin.deployment.ModuleDeploymentStatus}
* object that is returned. This invocation will block until:
* <ul>
* <li>all containers have "responded" by updating the ZooKeeper nodes</li>
* <li>the timeout period elapses without all containers responding.</li>
* <li>the waiting thread is interrupted</li>
* </ul>
* The results may be examined to obtain detailed information about each deployment
* attempt and its result.
*
* @author Patrick Peralta
* @author Ilayaperumal Gopinathan
*
* @see org.springframework.xd.dirt.server.admin.deployment.DeploymentUnitStateCalculator
*/
public class ModuleDeploymentWriter {
/**
* Logger.
*/
private static final Logger logger = LoggerFactory.getLogger(ModuleDeploymentWriter.class);
@Autowired
private ZooKeeperConnection zkConnection;
/**
* Amount of time to wait for a status to be written to all module
* deployment request paths.
*
*/
@Value("${xd.admin.deploymentTimeout:30000}")
private long deploymentTimeout;
/**
* Write a module deployment request for the provided module descriptor
* using the provided properties to the given matched container.
*
* @param moduleDescriptor descriptor for module to deploy
* @param deploymentProperties deployment properties for module
* @param container the container to deploy
* @return result of request
* @throws InterruptedException if the executing thread is interrupted
* @throws NoContainerException if there are no containers that match the criteria
* for module deployment
*/
protected ModuleDeploymentStatus writeDeployment(ModuleDescriptor moduleDescriptor,
RuntimeModuleDeploymentProperties deploymentProperties, Container container)
throws InterruptedException, NoContainerException {
ResultCollector collector = new ResultCollector();
writeDeployment(moduleDescriptor, deploymentProperties, container, collector);
Collection<ModuleDeploymentStatus> statuses = processResults(collector);
if (statuses.isEmpty()) {
throw new NoContainerException();
}
return statuses.iterator().next();
}
/**
* Write module deployment requests for the provided module descriptor
* using the runtime deployment properties provided by the provider
* to the given matched containers.
*
* @param moduleDescriptor descriptor for module to deploy
* @param provider runtime deployment properties provider for the module
* @param containers the matched containers to deploy
* @return result of request
* @throws InterruptedException if the executing thread is interrupted
* @throws NoContainerException if there are no containers that match the criteria
* for module deployment
*/
protected Collection<ModuleDeploymentStatus> writeDeployment(ModuleDescriptor moduleDescriptor,
ModuleDeploymentPropertiesProvider<RuntimeModuleDeploymentProperties> provider,
Collection<Container> containers)
throws InterruptedException, NoContainerException {
ResultCollector collector = new ResultCollector();
for (Container container : containers) {
writeDeployment(moduleDescriptor, provider.propertiesForDescriptor(moduleDescriptor), container, collector);
}
Collection<ModuleDeploymentStatus> statuses = processResults(collector);
if (statuses.isEmpty()) {
throw new NoContainerException();
}
return statuses;
}
/**
* Writes the module deployment to the container.
*
* @param moduleDescriptor descriptor for module to deploy
* @param runtimeProperties runtime deployment properties provider for the module
* @param container the container to deploy
* @param collector the result collector
* @throws InterruptedException if the executing thread is interrupted
* @throws NoContainerException if there are no containers that match the criteria
* for module deployment
*/
private void writeDeployment(ModuleDescriptor moduleDescriptor,
RuntimeModuleDeploymentProperties runtimeProperties,
Container container, ResultCollector collector)
throws InterruptedException, NoContainerException {
int moduleSequence = runtimeProperties.getSequence();
String containerName = container.getName();
String deploymentPath = new ModuleDeploymentsPath()
.setContainer(containerName)
.setDeploymentUnitName(moduleDescriptor.getGroup())
.setModuleType(moduleDescriptor.getType().toString())
.setModuleLabel(moduleDescriptor.getModuleLabel())
.setModuleSequence(String.valueOf(moduleSequence)).build();
String statusPath = Paths.build(deploymentPath, Paths.STATUS);
collector.addPending(containerName, moduleSequence, moduleDescriptor.createKey());
try {
ensureModuleDeploymentPath(deploymentPath, statusPath, moduleDescriptor,
runtimeProperties, container);
// set the collector as a watch; it is possible that
// a. that the container has already updated this node (unlikely)
// b. the deployment was previously written; in this case read
// the status written by the container
byte[] data = zkConnection.getClient().getData().usingWatcher(collector).forPath(statusPath);
if (data != null && data.length > 0) {
collector.addResult(createResult(deploymentPath, data));
}
}
catch (InterruptedException e) {
throw e;
}
catch (Exception e) {
collector.addResult(createResult(deploymentPath, e));
}
}
/**
* Block the calling thread until all expected results are returned
* or until a timeout occurs. Additionally, remove any module deployment
* paths for deployments that failed or timed out.
*
* @param collector ZooKeeper watch used to collect results
* @return collection of results for module deployment requests
* @throws InterruptedException
*/
protected Collection<ModuleDeploymentStatus> processResults(ResultCollector collector) throws InterruptedException {
Collection<ModuleDeploymentStatus> statuses = collector.getResults();
// remove the ZK path for any failed deployments
for (ModuleDeploymentStatus deploymentStatus : statuses) {
if (deploymentStatus.getState() != ModuleDeploymentStatus.State.deployed) {
String path = new ModuleDeploymentsPath()
.setContainer(deploymentStatus.getContainer())
.setDeploymentUnitName(deploymentStatus.getKey().getGroup())
.setModuleType(deploymentStatus.getKey().getType().toString())
.setModuleLabel(deploymentStatus.getKey().getLabel())
.setModuleSequence(deploymentStatus.getModuleSequenceAsString()).build();
logger.debug("Unsuccessful deployment: {}; removing path {}", deploymentStatus, path);
try {
zkConnection.getClient().delete().deletingChildrenIfNeeded().forPath(path);
}
catch (InterruptedException e) {
throw e;
}
catch (KeeperException.NoNodeException e) {
// this node was already removed (perhaps by the supervisor
// as a result of the target container departing the cluster);
// this is safe to ignore
}
catch (Exception e) {
logger.warn("Error while cleaning up failed deployment " + path, e);
}
}
}
return statuses;
}
/**
* Ensure the creation of the provided path to deploy the module.
* If the path already exists, the {@link org.apache.zookeeper.KeeperException.NodeExistsException}
* is swallowed. All other exceptions are rethrown.
*
* @param deploymentPath ZooKeeper path to create for module deployment
* @param statusPath ZooKeeper path for module deployment status
* @param descriptor module descriptor for module to be deployed
* @param properties module deployment properties
* @param container target container for deployment
*
* @throws Exception if an exception is thrown during path creation
*/
private void ensureModuleDeploymentPath(String deploymentPath, String statusPath, ModuleDescriptor descriptor,
ModuleDeploymentProperties properties, Container container)
throws Exception {
try {
zkConnection.getClient().inTransaction()
.create().forPath(deploymentPath, ZooKeeperUtils.mapToBytes(properties)).and()
.create().forPath(statusPath).and().commit();
}
catch (KeeperException.NodeExistsException e) {
logger.info("Module {} is already deployed to container {}", descriptor, container);
}
catch (KeeperException.NoNodeException e) {
logger.error(String.format("Error creating the following deployment paths: %s, %s",
deploymentPath, statusPath), e);
throw e;
}
}
/**
* Create a {@link ModuleDeploymentStatus} from a ZooKeeper path and data.
*
* @param pathString ZooKeeper module deployment path
* @param data data for the path
* @return result based on data
*/
private ModuleDeploymentStatus createResult(String pathString, byte[] data) {
return createResult(pathString, ZooKeeperUtils.bytesToMap(data));
}
/**
* Create a {@link ModuleDeploymentStatus} from a ZooKeeper path and status map.
*
* @param pathString ZooKeeper module deployment path
* @param statusMap status map
* @return result based on status map
*/
private ModuleDeploymentStatus createResult(String pathString, Map<String, String> statusMap) {
ModuleDeploymentsPath path = new ModuleDeploymentsPath(pathString);
ModuleDescriptor.Key key = new ModuleDescriptor.Key(
path.getDeploymentUnitName(),
ModuleType.valueOf(path.getModuleType()),
path.getModuleLabel());
return new ModuleDeploymentStatus(path.getContainer(), path.getModuleSequence(), key,
statusMap);
}
/**
* Create a {@link ModuleDeploymentStatus} from a ZooKeeper path and a {@code Throwable}.
*
* @param pathString ZooKeeper module deployment path
* @param t exception thrown while attempting deployment
* @return result based on exception
*/
private ModuleDeploymentStatus createResult(String pathString, Throwable t) {
ModuleDeploymentsPath path = new ModuleDeploymentsPath(pathString);
ModuleDescriptor.Key key = new ModuleDescriptor.Key(
path.getDeploymentUnitName(),
ModuleType.valueOf(path.getModuleType()),
path.getModuleLabel());
return new ModuleDeploymentStatus(path.getContainer(), path.getModuleSequence(), key,
ModuleDeploymentStatus.State.failed, ZooKeeperUtils.getStackTrace(t));
}
/**
* Key used to track results of module deployments to a container.
*/
private class ContainerModuleKey {
/**
* Container name.
*/
private String container;
/**
* Module sequence number.
*/
private int moduleSequence;
/**
* Module descriptor key.
*/
private ModuleDescriptor.Key moduleDescriptorKey;
/**
* Construct a {@code ContainerModuleKey}.
*
* @param container container name
* @param moduleSequence module sequence number
* @param moduleDescriptorKey module descriptor key
*/
private ContainerModuleKey(String container, int moduleSequence, ModuleDescriptor.Key moduleDescriptorKey) {
this.container = container;
this.moduleSequence = moduleSequence;
this.moduleDescriptorKey = moduleDescriptorKey;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ContainerModuleKey that = (ContainerModuleKey) o;
return this.container.equals(that.container) &&
(this.moduleSequence == that.moduleSequence) &&
this.moduleDescriptorKey.equals(that.moduleDescriptorKey);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = container.hashCode();
result = 31 * result + moduleSequence;
result = 31 * result + moduleDescriptorKey.hashCode();
return result;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "ContainerModuleKey{" +
"container='" + container + '\'' +
"moduleSequence'" + moduleSequence + '\'' +
", moduleDescriptorKey=" + moduleDescriptorKey +
'}';
}
}
/**
* Implementation of {@link org.apache.curator.framework.api.CuratorWatcher}
* used to collect results from the target containers updating the
* module deployment paths.
*/
private class ResultCollector implements CuratorWatcher {
/**
* Pending requests for module deployments to containers.
*/
private final Set<ContainerModuleKey> pending = new HashSet<ContainerModuleKey>();
/**
* Deployment request results for modules and containers.
*/
private final Map<ContainerModuleKey, ModuleDeploymentStatus> results =
new HashMap<ContainerModuleKey, ModuleDeploymentStatus>();
/**
* {@inheritDoc}
*/
@Override
public void process(WatchedEvent event) throws Exception {
logger.trace("EventCollector received event: {}", event);
if (EnumSet.of(Watcher.Event.KeeperState.SyncConnected,
Watcher.Event.KeeperState.SaslAuthenticated,
Watcher.Event.KeeperState.ConnectedReadOnly).contains(event.getState())) {
if (event.getType() == Watcher.Event.EventType.NodeDataChanged) {
byte[] data = zkConnection.getClient().getData().forPath(event.getPath());
addResult(createResult(event.getPath(), data));
}
else {
logger.debug("Ignoring event: {}", event);
}
}
}
/**
* Indicate that a reply is expected for a module deployment request
* to the container for the module indicated by the module descriptor key.
*
* @param container container name
* @param moduleSequence module sequence
* @param key module descriptor key
*/
public synchronized void addPending(String container, int moduleSequence, ModuleDescriptor.Key key) {
pending.add(new ContainerModuleKey(container, moduleSequence, key));
}
/**
* Add an incoming result for a module deployment request.
*
* @param deploymentStatus incoming result
*/
public synchronized void addResult(ModuleDeploymentStatus deploymentStatus) {
ContainerModuleKey key = new ContainerModuleKey(deploymentStatus.getContainer(),
deploymentStatus.getModuleSequence(),
deploymentStatus.getKey());
pending.remove(key);
results.put(key, deploymentStatus);
notifyAll();
}
/**
* Block until all:
* <ul>
* <li>All pending requests have been responded to.</li>
* <li>A timeout occurs; in this case a collection of
* {@link ModuleDeploymentStatus}
* is returned. These results can be examined to see which container(s)
* timed out.</li>
* <li>The thread invoking this method is interrupted.</li>
* </ul>
*
* @return collection of results
* @throws InterruptedException
*/
public synchronized Collection<ModuleDeploymentStatus> getResults() throws InterruptedException {
long now = System.currentTimeMillis();
long expiryTime = now + deploymentTimeout;
while (pending.size() > 0 && now < expiryTime) {
wait(expiryTime - now);
now = System.currentTimeMillis();
}
// if there are any module descriptors in the pending set,
// this means the ZooKeeper node for that module deployment
// was never updated
for (ContainerModuleKey key : pending) {
results.put(key,
new ModuleDeploymentStatus(key.container, Integer.valueOf(key.moduleSequence),
key.moduleDescriptorKey,
ModuleDeploymentStatus.State.failed,
String.format("Deployment of module '%s' to container '%s' timed out after %d ms",
key.moduleDescriptorKey, key.container, deploymentTimeout)));
}
return results.values();
}
}
}