This is an automated email from the ASF dual-hosted git repository.

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new 5cc723fa feat: limit server connections per public listener (#3498)
5cc723fa is described below

commit 5cc723faed9112cab7cf94809c53f59886dfe3d5
Author: Jerry Zhao <[email protected]>
AuthorDate: Mon Sep 14 10:34:22 2026 +0800

    feat: limit server connections per public listener (#3498)
    
    * feat: limit Redis server connections
    
    * feat: update Redis connection limit at runtime
    
    * fix: handle partial Redis rejection writes
    
    * Fix Redis connection limit review nits
    
    Accept fd 0 in the Redis connection tests and describe every 
dedicated-listener requirement in startup and runtime errors.
    
    Validation: brpc_server_unittest rebuilt; dedicated-listener, 
plaintext/dynamic-limit, pre-TLS rejection, and idle-connection tests passed 
(4/4). Diff whitespace check passed. Full suite not run.
    
    * Clarify Redis rejection ownership and response delivery
    
    Document that rejection borrows the fd from the accept loop guard, which 
closes it on continue. Describe nonblocking best-effort error delivery in both 
server guides. Accumulate short TCP reads in rejection tests and verify 
plaintext EOF.
    
    Validation: rebuilt brpc_server_unittest; dedicated-listener, 
plaintext/dynamic-limit, pre-TLS rejection and idle-connection tests passed 
(4/4). Merge-base diff whitespace check passed. Full suite not run. Production 
behavior is unchanged.
    
    * Improve Redis rejection portability and regression coverage
    
    Guard MSG_NOSIGNAL and use SO_NOSIGPIPE when available; omit the optional 
error if per-socket signal suppression is unavailable. Share listener 
validation diagnostics, use RAII based on actual Server ownership in the 
invalid-configuration test, and verify new admission after slot recovery.
    
    Validation: cmake --build build --target brpc_server_unittest --parallel 4 
succeeded. Dedicated-listener, plaintext/dynamic-limit/slot-recovery, pre-TLS 
rejection, and idle-connection tests passed (4/4). Merge-base diff check 
passed. Full suite and macOS fallback were not run.
    
    * Generalize connection limits to public listeners
    
    * Address connection limit review feedback
---
 docs/cn/server.md             |  10 +
 docs/en/server.md             |  10 +
 src/brpc/acceptor.cpp         |  94 +++++++--
 src/brpc/acceptor.h           |  24 +++
 src/brpc/server.cpp           |  19 +-
 src/brpc/server.h             |  21 ++
 test/brpc_server_unittest.cpp | 442 ++++++++++++++++++++++++++++++++++++++----
 7 files changed, 569 insertions(+), 51 deletions(-)

diff --git a/docs/cn/server.md b/docs/cn/server.md
index 565b7bb3..efb6e9f2 100644
--- a/docs/cn/server.md
+++ b/docs/cn/server.md
@@ -378,6 +378,16 @@ Server.set_version(...)可以为server设置一个名称+版本,可通过/vers
 | ------------------------- | ----- | ---------------------------------------- 
| ------------------- |
 | log_idle_connection_close | false | Print log when an idle connection is 
closed | src/brpc/socket.cpp |
 
+## 限制服务端连接数
+
+设置`ServerOptions.max_connections`可以限制Server公共监听端口上的并发连接数。默认值为0,表示不限制。该端口上的所有协议和服务共享同一个上限,包括RPC、HTTP、Redis和内置服务,无需配置专用协议。
+
+Acceptor会在创建brpc 
Socket前预留连接名额,因此空闲连接和尚未完成TLS握手的连接也计入上限。超限连接会在协议解析或TLS认证前被直接关闭,不发送协议专用的错误响应。Socket回收时释放连接名额。
+
+通过`ServerOptions.internal_port`配置的内部监听端口不受限制,也不占用公共端口的连接名额,因此公共端口满载时,内部端口上的内置服务仍可访问。同一个业务Service注册到多个Server、监听多个端口时,各公共监听端口分别计数和限流;即使共享同一个Service对象,也不会合并连接数。`ServerStatistics.connection_count`包含公共和内部端口的连接总数,因此可能超过`max_connections`;`ServerStatistics.rejected_connection_count`记录公共端口因超限而拒绝的累计连接数。
+
+运行中的Server可以调用`Server::SetMaxConnections()`原子更新上限,也可以在以无限制值启动后动态开启限制。调高上限会影响后续连接准入;调低上限不会断开已有连接,公共端口的活跃连接数降到新上限以下后才会重新接受新连接。设置为0会关闭限制。Server未运行时该方法返回-1;该方法不会修改`options().max_connections`记录的启动值。每次`Start()`都使用该次传入选项中的上限。
+
 ## pid_file
 
 如果设置了此字段,Server启动时会创建一个同名文件,内容为进程号。默认为空。
diff --git a/docs/en/server.md b/docs/en/server.md
index f13193a8..30c7a867 100644
--- a/docs/en/server.md
+++ b/docs/en/server.md
@@ -375,6 +375,16 @@ If 
[-log_idle_connection_close](http://brpc.baidu.com:8765/flags/log_idle_connec
 | ------------------------- | ----- | ---------------------------------------- 
| ------------------- |
 | log_idle_connection_close | false | Print log when an idle connection is 
closed | src/brpc/socket.cpp |
 
+## Limit server connections
+
+Set `ServerOptions.max_connections` to limit simultaneous connections on the 
Server's public listener. The default value is 0 (unlimited). All protocols and 
services on that listener share the same limit, including RPC, HTTP, Redis, and 
builtin services. No dedicated protocol configuration is required.
+
+The acceptor reserves a slot before creating a brpc Socket, so idle 
connections and connections awaiting TLS handshakes count toward the limit. 
Connections over the limit are closed immediately without a protocol-specific 
response, before protocol parsing or TLS authentication. Connection slots are 
released when sockets are recycled.
+
+The internal listener configured by `ServerOptions.internal_port` is unlimited 
and does not consume public connection slots, so builtin services on that port 
remain available when the public listener is full. A business service 
registered with multiple Server instances on different ports has an independent 
limit on each public listener, even when those Servers share the same service 
object. `ServerStatistics.connection_count` includes both public and internal 
connections and may therefor [...]
+
+Call `Server::SetMaxConnections()` to atomically update the limit on a running 
Server, including one started without a limit. Raising the limit affects 
subsequent admission checks. Lowering it does not close existing connections; 
new connections are accepted again after the active public connection count 
falls below the limit. Set the limit to 0 to disable it. The setter returns -1 
if the Server is not running and leaves `options().max_connections` at its 
startup value. Each `Start()` us [...]
+
 ## pid_file
 
 If this field is non-empty, Server creates a file named so at start-up, with 
pid as the content. Empty by default.
diff --git a/src/brpc/acceptor.cpp b/src/brpc/acceptor.cpp
index 8333a171..99280037 100644
--- a/src/brpc/acceptor.cpp
+++ b/src/brpc/acceptor.cpp
@@ -16,7 +16,9 @@
 // under the License.
 
 
+#include <errno.h>
 #include <inttypes.h>
+#include <sys/socket.h>
 #include <gflags/gflags.h>
 #include "butil/fd_guard.h"                 // fd_guard 
 #include "butil/fd_utility.h"               // make_close_on_exec
@@ -38,6 +40,9 @@ Acceptor::Acceptor(bthread_keytable_pool_t* pool)
     , _listened_fd(-1)
     , _acception_id(0)
     , _empty_cond(&_map_mutex)
+    , _connection_count(0)
+    , _rejected_connection_count(0)
+    , _max_connections(0)
     , _force_ssl(false)
     , _ssl_ctx(nullptr) 
     , _socket_mode(SOCKET_MODE_TCP)
@@ -52,6 +57,13 @@ Acceptor::~Acceptor() {
 int Acceptor::StartAccept(int listened_fd, int idle_timeout_sec,
                           const std::shared_ptr<SocketSSLContext>& ssl_ctx,
                           bool force_ssl) {
+    return StartAccept(listened_fd, idle_timeout_sec, ssl_ctx, force_ssl, 0);
+}
+
+int Acceptor::StartAccept(int listened_fd, int idle_timeout_sec,
+                          const std::shared_ptr<SocketSSLContext>& ssl_ctx,
+                          bool force_ssl,
+                          size_t max_connections) {
     if (listened_fd < 0) {
         LOG(FATAL) << "Invalid listened_fd=" << listened_fd;
         return -1;
@@ -87,6 +99,7 @@ int Acceptor::StartAccept(int listened_fd, int 
idle_timeout_sec,
     _idle_timeout_sec = idle_timeout_sec;
     _force_ssl = force_ssl;
     _ssl_ctx = ssl_ctx;
+    SetMaxConnections(max_connections);
     
     // Creation of _acception_id is inside lock so that OnNewConnections
     // (which may run immediately) should see sane fields set below.
@@ -200,9 +213,37 @@ void Acceptor::Join() {
 }
 
 size_t Acceptor::ConnectionCount() const {
-    // Notice that _socket_map may be modified concurrently. This actually
-    // assumes that size() is safe to call concurrently.
-    return _socket_map.size();
+    return _connection_count.load(butil::memory_order_relaxed);
+}
+
+size_t Acceptor::RejectedConnectionCount() const {
+    return _rejected_connection_count.load(butil::memory_order_relaxed);
+}
+
+bool Acceptor::TryAcquireConnectionSlot() {
+    size_t count = _connection_count.load(butil::memory_order_relaxed);
+    do {
+        const size_t max_connections =
+            _max_connections.load(butil::memory_order_relaxed);
+        if (max_connections != 0 && count >= max_connections) {
+            return false;
+        }
+    } while (!_connection_count.compare_exchange_weak(
+        count, count + 1, butil::memory_order_relaxed));
+    return true;
+}
+
+void Acceptor::ReleaseConnectionSlot() {
+    const size_t previous =
+        _connection_count.fetch_sub(1, butil::memory_order_relaxed);
+    CHECK_GT(previous, 0u);
+}
+
+void Acceptor::SetMaxConnections(size_t max_connections) {
+    // The limit controls only future numeric admission decisions and does not
+    // publish socket state, so a relaxed store is sufficient.
+    _max_connections.store(
+        max_connections, butil::memory_order_relaxed);
 }
 
 void Acceptor::ListConnections(std::vector<SocketId>* conn_list,
@@ -275,7 +316,18 @@ void Acceptor::OnNewConnectionsUntilEAGAIN(Socket* 
acception) {
             acception->SetFailed(EINVAL, "Impossible! acception->user() MUST 
be Acceptor");
             return;
         }
-        
+
+        if (!am->TryAcquireConnectionSlot()) {
+            am->_rejected_connection_count.fetch_add(
+                1, butil::memory_order_relaxed);
+            LOG_EVERY_SECOND(WARNING)
+                << "Reject connection on " << acception->local_side()
+                << ": max_connections limit reached";
+            // in_fd closes the connection before Socket::Create(), protocol
+            // parsing or TLS authentication, without a protocol-specific 
reply.
+            continue;
+        }
+
         SocketId socket_id;
         SocketOptions options;
         options.keytable_pool = am->_keytable_pool;
@@ -288,21 +340,20 @@ void Acceptor::OnNewConnectionsUntilEAGAIN(Socket* 
acception) {
         options.socket_mode = am->_socket_mode;
         options.bthread_tag = am->_bthread_tag;
         if (Socket::Create(options, &socket_id) != 0) {
+            am->ReleaseConnectionSlot();
             LOG(ERROR) << "Fail to create Socket";
             continue;
         }
         in_fd.release(); // transfer ownership to socket_id
 
-        // There's a funny race condition here. After Socket::Create, messages
-        // from the socket are already handled and a RPC is possibly done
-        // before the socket is added into _socket_map below. This is found in
-        // ChannelTest.skip_parallel in test/brpc_channel_unittest.cpp (running
-        // on machines with few cores) where the _messenger.ConnectionCount()
-        // may surprisingly be 0 even if the RPC is already done.
+        // Socket::Create() may start processing messages immediately. The
+        // connection is already counted, but this loop owns its slot until
+        // the socket is pinned and inserted into _socket_map below.
 
         SocketUniquePtr sock;
         if (Socket::AddressFailedAsWell(socket_id, &sock) >= 0) {
             bool is_running = true;
+            bool registered = false;
             {
                 BAIDU_SCOPED_LOCK(am->_map_mutex);
                 is_running = (am->status() == RUNNING);
@@ -311,7 +362,13 @@ void Acceptor::OnNewConnectionsUntilEAGAIN(Socket* 
acception) {
                 // running or not. Otherwise, `Acceptor::BeforeRecycle'
                 // may be called (inside Socket::BeforeRecycled) after 
`Acceptor'
                 // has been destroyed
-                am->_socket_map.insert(socket_id, ConnectStatistics());
+                registered = (am->_socket_map.insert(
+                    socket_id, ConnectStatistics()) != nullptr);
+            }
+            if (!registered) {
+                am->ReleaseConnectionSlot();
+                sock->SetFailed(ENOMEM, "Fail to register accepted Socket");
+                continue;
             }
             if (!is_running) {
                 LOG(WARNING) << "Acceptor on fd=" << acception->fd()
@@ -321,8 +378,11 @@ void Acceptor::OnNewConnectionsUntilEAGAIN(Socket* 
acception) {
                         sock->description().c_str());
                 return;
             }
-        } // else: The socket has already been destroyed, Don't add its id
-          // into _socket_map
+        } else {
+            // The socket was recycled before insertion. BeforeRecycle() did
+            // not release its slot, which is still owned by this accept loop.
+            am->ReleaseConnectionSlot();
+        }
     }
 }
 
@@ -346,9 +406,11 @@ void Acceptor::BeforeRecycle(Socket* sock) {
         _empty_cond.Broadcast();
         return;
     }
-    // If a Socket could not be addressed shortly after its creation, it
-    // was not added into `_socket_map'.
-    _socket_map.erase(sock->id());
+    // Socket::Create() can fail or the socket can be recycled before it is
+    // inserted into the map. In those cases the accept loop releases the slot.
+    if (_socket_map.erase(sock->id()) != 0) {
+        ReleaseConnectionSlot();
+    }
     if (_socket_map.empty()) {
         _empty_cond.Broadcast();
     }
diff --git a/src/brpc/acceptor.h b/src/brpc/acceptor.h
index 2f138b30..01a2b26a 100644
--- a/src/brpc/acceptor.h
+++ b/src/brpc/acceptor.h
@@ -19,6 +19,7 @@
 #define BRPC_ACCEPTOR_H
 
 #include "bthread/bthread.h"                       // bthread_t
+#include "butil/atomicops.h"                       // butil::atomic
 #include "butil/synchronization/condition_variable.h"
 #include "butil/containers/flat_map.h"
 #include "brpc/input_messenger.h"
@@ -58,6 +59,12 @@ public:
     int StartAccept(int listened_fd, int idle_timeout_sec,
                     const std::shared_ptr<SocketSSLContext>& ssl_ctx,
                     bool force_ssl);
+    // Limit simultaneous connections on this listener. 0 means unlimited.
+    // Excess connections are closed before protocol parsing or TLS.
+    int StartAccept(int listened_fd, int idle_timeout_sec,
+                    const std::shared_ptr<SocketSSLContext>& ssl_ctx,
+                    bool force_ssl,
+                    size_t max_connections);
 
     // [thread-safe] Stop accepting connections.
     // `closewait_ms' is not used anymore.
@@ -72,6 +79,10 @@ public:
     // Get number of existing connections.
     size_t ConnectionCount() const;
 
+    // Get the cumulative number of connections rejected by this listener's
+    // connection limit.
+    size_t RejectedConnectionCount() const;
+
     // Clear `conn_list' and append all connections into it.
     void ListConnections(std::vector<SocketId>* conn_list);
 
@@ -93,6 +104,10 @@ private:
     // Remove the accepted socket `sock' from inside
     void BeforeRecycle(Socket* sock) override;
 
+    bool TryAcquireConnectionSlot();
+    void ReleaseConnectionSlot();
+    void SetMaxConnections(size_t max_connections);
+
     bthread_keytable_pool_t* _keytable_pool; // owned by Server
     Status _status;
     int _idle_timeout_sec;
@@ -108,6 +123,15 @@ private:
     // The map containing all the accepted sockets
     SocketMap _socket_map;
 
+    // A slot is reserved before Socket::Create(), closing the race where a
+    // socket starts processing before it is inserted into _socket_map. Until
+    // insertion, the accept loop owns the slot; afterwards BeforeRecycle()
+    // releases it. These atomics publish no socket state, so relaxed memory
+    // ordering is sufficient.
+    butil::atomic<size_t> _connection_count;
+    butil::atomic<size_t> _rejected_connection_count;
+    butil::atomic<size_t> _max_connections;
+
     bool _force_ssl;
     std::shared_ptr<SocketSSLContext> _ssl_ctx;
 
diff --git a/src/brpc/server.cpp b/src/brpc/server.cpp
index baf8b8fa..5a29ae23 100644
--- a/src/brpc/server.cpp
+++ b/src/brpc/server.cpp
@@ -138,6 +138,7 @@ ServerOptions::ServerOptions()
     , server_owns_interceptor(false)
     , num_threads(8)
     , max_concurrency(0)
+    , max_connections(0)
     , session_local_data_factory(nullptr)
     , reserved_session_local_data(0)
     , thread_local_data_factory(nullptr)
@@ -1167,7 +1168,8 @@ int Server::StartInternal(const butil::EndPoint& endpoint,
         // Pass ownership of `sockfd' to `_am'
         if (_am->StartAccept(sockfd, _options.idle_timeout_sec,
                              _default_ssl_ctx,
-                             _options.force_ssl) != 0) {
+                             _options.force_ssl,
+                             _options.max_connections) != 0) {
             LOG(ERROR) << "Fail to start acceptor";
             return -1;
         }
@@ -1209,7 +1211,8 @@ int Server::StartInternal(const butil::EndPoint& endpoint,
         // Pass ownership of `sockfd' to `_internal_am'
         if (_internal_am->StartAccept(sockfd, _options.idle_timeout_sec,
                                       _default_ssl_ctx,
-                                      false) != 0) {
+                                      false,
+                                      0) != 0) {
             LOG(ERROR) << "Fail to start internal_acceptor";
             return -1;
         }
@@ -1791,8 +1794,11 @@ google::protobuf::Service* Server::FindServiceByName(
 
 void Server::GetStat(ServerStatistics* stat) const {
     stat->connection_count = 0;
+    stat->rejected_connection_count = 0;
     if (_am) {
         stat->connection_count += _am->ConnectionCount();
+        stat->rejected_connection_count +=
+            _am->RejectedConnectionCount();
     }
     if (_internal_am) {
         stat->connection_count += _internal_am->ConnectionCount();
@@ -1801,6 +1807,15 @@ void Server::GetStat(ServerStatistics* stat) const {
     stat->builtin_service_count = builtin_service_count();
 }
 
+int Server::SetMaxConnections(size_t max_connections) {
+    if (!IsRunning() || _am == nullptr) {
+        LOG(WARNING) << "SetMaxConnections requires a running Server";
+        return -1;
+    }
+    _am->SetMaxConnections(max_connections);
+    return 0;
+}
+
 void Server::ListServices(std::vector<google::protobuf::Service*> *services) {
     if (!services) {
         return;
diff --git a/src/brpc/server.h b/src/brpc/server.h
index 6e7d2b2b..ccf125f9 100644
--- a/src/brpc/server.h
+++ b/src/brpc/server.h
@@ -132,6 +132,17 @@ struct ServerOptions {
     // Default: 0 (unlimited)
     int max_concurrency;
 
+    // Maximum number of simultaneous connections on the public listener,
+    // shared by all protocols and services, including builtin services.
+    // Idle connections and connections awaiting TLS handshakes count too.
+    // Excess connections are closed before protocol parsing or TLS, without
+    // a response. The internal listener has a separate, unlimited count.
+    // Other Server instances have independent limits, even if they share
+    // the same service object on different ports.
+    // Use Server::SetMaxConnections() to update the limit at runtime.
+    // Default: 0 (unlimited)
+    size_t max_connections;
+
     // Default value of method-level max concurrencies,
     // Overridable by Server.MaxConcurrencyOf().
     AdaptiveMaxConcurrency method_max_concurrency;
@@ -309,7 +320,10 @@ private:
 // This struct is originally designed to contain basic statistics of the
 // server. But bvar contains more stats and is more convenient.
 struct ServerStatistics {
+    // Total connections on the public and internal listeners.
     size_t connection_count;
+    // Cumulative connections rejected by the public listener's limit.
+    size_t rejected_connection_count;
     int user_service_count;
     int builtin_service_count;
 };
@@ -546,6 +560,13 @@ public:
     // Get statistics of this server
     void GetStat(ServerStatistics* stat) const;
 
+    // Atomically update the connection limit of the running public listener.
+    // Existing connections are not closed when the limit is lowered.
+    // Set to 0 to disable the limit. Does not change 
options().max_connections,
+    // which records the startup setting.
+    // Returns 0 on success, -1 if this Server is not running.
+    int SetMaxConnections(size_t max_connections);
+
     // Get the options passed to Start().
     const ServerOptions& options() const { return _options; }
 
diff --git a/test/brpc_server_unittest.cpp b/test/brpc_server_unittest.cpp
index e94d8508..898e3e79 100644
--- a/test/brpc_server_unittest.cpp
+++ b/test/brpc_server_unittest.cpp
@@ -19,9 +19,12 @@
 
 // Date: Sun Jul 13 15:04:18 CST 2014
 
+#include <errno.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <fstream>
+#include <limits>
+#include <memory>
 #include <gtest/gtest.h>
 #include <google/protobuf/descriptor.h>
 #include "butil/time.h"
@@ -49,8 +52,10 @@
 #include "brpc/builtin/bad_method_service.h"
 #include "brpc/server.h"
 #include "brpc/nshead_service.h"
+#include "brpc/acceptor.h"
 #include "brpc/restful.h"
 #include "brpc/channel.h"
+#include "brpc/redis.h"
 #include "brpc/socket_map.h"
 #include "brpc/controller.h"
 #include "brpc/compress.h"
@@ -1426,6 +1431,410 @@ TEST_F(ServerTest, close_idle_connections) {
     ASSERT_EQ(0ul, stat.connection_count);
 }
 
+// Returns a port nothing is listening on, or -1. `ServerOptions.internal_port`
+// has to be an explicit number, Server::Start() rejects 0 because it stands
+// for an ephemeral port, so ask the system for a free one rather than hardcode
+// a port that another test may be listening on.
+int PickUnusedPort() {
+    butil::fd_guard sockfd(butil::tcp_listen(butil::EndPoint(butil::IP_ANY, 
0)));
+    if (sockfd < 0) {
+        return -1;
+    }
+    butil::EndPoint point;
+    if (butil::get_local_side(sockfd, &point) != 0) {
+        return -1;
+    }
+    return point.port;
+}
+
+// Starts `server` on an ephemeral port and fills `options->internal_port` with
+// another one. Both are released before Start() binds them and something else
+// may take one in between, hence the retries. Returns 0 on success.
+int StartWithInternalPort(brpc::Server* server, brpc::ServerOptions* options) {
+    for (int i = 0; i < 10; ++i) {
+        int internal_port = PickUnusedPort();
+        if (internal_port < 0) {
+            continue;
+        }
+        options->internal_port = internal_port;
+        if (0 == server->Start("127.0.0.1:0", options)) {
+            return 0;
+        }
+    }
+    return -1;
+}
+
+static testing::AssertionResult WaitForServerConnections(
+    const brpc::Server& server, size_t expected) {
+    brpc::ServerStatistics stat;
+    const int64_t deadline = butil::gettimeofday_us() + 1000000;
+    do {
+        server.GetStat(&stat);
+        if (stat.connection_count == expected) {
+            return testing::AssertionSuccess();
+        }
+        usleep(1000);
+    } while (butil::gettimeofday_us() < deadline);
+    return testing::AssertionFailure()
+        << "Expected " << expected << " connections, got "
+        << stat.connection_count;
+}
+
+static void ExpectConnectionClosed(int fd) {
+    const struct timeval timeout = {1, 0};
+    ASSERT_EQ(0, setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO,
+                           &timeout, sizeof(timeout)));
+    char response;
+    ssize_t nr;
+    do {
+        nr = recv(fd, &response, sizeof(response), 0);
+    } while (nr < 0 && errno == EINTR);
+    // Fresh idle clients have sent no data: rejection must close the fd
+    // without sending a protocol-specific error or waiting for a request.
+    EXPECT_EQ(0, nr);
+}
+
+class ConnectionLimitPingHandler : public brpc::RedisCommandHandler {
+public:
+    brpc::RedisCommandHandlerResult Run(
+        brpc::RedisConnContext*, const std::vector<butil::StringPiece>&,
+        brpc::RedisReply* output, bool) override {
+        output->SetStatus("PONG");
+        return brpc::REDIS_CMD_HANDLED;
+    }
+};
+
+TEST_F(ServerTest, connection_limit_with_mixed_protocols) {
+    ConnectionLimitPingHandler ping_handler;
+    EchoServiceImpl echo_service;
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(
+        &echo_service, brpc::SERVER_DOESNT_OWN_SERVICE));
+    brpc::ServerOptions opt;
+    opt.redis_service = new brpc::RedisService;
+    opt.redis_service->AddCommandHandler("ping", &ping_handler);
+    opt.max_connections = 3;
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", &opt));
+
+    // Each protocol uses a separate persistent connection on the same port.
+    brpc::ChannelOptions copt;
+    copt.connection_type = "single";
+    copt.timeout_ms = 1000;
+    copt.max_retry = 0;
+    copt.connection_group = "connection_limit_rpc";
+    brpc::Channel rpc_channel;
+    ASSERT_EQ(0, rpc_channel.Init(server.listen_address(), &copt));
+    brpc::Controller rpc_cntl;
+    test::EchoRequest req;
+    test::EchoResponse res;
+    req.set_message(EXP_REQUEST);
+    test::EchoService_Stub stub(&rpc_channel);
+    stub.Echo(&rpc_cntl, &req, &res, nullptr);
+    ASSERT_FALSE(rpc_cntl.Failed()) << rpc_cntl.ErrorText();
+
+    copt.protocol = "redis";
+    copt.connection_group = "connection_limit_redis";
+    brpc::Channel redis_channel;
+    ASSERT_EQ(0, redis_channel.Init(server.listen_address(), &copt));
+    brpc::RedisRequest redis_req;
+    brpc::RedisResponse redis_res;
+    brpc::Controller redis_cntl;
+    ASSERT_TRUE(redis_req.AddCommand("ping"));
+    redis_channel.CallMethod(
+        nullptr, &redis_cntl, &redis_req, &redis_res, nullptr);
+    ASSERT_FALSE(redis_cntl.Failed()) << redis_cntl.ErrorText();
+    ASSERT_EQ(1, redis_res.reply_size());
+    ASSERT_STREQ("PONG", redis_res.reply(0).c_str());
+
+    copt.protocol = "http";
+    copt.connection_type = "pooled";
+    copt.connection_group = "connection_limit_http";
+    brpc::Channel http_channel;
+    ASSERT_EQ(0, http_channel.Init(server.listen_address(), &copt));
+    brpc::Controller http_cntl;
+    http_cntl.http_request().uri() = "/status";
+    http_channel.CallMethod(nullptr, &http_cntl, nullptr, nullptr, nullptr);
+    ASSERT_FALSE(http_cntl.Failed()) << http_cntl.ErrorText();
+    ASSERT_TRUE(WaitForServerConnections(server, 3));
+
+    butil::fd_guard rejected_client(
+        tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(rejected_client, 0);
+    ExpectConnectionClosed(rejected_client);
+    brpc::ServerStatistics stat;
+    server.GetStat(&stat);
+    EXPECT_EQ(3ul, stat.connection_count);
+    EXPECT_EQ(1ul, stat.rejected_connection_count);
+
+    // Requests on admitted sockets still work when the listener is full.
+    rpc_cntl.Reset();
+    stub.Echo(&rpc_cntl, &req, &res, nullptr);
+    ASSERT_FALSE(rpc_cntl.Failed()) << rpc_cntl.ErrorText();
+}
+
+TEST_F(ServerTest, connection_limit_runtime_updates) {
+    brpc::Server server;
+    EXPECT_EQ(-1, server.SetMaxConnections(1));
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", nullptr));
+    const butil::EndPoint ep = server.listen_address();
+    butil::fd_guard first_client(tcp_connect(ep, nullptr));
+    butil::fd_guard second_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(first_client, 0);
+    ASSERT_GE(second_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 2));
+
+    // Enabling a limit must count clients admitted while it was unlimited.
+    ASSERT_EQ(0, server.SetMaxConnections(1));
+    butil::fd_guard rejected_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(rejected_client, 0);
+    ExpectConnectionClosed(rejected_client);
+    ASSERT_TRUE(WaitForServerConnections(server, 2));
+
+    // A separate Server remains reachable while this public listener is full.
+    EchoServiceImpl rpc_service;
+    brpc::Server rpc_server;
+    ASSERT_EQ(0, rpc_server.AddService(
+        &rpc_service, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, rpc_server.Start("127.0.0.1:0", nullptr));
+    SendSleepRPC(rpc_server.listen_address(), 0, true);
+
+    ASSERT_EQ(0, server.SetMaxConnections(3));
+    butil::fd_guard third_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(third_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 3));
+
+    // Lowering the limit leaves established clients connected, but rejects
+    // new clients until the active count drops strictly below the limit.
+    ASSERT_EQ(0, server.SetMaxConnections(1));
+    ASSERT_TRUE(WaitForServerConnections(server, 3));
+    butil::fd_guard lowered_limit_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(lowered_limit_client, 0);
+    ExpectConnectionClosed(lowered_limit_client);
+    first_client.reset(-1);
+    second_client.reset(-1);
+    ASSERT_TRUE(WaitForServerConnections(server, 1));
+    butil::fd_guard at_limit_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(at_limit_client, 0);
+    ExpectConnectionClosed(at_limit_client);
+
+    third_client.reset(-1);
+    ASSERT_TRUE(WaitForServerConnections(server, 0));
+    butil::fd_guard recovered_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(recovered_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 1));
+
+    ASSERT_EQ(0, server.SetMaxConnections(0));
+    first_client.reset(tcp_connect(ep, nullptr));
+    second_client.reset(tcp_connect(ep, nullptr));
+    ASSERT_GE(first_client, 0);
+    ASSERT_GE(second_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 3));
+    brpc::ServerStatistics stat;
+    server.GetStat(&stat);
+    EXPECT_EQ(3ul, stat.rejected_connection_count);
+    EXPECT_EQ(0ul, server.options().max_connections);
+
+    ASSERT_EQ(0, server.Stop(0));
+    EXPECT_EQ(-1, server.SetMaxConnections(1));
+    ASSERT_EQ(0, server.Join());
+    ASSERT_TRUE(WaitForServerConnections(server, 0));
+}
+
+TEST_F(ServerTest, connection_limit_keeps_internal_listener_available) {
+    brpc::Server server;
+    brpc::ServerOptions opt;
+    opt.max_connections = 1;
+    ASSERT_EQ(0, StartWithInternalPort(&server, &opt));
+    butil::fd_guard public_client(
+        tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(public_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 1));
+
+    butil::EndPoint internal_ep = server.listen_address();
+    internal_ep.port = opt.internal_port;
+    butil::fd_guard first_internal(tcp_connect(internal_ep, nullptr));
+    butil::fd_guard second_internal(tcp_connect(internal_ep, nullptr));
+    ASSERT_GE(first_internal, 0);
+    ASSERT_GE(second_internal, 0);
+    // GetStat includes both listeners; only the public count is limited.
+    ASSERT_TRUE(WaitForServerConnections(server, 3));
+    brpc::ChannelOptions copt;
+    copt.protocol = "http";
+    copt.connection_type = "short";
+    copt.timeout_ms = 1000;
+    copt.max_retry = 0;
+    brpc::Channel channel;
+    ASSERT_EQ(0, channel.Init(internal_ep, &copt));
+    brpc::Controller cntl;
+    cntl.http_request().uri() = "/status";
+    channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr);
+    ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+    ASSERT_TRUE(WaitForServerConnections(server, 3));
+
+    butil::fd_guard rejected_client(
+        tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(rejected_client, 0);
+    ExpectConnectionClosed(rejected_client);
+    brpc::ServerStatistics stat;
+    server.GetStat(&stat);
+    EXPECT_EQ(3ul, stat.connection_count);
+    EXPECT_EQ(1ul, stat.rejected_connection_count);
+
+    // Internal connections must not prevent public connection slot recovery.
+    public_client.reset(-1);
+    ASSERT_TRUE(WaitForServerConnections(server, 2));
+    public_client.reset(tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(public_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 3));
+}
+
+TEST_F(ServerTest, connection_limit_is_per_listener) {
+    // One business service is shared by two Servers listening on different
+    // ports. Each port must have its own limit, count and runtime updates.
+    EchoServiceImpl shared_service;
+    brpc::Server servers[2];
+    for (size_t i = 0; i < 2; ++i) {
+        ASSERT_EQ(0, servers[i].AddService(
+            &shared_service, brpc::SERVER_DOESNT_OWN_SERVICE));
+        brpc::ServerOptions opt;
+        opt.max_connections = i + 1;
+        ASSERT_EQ(0, servers[i].Start("127.0.0.1:0", &opt));
+    }
+    butil::fd_guard first_client(
+        tcp_connect(servers[0].listen_address(), nullptr));
+    ASSERT_GE(first_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(servers[0], 1));
+    butil::fd_guard second_port_clients[2];
+    for (auto& client : second_port_clients) {
+        client.reset(tcp_connect(servers[1].listen_address(), nullptr));
+        ASSERT_GE(client, 0);
+    }
+    ASSERT_TRUE(WaitForServerConnections(servers[1], 2));
+
+    for (auto& server : servers) {
+        butil::fd_guard rejected_client(
+            tcp_connect(server.listen_address(), nullptr));
+        ASSERT_GE(rejected_client, 0);
+        ExpectConnectionClosed(rejected_client);
+        brpc::ServerStatistics stat;
+        server.GetStat(&stat);
+        EXPECT_EQ(1ul, stat.rejected_connection_count);
+    }
+
+    ASSERT_EQ(0, servers[0].SetMaxConnections(2));
+    butil::fd_guard extra_client(
+        tcp_connect(servers[0].listen_address(), nullptr));
+    ASSERT_GE(extra_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(servers[0], 2));
+    butil::fd_guard still_rejected(
+        tcp_connect(servers[1].listen_address(), nullptr));
+    ASSERT_GE(still_rejected, 0);
+    ExpectConnectionClosed(still_rejected);
+    brpc::ServerStatistics stat;
+    servers[1].GetStat(&stat);
+    EXPECT_EQ(2ul, stat.connection_count);
+    EXPECT_EQ(2ul, stat.rejected_connection_count);
+}
+
+TEST_F(ServerTest, connection_limit_restarts_with_startup_setting) {
+    brpc::Server server;
+    brpc::ServerOptions opt;
+    opt.max_connections = 1;
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", &opt));
+    ASSERT_EQ(0, server.SetMaxConnections(0));
+    EXPECT_EQ(1ul, server.options().max_connections);
+    butil::fd_guard clients[2];
+    for (auto& client : clients) {
+        client.reset(tcp_connect(server.listen_address(), nullptr));
+        ASSERT_GE(client, 0);
+    }
+    ASSERT_TRUE(WaitForServerConnections(server, 2));
+    ASSERT_EQ(0, server.Stop(0));
+    ASSERT_EQ(0, server.Join());
+    ASSERT_TRUE(WaitForServerConnections(server, 0));
+
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", &opt));
+    clients[0].reset(tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(clients[0], 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 1));
+    clients[1].reset(tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(clients[1], 0);
+    ExpectConnectionClosed(clients[1]);
+}
+
+TEST_F(ServerTest, connection_limit_rejects_before_tls_handshake) {
+    brpc::Server server;
+    brpc::ServerOptions opt;
+    opt.max_connections = 1;
+    opt.force_ssl = true;
+    brpc::CertInfo& cert = opt.mutable_ssl_options()->default_cert;
+    cert.certificate = "cert1.crt";
+    cert.private_key = "cert1.key";
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", &opt));
+
+    const butil::EndPoint ep = server.listen_address();
+    // An idle TCP socket consumes a slot without initiating a TLS handshake.
+    butil::fd_guard first_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(first_client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 1));
+    butil::fd_guard rejected_client(tcp_connect(ep, nullptr));
+    ASSERT_GE(rejected_client, 0);
+    // EOF without a ClientHello verifies rejection before TLS authentication.
+    ExpectConnectionClosed(rejected_client);
+    brpc::ServerStatistics stat;
+    server.GetStat(&stat);
+    EXPECT_EQ(1ul, stat.connection_count);
+    EXPECT_EQ(1ul, stat.rejected_connection_count);
+}
+
+TEST_F(ServerTest, connection_limit_socket_creation_failure) {
+    brpc::Server server;
+    brpc::ServerOptions opt;
+    opt.max_connections = 1;
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", &opt));
+    brpc::Acceptor* acceptor = server._am;
+    ASSERT_TRUE(acceptor->TryAcquireConnectionSlot());
+
+    brpc::SocketOptions socket_opt;
+    socket_opt.user = acceptor;
+    // Avoid STREAM_FAKE_FD, which is intentionally accepted without an OS fd.
+    socket_opt.fd = std::numeric_limits<int>::max() - 1;
+    brpc::SocketId id;
+    ASSERT_NE(0, brpc::Socket::Create(socket_opt, &id));
+    // Socket::Create can call BeforeRecycle on failure. Since the socket was
+    // never inserted into the map, the accept loop must still own the slot.
+    ASSERT_EQ(1ul, acceptor->ConnectionCount());
+    acceptor->ReleaseConnectionSlot();
+    butil::fd_guard client(tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 1));
+}
+
+TEST_F(ServerTest, connection_limit_socket_recycled_before_registration) {
+    brpc::Server server;
+    brpc::ServerOptions opt;
+    opt.max_connections = 1;
+    ASSERT_EQ(0, server.Start("127.0.0.1:0", &opt));
+    brpc::Acceptor* acceptor = server._am;
+    ASSERT_TRUE(acceptor->TryAcquireConnectionSlot());
+
+    brpc::SocketOptions socket_opt;
+    socket_opt.user = acceptor;
+    brpc::SocketId id;
+    ASSERT_EQ(0, brpc::Socket::Create(socket_opt, &id));
+    // Exercise a socket recycled after successful creation, before the accept
+    // loop can address it and insert it into the connection map.
+    ASSERT_EQ(0, brpc::Socket::SetFailed(id));
+    brpc::SocketUniquePtr socket;
+    ASSERT_EQ(-1, brpc::Socket::AddressFailedAsWell(id, &socket));
+    ASSERT_EQ(1ul, acceptor->ConnectionCount());
+    acceptor->ReleaseConnectionSlot();
+
+    butil::fd_guard client(tcp_connect(server.listen_address(), nullptr));
+    ASSERT_GE(client, 0);
+    ASSERT_TRUE(WaitForServerConnections(server, 1));
+}
+
 TEST_F(ServerTest, logoff_and_multiple_start) {
     butil::Timer timer;
     butil::EndPoint ep;
@@ -1737,39 +2146,6 @@ void CallEchoByHttp(const butil::EndPoint& ep, 
brpc::Controller* cntl) {
     chan.CallMethod(nullptr, cntl, &req, &res, nullptr);
 }
 
-// Returns a port nothing is listening on, or -1. `ServerOptions.internal_port`
-// has to be an explicit number, Server::Start() rejects 0 because it stands
-// for an ephemeral port, so ask the system for a free one rather than hardcode
-// a port that another test may be listening on.
-int PickUnusedPort() {
-    butil::fd_guard sockfd(butil::tcp_listen(butil::EndPoint(butil::IP_ANY, 
0)));
-    if (sockfd < 0) {
-        return -1;
-    }
-    butil::EndPoint point;
-    if (butil::get_local_side(sockfd, &point) != 0) {
-        return -1;
-    }
-    return point.port;
-}
-
-// Starts `server` on an ephemeral port and fills `options->internal_port` with
-// another one. Both are released before Start() binds them and something else
-// may take one in between, hence the retries. Returns 0 on success.
-int StartWithInternalPort(brpc::Server* server, brpc::ServerOptions* options) {
-    for (int i = 0; i < 10; ++i) {
-        int internal_port = PickUnusedPort();
-        if (internal_port < 0) {
-            continue;
-        }
-        options->internal_port = internal_port;
-        if (0 == server->Start("127.0.0.1:0", options)) {
-            return 0;
-        }
-    }
-    return -1;
-}
-
 TEST_F(ServerTest, ordinary_services_are_not_served_on_internal_port) {
     const struct {
         brpc::ProtocolType protocol;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to