/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.util.core.task;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.util.core.task.BasicExecutionContext;
import org.apache.brooklyn.util.core.task.BasicExecutionManager;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Stopwatch;
public class BasicTasksFutureTest {
private static final Logger log = LoggerFactory.getLogger(BasicTasksFutureTest.class);
private BasicExecutionManager em;
private BasicExecutionContext ec;
private Map<Object,Object> data;
private ExecutorService ex;
private Semaphore started;
private Semaphore waitInTask;
private Semaphore cancelledWhileSleeping;
@BeforeMethod(alwaysRun=true)
public void setUp() {
em = new BasicExecutionManager("mycontext");
ec = new BasicExecutionContext(em);
ex = Executors.newCachedThreadPool();
data = Collections.synchronizedMap(new LinkedHashMap<Object,Object>());
started = new Semaphore(0);
waitInTask = new Semaphore(0);
cancelledWhileSleeping = new Semaphore(0);
}
@AfterMethod(alwaysRun=true)
public void tearDown() throws Exception {
if (em != null) em.shutdownNow();
if (ex != null) ex.shutdownNow();
}
@Test
public void testBlockAndGetWithTimeoutsAndListenableFuture() throws InterruptedException {
Task<String> t = waitForSemaphore(Duration.FIVE_SECONDS, true, "x");
Assert.assertFalse(t.blockUntilEnded(Duration.millis(1)));
Assert.assertFalse(t.blockUntilEnded(Duration.ZERO));
boolean didNotThrow = false;
try { t.getUnchecked(Duration.millis(1)); didNotThrow = true; }
catch (Exception e) { /* expected */ }
Assert.assertFalse(didNotThrow);
try { t.getUnchecked(Duration.ZERO); didNotThrow = true; }
catch (Exception e) { /* expected */ }
Assert.assertFalse(didNotThrow);
addFutureListener(t, "before");
ec.submit(t);
Assert.assertFalse(t.blockUntilEnded(Duration.millis(1)));
Assert.assertFalse(t.blockUntilEnded(Duration.ZERO));
try { t.getUnchecked(Duration.millis(1)); didNotThrow = true; }
catch (Exception e) { /* expected */ }
Assert.assertFalse(didNotThrow);
try { t.getUnchecked(Duration.ZERO); didNotThrow = true; }
catch (Exception e) { /* expected */ }
Assert.assertFalse(didNotThrow);
addFutureListener(t, "during");
synchronized (data) {
// now let it finish
waitInTask.release();
Assert.assertTrue(t.blockUntilEnded(Duration.TEN_SECONDS));
Assert.assertEquals(t.getUnchecked(Duration.millis(1)), "x");
Assert.assertEquals(t.getUnchecked(Duration.ZERO), "x");
Assert.assertNull(data.get("before"));
Assert.assertNull(data.get("during"));
// can't set the data(above) until we release the lock (in assert call below)
assertSoonGetsData("before");
assertSoonGetsData("during");
}
// and see that a listener added late also runs
synchronized (data) {
addFutureListener(t, "after");
Assert.assertNull(data.get("after"));
assertSoonGetsData("after");
}
}
private void addFutureListener(Task<String> t, final String key) {
t.addListener(new Runnable() { public void run() {
synchronized (data) {
log.info("notifying for "+key);
data.notifyAll();
data.put(key, true);
}
}}, ex);
}
private void assertSoonGetsData(String key) throws InterruptedException {
for (int i=0; i<10; i++) {
if (Boolean.TRUE.equals(data.get(key))) {
log.info("got data for "+key);
return;
}
data.wait(Duration.ONE_SECOND.toMilliseconds());
}
Assert.fail("did not get data for '"+key+"' in time");
}
private <T> Task<T> waitForSemaphore(final Duration time, final boolean requireSemaphore, final T result) {
return Tasks.<T>builder().body(new Callable<T>() {
public T call() {
try {
started.release();
log.info("waiting up to "+time+" to acquire before returning "+result);
if (!waitInTask.tryAcquire(time.toMilliseconds(), TimeUnit.MILLISECONDS)) {
log.info("did not get semaphore");
if (requireSemaphore) Assert.fail("task did not get semaphore");
} else {
log.info("got semaphore");
}
} catch (Exception e) {
log.info("cancelled before returning "+result);
cancelledWhileSleeping.release();
throw Exceptions.propagate(e);
}
log.info("task returning "+result);
return result;
}
}).build();
}
@Test
public void testCancelAfterStartTriggersListenableFuture() throws Exception {
doTestCancelTriggersListenableFuture(Duration.millis(50));
}
@Test
public void testCancelImmediateTriggersListenableFuture() throws Exception {
// if cancel fires after submit but before it passes to the executor,
// that needs handling separately; this doesn't guarantee this code path,
// but it happens sometimes (and it should be handled)
doTestCancelTriggersListenableFuture(Duration.ZERO);
}
public void doTestCancelTriggersListenableFuture(Duration delay) throws Exception {
Task<String> t = waitForSemaphore(Duration.TEN_SECONDS, true, "x");
addFutureListener(t, "before");
Stopwatch watch = Stopwatch.createStarted();
ec.submit(t);
addFutureListener(t, "during");
log.info("test cancelling "+t+" ("+t.getClass()+") after "+delay);
// NB: two different code paths (callers to this method) for notifying futures
// depending whether task is started
Time.sleep(delay);
synchronized (data) {
t.cancel(true);
assertSoonGetsData("before");
assertSoonGetsData("during");
addFutureListener(t, "after");
Assert.assertNull(data.get("after"));
assertSoonGetsData("after");
}
Assert.assertTrue(t.isDone());
Assert.assertTrue(t.isCancelled());
try {
t.get();
Assert.fail("should have thrown CancellationException");
} catch (CancellationException e) { /* expected */ }
Assert.assertTrue(watch.elapsed(TimeUnit.MILLISECONDS) < Duration.FIVE_SECONDS.toMilliseconds(),
Time.makeTimeStringRounded(watch.elapsed(TimeUnit.MILLISECONDS))+" is too long; should have cancelled very quickly");
if (started.tryAcquire())
// if the task is begun, this should get released
Assert.assertTrue(cancelledWhileSleeping.tryAcquire(5, TimeUnit.SECONDS));
}
}