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 64663837 Support progressive HTTP read timeout (#3469)
64663837 is described below

commit 64663837f66b700907adcefcb92a8c967ecdb7de
Author: Chuang Zhang <[email protected]>
AuthorDate: Sat Aug 22 23:39:01 2026 +0800

    Support progressive HTTP read timeout (#3469)
    
    * Add timeout support for progressive HTTP reads (#15)
    
    Co-authored-by: zchuango <[email protected]>
    
    * Potential fix for pull request finding
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    ---------
    
    Co-authored-by: BGQ99 <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 example/http_c++/http_client.cpp               |  37 ++++
 example/http_c++/http_server.cpp               |  16 +-
 src/brpc/controller.cpp                        | 258 ++++++++++++++++++++++++-
 src/brpc/controller.h                          |   9 +
 src/brpc/details/controller_private_accessor.h |   6 +
 src/brpc/errno.proto                           |   1 +
 src/brpc/policy/http_rpc_protocol.cpp          |   4 +-
 src/brpc/policy/http_rpc_protocol.h            |  10 +-
 test/brpc_http_rpc_protocol_unittest.cpp       | 238 ++++++++++++++++++++++-
 9 files changed, 573 insertions(+), 6 deletions(-)

diff --git a/example/http_c++/http_client.cpp b/example/http_c++/http_client.cpp
index 5c2c94b4..4f588f28 100644
--- a/example/http_c++/http_client.cpp
+++ b/example/http_c++/http_client.cpp
@@ -22,11 +22,17 @@
 // - Access www.foo.com
 //   ./http_client www.foo.com
 
+#include <string>
 #include <gflags/gflags.h>
 #include <butil/logging.h>
 #include <brpc/channel.h>
+#include "bthread/countdown_event.h"
 
 DEFINE_string(d, "", "POST this data to the http server");
+DEFINE_bool(progressive, false,
+            "whether or not progressive read data from server");
+DEFINE_int32(progressive_read_timeout_ms, 5000,
+             "progressive read data idle timeout in milliseconds");
 DEFINE_string(load_balancer, "", "The algorithm for load balancing");
 DEFINE_int32(timeout_ms, 2000, "RPC timeout in milliseconds");
 DEFINE_int32(max_retry, 3, "Max retries(not including the first RPC)"); 
@@ -36,6 +42,25 @@ namespace brpc {
 DECLARE_bool(http_verbose);
 }
 
+class PartDataReader : public brpc::ProgressiveReader {
+public:
+    explicit PartDataReader(bthread::CountdownEvent* done) : _done(done) {}
+
+    butil::Status OnReadOnePart(const void* data, size_t length) override {
+        const std::string part(static_cast<const char*>(data), length);
+        LOG(INFO) << "data: " << part << " size: " << length;
+        return butil::Status::OK();
+    }
+
+    void OnEndOfMessage(const butil::Status& status) override {
+        LOG(INFO) << "progressive read data final status : " << status;
+        _done->signal();
+        delete this;
+    }
+private:
+    bthread::CountdownEvent* _done;
+};
+
 int main(int argc, char* argv[]) {
     // Parse gflags. We recommend you to use gflags as well.
     GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
@@ -71,6 +96,11 @@ int main(int argc, char* argv[]) {
         cntl.request_attachment().append(FLAGS_d);
     }
 
+    if (FLAGS_progressive) {
+        
cntl.set_progressive_read_timeout_ms(FLAGS_progressive_read_timeout_ms);
+        cntl.response_will_be_read_progressively();
+    }
+
     // Because `done'(last parameter) is nullptr, this function waits until
     // the response comes back or error occurs(including timedout).
     channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr);
@@ -78,6 +108,13 @@ int main(int argc, char* argv[]) {
         std::cerr << cntl.ErrorText() << std::endl;
         return -1;
     }
+
+    if (FLAGS_progressive) {
+        bthread::CountdownEvent done(1);
+        cntl.ReadProgressiveAttachmentBy(new PartDataReader(&done));
+        done.wait();
+        LOG(INFO) << "wait client progressive read done safely";
+    }
     // If -http_verbose is on, brpc already prints the response to stderr.
     if (!brpc::FLAGS_http_verbose) {
         std::cout << cntl.response_attachment() << std::endl;
diff --git a/example/http_c++/http_server.cpp b/example/http_c++/http_server.cpp
index 9f905dbb..134071b5 100644
--- a/example/http_c++/http_server.cpp
+++ b/example/http_c++/http_server.cpp
@@ -31,6 +31,8 @@ DEFINE_int32(idle_timeout_s, -1, "Connection will be closed 
if there is no "
 DEFINE_string(certificate, "cert.pem", "Certificate file path to enable SSL");
 DEFINE_string(private_key, "key.pem", "Private key file path to enable SSL");
 DEFINE_string(ciphers, "", "Cipher suite used for SSL connections");
+DEFINE_bool(enable_progressive_timeout, false,
+            "Simulate a long stall mid-progressive response to trigger 
client-side progressive read idle timeout");
 
 namespace example {
 
@@ -100,10 +102,15 @@ public:
         for (int i = 0; i < 100; ++i) {
             char buf[16];
             int len = snprintf(buf, sizeof(buf), "part_%d ", i);
-            args->pa->Write(buf, len);
+            if (args->pa->Write(buf, len) != 0) {
+                break;
+            }
 
             // sleep a while to send another part.
             bthread_usleep(10000);
+            if (FLAGS_enable_progressive_timeout && i == 50) {
+                bthread_usleep(100000000UL);
+            }
         }
         return nullptr;
     }
@@ -190,10 +197,15 @@ public:
         for (int i = 0; i < 100; ++i) {
             char buf[48];
             int len = snprintf(buf, sizeof(buf), "event: foo\ndata: Hello, 
world! (%d)\n\n", i);
-            args->pa->Write(buf, len);
+            if (args->pa->Write(buf, len) != 0) {
+                break;
+            }
 
             // sleep a while to send another part.
             bthread_usleep(10000 * 10);
+            if (FLAGS_enable_progressive_timeout && i == 50) {
+                bthread_usleep(100000000UL);
+            }
         }
         return nullptr;
     }
diff --git a/src/brpc/controller.cpp b/src/brpc/controller.cpp
index 062d62f0..6c3ecee3 100644
--- a/src/brpc/controller.cpp
+++ b/src/brpc/controller.cpp
@@ -73,6 +73,7 @@ BAIDU_REGISTER_ERRNO(brpc::EEOF, "Got EOF");
 BAIDU_REGISTER_ERRNO(brpc::EUNUSED, "The socket was not needed");
 BAIDU_REGISTER_ERRNO(brpc::ESSL, "SSL related operation failed");
 BAIDU_REGISTER_ERRNO(brpc::EH2RUNOUTSTREAMS, "The H2 socket was run out of 
streams");
+BAIDU_REGISTER_ERRNO(brpc::EPROGREADTIMEOUT, "Progressive read timed out");
 
 BAIDU_REGISTER_ERRNO(brpc::EINTERNAL, "General internal error");
 BAIDU_REGISTER_ERRNO(brpc::ERESPONSE, "Bad response");
@@ -95,7 +96,8 @@ DEFINE_bool(graceful_quit_on_sigterm, false,
             "Register SIGTERM handle func to quit graceful");
 DEFINE_bool(graceful_quit_on_sighup, false,
             "Register SIGHUP handle func to quit graceful");            
-
+DEFINE_bool(log_idle_progressive_read_close, false,
+            "Print log when an idle progressive read is closed");
 const IdlNames idl_single_req_single_res = { "req", "res" };
 const IdlNames idl_single_req_multi_res = { "req", "" };
 const IdlNames idl_multi_req_single_res = { "", "res" };
@@ -174,6 +176,226 @@ public:
     void OnEndOfMessage(const butil::Status&) {}
 };
 
+struct ProgressiveReadTimeoutTask;
+
+struct ProgressiveReadTimeoutState {
+    ProgressiveReadTimeoutState(SocketId id, int32_t timeout_ms)
+        : socket_id(id)
+        , read_timeout_ms(timeout_ms)
+        , deadline_us(butil::cpuwide_time_us() + timeout_ms * 1000L)
+        , timer_id(0)
+        , timer_task(nullptr)
+        , user_callback_running(false)
+        , reader_failed(false)
+        , timeout_triggered(false)
+        , end_delivered(false) {}
+
+    butil::Mutex mutex;
+    const SocketId socket_id;
+    const int32_t read_timeout_ms;
+    int64_t deadline_us;
+    bthread_timer_t timer_id;
+    ProgressiveReadTimeoutTask* timer_task;
+    bool user_callback_running;
+    bool reader_failed;
+    bool timeout_triggered;
+    bool end_delivered;
+    butil::Status timer_error;
+};
+
+struct ProgressiveReadTimeoutTask {
+    explicit ProgressiveReadTimeoutTask(
+        const std::shared_ptr<ProgressiveReadTimeoutState>& state_in)
+        : state(state_in) {}
+
+    std::shared_ptr<ProgressiveReadTimeoutState> state;
+};
+
+class ProgressiveTimeoutReader : public ProgressiveReader {
+public:
+    ProgressiveTimeoutReader(SocketId id, int32_t read_timeout_ms,
+                             ProgressiveReader* reader)
+        : _reader(reader)
+        , _state(new ProgressiveReadTimeoutState(id, read_timeout_ms)) {}
+
+    int Start() {
+        std::unique_lock<butil::Mutex> mu(_state->mutex);
+        return AddWatchdogLocked(_state, _state->read_timeout_ms * 1000L);
+    }
+
+    butil::Status OnReadOnePart(const void* data, size_t length) override {
+        {
+            std::unique_lock<butil::Mutex> mu(_state->mutex);
+            if (_state->timeout_triggered) {
+                return MakeTimeoutStatus(_state->read_timeout_ms);
+            }
+            if (!_state->timer_error.ok()) {
+                return _state->timer_error;
+            }
+            _state->user_callback_running = true;
+        }
+
+        butil::Status status = _reader->OnReadOnePart(data, length);
+        {
+            std::unique_lock<butil::Mutex> mu(_state->mutex);
+            _state->user_callback_running = false;
+            if (_state->timeout_triggered) {
+                status = MakeTimeoutStatus(_state->read_timeout_ms);
+            } else if (!_state->timer_error.ok()) {
+                status = _state->timer_error;
+            } else if (status.ok() && !_state->end_delivered) {
+                _state->deadline_us = butil::cpuwide_time_us() +
+                    _state->read_timeout_ms * 1000L;
+            } else if (!status.ok()) {
+                _state->reader_failed = true;
+            }
+        }
+        return status;
+    }
+
+    void OnEndOfMessage(const butil::Status& status) override {
+        bthread_timer_t timer_id = 0;
+        ProgressiveReadTimeoutTask* timer_task = nullptr;
+        butil::Status final_status = status;
+        ProgressiveReader* reader = nullptr;
+        {
+            std::unique_lock<butil::Mutex> mu(_state->mutex);
+            if (_state->end_delivered) {
+                LOG(ERROR) << "ProgressiveReader::OnEndOfMessage was called 
more than once";
+                return;
+            }
+            _state->end_delivered = true;
+            timer_id = _state->timer_id;
+            timer_task = _state->timer_task;
+            _state->timer_id = 0;
+            _state->timer_task = nullptr;
+            if (_state->timeout_triggered) {
+                final_status = MakeTimeoutStatus(_state->read_timeout_ms);
+            } else if (!_state->timer_error.ok()) {
+                final_status = _state->timer_error;
+            }
+            reader = _reader;
+            _reader = nullptr;
+        }
+
+        CancelWatchdog(timer_id, timer_task);
+        reader->OnEndOfMessage(final_status);
+        delete this;
+    }
+
+private:
+    ~ProgressiveTimeoutReader() override {}
+
+    static butil::Status MakeTimeoutStatus(int32_t timeout_ms) {
+        return butil::Status(
+            EPROGREADTIMEOUT,
+            "Progressive read timed out after %d ms", timeout_ms);
+    }
+
+    static butil::Status MakeTimerErrorStatus(int error_code) {
+        return butil::Status(
+            error_code, "Fail to add progressive read timeout timer: %s",
+            berror(error_code));
+    }
+
+    static void CancelWatchdog(
+        bthread_timer_t timer_id, ProgressiveReadTimeoutTask* timer_task) {
+        if (timer_id == 0) {
+            return;
+        }
+        const int rc = bthread_timer_del(timer_id);
+        if (rc == 0) {
+            delete timer_task;
+        } else if (rc == 1 || rc == EINVAL) {
+            // The callback owns timer_task once it starts running. EINVAL 
means
+            // that the callback has already finished and released the task.
+        } else {
+            LOG(ERROR) << "Unexpected bthread_timer_del error=" << rc;
+        }
+    }
+
+    static int AddWatchdogLocked(
+        const std::shared_ptr<ProgressiveReadTimeoutState>& state,
+        int64_t delay_us) {
+        if (state->end_delivered || state->reader_failed) {
+            return ECANCELED;
+        }
+        if (delay_us <= 0) {
+            delay_us = 1;
+        }
+        ProgressiveReadTimeoutTask* task =
+            new (std::nothrow) ProgressiveReadTimeoutTask(state);
+        if (task == nullptr) {
+            return ENOMEM;
+        }
+        bthread_timer_t timer_id = 0;
+        const int rc = bthread_timer_add(
+            &timer_id, butil::microseconds_from_now(delay_us),
+            HandleIdleProgressiveReader, task);
+        if (rc != 0) {
+            delete task;
+            return rc;
+        }
+        state->timer_id = timer_id;
+        state->timer_task = task;
+        return 0;
+    }
+
+    static void HandleIdleProgressiveReader(void* arg) {
+        std::unique_ptr<ProgressiveReadTimeoutTask> task(
+            static_cast<ProgressiveReadTimeoutTask*>(arg));
+        const std::shared_ptr<ProgressiveReadTimeoutState> state = task->state;
+        bool fail_socket = false;
+        int error_code = 0;
+        std::string error_text;
+        {
+            std::unique_lock<butil::Mutex> mu(state->mutex);
+            if (state->timer_task == task.get()) {
+                state->timer_id = 0;
+                state->timer_task = nullptr;
+            }
+            if (state->end_delivered || state->reader_failed) {
+                return;
+            }
+
+            const int64_t now_us = butil::cpuwide_time_us();
+            if (state->user_callback_running || now_us < state->deadline_us) {
+                const int64_t delay_us = state->user_callback_running
+                    ? state->read_timeout_ms * 1000L
+                    : state->deadline_us - now_us;
+                const int rc = AddWatchdogLocked(state, delay_us);
+                if (rc != 0) {
+                    state->timer_error = MakeTimerErrorStatus(rc);
+                    fail_socket = true;
+                    error_code = rc;
+                    error_text = state->timer_error.error_str();
+                }
+            } else {
+                state->timeout_triggered = true;
+                fail_socket = true;
+                error_code = EPROGREADTIMEOUT;
+                error_text = 
MakeTimeoutStatus(state->read_timeout_ms).error_str();
+            }
+        }
+
+        if (!fail_socket) {
+            return;
+        }
+        SocketUniquePtr socket;
+        if (Socket::Address(state->socket_id, &socket) != 0) {
+            LOG(ERROR) << "Fail to address socket_id=" << state->socket_id
+                       << " after progressive read timeout";
+        } else {
+            LOG_IF(INFO, FLAGS_log_idle_progressive_read_close)
+                << error_text << ", socket_id=" << state->socket_id;
+            socket->SetFailed(error_code, "%s", error_text.c_str());
+        }
+    }
+
+    ProgressiveReader* _reader;
+    const std::shared_ptr<ProgressiveReadTimeoutState> _state;
+};
+
 static IgnoreAllRead* s_ignore_all_read = nullptr;
 static pthread_once_t s_ignore_all_read_once = PTHREAD_ONCE_INIT;
 static void CreateIgnoreAllRead() { s_ignore_all_read = new IgnoreAllRead; }
@@ -261,6 +483,8 @@ void Controller::ResetPods() {
     _backup_request_ms = UNSET_MAGIC_NUM;
     _backup_request_policy = nullptr;
     _connect_timeout_ms = UNSET_MAGIC_NUM;
+    _progressive_read_timeout_ms = UNSET_MAGIC_NUM;
+    _progressive_read_socket_id = INVALID_SOCKET_ID;
     _real_timeout_ms = UNSET_MAGIC_NUM;
     _deadline_us = -1;
     _timeout_id = 0;
@@ -336,6 +560,11 @@ void Controller::Call::Reset() {
     stream_user_data = nullptr;
 }
 
+void Controller::set_progressive_read_timeout_ms(
+    int32_t progressive_read_timeout_ms) {
+    _progressive_read_timeout_ms = progressive_read_timeout_ms;
+}
+
 void Controller::set_timeout_ms(int64_t timeout_ms) {
     if (timeout_ms <= 0x7fffffff) {
         _timeout_ms = timeout_ms;
@@ -1609,6 +1838,33 @@ void 
Controller::ReadProgressiveAttachmentBy(ProgressiveReader* r) {
                          __FUNCTION__));
     }
     add_flag(FLAGS_PROGRESSIVE_READER);
+    if (progressive_read_timeout_ms() > 0) {
+        if (_request_protocol != PROTOCOL_HTTP ||
+            _progressive_read_socket_id == INVALID_SOCKET_ID) {
+            pthread_once(&s_ignore_all_read_once, CreateIgnoreAllRead);
+            _rpa->ReadProgressiveAttachmentBy(s_ignore_all_read);
+            return r->OnEndOfMessage(butil::Status(
+                ENOTSUP,
+                "Progressive read timeout is only supported for HTTP/1.x"));
+        }
+        ProgressiveTimeoutReader* reader = new (std::nothrow)
+            ProgressiveTimeoutReader(
+                _progressive_read_socket_id, _progressive_read_timeout_ms, r);
+        if (reader == nullptr) {
+            pthread_once(&s_ignore_all_read_once, CreateIgnoreAllRead);
+            _rpa->ReadProgressiveAttachmentBy(s_ignore_all_read);
+            return r->OnEndOfMessage(
+                butil::Status(ENOMEM, "Fail to create progressive timeout 
reader"));
+        }
+        const int rc = reader->Start();
+        if (rc != 0) {
+            pthread_once(&s_ignore_all_read_once, CreateIgnoreAllRead);
+            _rpa->ReadProgressiveAttachmentBy(s_ignore_all_read);
+            return reader->OnEndOfMessage(butil::Status(
+                rc, "Fail to add progressive read timeout timer: %s", 
berror(rc)));
+        }
+        return _rpa->ReadProgressiveAttachmentBy(reader);
+    }
     return _rpa->ReadProgressiveAttachmentBy(r);
 }
 
diff --git a/src/brpc/controller.h b/src/brpc/controller.h
index 90215e47..c05dbb75 100644
--- a/src/brpc/controller.h
+++ b/src/brpc/controller.h
@@ -197,6 +197,13 @@ public:
     // are undefined on the server side (may crash).
     // ------------------------------------------------------------------
 
+    // Set/get the maximum idle interval in milliseconds between body parts of
+    // an HTTP/1.x response read progressively. A non-positive value disables
+    // the timeout. The timer starts when ReadProgressiveAttachmentBy() is
+    // called.
+    void set_progressive_read_timeout_ms(int32_t progressive_read_timeout_ms);
+    int32_t progressive_read_timeout_ms() const { return 
_progressive_read_timeout_ms; }
+
     // Set/get timeout in milliseconds for the RPC call. Use
     // ChannelOptions.timeout_ms on unset.
     void set_timeout_ms(int64_t timeout_ms);
@@ -911,6 +918,8 @@ private:
     int32_t _timeout_ms;
     int32_t _connect_timeout_ms;
     int32_t _backup_request_ms;
+    int32_t _progressive_read_timeout_ms;
+    SocketId _progressive_read_socket_id;
     // Priority: `_backup_request_policy' > `_backup_request_ms'.
     BackupRequestPolicy* _backup_request_policy;
     // If this rpc call has retry/backup request,this var save the real 
timeout for current call
diff --git a/src/brpc/details/controller_private_accessor.h 
b/src/brpc/details/controller_private_accessor.h
index ea0d30e1..fc4b9666 100644
--- a/src/brpc/details/controller_private_accessor.h
+++ b/src/brpc/details/controller_private_accessor.h
@@ -132,6 +132,12 @@ public:
     void set_readable_progressive_attachment(ReadableProgressiveAttachment* s)
     { _cntl->_rpa.reset(s); }
 
+    void set_readable_progressive_attachment(
+        ReadableProgressiveAttachment* s, SocketId socket_id) {
+        _cntl->_rpa.reset(s);
+        _cntl->_progressive_read_socket_id = socket_id;
+    }
+
     void set_auth_flags(uint32_t auth_flags) {
         _cntl->_auth_flags = auth_flags;
     }
diff --git a/src/brpc/errno.proto b/src/brpc/errno.proto
index 26ffadc2..166d82dc 100644
--- a/src/brpc/errno.proto
+++ b/src/brpc/errno.proto
@@ -41,6 +41,7 @@ enum Errno {
     ESSL                    = 1016;  // SSL related error
     EH2RUNOUTSTREAMS        = 1017;  // The H2 socket was run out of streams
     EREJECT                 = 1018;  // The Request is rejected
+    EPROGREADTIMEOUT        = 1019;  // The Progressive read timeout
 
     // Errno caused by server
     EINTERNAL               = 2001;  // Internal Server Error
diff --git a/src/brpc/policy/http_rpc_protocol.cpp 
b/src/brpc/policy/http_rpc_protocol.cpp
index ca1d30d1..9d63de23 100644
--- a/src/brpc/policy/http_rpc_protocol.cpp
+++ b/src/brpc/policy/http_rpc_protocol.cpp
@@ -434,7 +434,8 @@ void ProcessHttpResponse(InputMessageBase* msg) {
 
         if (imsg_guard->read_body_progressively()) {
             // Set RPA if needed
-            accessor.set_readable_progressive_attachment(imsg_guard.get());
+            accessor.set_readable_progressive_attachment(
+                imsg_guard.get(), imsg_guard->socket_id());
             const int sc = res_header->status_code();
             if (sc < 200 || sc >= 300) {
                 // Even if the body is for streaming purpose, a non-OK status
@@ -1196,6 +1197,7 @@ ParseResult ParseHttpMessage(butil::IOBuf *source, Socket 
*socket,
         }
         http_imsg = new HttpContext(socket->is_read_progressive(),
                                     socket->http_request_method());
+        http_imsg->SetSocketId(socket->id());
         // Parsing http is costly, parsing an incomplete http message from the
         // beginning repeatedly should be avoided, otherwise the cost may reach
         // O(n^2) in the worst case. Save incomplete http messages in sockets
diff --git a/src/brpc/policy/http_rpc_protocol.h 
b/src/brpc/policy/http_rpc_protocol.h
index bc8bd065..92d0cae7 100644
--- a/src/brpc/policy/http_rpc_protocol.h
+++ b/src/brpc/policy/http_rpc_protocol.h
@@ -87,11 +87,18 @@ class HttpContext : public ReadableProgressiveAttachment
                   , public InputMessageBase
                   , public HttpMessage {
 public:
+    void SetSocketId(SocketId id) {
+        _socket_id = id;
+    }
+
+    SocketId socket_id() const { return _socket_id; }
+
     explicit HttpContext(bool read_body_progressively,
                          HttpMethod request_method = HTTP_METHOD_GET)
         : InputMessageBase()
         , HttpMessage(read_body_progressively, request_method)
-        , _is_stage2(false) {
+        , _is_stage2(false)
+        , _socket_id(INVALID_SOCKET_ID) {
         // add one ref for Destroy
         butil::intrusive_ptr<HttpContext>(this).detach();
     }
@@ -122,6 +129,7 @@ public:
 
 private:
     bool _is_stage2;
+    SocketId _socket_id;
 };
 
 // Implement functions required in protocol.h
diff --git a/test/brpc_http_rpc_protocol_unittest.cpp 
b/test/brpc_http_rpc_protocol_unittest.cpp
index df5c838f..af2e17bc 100644
--- a/test/brpc_http_rpc_protocol_unittest.cpp
+++ b/test/brpc_http_rpc_protocol_unittest.cpp
@@ -19,6 +19,7 @@
 
 // Date: Sun Jul 13 15:04:18 CST 2014
 
+#include <atomic>
 #include <cstddef>
 #include <string>
 #include <sys/ioctl.h>
@@ -736,9 +737,13 @@ static void CopyPAPrefixedWithSeqNo(char* buf, uint64_t 
seq_no) {
 class DownloadServiceImpl : public ::test::DownloadService {
 public:
     DownloadServiceImpl(DonePlace done_place = DONE_BEFORE_CREATE_PA,
-                        size_t num_repeat = 1)
+                        size_t num_repeat = 1,
+                        int write_interval_us = 0,
+                        int initial_write_delay_us = 0)
         : _done_place(done_place)
         , _nrep(num_repeat)
+        , _write_interval_us(write_interval_us)
+        , _initial_write_delay_us(initial_write_delay_us)
         , _nwritten(0)
         , _ever_full(false)
         , _last_errno(0) {}
@@ -762,6 +767,9 @@ public:
         if (_done_place == DONE_BEFORE_CREATE_PA) {
             done_guard.reset(nullptr);
         }
+        if (_initial_write_delay_us > 0) {
+            bthread_usleep(_initial_write_delay_us);
+        }
         ASSERT_GT(PA_DATA_LEN, 8u);  // long enough to hold a 64-bit decimal.
         char buf[PA_DATA_LEN];
         for (size_t c = 0; c < _nrep;) {
@@ -778,6 +786,9 @@ public:
                 }
             } else {
                 _nwritten += PA_DATA_LEN;
+                if (_write_interval_us > 0) {
+                    bthread_usleep(_write_interval_us);
+                }
             }
             ++c;
         }
@@ -840,6 +851,8 @@ public:
 private:
     DonePlace _done_place;
     size_t _nrep;
+    int _write_interval_us;
+    int _initial_write_delay_us;
     size_t _nwritten;
     bool _ever_full;
     int _last_errno;
@@ -941,6 +954,47 @@ private:
     butil::Status _destroying_st;
 };
 
+class TimeoutReadBody : public brpc::ProgressiveReader,
+                        public brpc::SharedObject {
+public:
+    explicit TimeoutReadBody(int read_delay_us = 0, int read_error = 0)
+        : _read_delay_us(read_delay_us)
+        , _read_error(read_error)
+        , _nread(0)
+        , _nend(0)
+        , _end_error(0) {
+        butil::intrusive_ptr<TimeoutReadBody>(this).detach();
+    }
+
+    butil::Status OnReadOnePart(const void*, size_t length) override {
+        if (_read_delay_us > 0) {
+            bthread_usleep(_read_delay_us);
+        }
+        _nread.fetch_add(length);
+        if (_read_error != 0) {
+            return butil::Status(_read_error, "intended progressive read 
failure");
+        }
+        return butil::Status::OK();
+    }
+
+    void OnEndOfMessage(const butil::Status& status) override {
+        _end_error.store(status.error_code());
+        _nend.fetch_add(1);
+        butil::intrusive_ptr<TimeoutReadBody>(this, false);
+    }
+
+    size_t read_bytes() const { return _nread.load(); }
+    int end_count() const { return _nend.load(); }
+    int end_error() const { return _end_error.load(); }
+
+private:
+    const int _read_delay_us;
+    const int _read_error;
+    std::atomic<size_t> _nread;
+    std::atomic<int> _nend;
+    std::atomic<int> _end_error;
+};
+
 #ifdef BUTIL_USE_ASAN
 static const int GENERAL_DELAY_US = 1000000; // 1s
 #else
@@ -1034,6 +1088,188 @@ TEST_F(HttpTest, read_short_body_progressively) {
     }
 }
 
+TEST_F(HttpTest, progressive_read_timeout_keeps_active_reader_alive) {
+    const int port = 8923;
+    DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 8, 100000);
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(port, nullptr));
+
+    brpc::Channel channel;
+    brpc::ChannelOptions options;
+    options.protocol = brpc::PROTOCOL_HTTP;
+    ASSERT_EQ(0, channel.Init(butil::EndPoint(butil::my_ip(), port), 
&options));
+
+    brpc::Controller cntl;
+    cntl.response_will_be_read_progressively();
+    cntl.set_progressive_read_timeout_ms(500);
+    cntl.http_request().uri() = "/DownloadService/Download";
+    channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr);
+    ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+
+    butil::intrusive_ptr<TimeoutReadBody> reader(new TimeoutReadBody);
+    cntl.ReadProgressiveAttachmentBy(reader.get());
+    for (int i = 0; i < 200 && reader->end_count() == 0; ++i) {
+        bthread_usleep(10000);
+    }
+    ASSERT_EQ(1, reader->end_count());
+    EXPECT_EQ(0, reader->end_error());
+    EXPECT_EQ(8 * PA_DATA_LEN, reader->read_bytes());
+}
+
+TEST_F(HttpTest, progressive_read_timeout_closes_idle_http1_reader_once) {
+    const int port = 8923;
+    DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 2, 300000);
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(port, nullptr));
+
+    butil::intrusive_ptr<TimeoutReadBody> reader(new TimeoutReadBody);
+    {
+        brpc::Channel channel;
+        brpc::ChannelOptions options;
+        options.protocol = brpc::PROTOCOL_HTTP;
+        ASSERT_EQ(0, channel.Init(butil::EndPoint(butil::my_ip(), port), 
&options));
+        {
+            brpc::Controller cntl;
+            cntl.response_will_be_read_progressively();
+            cntl.set_progressive_read_timeout_ms(50);
+            cntl.http_request().uri() = "/DownloadService/Download";
+            channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr);
+            ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+            cntl.ReadProgressiveAttachmentBy(reader.get());
+            bthread_usleep(400000);
+            EXPECT_EQ(0, reader->end_count());
+        }
+    }
+    for (int i = 0; i < 100 && reader->end_count() == 0; ++i) {
+        bthread_usleep(10000);
+    }
+    ASSERT_EQ(1, reader->end_count());
+    EXPECT_EQ(brpc::EPROGREADTIMEOUT, reader->end_error());
+    bthread_usleep(400000);
+    EXPECT_EQ(1, reader->end_count());
+}
+
+TEST_F(HttpTest, progressive_read_timeout_before_first_body_part) {
+    const int port = 8923;
+    DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 1, 0, 300000);
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(port, nullptr));
+
+    butil::intrusive_ptr<TimeoutReadBody> reader(new TimeoutReadBody);
+    {
+        brpc::Channel channel;
+        brpc::ChannelOptions options;
+        options.protocol = brpc::PROTOCOL_HTTP;
+        ASSERT_EQ(0, channel.Init(butil::EndPoint(butil::my_ip(), port), 
&options));
+        {
+            brpc::Controller cntl;
+            cntl.response_will_be_read_progressively();
+            cntl.set_progressive_read_timeout_ms(50);
+            cntl.http_request().uri() = "/DownloadService/Download";
+            channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr);
+            ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+            cntl.ReadProgressiveAttachmentBy(reader.get());
+            bthread_usleep(400000);
+            EXPECT_EQ(size_t(0), reader->read_bytes());
+            EXPECT_EQ(0, reader->end_count());
+        }
+    }
+    for (int i = 0; i < 100 && reader->end_count() == 0; ++i) {
+        bthread_usleep(10000);
+    }
+    ASSERT_EQ(1, reader->end_count());
+    EXPECT_EQ(brpc::EPROGREADTIMEOUT, reader->end_error());
+}
+
+TEST_F(HttpTest, progressive_read_timeout_ignores_slow_user_callback) {
+    const int port = 8923;
+    DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 3, 50000);
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(port, nullptr));
+
+    brpc::Channel channel;
+    brpc::ChannelOptions options;
+    options.protocol = brpc::PROTOCOL_HTTP;
+    ASSERT_EQ(0, channel.Init(butil::EndPoint(butil::my_ip(), port), 
&options));
+
+    brpc::Controller cntl;
+    cntl.response_will_be_read_progressively();
+    cntl.set_progressive_read_timeout_ms(50);
+    cntl.http_request().uri() = "/DownloadService/Download";
+    channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr);
+    ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+
+    butil::intrusive_ptr<TimeoutReadBody> reader(
+        new TimeoutReadBody(200000));
+    cntl.ReadProgressiveAttachmentBy(reader.get());
+    for (int i = 0; i < 100 && reader->end_count() == 0; ++i) {
+        bthread_usleep(10000);
+    }
+    ASSERT_EQ(1, reader->end_count());
+    EXPECT_EQ(0, reader->end_error());
+    EXPECT_EQ(3 * PA_DATA_LEN, reader->read_bytes());
+}
+
+TEST_F(HttpTest, progressive_read_timeout_preserves_reader_error) {
+    const int port = 8923;
+    DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA, 10);
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(port, nullptr));
+
+    brpc::Channel channel;
+    brpc::ChannelOptions options;
+    options.protocol = brpc::PROTOCOL_HTTP;
+    ASSERT_EQ(0, channel.Init(butil::EndPoint(butil::my_ip(), port), 
&options));
+
+    brpc::Controller cntl;
+    cntl.response_will_be_read_progressively();
+    cntl.set_progressive_read_timeout_ms(1000);
+    cntl.http_request().uri() = "/DownloadService/Download";
+    channel.CallMethod(nullptr, &cntl, nullptr, nullptr, nullptr);
+    ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+
+    butil::intrusive_ptr<TimeoutReadBody> reader(
+        new TimeoutReadBody(0, EIO));
+    cntl.ReadProgressiveAttachmentBy(reader.get());
+    for (int i = 0; i < 100 && reader->end_count() == 0; ++i) {
+        bthread_usleep(10000);
+    }
+    ASSERT_EQ(1, reader->end_count());
+    EXPECT_EQ(EIO, reader->end_error());
+}
+
+TEST_F(HttpTest, progressive_read_timeout_rejects_http2) {
+    const int port = 8923;
+    brpc::Server server;
+    ASSERT_EQ(0, server.AddService(&_svc, brpc::SERVER_DOESNT_OWN_SERVICE));
+    ASSERT_EQ(0, server.Start(port, nullptr));
+
+    brpc::Channel channel;
+    brpc::ChannelOptions options;
+    options.protocol = brpc::PROTOCOL_H2;
+    ASSERT_EQ(0, channel.Init(butil::EndPoint(butil::my_ip(), port), 
&options));
+
+    brpc::Controller cntl;
+    cntl.response_will_be_read_progressively();
+    cntl.set_progressive_read_timeout_ms(1000);
+    cntl.http_request().uri() = "/EchoService/Echo";
+    test::EchoRequest req;
+    req.set_message(EXP_REQUEST);
+    channel.CallMethod(nullptr, &cntl, &req, nullptr, nullptr);
+    ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText();
+
+    butil::intrusive_ptr<TimeoutReadBody> reader(new TimeoutReadBody);
+    cntl.ReadProgressiveAttachmentBy(reader.get());
+    ASSERT_EQ(1, reader->end_count());
+    EXPECT_EQ(ENOTSUP, reader->end_error());
+    EXPECT_EQ(size_t(0), reader->read_bytes());
+}
+
 TEST_F(HttpTest, read_progressively_after_cntl_destroys) {
     DownloadServiceImpl svc(DONE_BEFORE_CREATE_PA,
                             std::numeric_limits<size_t>::max());


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

Reply via email to