Skip to content

Commit 69ca3f7

Browse files
authored
Merge pull request #13 from honza-kasik/fix-restart-timeout-windows
Fix Windows CI session timeout assertions
2 parents 5f4a186 + a27ecd3 commit 69ca3f7

4 files changed

Lines changed: 104 additions & 93 deletions

File tree

src/test/java/org/jboss/modcluster/test/session/SessionManagementTest.java

Lines changed: 46 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.jboss.modcluster.test.utils.HttpClient;
1010
import org.jboss.modcluster.test.utils.HttpClient.HttpResponse;
1111
import org.jboss.modcluster.test.utils.UndertowSessionCookieConfigurator;
12-
import org.jboss.modcluster.test.utils.TestMode;
1312
import org.jboss.modcluster.test.utils.TestTimeouts;
1413
import org.jboss.modcluster.test.utils.WildFlyWorker;
1514
import org.jboss.modcluster.test.apps.SessionTimeoutAppBuilder;
@@ -247,8 +246,11 @@ public void testSessionTimeoutPreservedAfterUndeploy(TestCluster cluster, HttpCl
247246
}
248247

249248
/**
250-
* Verifies that session timeout is NOT hit after context stop despite configured 1-minute timeout.
251-
* Passes if continuous requests for 65 seconds succeed after stop-context without session expiration.
249+
* Verifies that session timeout is preserved across failover triggered by STOP-APP.
250+
* Deploys a distributable app with 1-minute session timeout, establishes a session,
251+
* then stops the context on the session's worker via MCMP. The session should fail over
252+
* to the other worker and remain alive for the full 65-second test window (exceeding the
253+
* 1-minute timeout boundary), proving the timeout was not reset during replication.
252254
*/
253255
@Test
254256
public void testSessionTimeoutPreservedAfterStopContext(TestCluster cluster, HttpClient httpClient) throws Exception {
@@ -268,46 +270,53 @@ public void testSessionTimeoutPreservedAfterStopContext(TestCluster cluster, Htt
268270
// Wait for both workers to register on the balancer
269271
httpClient.waitForWorkerRegistration(url, 2, TestTimeouts.CLUSTER_FORMATION);
270272

271-
// Establish session
273+
// Establish session — balancer picks the worker
272274
final HttpResponse initial = httpClient.get(url);
273275
final String sessionCookie = initial.getCookie("JSESSIONID");
276+
final String sessionWorkerName = extractJvmRoute(sessionCookie);
277+
final String otherWorkerName = sessionWorkerName.equals("worker1") ? "worker2" : "worker1";
278+
final WildFlyWorker sessionWorker = cluster.getWorkerByName(sessionWorkerName);
274279

275-
log.info("Session established: {}", sessionCookie);
280+
log.info("Session established: {} (on {})", sessionCookie, sessionWorkerName);
276281

277282
// Continuous requests for 65 seconds
278283
final ContinuousRequestRunner runner = new ContinuousRequestRunner(httpClient, url, sessionCookie);
279284
final Future<ContinuousRequestRunner.RequestResult> resultFuture = runner.startAsync(
280285
Duration.ofSeconds(65), Duration.ofMillis(1000));
281286

282-
// After 5 seconds warmup, stop context on worker1
287+
// After 5 seconds warmup, stop context on the worker that holds the session
283288
Thread.sleep(5000);
284-
log.info("Stopping context /timeout-test on worker1");
285-
cluster.getWorker1().modCluster().stopContext("/timeout-test", "default-host");
289+
log.info("Stopping context /timeout-test on {}", sessionWorkerName);
290+
sessionWorker.modCluster().stopContext("/timeout-test", "default-host");
286291

287292
// Wait for continuous requests to complete
288293
final ContinuousRequestRunner.RequestResult result = resultFuture.get(90, TimeUnit.SECONDS);
289294

290-
log.info("Continuous requests completed: {} total, {} failed",
291-
result.getTotalCount(), result.getFailedCount());
295+
log.info("Continuous requests completed: {} total, {} failed, firstWorker={}, lastWorker={}",
296+
result.getTotalCount(), result.getFailedCount(), result.getFirstWorker(), result.getLastWorker());
292297

293-
// Verify
294298
softly.assertThat(result.getFailedCount())
295299
.as("Few requests may fail during stop-context")
296300
.isLessThan(10);
297301

298-
int minExpected = TestMode.isWindows() ? 50 : 60;
299-
softly.assertThat(result.getTotalCount())
300-
.as("Should complete at least %d of ~65 requests", minExpected)
301-
.isGreaterThan(minExpected);
302+
softly.assertThat(result.getFirstWorker())
303+
.as("First request should be served by the session worker")
304+
.isEqualTo(sessionWorkerName);
305+
306+
softly.assertThat(result.getLastWorker())
307+
.as("Last request should be served by the other worker (failover after stop-context, session still alive)")
308+
.isEqualTo(otherWorkerName);
302309

303310
softly.assertThat(result.getSessionIdChanges())
304-
.as("Session ID should remain constant or change at most once during failover")
305-
.isLessThanOrEqualTo(1);
311+
.as("Session ID must not change during graceful stop-context failover")
312+
.isEqualTo(0);
306313
}
307314

308315
/**
309-
* Verifies that session timeout is NOT hit after context disable despite configured 1-minute timeout.
310-
* Passes if continuous requests for 65 seconds succeed after disable-context without session expiration.
316+
* Verifies that session timeout is preserved after DISABLE-APP on the session's worker.
317+
* Unlike STOP-APP, DISABLE-APP does not force failover — existing sticky sessions continue
318+
* on the same worker. The session should remain alive for the full 65-second test window
319+
* with zero failures and no session ID changes.
311320
*/
312321
@Test
313322
public void testSessionTimeoutPreservedAfterDisableContext(TestCluster cluster, HttpClient httpClient) throws Exception {
@@ -327,41 +336,37 @@ public void testSessionTimeoutPreservedAfterDisableContext(TestCluster cluster,
327336
// Wait for both workers to register on the balancer
328337
httpClient.waitForWorkerRegistration(url, 2, TestTimeouts.CLUSTER_FORMATION);
329338

330-
// Establish session
339+
// Establish session — balancer picks the worker
331340
final HttpResponse initial = httpClient.get(url);
332341
final String sessionCookie = initial.getCookie("JSESSIONID");
342+
final String sessionWorkerName = extractJvmRoute(sessionCookie);
343+
final WildFlyWorker sessionWorker = cluster.getWorkerByName(sessionWorkerName);
333344

334-
log.info("Session established: {}", sessionCookie);
345+
log.info("Session established: {} (on {})", sessionCookie, sessionWorkerName);
335346

336347
// Continuous requests for 65 seconds
337348
final ContinuousRequestRunner runner = new ContinuousRequestRunner(httpClient, url, sessionCookie);
338349
final Future<ContinuousRequestRunner.RequestResult> resultFuture = runner.startAsync(
339350
Duration.ofSeconds(65), Duration.ofMillis(1000));
340351

341-
// After 5 seconds warmup, disable context on worker1
352+
// After 5 seconds warmup, disable context on the worker that holds the session
342353
Thread.sleep(5000);
343-
log.info("Disabling context /timeout-test on worker1");
344-
cluster.getWorker1().modCluster().disableContext("/timeout-test", "default-host");
354+
log.info("Disabling context /timeout-test on {}", sessionWorkerName);
355+
sessionWorker.modCluster().disableContext("/timeout-test", "default-host");
345356

346357
// Wait for continuous requests to complete
347358
final ContinuousRequestRunner.RequestResult result = resultFuture.get(90, TimeUnit.SECONDS);
348359

349-
log.info("Continuous requests completed: {} total, {} failed",
350-
result.getTotalCount(), result.getFailedCount());
360+
log.info("Continuous requests completed: {} total, {} failed, firstWorker={}, lastWorker={}",
361+
result.getTotalCount(), result.getFailedCount(), result.getFirstWorker(), result.getLastWorker());
351362

352-
// Verify
353363
softly.assertThat(result.getFailedCount())
354-
.as("Few requests may fail during disable-context")
355-
.isLessThan(10);
356-
357-
int minExpected = TestMode.isWindows() ? 50 : 60;
358-
softly.assertThat(result.getTotalCount())
359-
.as("Should complete at least %d of ~65 requests", minExpected)
360-
.isGreaterThan(minExpected);
364+
.as("No requests should fail — disable-context keeps the session on the original worker")
365+
.isEqualTo(0);
361366

362367
softly.assertThat(result.getSessionIdChanges())
363-
.as("Session ID should remain constant or change at most once during failover")
364-
.isLessThanOrEqualTo(1);
368+
.as("Session ID must not change — disable-context keeps the session on the original worker")
369+
.isEqualTo(0);
365370
}
366371

367372
/**
@@ -487,7 +492,7 @@ private void testCookieNameScenario(final String cookieName, final boolean reloa
487492
final HttpResponse initial = initialRef.get();
488493
final String cookie = initial.getCookie(effectiveCookieName);
489494
final String sessionId = extractSessionIdOnly(cookie);
490-
final String worker = extractWorkerFromResponse(initial);
495+
final String worker = initial.getWorkerName();
491496

492497
log.info("Session {} established on {} using cookie name '{}'", sessionId, worker, effectiveCookieName);
493498

@@ -503,7 +508,7 @@ private void testCookieNameScenario(final String cookieName, final boolean reloa
503508
softly.assertThat(response.getStatusCode())
504509
.as("Request %d should succeed", i)
505510
.isEqualTo(200);
506-
softly.assertThat(extractWorkerFromResponse(response))
511+
softly.assertThat(response.getWorkerName())
507512
.as("Request %d should stick to worker %s", i, worker)
508513
.isEqualTo(worker);
509514
} catch (IOException e) {
@@ -548,7 +553,7 @@ private void testCookieNameScenario(final String cookieName, final boolean reloa
548553
.isEqualTo(200);
549554

550555
if (response.getStatusCode() == 200) {
551-
final String currentWorker = extractWorkerFromResponse(response);
556+
final String currentWorker = response.getWorkerName();
552557
if (failoverWorker == null) {
553558
failoverWorker = currentWorker;
554559
softly.assertThat(currentWorker)
@@ -656,7 +661,7 @@ public void testJvmRouteLostJoinAtRuntime(TestCluster cluster, HttpClient httpCl
656661

657662
final String sessionId = extractSessionIdOnly(cookie);
658663
final String route = extractJvmRoute(cookie);
659-
final String worker = extractWorkerFromResponse(response);
664+
final String worker = response.getWorkerName();
660665

661666
initialRoute.set(route);
662667
initialWorker.set(worker);
@@ -699,7 +704,7 @@ public void testJvmRouteLostJoinAtRuntime(TestCluster cluster, HttpClient httpCl
699704
.as("Cycle %d request %d should succeed", currentCycle, i)
700705
.isEqualTo(200);
701706

702-
final String reqWorker = extractWorkerFromResponse(req);
707+
final String reqWorker = req.getWorkerName();
703708
assertThat(reqWorker)
704709
.as("[JBEAP-6683] Cycle %d request %d: Should stick to worker %s", currentCycle, i, worker)
705710
.isEqualTo(worker);
@@ -798,33 +803,4 @@ private String extractJvmRoute(final String cookie) {
798803
return dotIndex > 0 ? cookie.substring(dotIndex + 1) : null;
799804
}
800805

801-
/**
802-
* Extracts worker name from HTTP response body.
803-
* Parses the {@code <strong>Worker:</strong>} tag from the demo app JSP output
804-
* to get the actual serving worker's {@code jboss.node.name}. This avoids false
805-
* matches from the session ID's JVM route (e.g., "abc.worker2" in the Session ID
806-
* field when worker1 is actually serving the request after failover).
807-
*
808-
* @param response HTTP response
809-
* @return Worker name (e.g., "worker1")
810-
*/
811-
private String extractWorkerFromResponse(final HttpResponse response) {
812-
final String body = response.getBody();
813-
// Parse from JSP output: <strong>Worker:</strong> worker1
814-
if (body.contains("<strong>Worker:</strong>")) {
815-
int startIdx = body.indexOf("<strong>Worker:</strong>") + "<strong>Worker:</strong>".length();
816-
int endIdx = body.indexOf("</p>", startIdx);
817-
if (endIdx > startIdx) {
818-
return body.substring(startIdx, endIdx).trim();
819-
}
820-
}
821-
// Fallback: simple contains check
822-
if (body.contains("worker1")) {
823-
return "worker1";
824-
}
825-
if (body.contains("worker2")) {
826-
return "worker2";
827-
}
828-
return "unknown";
829-
}
830806
}

src/test/java/org/jboss/modcluster/test/utils/ContinuousRequestRunner.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public Future<RequestResult> startAsync(final Duration duration, final Duration
8181
}
8282
lastSessionId = currentSessionId;
8383
}
84+
85+
final String worker = response.getWorkerName();
86+
if (worker != null) {
87+
result.recordWorker(worker);
88+
}
8489
} else {
8590
result.incrementFailed();
8691
log.debug("Request failed with status: {}", response.getStatusCode());
@@ -136,6 +141,8 @@ public static class RequestResult {
136141
private final AtomicInteger successCount = new AtomicInteger(0);
137142
private final AtomicInteger failedCount = new AtomicInteger(0);
138143
private final AtomicInteger sessionIdChanges = new AtomicInteger(0);
144+
private volatile String firstWorker;
145+
private volatile String lastWorker;
139146

140147
void incrementTotal() {
141148
totalCount.incrementAndGet();
@@ -153,6 +160,13 @@ void incrementSessionIdChanges() {
153160
sessionIdChanges.incrementAndGet();
154161
}
155162

163+
void recordWorker(final String worker) {
164+
if (firstWorker == null) {
165+
firstWorker = worker;
166+
}
167+
lastWorker = worker;
168+
}
169+
156170
public int getTotalCount() {
157171
return totalCount.get();
158172
}
@@ -168,5 +182,13 @@ public int getFailedCount() {
168182
public int getSessionIdChanges() {
169183
return sessionIdChanges.get();
170184
}
185+
186+
public String getFirstWorker() {
187+
return firstWorker;
188+
}
189+
190+
public String getLastWorker() {
191+
return lastWorker;
192+
}
171193
}
172194
}

src/test/java/org/jboss/modcluster/test/utils/HttpClient.java

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ public Map<String, Integer> testLoadDistribution(String url, int requestCount) t
413413
Map<String, String> headers = new HashMap<>();
414414
headers.put("Connection", "close");
415415
HttpResponse response = get(url, headers);
416-
String worker = extractWorkerName(response.getBody());
416+
String worker = response.getWorkerName();
417417

418418
workerHits.merge(worker, 1, Integer::sum);
419419
successfulRequests++;
@@ -432,28 +432,6 @@ public Map<String, Integer> testLoadDistribution(String url, int requestCount) t
432432
return workerHits;
433433
}
434434

435-
/**
436-
* Extract worker name from response body.
437-
* Looks for pattern: <strong>Worker:</strong> worker1
438-
*/
439-
private String extractWorkerName(String body) {
440-
// Extract from JSP output: <strong>Worker:</strong> worker1
441-
if (body.contains("<strong>Worker:</strong>")) {
442-
int startIdx = body.indexOf("<strong>Worker:</strong>") + "<strong>Worker:</strong>".length();
443-
int endIdx = body.indexOf("</p>", startIdx);
444-
if (endIdx > startIdx) {
445-
return body.substring(startIdx, endIdx).trim(); // Returns "worker1" or "worker2"
446-
}
447-
}
448-
449-
// Fallback: simple contains check
450-
if (body.contains("worker1")) return "worker1";
451-
if (body.contains("worker2")) return "worker2";
452-
if (body.contains("worker3")) return "worker3";
453-
if (body.contains("worker4")) return "worker4";
454-
return "unknown";
455-
}
456-
457435
private Map<String, String> extractCookies(Response response) {
458436
Map<String, String> cookies = new HashMap<>();
459437
response.headers("Set-Cookie").forEach(cookie -> {
@@ -552,5 +530,24 @@ public String getCookie(String name) {
552530
public String getHeader(String name) {
553531
return headers.get(name);
554532
}
533+
534+
/**
535+
* Extracts the worker name ({@code jboss.node.name}) from the response body.
536+
* Parses the {@code <strong>Worker:</strong> workerN} tag emitted by the
537+
* demo and timeout-test JSP applications.
538+
*
539+
* @return worker name (e.g. "worker1"), or {@code null} if the body does not
540+
* contain the expected tag
541+
*/
542+
public String getWorkerName() {
543+
if (body != null && body.contains("<strong>Worker:</strong>")) {
544+
int startIdx = body.indexOf("<strong>Worker:</strong>") + "<strong>Worker:</strong>".length();
545+
int endIdx = body.indexOf("</p>", startIdx);
546+
if (endIdx > startIdx) {
547+
return body.substring(startIdx, endIdx).trim();
548+
}
549+
}
550+
return null;
551+
}
555552
}
556553
}

src/test/java/org/jboss/modcluster/test/utils/NativeWildFlyWorker.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,22 @@ public void stop() {
357357
log.info("WildFly worker '{}' stopped", getName());
358358
}
359359

360+
@Override
361+
public void restartServer() throws Exception {
362+
log.info("Restarting worker '{}' via process stop+start", getName());
363+
stop();
364+
365+
List<String> command = buildStartCommand();
366+
Map<String, String> env = buildEnvironment();
367+
368+
processManager = new NativeProcessManager(getName(), command, serverHome, env);
369+
processManager.start();
370+
processManager.waitForStartup(STARTUP_LOG_PATTERN, STARTUP_TIMEOUT);
371+
372+
deployment().deployDemoApp();
373+
log.info("Worker '{}' restarted successfully", getName());
374+
}
375+
360376
@Override
361377
public void kill() throws Exception {
362378
closeManagementClient();

0 commit comments

Comments
 (0)