Skip to content

Expand documentation about Singleton Containers and lambdas #6587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
80 changes: 78 additions & 2 deletions docs/test_framework_integration/manual_lifecycle_control.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Manual container lifecycle control

While Testcontainers was originally built with JUnit 4 integration in mind, it is fully usable with other test
While Testcontainers was originally built with JUnit 4 integration in mind, it is fully usable with other test
frameworks, or with no framework at all.

## Manually starting/stopping containers
Expand All @@ -13,7 +13,7 @@ try (GenericContainer container = new GenericContainer("imagename")) {
container.start();
// ... use the container
// no need to call stop() afterwards
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A stray format, will revert.

```

## Singleton containers
Expand Down Expand Up @@ -48,3 +48,79 @@ The singleton container is started only once when the base class is loaded.
The container can then be used by all inheriting test classes.
At the end of the test suite the [Ryuk container](https://github.com/testcontainers/moby-ryuk)
that is started by Testcontainers core will take care of stopping the singleton container.

Please keep in mind that putting the container in a static block will influence the configuration of the container.
You will not be able to use methods such as `forResponsePredicate` by just providing a Lambda expression, you will have to use
anonymous classes, or alternatively provide them from non-abstract class.
This is not due to the limitation of `TestContainers`, but because of how they work under the hood, namely that lambdas get compiled
to static methods on a class level. Since your container is in a static block, the container gets created
before your parent and children classes get initialized and as such you cannot pass the reference to them.

Therefore, once again - it is advised to use anonymous classes in such case or full predicates coming in from different, non-abstract class.

Instead of:

```java
abstract class AbstractContainerBaseTest {

static final GenericContainer GENERIC_CONTAINER;

static {
GENERIC_CONTAINER = new GENERIC_CONTAINER(
new ImageFromDockerfile().waitingFor(Wait.forHttp('/path'))
.forStatusCode(200)
.forResponsePredicate(yourLambda -> yourLambda(here)) //This is never going to get executed due to NoClassDefFoundError
);
GENERIC_CONTAINER.start();
}
}
```

You can do an anonymous class:

```java
abstract class AbstractContainerBaseTest {

static final GenericContainer GENERIC_CONTAINER;

static {
GENERIC_CONTAINER = new GENERIC_CONTAINER(
new ImageFromDockerfile().waitingFor(Wait.forHttp('/path'))
.forStatusCode(200)
.forResponsePredicate(new Predicate<String>() {

@Override
public boolean test(String s) {
return yourConditionHere;
}
})
);

GENERIC_CONTAINER.start();
}
}
```
Or full predicate coming in from different class:

```java
abstract class AbstractContainerBaseTest {

static final GenericContainer GENERIC_CONTAINER;

static {
GENERIC_CONTAINER = new GENERIC_CONTAINER(
new ImageFromDockerfile().waitingFor(Wait.forHttp('/path'))
.forStatusCode(200)
.forResponsePredicate(PredicateHolder.getPredicate())
);
GENERIC_CONTAINER.start();
}
}

public class PredicateHolder {

public static Predicate<String> getPredicate() {
return yourLambda -> yourLambda(here);
}
}
```
Comment on lines +52 to +126
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue reported in JDK which describe what's happening here. So, static methods from other classes can not be called from a lambda in a static block. But, if those are used as a method reference then it will work.

Also, if a container class is created then this issue can be avoided we would encourage to do that

class HWContainer extends GenericContainer<HWContainer> {
    public HWContainer() {
        super("testcontainers/helloworld");
        withExposedPorts(8080);
        waitingFor(Wait.forHttp("/ping").forResponsePredicate(x -> Util.isContains(x)));
    }
}

I would like to make a note about this and give the proper context and recommendations. Also, Can you please move the inline code to actual code and use codeblock in the file to display them? I think code examples would be avoided if the note is quite clear.

Note: The project is Testcontainers not TestContainers :)

Copy link
Author

@pan-daniel pan-daniel Feb 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! Thanks for the response.

I do firmly believe that this is not about deadlocking per se (even though somewhat related), but rather that static block gets initialized before the initialization of a class - and since the lambda gets compiled to a static field, trying to reference something that does not exist is impossible.

The reason why I am thinking that is we will not even get to the class initialization yet, as the container is just getting started.

Could you point me in the direction why do you believe that this is the deadlock situation?

Also, when you respond I will:

  1. Move out the examples to examples/ module. Question here, does it need to be completely valid code with imports etc. or can I just copy this somewhat pseudocode?
  2. Reference the JDK bug/discussion.
  3. Would you rather me deleting some examples rather than including them at all? From your message I got a feeling you'd delete some of them.