From fd2912ae47ffe64bd23d5dce1962424e3cc10028 Mon Sep 17 00:00:00 2001 From: Florin POP Date: Fri, 6 Mar 2026 05:55:21 +0200 Subject: [PATCH 1/4] [JCS-244] Fix the LaterlatTCPListener.handleClient to survive also a java.nio.BufferUnderflowException or other exceptions --- .../jcs3/auxiliary/lateral/socket/tcp/LateralTCPListener.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commons-jcs-core/src/main/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListener.java b/commons-jcs-core/src/main/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListener.java index 0d96ca37e..039f312a1 100644 --- a/commons-jcs-core/src/main/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListener.java +++ b/commons-jcs-core/src/main/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListener.java @@ -716,6 +716,10 @@ private void handleClient(final SelectionKey key) { log.error( "Deserialization failed reading from socket", e ); } + catch (final Exception e) + { + log.error( "Unexpected exception when reading from socket", e ); + } } /** From 719c0d2f84257856276e6147cf98d70e3a6d5c13 Mon Sep 17 00:00:00 2001 From: Florin POP Date: Fri, 6 Mar 2026 06:40:48 +0200 Subject: [PATCH 2/4] Update actions/cache version in coverage.yml Updated the version as per the warning message --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ce7402a88..f56166986 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v3.5.2 with: persist-credentials: false - - uses: actions/cache@v3.3.1 + - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} From 489fc6780e558d96256d1fb10099a149230761f1 Mon Sep 17 00:00:00 2001 From: Florin POP Date: Fri, 6 Mar 2026 06:47:55 +0200 Subject: [PATCH 3/4] Update Maven CI workflow with new syntax Updated cache version to v3 as per the warning message --- .github/workflows/maven.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 7db7e114f..34bbb7fc2 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -41,9 +41,9 @@ jobs: steps: - uses: actions/checkout@v3.5.2 - with: - persist-credentials: false - - uses: actions/cache@v3.3.1 + with: + persist-credentials: false + - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -56,3 +56,4 @@ jobs: java-version: ${{ matrix.java }} - name: Build with Maven run: mvn -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false + From 0fbcb85501b94588d89c72523e643e962eb30708 Mon Sep 17 00:00:00 2001 From: Florin POP Date: Sat, 7 Mar 2026 07:00:16 +0200 Subject: [PATCH 4/4] Testcase for the survival of LateralTCPListener after bad data --- .../tcp/LateralTCPListenerBadDataTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 commons-jcs-core/src/test/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListenerBadDataTest.java diff --git a/commons-jcs-core/src/test/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListenerBadDataTest.java b/commons-jcs-core/src/test/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListenerBadDataTest.java new file mode 100644 index 000000000..2bf1e7b8e --- /dev/null +++ b/commons-jcs-core/src/test/java/org/apache/commons/jcs3/auxiliary/lateral/socket/tcp/LateralTCPListenerBadDataTest.java @@ -0,0 +1,134 @@ +package org.apache.commons.jcs3.auxiliary.lateral.socket.tcp; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import static org.junit.Assert.assertTrue; + +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.jcs3.JCS; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Test that a lateral TCP listener in a clustered setup survives receiving + * bad/corrupted data on its socket connection that triggers a + * {@link java.nio.BufferUnderflowException} during deserialization. + *

+ * Without the fix, the listener thread dies upon encountering truncated + * serialized data because the {@code BufferUnderflowException} propagates + * out of the read loop. With the fix, the listener catches the exception, + * logs it, and continues processing subsequent valid messages. + *

+ * This test sets up two lateral TCP cache nodes, sends the truncated data + * directly to one node's listener port, then verifies the listener is still + * alive. + */ +public class LateralTCPListenerBadDataTest +{ + /** Port for the first lateral node */ + private static final int LISTENER_PORT_1 = 11261; + + /** Port for the second lateral node */ + private static final int LISTENER_PORT_2 = 11262; + + /** Cache region name used in the test */ + private static final String CACHE_REGION = "testBadDataRegion"; + + @BeforeClass + public static void setUp() throws Exception + { + // Configure a 2-node lateral TCP cluster programmatically + final java.util.Properties props = new java.util.Properties(); + + // Default cache region + props.put("jcs.default", ""); + props.put("jcs.default.cacheattributes", + "org.apache.commons.jcs3.engine.CompositeCacheAttributes"); + props.put("jcs.default.cacheattributes.MaxObjects", "100"); + + // Test region with lateral TCP auxiliary + props.put("jcs.region." + CACHE_REGION, "LTCP"); + props.put("jcs.region." + CACHE_REGION + ".cacheattributes", + "org.apache.commons.jcs3.engine.CompositeCacheAttributes"); + props.put("jcs.region." + CACHE_REGION + ".cacheattributes.MaxObjects", "100"); + + // Lateral TCP auxiliary - this node listens on LISTENER_PORT_1 + // and connects to the peer on LISTENER_PORT_2 + props.put("jcs.auxiliary.LTCP", + "org.apache.commons.jcs3.auxiliary.lateral.socket.tcp.LateralTCPCacheFactory"); + props.put("jcs.auxiliary.LTCP.attributes", + "org.apache.commons.jcs3.auxiliary.lateral.socket.tcp.TCPLateralCacheAttributes"); + props.put("jcs.auxiliary.LTCP.attributes.TcpListenerPort", + String.valueOf(LISTENER_PORT_1)); + props.put("jcs.auxiliary.LTCP.attributes.TcpServers", + "localhost:" + LISTENER_PORT_2); + props.put("jcs.auxiliary.LTCP.attributes.IssueRemoveOnPut", "false"); + props.put("jcs.auxiliary.LTCP.attributes.AllowGet", "true"); + props.put("jcs.auxiliary.LTCP.attributes.UdpDiscoveryEnabled", "false"); + + JCS.setConfigProperties(props); + JCS.getInstance(CACHE_REGION); + + // Give the listener a moment to start up + TimeUnit.MILLISECONDS.sleep(500); + } + + @AfterClass + public static void tearDown() throws Exception + { + JCS.shutdown(); + } + + /** + * Verifies that the listener continues to accept new connections after + * receiving bad data. This is a simple check — just confirm that + * connecting to the port doesn't throw an exception. Before the fix this fails. + */ + @Test + public void testListenerAcceptsConnectionsAfterBadData() throws Exception + { + // Send bad data first + try (Socket badSocket = new Socket("localhost", LISTENER_PORT_1)) + { + final ObjectOutputStream oos = + new ObjectOutputStream(badSocket.getOutputStream()); + oos.writeObject(0); // Not a LateralElementDescriptor + oos.flush(); + } + + TimeUnit.MILLISECONDS.sleep(500); + + // Verify we can still connect — the ServerSocket is still accepting + boolean connected; + try (Socket testSocket = new Socket("localhost", LISTENER_PORT_1)) + { + connected = testSocket.isConnected(); + } + + assertTrue( + "Should still be able to connect to the listener port after bad data was sent.", + connected); + } + +} \ No newline at end of file