99import org .jboss .modcluster .test .utils .HttpClient ;
1010import org .jboss .modcluster .test .utils .HttpClient .HttpResponse ;
1111import org .jboss .modcluster .test .utils .UndertowSessionCookieConfigurator ;
12- import org .jboss .modcluster .test .utils .TestMode ;
1312import org .jboss .modcluster .test .utils .TestTimeouts ;
1413import org .jboss .modcluster .test .utils .WildFlyWorker ;
1514import 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}
0 commit comments