Skip to content

Commit e383b40

Browse files
committed
Move per-class fixture state from static to instance fields in lettuce-5.1 abstract test bases
When an abstract test base is shared across multiple concrete subclasses run in the same JVM fork, static fixture state (containers, clients, host/port, the AutoCleanupExtension itself, etc.) becomes JVM-wide state that subclasses fight over. Two concrete failure modes were observed/possible: 1. AutoCleanupExtension.afterAll latches rootContext on the first subclass's @BeforeAll and never resets it, so deferAfterAll callbacks only run for the first subclass; subsequent subclasses leak their containers/clients/connections. 2. The auth subclass's redisServer.withCommand("--requirepass password") becomes a no-op when an earlier subclass has already started the (shared static) container — the suspected root cause of the LettuceSyncClientAuthTest flake. Fix: convert per-class state to instance fields on classes already annotated @testinstance(PER_CLASS), so each concrete subclass gets its own container/client lifecycle. spanName(String) becomes an instance method since it reads instance host/port. The auth subclass no longer needs a 'set back' withCommand cleanup, since each class has its own container instance. Codifies the pattern in .github/agents/knowledge/testing-general-patterns.md.
1 parent e8169cb commit e383b40

8 files changed

Lines changed: 46 additions & 21 deletions

File tree

.github/agents/knowledge/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Load only files relevant to the current scope to reduce noise and avoid over-con
1818
| `javaagent-singletons-patterns.md` | `*Singletons`, `*SpanNaming`, and similar holder classes; singleton accessors; callers of singleton accessors/fields |
1919
| `library-patterns.md` | Library instrumentation telemetry, builder, getter, or setter pattern changes |
2020
| `module-naming.md` | New or renamed modules or packages; settings includes |
21-
| `testing-general-patterns.md` | Test files in scope — assertion style, test method signatures and throws clauses, resource cleanup patterns, attribute assertion patterns, `satisfies()` lambda usage |
21+
| `testing-general-patterns.md` | Test files in scope — assertion style, test method signatures and throws clauses, resource cleanup patterns, abstract test base class state shape, attribute assertion patterns, `satisfies()` lambda usage |
2222
| `testing-experimental-flags.md` | `testExperimental` task or experimental span-attribute assertions |
2323
| `testing-semconv-stability.md` | Semconv opt-in modes, `emitOld*`/`emitStable*`, `maybeStable`, Semconv test tasks |
2424

.github/agents/knowledge/testing-general-patterns.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@
7777
- If the test intentionally closes the resource mid-test or asserts behavior around explicit
7878
close, keep the direct close or try-with-resources in the test body.
7979

80+
## Abstract Test Base Classes — Per-Class State Goes On Instance Fields
81+
82+
When an abstract test base is shared across multiple concrete subclasses run in the same
83+
JVM fork, treat per-class fixture state (containers, clients, connections, host/port, the
84+
`AutoCleanupExtension` itself, etc.) as **instance state on a `@TestInstance(PER_CLASS)`
85+
class**, not as `static` fields.
86+
87+
- Annotate the base with `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`. JUnit then creates
88+
one instance per concrete subclass and reuses it for every `@Test` in that class, so
89+
instance fields give the same once-per-class lifecycle that `static` fields used to provide,
90+
without leaking across subclasses.
91+
- Make `redisServer` / `kafkaContainer` / `client` / `host` / `port` / etc. instance fields
92+
(`protected`, not `protected static`). Each concrete subclass then gets its own container
93+
and client, freshly configured by its own `@BeforeAll`.
94+
- Make `@RegisterExtension AutoCleanupExtension cleanup` an **instance** field, not `static`.
95+
A shared `static` `AutoCleanupExtension` only runs `deferAfterAll(...)` callbacks for the
96+
first subclass JUnit invokes — its `rootContext` is set on first `beforeAll` and never
97+
reset — so subsequent subclasses leak deferred resources (containers, clients, connections).
98+
- Helpers that read instance fields (e.g. `spanName(String)` reading `host`/`port`) must
99+
also be instance methods, not `static`.
100+
- This is also the right shape when a subclass needs to mutate the container's builder
101+
configuration (e.g. `withCommand("--requirepass password")`). With shared `static` state the
102+
earlier subclass may have already started the container, so `withCommand` on the running
103+
instance has no effect; with per-instance state each subclass mutates and starts its own
104+
fresh container.
105+
- Reserve `static` for true JVM-wide constants (`DB_INDEX`, loggers, `*_HASH_MAP` literals,
106+
pure helpers like `addExtraAttributes(...)` / `assertCommandEncodeEvents(...)`).
107+
80108
## Span Attribute Assertions
81109

82110
- Use `span.hasAttributesSatisfyingExactly(...)` with `equalTo(...)`/`satisfies(...)` for

instrumentation/lettuce/lettuce-5.1/javaagent/src/testCompatibility/java/io/opentelemetry/javaagent/instrumentation/lettuce/v5_1/LettuceCompatibilityTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected RedisClient createClient(String uri) {
3434
return RedisClient.create(uri);
3535
}
3636

37-
private static RedisCommands<String, String> syncCommands;
37+
private RedisCommands<String, String> syncCommands;
3838

3939
@BeforeAll
4040
void setUp() throws UnknownHostException {
@@ -54,7 +54,7 @@ void setUp() throws UnknownHostException {
5454
}
5555

5656
@AfterAll
57-
static void cleanUp() {
57+
void cleanUp() {
5858
connection.close();
5959
redisClient.shutdown();
6060
redisServer.stop();

instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceAsyncClientTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@
5151

5252
@SuppressWarnings({"InterruptedExceptionSwallowed", "deprecation"}) // using deprecated semconv
5353
public abstract class AbstractLettuceAsyncClientTest extends AbstractLettuceClientTest {
54-
private static String dbUriNonExistent;
55-
private static int incorrectPort;
54+
private String dbUriNonExistent;
55+
private int incorrectPort;
5656

5757
private static final ImmutableMap<String, String> TEST_HASH_MAP =
5858
ImmutableMap.of(
5959
"firstname", "John",
6060
"lastname", "Doe",
6161
"age", "53");
6262

63-
private static RedisAsyncCommands<String, String> asyncCommands;
63+
private RedisAsyncCommands<String, String> asyncCommands;
6464

6565
@BeforeAll
6666
void setUp() throws UnknownHostException {

instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceClientTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@
2828
public abstract class AbstractLettuceClientTest {
2929
private static final Logger logger = LoggerFactory.getLogger(AbstractLettuceClientTest.class);
3030

31-
@RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
31+
@RegisterExtension final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
3232

3333
protected static final int DB_INDEX = 0;
3434

35-
protected static GenericContainer<?> redisServer =
35+
protected GenericContainer<?> redisServer =
3636
new GenericContainer<>("redis:6.2.3-alpine")
3737
.withExposedPorts(6379)
3838
.withLogConsumer(new Slf4jLogConsumer(logger))
3939
.waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1));
4040

41-
protected static RedisClient redisClient;
42-
protected static StatefulRedisConnection<String, String> connection;
43-
protected static String host;
44-
protected static String ip;
45-
protected static int port;
46-
protected static String embeddedDbUri;
41+
protected RedisClient redisClient;
42+
protected StatefulRedisConnection<String, String> connection;
43+
protected String host;
44+
protected String ip;
45+
protected int port;
46+
protected String embeddedDbUri;
4747

4848
protected abstract RedisClient createClient(String uri);
4949

@@ -91,7 +91,7 @@ protected static void assertCommandEncodeEvents(SpanData span) {
9191
event -> event.hasName("redis.encode.end"));
9292
}
9393

94-
protected static String spanName(String operation) {
94+
protected String spanName(String operation) {
9595
return spanName(operation, host, port);
9696
}
9797

instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceReactiveClientTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
@SuppressWarnings({"InterruptedExceptionSwallowed", "deprecation"}) // using deprecated semconv
3636
public abstract class AbstractLettuceReactiveClientTest extends AbstractLettuceClientTest {
3737

38-
protected static RedisReactiveCommands<String, String> reactiveCommands;
38+
protected RedisReactiveCommands<String, String> reactiveCommands;
3939

4040
@BeforeAll
4141
void setUp() throws UnknownHostException {

instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientAuthTest.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ public abstract class AbstractLettuceSyncClientAuthTest extends AbstractLettuceC
4040
void setUp() throws UnknownHostException {
4141
redisServer = redisServer.withCommand("redis-server", "--requirepass password");
4242
redisServer.start();
43-
// Set back so other tests don't fail due to NOAUTH error.
44-
cleanup.deferAfterAll(
45-
() -> redisServer = redisServer.withCommand("redis-server", "--requirepass \"\""));
4643
cleanup.deferAfterAll(redisServer::stop);
4744

4845
host = redisServer.getHost();

instrumentation/lettuce/lettuce-5.1/testing/src/main/java/io/opentelemetry/instrumentation/lettuce/v5_1/AbstractLettuceSyncClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@
5353
@SuppressWarnings("deprecation") // using deprecated semconv
5454
public abstract class AbstractLettuceSyncClientTest extends AbstractLettuceClientTest {
5555

56-
private static String dbUriNonExistent;
56+
private String dbUriNonExistent;
5757

5858
private static final ImmutableMap<String, String> TEST_HASH_MAP =
5959
ImmutableMap.of(
6060
"firstname", "John",
6161
"lastname", "Doe",
6262
"age", "53");
6363

64-
private static RedisCommands<String, String> syncCommands;
64+
private RedisCommands<String, String> syncCommands;
6565

6666
@BeforeAll
6767
void setUp() throws UnknownHostException {

0 commit comments

Comments
 (0)