/*
* JBoss, Home of Professional Open Source.
* Copyright 2015, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.server.deployment.scanner;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CANCELLED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.COMPOSITE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEPLOY;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ENABLED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FULL_REPLACE_DEPLOYMENT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.PERSISTENT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_CHILDREN_RESOURCES_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ROLLED_BACK;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STEPS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.UNDEPLOY;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.as.controller.client.Operation;
import org.jboss.as.controller.client.OperationMessageHandler;
import org.jboss.as.controller.client.OperationResponse;
import org.jboss.as.server.deployment.scanner.api.DeploymentOperations;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.jboss.threads.AsyncFuture;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Unit tests of {@link FileSystemDeploymentService}.
*
* @author Brian Stansberry (c) 2011 Red Hat Inc.
*/
public class ShutdownFileSystemDeploymentServiceUnitTestCase {
private static Logger logger = Logger.getLogger(ShutdownFileSystemDeploymentServiceUnitTestCase.class);
private static long count = System.currentTimeMillis();
private static final Random random = new Random(System.currentTimeMillis());
private static final DiscardTaskExecutor executor = new DiscardTaskExecutor();
private static final PathAddress resourceAddress = PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, DeploymentScannerExtension.SUBSYSTEM_NAME),
PathElement.pathElement(DeploymentScannerExtension.SCANNERS_PATH.getKey(), DeploymentScannerExtension.DEFAULT_SCANNER_NAME));
private static AutoDeployTestSupport testSupport;
private File tmpDir;
@BeforeClass
public static void createTestSupport() throws Exception {
testSupport = new AutoDeployTestSupport(ShutdownFileSystemDeploymentServiceUnitTestCase.class.getSimpleName());
}
@AfterClass
public static void cleanup() throws Exception {
if (testSupport != null) {
testSupport.cleanupFiles();
}
}
@Before
public void setup() throws Exception {
executor.clear();
File root = testSupport.getTempDir();
for (int i = 0; i < 200; i++) {
tmpDir = new File(root, String.valueOf(count++));
if (!tmpDir.exists() && tmpDir.mkdirs()) {
break;
}
}
if (!tmpDir.exists()) {
throw new RuntimeException("cannot create tmpDir");
}
}
@After
public void tearDown() throws Exception {
testSupport.cleanupChannels();
}
@Test
public void testUncleanShutdown() throws Exception {
File deployment = new File(tmpDir, "foo.war");
final DiscardTaskExecutor myExecutor = new DiscardTaskExecutor();
MockServerController sc = new MockServerController(myExecutor);
final BlockingDeploymentOperations ops = new BlockingDeploymentOperations(sc);
final FileSystemDeploymentService testee = new FileSystemDeploymentService(resourceAddress, null, tmpDir, null, sc, myExecutor, null);
testee.setAutoDeployZippedContent(true);
sc.addCompositeSuccessResponse(1);
testSupport.createZip(deployment, 0, false, false, true, true);
Future<Boolean> lockDone = Executors.newSingleThreadExecutor().submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
try {
synchronized (ops.lock) {
logger.info("Executor service should be locked");
while (!ops.ready) {//Waiting for deployment to start.
Thread.sleep(100);
}
logger.info("About to stop the scanner");
testee.stopScanner();
logger.info("Closing executor service " + myExecutor);
myExecutor.shutdown();
logger.info("Executor service should be closed");
}
return true;
} catch (InterruptedException ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
});
final File dodeploy = new File(tmpDir, "foo.war" + FileSystemDeploymentService.DO_DEPLOY);
Files.createFile(dodeploy.toPath());
testee.startScanner(ops);
testee.scan();
lockDone.get(100000, TimeUnit.MILLISECONDS);
}
private static class MockServerController implements ModelControllerClient, DeploymentOperations.Factory {
private final DiscardTaskExecutor executorService;
private final List<ModelNode> requests = new ArrayList<ModelNode>(1);
private final List<Response> responses = new ArrayList<Response>(1);
private final Map<String, byte[]> added = new HashMap<String, byte[]>();
private final Map<String, byte[]> deployed = new HashMap<String, byte[]>();
private final Set<String> externallyDeployed = new HashSet<String>();
@Override
public ModelNode execute(ModelNode operation) throws IOException {
requests.add(operation);
return processOp(operation);
}
@Override
public ModelNode execute(Operation operation) throws IOException {
return execute(operation.getOperation());
}
@Override
public ModelNode execute(ModelNode operation, OperationMessageHandler messageHandler) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public ModelNode execute(Operation operation, OperationMessageHandler messageHandler) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public OperationResponse executeOperation(Operation operation, OperationMessageHandler messageHandler) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public AsyncFuture<ModelNode> executeAsync(final ModelNode operation, OperationMessageHandler messageHandler) {
logger.info("Executing deploy command from MockServerController, its executor service should be closed");
return executorService.submit(new Callable<ModelNode>() {
@Override
public ModelNode call() throws Exception {
return execute(operation);
}
});
}
@Override
public AsyncFuture<ModelNode> executeAsync(Operation operation, OperationMessageHandler messageHandler) {
throw new UnsupportedOperationException();
}
@Override
public AsyncFuture<OperationResponse> executeOperationAsync(Operation operation, OperationMessageHandler messageHandler) {
throw new UnsupportedOperationException();
}
@Override
public void close() throws IOException {
}
@Override
public DeploymentOperations create() {
return new DefaultDeploymentOperations(this);
}
private static class Response {
private final boolean ok;
private final ModelNode rsp;
Response(boolean ok, ModelNode rsp) {
this.ok = ok;
this.rsp = rsp;
}
}
MockServerController(String... existingDeployments) {
this(executor, existingDeployments);
}
MockServerController(DiscardTaskExecutor executorService, String... existingDeployments) {
for (String dep : existingDeployments) {
added.put(dep, randomHash());
deployed.put(dep, added.get(dep));
}
this.executorService = executorService;
}
public void addCompositeSuccessResponse(int count) {
ModelNode rsp = new ModelNode();
rsp.get(OUTCOME).set(SUCCESS);
ModelNode result = rsp.get(RESULT);
for (int i = 1; i <= count; i++) {
result.get("step-" + i, OUTCOME).set(SUCCESS);
result.get("step-" + i, RESULT);
}
responses.add(new Response(true, rsp));
}
public void addCompositeFailureResponse(int count, int failureStep) {
if (count < failureStep) {
throw new IllegalArgumentException("failureStep must be > count");
}
ModelNode rsp = new ModelNode();
rsp.get(OUTCOME).set(FAILED);
ModelNode result = rsp.get(RESULT);
for (int i = 1; i <= count; i++) {
String step = "step-" + i;
if (i < failureStep) {
result.get(step, OUTCOME).set(FAILED);
result.get(step, RESULT);
result.get(step, ROLLED_BACK).set(true);
} else if (i == failureStep) {
result.get(step, OUTCOME).set(FAILED);
result.get(step, FAILURE_DESCRIPTION).set(new ModelNode().set("badness happened"));
result.get(step, ROLLED_BACK).set(true);
} else {
result.get(step, OUTCOME).set(CANCELLED);
}
}
rsp.get(FAILURE_DESCRIPTION).set(new ModelNode().set("badness happened"));
rsp.get(ROLLED_BACK).set(true);
responses.add(new Response(true, rsp));
}
public void addCompositeFailureResultResponse(int count, int failureStep) {
if (count < failureStep) {
throw new IllegalArgumentException("failureStep must be > count");
}
ModelNode rsp = new ModelNode();
rsp.get(OUTCOME).set(SUCCESS);
ModelNode result = rsp.get(RESULT);
ModelNode failedStep = result.get("step-" + failureStep);
failedStep.get(OUTCOME).set(SUCCESS);
ModelNode stepResult = failedStep.get(RESULT);
for (int i = 1; i <= count; i++) {
String step = "step-" + i;
if (i < failureStep) {
stepResult.get(step, OUTCOME).set(SUCCESS);
stepResult.get(step, RESULT);
stepResult.get(step, ROLLED_BACK).set(true);
} else if (i == failureStep) {
stepResult.get(step, OUTCOME).set(FAILED);
stepResult.get(step, FAILURE_DESCRIPTION).set(new ModelNode().set("true failed step"));
stepResult.get(step, ROLLED_BACK).set(true);
} else {
stepResult.get(step, OUTCOME).set(CANCELLED);
}
}
rsp.get(FAILURE_DESCRIPTION).set(new ModelNode().set("badness happened"));
rsp.get(ROLLED_BACK).set(true);
responses.add(new Response(true, rsp));
}
public void addPartialCompositeFailureResultResponse(int count, int failureStep) {
if (count < failureStep) {
throw new IllegalArgumentException("failureStep must be > count");
}
ModelNode rsp = new ModelNode();
rsp.get(OUTCOME).set(SUCCESS);
ModelNode result = rsp.get(RESULT);
for (int i = 1; i <= count; i++) {
String step = "step-" + i;
if (i < failureStep) {
result.get(step, OUTCOME).set(SUCCESS);
result.get(step, RESULT);
result.get(step, RESULT, "step-1", OUTCOME).set(SUCCESS);
result.get(step, RESULT, "step-1", RESULT);
result.get(step, RESULT, "step-2", OUTCOME).set(SUCCESS);
result.get(step, RESULT, "step-2", RESULT);
} else if (i == failureStep) {
result.get(step, OUTCOME).set(SUCCESS);
result.get(step, RESULT);
result.get(step, RESULT, "step-1", OUTCOME).set(SUCCESS);
result.get(step, RESULT, "step-1", RESULT);
result.get(step, RESULT, "step-2", OUTCOME).set(FAILED);
result.get(step, RESULT, "step-2", FAILURE_DESCRIPTION).set(new ModelNode().set("badness happened"));
} else {
result.get(step, OUTCOME).set(CANCELLED);
}
}
responses.add(new Response(true, rsp));
}
private ModelNode getDeploymentNamesResponse() {
ModelNode content = new ModelNode();
content.get(OUTCOME).set(SUCCESS);
ModelNode result = content.get(RESULT);
result.setEmptyObject();
for (String deployment : added.keySet()) {
result.get(deployment, ENABLED).set(deployed.containsKey(deployment));
result.get(deployment, PERSISTENT).set(externallyDeployed.contains(deployment));
}
return content;
}
private ModelNode processOp(ModelNode op) {
String opName = op.require(OP).asString();
if (READ_CHILDREN_RESOURCES_OPERATION.equals(opName)) {
return getDeploymentNamesResponse();
} else if (COMPOSITE.equals(opName)) {
for (ModelNode child : op.require(STEPS).asList()) {
opName = child.require(OP).asString();
if (COMPOSITE.equals(opName)) {
return processOp(child);
}
if (responses.isEmpty()) {
Assert.fail("unexpected request " + op);
return null; // unreachable
}
if (!responses.get(0).ok) {
// don't change state for a failed response
continue;
}
PathAddress address = PathAddress.pathAddress(child.require(OP_ADDR));
if (ADD.equals(opName)) {
// Since AS7-431 the content is no longer managed
//added.put(address.getLastElement().getValue(), child.require(CONTENT).require(0).require(HASH).asBytes());
added.put(address.getLastElement().getValue(), randomHash());
} else if (REMOVE.equals(opName)) {
added.remove(address.getLastElement().getValue());
} else if (DEPLOY.equals(opName)) {
String name = address.getLastElement().getValue();
deployed.put(name, added.get(name));
} else if (UNDEPLOY.equals(opName)) {
deployed.remove(address.getLastElement().getValue());
} else if (FULL_REPLACE_DEPLOYMENT.equals(opName)) {
String name = child.require(NAME).asString();
// Since AS7-431 the content is no longer managed
//byte[] hash = child.require(CONTENT).require(0).require(HASH).asBytes();
final byte[] hash = randomHash();
added.put(name, hash);
deployed.put(name, hash);
} else {
throw new IllegalArgumentException("unexpected step " + opName);
}
}
return responses.remove(0).rsp;
} else {
throw new IllegalArgumentException("unexpected operation " + opName);
}
}
}
private static class DiscardTaskExecutor extends ScheduledThreadPoolExecutor {
private final List<Runnable> tasks = new ArrayList<Runnable>();
private final Set<CallOnGetFuture<?>> futures = new HashSet<CallOnGetFuture<?>>();
private DiscardTaskExecutor() {
super(0);
}
@Override
public ScheduledFuture<?> schedule(final Runnable command, final long delay, final TimeUnit delayUnit) {
tasks.add(command);
return new RunnableScheduledFuture() {
private FutureTask<?> task = new FutureTask<>(command, new Object());
@Override
public boolean isPeriodic() {
return false;
}
@Override
public void run() {
task.run();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
logger.info("Task is being cancelled " + mayInterruptIfRunning);
return task.cancel(mayInterruptIfRunning);
}
@Override
public boolean isCancelled() {
return task.isCancelled();
}
@Override
public boolean isDone() {
return task.isDone();
}
@Override
public Object get() throws InterruptedException, ExecutionException {
return task.get();
}
@Override
public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return task.get(timeout, unit);
}
@Override
public long getDelay(TimeUnit unit) {
return delayUnit.convert(delay, unit);
}
@Override
public int compareTo(Object o) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
};
}
@Override
public <T> AsyncFuture<T> submit(Callable<T> tCallable) {
if (isShutdown() || isTerminating()) {
throw new RejectedExecutionException("DiscardTaskExecutor has shutdown we can't run " + tCallable);
}
CallOnGetFuture<T> future = new CallOnGetFuture<>(tCallable);
futures.add(future);
return future;
}
@Override
public List<Runnable> shutdownNow() {
List<Runnable> superList = super.shutdownNow();
for(CallOnGetFuture<?> future : futures) {
future.cancel(true);
}
superList.addAll(tasks);
return superList;
}
@Override
public void shutdown() {
super.shutdown();
for(CallOnGetFuture<?> future : futures) {
future.cancel(false);
}
}
void clear() {
tasks.clear();
}
}
private static class CallOnGetFuture<T> implements AsyncFuture<T> {
final Callable<T> callable;
private boolean cancelled = false;
private CallOnGetFuture(Callable<T> callable) {
this.callable = callable;
}
@Override
public boolean cancel(boolean interrupt) {
this.cancelled = true;
logger.info("CallOnGetFuture is to be cancelled " + callable);
if (interrupt) {
logger.info("CallOnGetFuture interrupted " + callable);
Thread.currentThread().interrupt();
return true;
}
return false;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public boolean isDone() {
return false;
}
@Override
public T get() throws InterruptedException, ExecutionException {
try {
logger.info("CallOnGetFuture get " + callable);
return callable.call();
} catch (InterruptedException | ExecutionException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public T get(long l, TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException {
return get();
}
@Override
public AsyncFuture.Status await() throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public AsyncFuture.Status await(long timeout, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public T getUninterruptibly() throws CancellationException, ExecutionException {
throw new UnsupportedOperationException();
}
@Override
public T getUninterruptibly(long timeout, TimeUnit unit) throws CancellationException, ExecutionException, TimeoutException {
throw new UnsupportedOperationException();
}
@Override
public AsyncFuture.Status awaitUninterruptibly() {
throw new UnsupportedOperationException();
}
@Override
public AsyncFuture.Status awaitUninterruptibly(long timeout, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public AsyncFuture.Status getStatus() {
throw new UnsupportedOperationException();
}
@Override
public <A> void addListener(AsyncFuture.Listener<? super T, A> listener, A attachment) {
throw new UnsupportedOperationException();
}
@Override
public void asyncCancel(boolean interruptionDesired) {
throw new UnsupportedOperationException();
}
}
private static class BlockingDeploymentOperations implements DeploymentOperations {
private volatile boolean ready = false;
private final DefaultDeploymentOperations delegate;
private final Object lock = new Object();
BlockingDeploymentOperations(final ModelControllerClient controllerClient) {
delegate = new DefaultDeploymentOperations(controllerClient);
}
@Override
public Future<ModelNode> deploy(final ModelNode operation, ExecutorService executorService) {
ready = true;
logger.info("Ready to deploy");
synchronized(lock) {
logger.info("Deploying on delegate.");
return delegate.deploy(operation, null);
}
}
@Override
public Map<String, Boolean> getDeploymentsStatus() {
return delegate.getDeploymentsStatus();
}
@Override
public void close() throws IOException {
delegate.close();
}
@Override
public Set<String> getUnrelatedDeployments(ModelNode owner) {
return delegate.getUnrelatedDeployments(owner);
}
}
private static byte[] randomHash() {
final byte[] hash = new byte[20];
random.nextBytes(hash);
return hash;
}
}