-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement
dependsOn
for cross-container dependencies (#1404)
* Implement `dependsOn` for cross-container dependencies * Add transitive test case * fix the compilation on JDK 11 * avoid a recursive update in `computeIfAbsent` * javadocs, add a test case for the diamond graph
- Loading branch information
Showing
4 changed files
with
261 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
core/src/main/java/org/testcontainers/lifecycle/Startable.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
core/src/main/java/org/testcontainers/lifecycle/Startables.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package org.testcontainers.lifecycle; | ||
|
||
import lombok.experimental.UtilityClass; | ||
|
||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.concurrent.*; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import java.util.stream.Stream; | ||
|
||
@UtilityClass | ||
public class Startables { | ||
|
||
private static final Executor EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() { | ||
|
||
private final AtomicLong COUNTER = new AtomicLong(0); | ||
|
||
@Override | ||
public Thread newThread(Runnable r) { | ||
Thread thread = new Thread(r, "testcontainers-lifecycle-" + COUNTER.getAndIncrement()); | ||
thread.setDaemon(true); | ||
return thread; | ||
} | ||
}); | ||
|
||
/** | ||
* @see #deepStart(Stream) | ||
*/ | ||
public CompletableFuture<Void> deepStart(Collection<Startable> startables) { | ||
return deepStart(startables.stream()); | ||
} | ||
|
||
/** | ||
* Start every {@link Startable} recursively and asynchronously and join on the result. | ||
* | ||
* Performance note: | ||
* The method uses and returns {@link CompletableFuture}s to resolve as many {@link Startable}s at once as possible. | ||
* This way, for the following graph: | ||
* / b \ | ||
* a e | ||
* c / | ||
* d / | ||
* "a", "c" and "d" will resolve in parallel, then "b". | ||
* | ||
* If we would call blocking {@link Startable#start()}, "e" would wait for "b", "b" for "a", and only then "c", and then "d". | ||
* But, since "c" and "d" are independent from "a", there is no point in waiting for "a" to be resolved first. | ||
* | ||
* @param startables a {@link Stream} of {@link Startable}s to start and scan for transitive dependencies. | ||
* @return a {@link CompletableFuture} that resolves once all {@link Startable}s have started. | ||
*/ | ||
public CompletableFuture<Void> deepStart(Stream<Startable> startables) { | ||
return deepStart(new HashMap<>(), startables); | ||
} | ||
|
||
/** | ||
* | ||
* @param started an intermediate storage for already started {@link Startable}s to prevent multiple starts. | ||
* @param startables a {@link Stream} of {@link Startable}s to start and scan for transitive dependencies. | ||
*/ | ||
private CompletableFuture<Void> deepStart(Map<Startable, CompletableFuture<Void>> started, Stream<Startable> startables) { | ||
CompletableFuture[] futures = startables | ||
.map(it -> { | ||
// avoid a recursive update in `computeIfAbsent` | ||
Map<Startable, CompletableFuture<Void>> subStarted = new HashMap<>(started); | ||
CompletableFuture<Void> future = started.computeIfAbsent(it, startable -> { | ||
return deepStart(subStarted, startable.getDependencies().stream()).thenRunAsync(startable::start, EXECUTOR); | ||
}); | ||
started.putAll(subStarted); | ||
return future; | ||
}) | ||
.toArray(CompletableFuture[]::new); | ||
|
||
return CompletableFuture.allOf(futures); | ||
} | ||
} |
142 changes: 142 additions & 0 deletions
142
core/src/test/java/org/testcontainers/junit/DependenciesTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package org.testcontainers.junit; | ||
|
||
import lombok.Getter; | ||
import org.junit.Test; | ||
import org.rnorth.visibleassertions.VisibleAssertions; | ||
import org.testcontainers.containers.GenericContainer; | ||
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; | ||
import org.testcontainers.lifecycle.Startable; | ||
import org.testcontainers.lifecycle.Startables; | ||
|
||
import java.util.HashSet; | ||
import java.util.Set; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import java.util.stream.Stream; | ||
|
||
public class DependenciesTest { | ||
|
||
@Test | ||
public void shouldWorkWithSimpleDependency() { | ||
InvocationCountingStartable startable = new InvocationCountingStartable(); | ||
|
||
try ( | ||
GenericContainer container = new GenericContainer() | ||
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()) | ||
.dependsOn(startable) | ||
) { | ||
container.start(); | ||
} | ||
|
||
VisibleAssertions.assertEquals("Started once", 1, startable.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("Does not trigger .stop()", 0, startable.getStopInvocationCount().intValue()); | ||
} | ||
|
||
@Test | ||
public void shouldWorkWithMutlipleDependencies() { | ||
InvocationCountingStartable startable1 = new InvocationCountingStartable(); | ||
InvocationCountingStartable startable2 = new InvocationCountingStartable(); | ||
|
||
try ( | ||
GenericContainer container = new GenericContainer() | ||
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()) | ||
.dependsOn(startable1, startable2) | ||
) { | ||
container.start(); | ||
} | ||
|
||
VisibleAssertions.assertEquals("Startable1 started once", 1, startable1.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("Startable2 started once", 1, startable2.getStartInvocationCount().intValue()); | ||
} | ||
|
||
@Test | ||
public void shouldStartEveryTime() { | ||
InvocationCountingStartable startable = new InvocationCountingStartable(); | ||
|
||
try ( | ||
GenericContainer container = new GenericContainer() | ||
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()) | ||
.dependsOn(startable) | ||
) { | ||
container.start(); | ||
container.stop(); | ||
|
||
container.start(); | ||
container.stop(); | ||
|
||
container.start(); | ||
} | ||
|
||
VisibleAssertions.assertEquals("Started multiple times", 3, startable.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("Does not trigger .stop()", 0, startable.getStopInvocationCount().intValue()); | ||
} | ||
|
||
@Test | ||
public void shouldStartTransitiveDependencies() { | ||
InvocationCountingStartable transitiveOfTransitiveStartable = new InvocationCountingStartable(); | ||
InvocationCountingStartable transitiveStartable = new InvocationCountingStartable(); | ||
transitiveStartable.getDependencies().add(transitiveOfTransitiveStartable); | ||
|
||
InvocationCountingStartable startable = new InvocationCountingStartable(); | ||
startable.getDependencies().add(transitiveStartable); | ||
|
||
try ( | ||
GenericContainer container = new GenericContainer() | ||
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()) | ||
.dependsOn(startable) | ||
) { | ||
container.start(); | ||
container.stop(); | ||
} | ||
|
||
VisibleAssertions.assertEquals("Root started", 1, startable.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("Transitive started", 1, transitiveStartable.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("Transitive of transitive started", 1, transitiveOfTransitiveStartable.getStartInvocationCount().intValue()); | ||
} | ||
|
||
@Test | ||
public void shouldHandleDiamondDependencies() throws Exception { | ||
InvocationCountingStartable a = new InvocationCountingStartable(); | ||
InvocationCountingStartable b = new InvocationCountingStartable(); | ||
InvocationCountingStartable c = new InvocationCountingStartable(); | ||
InvocationCountingStartable d = new InvocationCountingStartable(); | ||
// / b \ | ||
// a d | ||
// \ c / | ||
b.getDependencies().add(a); | ||
c.getDependencies().add(a); | ||
|
||
d.getDependencies().add(b); | ||
d.getDependencies().add(c); | ||
|
||
Startables.deepStart(Stream.of(d)).get(1, TimeUnit.SECONDS); | ||
|
||
VisibleAssertions.assertEquals("A started", 1, a.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("B started", 1, b.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("C started", 1, c.getStartInvocationCount().intValue()); | ||
VisibleAssertions.assertEquals("D started", 1, d.getStartInvocationCount().intValue()); | ||
} | ||
|
||
private static class InvocationCountingStartable implements Startable { | ||
|
||
@Getter | ||
Set<Startable> dependencies = new HashSet<>(); | ||
|
||
@Getter | ||
AtomicLong startInvocationCount = new AtomicLong(0); | ||
|
||
@Getter | ||
AtomicLong stopInvocationCount = new AtomicLong(0); | ||
|
||
@Override | ||
public void start() { | ||
startInvocationCount.getAndIncrement(); | ||
|
||
} | ||
|
||
@Override | ||
public void stop() { | ||
stopInvocationCount.getAndIncrement(); | ||
} | ||
} | ||
} |