GH-9430: Fix CachingClientCF for collaborating channel adapters (#9431)
Fixes: #9430 When `CachingClientConnectionFactory` is used in combination of `Tcp.outboundAdapter()` & `Tcp.inboundAdapter()`, the connection is not released back to the cache because `CachingClientConnectionFactory` does not store created connections into its `connections` property. So, when `TcpReceivingChannelAdapter` calls `this.clientConnectionFactory.closeConnection(connectionId);` it returned immediately because there is nothing to remove from the `this.connections` * Add `protected removeConnection()` in the `AbstractConnectionFactory` and override it in the `CachingClientConnectionFactory` with delegation to the `this.targetConnectionFactory` * Demonstrate the problem in the new `IpIntegrationTests.allRepliesAreReceivedViaLimitedCachingConnectionFactory()` test and ensure that all 27 letters from English alphabet are sent to the server and received in uppercase while size of the `CachingClientConnectionFactory` is only `10` **Auto-cherry-pick to `6.3.x` & `6.2.x`**
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -977,7 +977,7 @@ public abstract class AbstractConnectionFactory extends IntegrationObjectSupport
|
|||||||
this.connectionsMonitor.lock();
|
this.connectionsMonitor.lock();
|
||||||
try {
|
try {
|
||||||
boolean closed = false;
|
boolean closed = false;
|
||||||
TcpConnectionSupport connection = this.connections.remove(connectionId);
|
TcpConnectionSupport connection = removeConnection(connectionId);
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
try {
|
try {
|
||||||
connection.close();
|
connection.close();
|
||||||
@@ -996,6 +996,11 @@ public abstract class AbstractConnectionFactory extends IntegrationObjectSupport
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected TcpConnectionSupport removeConnection(String connectionId) {
|
||||||
|
return this.connections.remove(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return super.toString()
|
return super.toString()
|
||||||
|
|||||||
@@ -358,6 +358,11 @@ public class CachingClientConnectionFactory extends AbstractClientConnectionFact
|
|||||||
this.targetConnectionFactory.enableManualListenerRegistration();
|
this.targetConnectionFactory.enableManualListenerRegistration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TcpConnectionSupport removeConnection(String connectionId) {
|
||||||
|
return this.targetConnectionFactory.removeConnection(connectionId.replaceFirst("Cached:", ""));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
setActive(true);
|
setActive(true);
|
||||||
|
|||||||
@@ -16,11 +16,14 @@
|
|||||||
|
|
||||||
package org.springframework.integration.ip.dsl;
|
package org.springframework.integration.ip.dsl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import org.aopalliance.intercept.MethodInterceptor;
|
import org.aopalliance.intercept.MethodInterceptor;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -47,6 +50,7 @@ import org.springframework.integration.ip.tcp.TcpReceivingChannelAdapter;
|
|||||||
import org.springframework.integration.ip.tcp.TcpSendingMessageHandler;
|
import org.springframework.integration.ip.tcp.TcpSendingMessageHandler;
|
||||||
import org.springframework.integration.ip.tcp.connection.AbstractClientConnectionFactory;
|
import org.springframework.integration.ip.tcp.connection.AbstractClientConnectionFactory;
|
||||||
import org.springframework.integration.ip.tcp.connection.AbstractServerConnectionFactory;
|
import org.springframework.integration.ip.tcp.connection.AbstractServerConnectionFactory;
|
||||||
|
import org.springframework.integration.ip.tcp.connection.CachingClientConnectionFactory;
|
||||||
import org.springframework.integration.ip.tcp.connection.TcpConnectionServerListeningEvent;
|
import org.springframework.integration.ip.tcp.connection.TcpConnectionServerListeningEvent;
|
||||||
import org.springframework.integration.ip.tcp.connection.TcpNetClientConnectionFactory;
|
import org.springframework.integration.ip.tcp.connection.TcpNetClientConnectionFactory;
|
||||||
import org.springframework.integration.ip.tcp.connection.TcpNetServerConnectionFactory;
|
import org.springframework.integration.ip.tcp.connection.TcpNetServerConnectionFactory;
|
||||||
@@ -55,12 +59,12 @@ import org.springframework.integration.ip.tcp.serializer.TcpCodecs;
|
|||||||
import org.springframework.integration.ip.udp.MulticastSendingMessageHandler;
|
import org.springframework.integration.ip.udp.MulticastSendingMessageHandler;
|
||||||
import org.springframework.integration.ip.udp.UdpServerListeningEvent;
|
import org.springframework.integration.ip.udp.UdpServerListeningEvent;
|
||||||
import org.springframework.integration.ip.udp.UnicastReceivingChannelAdapter;
|
import org.springframework.integration.ip.udp.UnicastReceivingChannelAdapter;
|
||||||
import org.springframework.integration.ip.udp.UnicastSendingMessageHandler;
|
|
||||||
import org.springframework.integration.ip.util.TestingUtilities;
|
import org.springframework.integration.ip.util.TestingUtilities;
|
||||||
import org.springframework.integration.support.MessageBuilder;
|
import org.springframework.integration.support.MessageBuilder;
|
||||||
import org.springframework.integration.test.util.TestUtils;
|
import org.springframework.integration.test.util.TestUtils;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.MessageChannel;
|
import org.springframework.messaging.MessageChannel;
|
||||||
|
import org.springframework.messaging.PollableChannel;
|
||||||
import org.springframework.messaging.support.GenericMessage;
|
import org.springframework.messaging.support.GenericMessage;
|
||||||
import org.springframework.test.annotation.DirtiesContext;
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
@@ -102,9 +106,6 @@ public class IpIntegrationTests {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UnicastReceivingChannelAdapter udpInbound;
|
private UnicastReceivingChannelAdapter udpInbound;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UnicastSendingMessageHandler udpOutbound;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private QueueChannel udpIn;
|
private QueueChannel udpIn;
|
||||||
|
|
||||||
@@ -236,6 +237,45 @@ public class IpIntegrationTests {
|
|||||||
assertThat(TestUtils.getPropertyValue(this.tcpOutAsync, "async", Boolean.class)).isTrue();
|
assertThat(TestUtils.getPropertyValue(this.tcpOutAsync, "async", Boolean.class)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AbstractServerConnectionFactory server2;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TcpNetClientConnectionFactory client3;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("outboundFlow.input")
|
||||||
|
MessageChannel outboundFlowInput;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
PollableChannel cachingRepliesChannel;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allRepliesAreReceivedViaLimitedCachingConnectionFactory() {
|
||||||
|
this.client3.stop();
|
||||||
|
TestingUtilities.waitListening(this.server2, null);
|
||||||
|
this.client3.setPort(this.server2.getPort());
|
||||||
|
this.client3.start();
|
||||||
|
|
||||||
|
List<String> expected =
|
||||||
|
IntStream.rangeClosed('a', 'z')
|
||||||
|
.mapToObj((characterCode) -> (char) characterCode)
|
||||||
|
.map((character) -> "" + character)
|
||||||
|
.peek((character) -> this.outboundFlowInput.send(new GenericMessage<>(character)))
|
||||||
|
.map(String::toUpperCase)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<String> replies = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < expected.size(); i++) {
|
||||||
|
Message<?> replyMessage = this.cachingRepliesChannel.receive(10_000);
|
||||||
|
assertThat(replyMessage).isNotNull();
|
||||||
|
replies.add(replyMessage.getPayload().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(replies).containsAll(expected);
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableIntegration
|
@EnableIntegration
|
||||||
public static class Config {
|
public static class Config {
|
||||||
@@ -318,8 +358,9 @@ public class IpIntegrationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TcpNetClientConnectionFactorySpec client1(TcpNetServerConnectionFactory server1) {
|
public TcpNetClientConnectionFactorySpec client1() {
|
||||||
return Tcp.netClient("localhost", server1.getPort())
|
// The port from server is assigned
|
||||||
|
return Tcp.netClient("localhost", 0)
|
||||||
.serializer(TcpCodecs.crlf())
|
.serializer(TcpCodecs.crlf())
|
||||||
.deserializer(TcpCodecs.lengthHeader1());
|
.deserializer(TcpCodecs.lengthHeader1());
|
||||||
}
|
}
|
||||||
@@ -337,8 +378,9 @@ public class IpIntegrationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TcpNetClientConnectionFactorySpec client2(TcpNetServerConnectionFactory server1) {
|
public TcpNetClientConnectionFactorySpec client2() {
|
||||||
return Tcp.netClient("localhost", server1.getPort())
|
// The port from server is assigned
|
||||||
|
return Tcp.netClient("localhost", 0)
|
||||||
.serializer(TcpCodecs.crlf())
|
.serializer(TcpCodecs.crlf())
|
||||||
.deserializer(TcpCodecs.lengthHeader1());
|
.deserializer(TcpCodecs.lengthHeader1());
|
||||||
}
|
}
|
||||||
@@ -370,6 +412,45 @@ public class IpIntegrationTests {
|
|||||||
.transform(Transformers.objectToString());
|
.transform(Transformers.objectToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TcpNetServerConnectionFactorySpec server2() {
|
||||||
|
return Tcp.netServer(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public IntegrationFlow server2Flow(TcpNetServerConnectionFactory server2) {
|
||||||
|
return IntegrationFlow.from(Tcp.inboundGateway(server2))
|
||||||
|
.transform(Transformers.objectToString())
|
||||||
|
.<String, String>transform(String::toUpperCase)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TcpNetClientConnectionFactorySpec client3() {
|
||||||
|
// The port from server is assigned
|
||||||
|
return Tcp.netClient("localhost", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
CachingClientConnectionFactory cachingClient(TcpNetClientConnectionFactory client3) {
|
||||||
|
var cachingClientConnectionFactory = new CachingClientConnectionFactory(client3, 10);
|
||||||
|
cachingClientConnectionFactory.setConnectionWaitTimeout(10_000);
|
||||||
|
return cachingClientConnectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
IntegrationFlow outboundFlow(CachingClientConnectionFactory cachingClient) {
|
||||||
|
return (flow) -> flow.handle(Tcp.outboundAdapter(cachingClient));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
IntegrationFlow inboundFlow(CachingClientConnectionFactory cachingClient) {
|
||||||
|
return IntegrationFlow.from(Tcp.inboundAdapter(cachingClient))
|
||||||
|
.transform(Transformers.objectToString())
|
||||||
|
.channel((channels) -> channels.queue("cachingRepliesChannel"))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user