New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enrich ConnectionPoolListener interface to listen more connection events #5580
base: main
Are you sure you want to change the base?
Enrich ConnectionPoolListener interface to listen more connection events #5580
Conversation
b52f5a3
to
2851ee6
Compare
9f7edb8
to
1da16f2
Compare
@@ -181,6 +189,26 @@ public final void onReadOrWrite() { | |||
lastConnectionIdleTime = System.nanoTime(); | |||
} | |||
|
|||
if (!isServer && !isActive) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be useful if we also support ConnectionPoolListener
for servers. Although we are focusing on the client-side implementation, the public API design should take extensibility into consideration.
I propose to add a new ConnectionPoolListener
to c.l.a.common
package. The new methods will be added to the new ConnectionPoolListener
which extends the old ConnectionPoolListener
for compatibility. For example:
armeria/core/src/main/java/com/linecorp/armeria/common/encoding/StreamDecoder.java
Line 27 in 1d268aa
public interface StreamDecoder extends com.linecorp.armeria.client.encoding.StreamDecoder { |
Related issue: #3722
core/src/main/java/com/linecorp/armeria/internal/common/AbstractKeepAliveHandler.java
Outdated
Show resolved
Hide resolved
416f42b
to
4be97ef
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5580 +/- ##
============================================
- Coverage 74.05% 74.03% -0.03%
+ Complexity 21253 21197 -56
============================================
Files 1850 1844 -6
Lines 78600 78438 -162
Branches 10032 10010 -22
============================================
- Hits 58209 58069 -140
+ Misses 15686 15668 -18
+ Partials 4705 4701 -4 ☔ View full report in Codecov by Sentry. |
918d3ac
to
6250594
Compare
core/src/main/java/com/linecorp/armeria/client/ConnectionPoolListener.java
Outdated
Show resolved
Hide resolved
edcff6b
to
5a4530a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really good. Thanks for your high quality contribution, @festinalente91!
core/src/main/java/com/linecorp/armeria/client/ConnectionPoolListener.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/common/ConnectionPoolListenerAdapter.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/ConnectionPoolListener.java
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/common/ConnectionPoolMetrics.java
Outdated
Show resolved
Hide resolved
/** | ||
* A {@link ConnectionPoolListener} that collects metrics for connection pool events. | ||
*/ | ||
public final class MetricCollectingConnectionPoolListener implements ConnectionPoolListener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we remove MetricCollectingConnectionPoolListener
from the public API to minimize a breaking change in the future?
public final class MetricCollectingConnectionPoolListener implements ConnectionPoolListener { | |
final class MetricCollectingConnectionPoolListener implements ConnectionPoolListener { |
core/src/main/java/com/linecorp/armeria/client/ConnectionPoolListener.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/HttpChannelPool.java
Outdated
Show resolved
Hide resolved
private void setConnectionKey(SessionProtocol desiredProtocol, | ||
PoolKey poolkey, | ||
ConnectionKey connectionKey) { | ||
connectionKeys[desiredProtocol.ordinal()].put(poolkey, connectionKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of adding ConnectionKey
to Channel.attr
? We may fix ChannelUtil.localAddress()
and ChannelUtil.remoteAddress()
so that we can see ConnectionKey
first.
For example:
class ChannelUtil {
public static InetSocketAddress localAddress(@Nullable Channel ch) {
final Attribute<ConnectionKey> attr = ch.attr(CONNECTION_KEY);
if (attr != null) {
final ConnectionKey key = attr.get();
if (key != null) {
return key.localAddress();
}
}
...
}
}
core/src/main/java/com/linecorp/armeria/client/HttpChannelPool.java
Outdated
Show resolved
Hide resolved
/** | ||
* Listens to the client connection pool events. | ||
*/ | ||
public interface ConnectionPoolListener extends com.linecorp.armeria.client.ConnectionPoolListener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, ConnectionEventListener
would be a better name for the new API than ConnectionPoolListener
. Because:
- If we use the same name, setter methods can be easily overloaded but it is difficult to name the new API while deprecating the old one.
pool
is not a good word for the server side. Servers accept incoming connections but do not manage pooled connections for reuse. Therefore, a more general term,event
would make more sense.
public interface ConnectionPoolListener extends com.linecorp.armeria.client.ConnectionPoolListener { | |
public interface ConnectionEventListener { |
Should we deprecate ClientFactoryBuilder.connectionPoolListener()
and related methods in favor of ClientFactoryBuilder.connectionEventListener()
?
For backward compatibility, we need to convert ConnectionPoolListener
to ConnectionEventListener
internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the name. 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we deprecate ClientFactoryBuilder.connectionPoolListener() and related methods in favor of ClientFactoryBuilder.connectionEventListener()?
Yes, I think so.
For backward compatibility, we need to convert ConnectionPoolListener to ConnectionEventListener internally.
Okay. I'll convert it.👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I converted the deprecated ConnectionPoolListener into ClientConnectionEventListener in the ClientFactoryBuilder. if there's already CONNECTION_EVENT_LISTENER, then it just ignores the ConnectionPoolListener.
// ClientConnectionEventListener.class
static final ClientConnectionEventListenerAdapter NOOP = new ClientConnectionEventListenerAdapter();
/**
* Convert the {@link ConnectionPoolListener} into a {@link ClientConnectionEventListener}.
*/
static ClientConnectionEventListener of(ConnectionPoolListener connectionPoolListener) {
return new ClientConnectionEventListenerAdapter() {
@Override
public void connectionOpened(@Nullable SessionProtocol desiredProtocol,
SessionProtocol protocol,
InetSocketAddress remoteAddress,
InetSocketAddress localAddress,
AttributeMap attrs) throws Exception {
connectionPoolListener.connectionOpen(protocol,
remoteAddress,
localAddress,
attrs);
}
@Override
public void connectionClosed(SessionProtocol protocol,
InetSocketAddress remoteAddress,
InetSocketAddress localAddress,
boolean isIdle,
AttributeMap attrs) throws Exception {
connectionPoolListener.connectionClosed(protocol,
remoteAddress,
localAddress,
attrs);
}
};
}
// ClientFactoryBuilder.class
private ClientFactoryOptions buildOptions() {
...
if (options.containsKey(ClientFactoryOptions.CONNECTION_POOL_LISTENER) &&
!options.containsKey(ClientFactoryOptions.CONNECTION_EVENT_LISTENER)) {
final ConnectionPoolListener connectionPoolListener =
(ConnectionPoolListener) options.get(ClientFactoryOptions.CONNECTION_POOL_LISTENER).value();
options.put(ClientFactoryOptions.CONNECTION_EVENT_LISTENER,
ClientFactoryOptions.CONNECTION_EVENT_LISTENER
.newValue(ClientConnectionEventListenerAdapter.of(connectionPoolListener)));
}
5a4530a
to
89e0257
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically, looks good. 👍
core/src/main/java/com/linecorp/armeria/client/ClientConnectionEventListener.java
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/ClientConnectionEventListener.java
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/ClientFactoryOptions.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/Http1ClientKeepAliveHandler.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/ClientFactoryOptions.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/HttpClientFactory.java
Outdated
Show resolved
Hide resolved
final ConnectionEventKey connectionEventKey = connectionEventKey(channel); | ||
|
||
try { | ||
connectionEventListener.connectionOpened(connectionEventKey, channel); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If NoopKeepAliveHandler
is used, this event isn't called anymore.
keepAliveHandler = new NoopKeepAliveHandler(); |
Is there any reason that you've moved this logic from
HttpChannelPool
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to move open and close event trigger logic into the AbstractKeepAliveHandler to reuse this on the server side. However, as you said, I had to make extra changes to do that. Any feedback for this? @minwoox @ikhoon
- Added connection opened and closed event trigger logic into the NoopKeepAliveHandler
- To share the open and close event trigger logic on server side.
- Handled HTTP Upgrade cases with context-checking(HttpProtocolUpgradeHandler)
- When the protocol is upgraded, AbstractKeepAliveHandler is called twice.
- So, I had to ignore the first one which was related to the protocol upgrade process.
- (Still struggling to handle HTTP2 Max connection setting test case.
- void exceededMaxStreamsPropagatesFailureCorrectly() throws Exception {)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to move open and close event trigger logic into the AbstractKeepAliveHandler to reuse this on the server side
Since the connection mechanisms differ between the server-side and client-side, I don't think we have to put the logic into the same class.
Instead, we could consider incorporating the logic for the server-side into ConnectionLimitingHandler, which is already used for managing connections. You can find the ConnectionLimitingHandler implementation here: Link
core/src/main/java/com/linecorp/armeria/common/ConnectionEventKey.java
Outdated
Show resolved
Hide resolved
@Nullable | ||
private SessionProtocol desiredProtocol; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Nullable | |
private SessionProtocol desiredProtocol; |
because this isn't used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot to use it. 😆 Thank you for the review.
This field is going to be used for identifying pending connection events on the client side.
Suppose you count a connection pending event (Like the ConnectionPoolMetrics class) and want to decrease the count after the connection is opened. In that case, you need a key to identify the previous connection pending event. Since the final protocol is undetermined in the connection pending phase, (desiredProtocol, remoteAddress, localAddress) can be used as the connection event key.
void connectionPending(SessionProtocol desiredProtocol,
InetSocketAddress remoteAddress,
InetSocketAddress localAddress,
AttributeMap attrs) throws Exception;
void connectionOpened(@Nullable SessionProtocol desiredProtocol,
SessionProtocol protocol,
InetSocketAddress remoteAddress,
InetSocketAddress localAddress,
AttributeMap attrs) throws Exception;
core/src/main/java/com/linecorp/armeria/common/ConnectionEventKey.java
Outdated
Show resolved
Hide resolved
89e0257
to
33f16ec
Compare
6e4e7e5
to
15bb579
Compare
15bb579
to
f23ca82
Compare
final ConnectionEventKey connectionEventKey = connectionEventKey(ctx.channel()); | ||
|
||
try { | ||
connectionEventListener.connectionIdle(protocol, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idle connections refer to connections that do not currently have any requests in progress. We've encountered a limitation with AbstractKeepAliveHandler
: it resets the connection's idle time whenever it receives a ping.
armeria/core/src/main/java/com/linecorp/armeria/internal/common/AbstractKeepAliveHandler.java
Lines 269 to 271 in e99fa5d
if (connectionIdleTimeNanos > 0 && keepAliveOnPing) { | |
lastConnectionIdleTime = System.nanoTime(); | |
} |
Given this behavior, we're considering an alternative approach: utilizing the unfinishedResponses
property in AbstractHttpResponseDecoder
. By monitoring changes in unfinishedResponses
, such as transitions from 0 to 1 indicating activity and from 1 to 0 suggesting idle status, we can effectively manage connection states without interfering with ping handling.
Side note: For HTTP/1.1 connections, the connectionActive and connectionIdle methods will be invoked frequently.
Also, what do you think of splitting this feature into separate PRs for client and server-side?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detailed explanation! 🙏 I'll try to utilize unfinishedResponses
.
Side note: For HTTP/1.1 connections, the connectionActive and connectionIdle methods will be invoked frequently.
Agreed. Also, I'm a little bit worried about performance when a user adds heavy logic to the handler 🤔
Also, what do you think of splitting this feature into separate PRs for client and server-side?
Okay. I'll focus on the client side in this PR and apply it to the server side later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'm a little bit worried about performance when a user adds heavy logic to the handler 🤔
It shouldn't be a problem because the default is no-op. 😉
Motivation:
ConnectionPoolListener provides a limited way to listen to connection events. (open, close)
We could provide a more sophisticated event interface like the one below.
Furthermore, as @ikhoon mentioned here
we can also expand this connection event interface to be used on the server side.
Modifications:
Result:
ConnectionPoolListener
interface to listen to more connection events #4649 .