/*
* Copyright 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.EnumSet;
import java.util.List;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Assert;
import org.springframework.xd.dirt.core.RuntimeTimeoutException;
import org.springframework.xd.dirt.server.admin.deployment.DeploymentException;
import org.springframework.xd.dirt.server.admin.deployment.DeploymentMessage;
import org.springframework.xd.dirt.server.admin.deployment.DeploymentMessagePublisher;
import org.springframework.xd.dirt.zookeeper.Paths;
/**
* ZooKeeper based {@link DeploymentMessagePublisher} that publishes
* {@link DeploymentMessage deployment messages} into a ZooKeeper/Curator
* distributed queue.
* <p>
* The implementation of {@link #poll} blocks the executing thread until
* the message has been processed by the recipient. This is done by
* writing the {@link DeploymentMessage#requestId} to ZooKeeper and
* placing a watch on the children of the node to expect a response
* by the message handler.
*
* @author Ilayaperumal Gopinathan
* @author Patrick Peralta
*/
public class ZKDeploymentMessagePublisher implements DeploymentMessagePublisher {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Name of ZooKeeper node that indicates the deployment message
* was succesfully processed.
*/
public static final String SUCCESS = "success";
/**
* Name of ZooKeeper node that indicates the deployment message
* encountered an error during processing.
*/
public static final String ERROR = "error";
/**
* Queue for publishing deployment messages.
*/
private final DeploymentQueue deploymentQueue;
/**
* Amount of time to wait for a status to be written to all module
* deployment request paths.
*
* @see ModuleDeploymentWriter
* @see #getTimeout()
*/
@Value("${xd.admin.deploymentTimeout:30000}")
private long deploymentTimeout;
/**
* Construct the deployment message producer.
*
* @param deploymentQueue the deployment queue
*/
public ZKDeploymentMessagePublisher(DeploymentQueue deploymentQueue) {
this.deploymentQueue = deploymentQueue;
}
private CuratorFramework getClient() {
return this.deploymentQueue.getClient();
}
/**
* Return the amount of time in milliseconds to wait for the message
* consumer to process the deployment message. This value is 3 times
* the value of the configured module deployment timeout.
*
* @see #deploymentTimeout
* @return timeout for deployment message processing
*/
private long getTimeout() {
return deploymentTimeout * 3;
}
@Override
public void publish(DeploymentMessage message) {
try {
this.deploymentQueue.getDistributedQueue().put(message);
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void poll(DeploymentMessage message) {
CuratorFramework client = getClient();
String requestId = message.getRequestId();
Assert.hasText(requestId, "requestId for message required");
ResultWatcher watcher = new ResultWatcher();
String resultPath = Paths.build(Paths.DEPLOYMENTS, Paths.RESPONSES, requestId);
try {
logger.trace("result path: {}", resultPath);
client.create().creatingParentsIfNeeded().forPath(resultPath);
client.getChildren().usingWatcher(watcher).forPath(resultPath);
publish(message);
long timeout = getTimeout();
long expiry = System.currentTimeMillis() + timeout;
synchronized (this) {
while (watcher.getState() == State.incomplete && System.currentTimeMillis() < expiry) {
wait(timeout);
}
}
// reading the non volatile "state" and "errorDesc" outside of
// the synchronized block is safe because they are updated
// in a synchronized block and read after exiting the "wait"
// thus enforcing the "happens before" semantics of the JMM
switch (watcher.getState()) {
case incomplete:
throw new RuntimeTimeoutException(String.format("Request %s timed out after %d ms",
message, expiry));
case error:
throw new DeploymentException(watcher.getErrorDesc());
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new RuntimeException(e);
}
finally {
try {
client.delete().deletingChildrenIfNeeded().forPath(resultPath);
}
catch (Exception e) {
// logging this as debug; consequence is that the result
// path isn't deleted...this is not a fatal error
logger.debug("Exception while removing result path " + resultPath, e);
}
}
}
/**
* Enumeration of deployment message response states.
*/
private enum State {
/**
* The message handler has not posted a response to the message.
*/
incomplete,
/**
* The message handler successfully processed the message.
*/
success,
/**
* The message handler reported an error while processing the message.
*/
error
}
/**
* Watch that is triggered when a child is added to the deployment
* message result ZooKeeper node.
*/
private class ResultWatcher implements CuratorWatcher {
/**
* State of deployment message handling.
*/
private State state = State.incomplete;
/**
* Description of error encountered while handling the deployment
* message; this is only populated if an error occurs.
*/
private String errorDesc;
public State getState() {
return state;
}
public String getErrorDesc() {
return errorDesc;
}
@Override
public void process(WatchedEvent event) throws Exception {
logger.trace("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.NodeChildrenChanged) {
List<String> children = getClient().getChildren().forPath(event.getPath());
Assert.state(children.size() == 1);
synchronized (ZKDeploymentMessagePublisher.this) {
if (children.contains(ERROR)) {
errorDesc = new String(getClient().getData().forPath(Paths.build(event.getPath(), ERROR)));
state = State.error;
}
else {
state = State.success;
}
ZKDeploymentMessagePublisher.this.notifyAll();
}
}
else {
logger.debug("Ignoring event: {}", event);
}
}
}
}
}