Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

festinalente91
Copy link

@festinalente91 festinalente91 commented Apr 8, 2024

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.

  • Connection pending
  • Connection failed
  • Connection active
  • Connection idle
                                                                                           
                                    +------------------+                                   
        +--------------+            | +--------------+ |            +--------------+       
        |              |            | |              | |            |              |       
        |    Pending   |----------->| |    Opened    | |----------> |    Closed    |       
        |              |            | |              | |            |              |       
        +--------------+            | +--------------+ |            +--------------+       
                |                   |                  |                                   
                |                   |      Active      |                                   
                |                   |         ^        |                                   
                |                   |         |        |                                   
                |                   |         |        |                                   
                v                   |         |        |                                   
        +--------------+            |         |        |                                   
        |              |            |         v        |                                   
        |    Failed    |            |       Idle       |                                   
        |              |            |                  |                                   
        +--------------+            +------------------+                                   

Furthermore, as @ikhoon mentioned here
we can also expand this connection event interface to be used on the server side.

Modifications:

  • (Deprecated) com.linecorp.armeria.client.ConnectionPoolListener class is deprecated.
    • Use com.linecorp.armeria.client.ClientConnectionEventListener instead.
  • (Moved) com.linecorp.armeria.client.MetricCollectingConnectionPoolListener is moved.
    • to com.linecorp.armeria.common.MetricCollectingConnectionPoolListener
  • (Moved) com.linecorp.armeria.client.ConnectionPoolListenerAdapter is moved.
    • to com.linecorp.armeria.common.ConnectionPoolListenerAdapter

Result:

@CLAassistant
Copy link

CLAassistant commented Apr 8, 2024

CLA assistant check
All committers have signed the CLA.

@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch from b52f5a3 to 2851ee6 Compare April 8, 2024 08:41
@ikhoon ikhoon added new feature sprint Issues for OSS Sprint participants labels Apr 8, 2024
@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch 2 times, most recently from 9f7edb8 to 1da16f2 Compare April 17, 2024 01:12
@@ -181,6 +189,26 @@ public final void onReadOrWrite() {
lastConnectionIdleTime = System.nanoTime();
}

if (!isServer && !isActive) {
Copy link
Contributor

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:

public interface StreamDecoder extends com.linecorp.armeria.client.encoding.StreamDecoder {

Related issue: #3722

@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch from 416f42b to 4be97ef Compare April 21, 2024 10:38
Copy link

codecov bot commented Apr 21, 2024

Codecov Report

Attention: Patch coverage is 78.20513% with 17 lines in your changes are missing coverage. Please review.

Project coverage is 74.03%. Comparing base (14c5566) to head (4be97ef).
Report is 4 commits behind head on main.

❗ Current head 4be97ef differs from pull request most recent head f23ca82. Consider uploading reports for the commit f23ca82 to get more accurate results

Files Patch % Lines
...eria/internal/common/AbstractKeepAliveHandler.java 66.66% 8 Missing and 2 partials ⚠️
.../armeria/client/ConnectionPoolListenerWrapper.java 0.00% 4 Missing ⚠️
...a/com/linecorp/armeria/client/HttpChannelPool.java 50.00% 1 Missing ⚠️
...inecorp/armeria/common/ConnectionPoolListener.java 0.00% 1 Missing ⚠️
...linecorp/armeria/common/ConnectionPoolMetrics.java 96.00% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch from 918d3ac to 6250594 Compare April 28, 2024 03:42
@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch 5 times, most recently from edcff6b to 5a4530a Compare May 7, 2024 01:52
@festinalente91 festinalente91 requested a review from ikhoon May 7, 2024 04:46
@festinalente91 festinalente91 marked this pull request as ready for review May 7, 2024 04:50
Copy link
Member

@trustin trustin left a 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!

/**
* A {@link ConnectionPoolListener} that collects metrics for connection pool events.
*/
public final class MetricCollectingConnectionPoolListener implements ConnectionPoolListener {
Copy link
Contributor

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?

Suggested change
public final class MetricCollectingConnectionPoolListener implements ConnectionPoolListener {
final class MetricCollectingConnectionPoolListener implements ConnectionPoolListener {

private void setConnectionKey(SessionProtocol desiredProtocol,
PoolKey poolkey,
ConnectionKey connectionKey) {
connectionKeys[desiredProtocol.ordinal()].put(poolkey, connectionKey);
Copy link
Contributor

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();
      }
    }
    ...
  }
}

/**
* Listens to the client connection pool events.
*/
public interface ConnectionPoolListener extends com.linecorp.armeria.client.ConnectionPoolListener {
Copy link
Contributor

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.
Suggested change
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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the name. 😊

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ikhoon

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.👍

Copy link
Author

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)));
        }

@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch from 5a4530a to 89e0257 Compare May 12, 2024 03:34
Copy link
Member

@minwoox minwoox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, looks good. 👍

final ConnectionEventKey connectionEventKey = connectionEventKey(channel);

try {
connectionEventListener.connectionOpened(connectionEventKey, channel);
Copy link
Member

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?

Copy link
Author

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 {)

Copy link
Member

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

Comment on lines +17 to +53
@Nullable
private SessionProtocol desiredProtocol;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Nullable
private SessionProtocol desiredProtocol;

because this isn't used.

Copy link
Author

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;

@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch from 89e0257 to 33f16ec Compare May 14, 2024 02:39
@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch 2 times, most recently from 6e4e7e5 to 15bb579 Compare May 14, 2024 13:33
@festinalente91 festinalente91 force-pushed the feature/enrich-connection-pool-listener-event-interface branch from 15bb579 to f23ca82 Compare May 14, 2024 13:37
final ConnectionEventKey connectionEventKey = connectionEventKey(ctx.channel());

try {
connectionEventListener.connectionIdle(protocol,
Copy link
Member

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.

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?

Copy link
Author

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.

Copy link
Member

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. 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature sprint Issues for OSS Sprint participants
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enrich ConnectionPoolListener interface to listen to more connection events
5 participants