Skip to content

refactor: 重构核心,轻量化处理 PHP同步阻塞#3

Closed
watsonhaw5566 wants to merge 1 commit into
masterfrom
refactor_core
Closed

refactor: 重构核心,轻量化处理 PHP同步阻塞#3
watsonhaw5566 wants to merge 1 commit into
masterfrom
refactor_core

Conversation

@watsonhaw5566

@watsonhaw5566 watsonhaw5566 commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Improved server concurrency by distributing HTTP and WebSocket connections across worker threads.
    • Added timeout handling for PHP-backed HTTP requests, returning a clear 504 Gateway Timeout response when needed.
    • Improved WebSocket callback handling for connection, message, and disconnection events.
  • Bug Fixes

    • Serialized PHP callback execution to improve stability in non-thread-safe PHP environments.
    • Added support for safely handling blocking PHP operations alongside asynchronous requests.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The server now runs connection tasks on a multi-threaded Tokio runtime, moves thread-safe handler bundles into those tasks, serializes PHP callback execution, applies HTTP callback timeouts, and routes WebSocket lifecycle callbacks through the shared blocking helper.

Changes

PHP callback concurrency

Layer / File(s) Summary
Serialize PHP callback execution
rust/src/runtime.rs
A global mutex and run_php_blocking serialize synchronous PHP callbacks, while the child runtime uses two worker threads and eight blocking threads.
Move handlers into connection tasks
rust/src/http.rs, rust/src/websocket.rs
Handler bundles are marked Send/Sync; accepted connections use tokio::spawn with owned handlers, and WebSocket callbacks are grouped in WsHandlerBundle.
Dispatch callbacks and time out HTTP requests
rust/src/http.rs, rust/src/websocket.rs
HTTP callbacks run through the blocking helper with timeout-generated 504 responses; WebSocket open, message, and close callbacks use the same helper.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TokioTask
  participant HTTPHandler
  participant WebSocket
  participant run_php_blocking
  participant PHPCallback

  Client->>TokioTask: send HTTP request or WebSocket frame
  TokioTask->>HTTPHandler: process HTTP request
  HTTPHandler->>run_php_blocking: invoke PHP callback with timeout
  run_php_blocking->>PHPCallback: execute serialized callback
  Client->>WebSocket: establish connection or send frame
  WebSocket->>run_php_blocking: invoke open, message, or close callback
  run_php_blocking->>PHPCallback: execute serialized callback
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题与本次改动一致,准确概括了核心重构和 PHP 同步阻塞处理的主要变化。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor_core

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/src/http.rs`:
- Around line 293-299: Correct the response body length in the 504 Gateway
Timeout response constructed in the HTTP handling flow, matching Content-Length
to the actual body bytes. Also propagate the timeout state into the keep-alive
decision so timeout responses honor the existing Connection: close header and
terminate the socket.
- Around line 277-283: Update the timeout flow around run_php_blocking in the
HTTP response handling so the PHP call is executed by a genuinely asynchronous
future rather than being eagerly evaluated inside std::future::ready. Ensure
request_timeout begins applying before the blocking handler runs, allowing hung
handlers to reach the existing timeout/504 path and not hold PHP_CALL_LOCK
indefinitely.
- Around line 21-28: Remove the unsafe Send/Sync implementations for
HandlerBundle in rust/src/http.rs:21-28, keep PHP callback execution
thread-affine, and replace Tokio cross-thread movement with channel-based
handoff. Update the affected HTTP flow in rust/src/http.rs:107-138, runtime
paths in rust/src/runtime.rs:51-82 and 296-302, and websocket flows in
rust/src/websocket.rs:23-46, 108-111, 148-166, and 202-205 so
HandlerBundle/ZendCallable remains on its owning thread; the mutex must only
serialize execution, not justify cross-thread transfer.

In `@rust/src/websocket.rs`:
- Around line 158-160: Update the Message::Binary handling in the websocket
message flow to avoid converting invalid UTF-8 data into an empty string via
unwrap_or_default. Use a loss-aware conversion, explicitly reject invalid
frames, or pass the raw bytes/base64 through to the PHP block while preserving
valid payload content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 994a979a-5e17-4593-8752-b6b473ca01fe

📥 Commits

Reviewing files that changed from the base of the PR and between 1587007 and 0613a32.

📒 Files selected for processing (3)
  • rust/src/http.rs
  • rust/src/runtime.rs
  • rust/src/websocket.rs

Comment thread rust/src/http.rs
Comment on lines +21 to +28
// SAFETY:
// `ZendCallable` 内部只是指向 PHP 函数表的指针,进程生命周期内
// 地址不变。我们通过 `runtime::PHP_CALL_LOCK` 全局互斥锁保证同一
// 时刻只有一个线程进入 PHP 解释器,因此跨线程传递 `HandlerBundle`
// 是安全的。没有这个 `Send` impl,`tokio::spawn` 无法用它(它
// 要求 `Future: Send`)。
unsafe impl Send for HandlerBundle {}
unsafe impl Sync for HandlerBundle {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'PHP_ZTS|ZTS|ZendCallable|unsafe impl (Send|Sync)|run_php_blocking|new_multi_thread' .
fd -a -t f 'Cargo.toml|Cargo.lock|build.rs|config.m4' .

Repository: watsonhaw5566/lychee-worker

Length of output: 15043


🌐 Web query:

ext-php-rs ZendCallable Send Sync thread safety PHP NTS ZTS Tokio block_in_place mutex

💡 Result:

In the context of the ext-php-rs library, managing thread safety between Rust and PHP requires strict adherence to PHP's execution model and the specific configuration (NTS vs. ZTS) [1][2][3]. ZendCallable and Thread Safety The ZendCallable type in ext-php-rs is explicitly marked as not implementing Send and not implementing Sync [4]. This is because PHP's internal callable representation is bound to the Zend engine's state, which is not thread-safe to access from multiple threads simultaneously [4][1]. You must not share or move ZendCallable instances between threads. PHP Threading Model (NTS vs. ZTS) - NTS (Non-Thread-Safe): PHP operates in a single-threaded request mode. Accessing PHP state (such as Zend globals) is only safe from the thread currently executing the request [1]. - ZTS (Zend Thread Safety): PHP uses thread-local storage (TSRM) to maintain engine state per thread [1][5]. To safely interact with PHP from an external thread (e.g., one spawned by Rust), you must use the PhpThreadGuard RAII wrapper [5]. This ensures that the thread is properly registered with PHP's thread-local storage before executing any code that touches the PHP engine [5]. Tokio and Blocking When integrating Tokio with PHP code: - PHP Request Threads: If you are running inside a standard PHP request handler, you are in a synchronous, single-threaded context [1]. Using blocking calls (like std::sync::Mutex or blocking tokio operations) within a PHP function can block the entire PHP worker thread, preventing it from handling further requests or finishing its current execution. - block_in_place and spawn_blocking: If you are inside an asynchronous Tokio task (e.g., using #[php_async_impl]) and need to execute synchronous PHP code or block on a operation, use tokio::task::block_in_place or tokio::task::spawn_blocking [6][7][8]. This offloads the blocking work, preventing the async executor from stalling [8]. - Mutexes: Use caution with mutexes. If a thread holds a mutex while also needing to interact with the PHP engine (especially in ZTS mode), ensure that you do not hold that lock while performing operations that might re-enter PHP or perform long-running blocking tasks, as this is a common source of deadlocks [9]. If you must share data between Rust threads, perform the sharing at the Rust level using standard Rust concurrency primitives, and only enter the PHP context through appropriate thread-safe handles or guards when ready to execute PHP-specific logic [1][5].

Citations:


Remove the Send/Sync impls unless PHP callbacks stay on one owning thread. ext-php-rs treats ZendCallable as non-Send/non-Sync, and the current Tokio setup can move these bundles across worker threads; the mutex only serializes access, it does not make Zend/PHP state safe to transfer. Keep the PHP-owned execution path thread-affine and hand off work via channels instead.

📍 Affects 3 files
  • rust/src/http.rs#L21-L28 (this comment)
  • rust/src/http.rs#L107-L138
  • rust/src/runtime.rs#L51-L82
  • rust/src/runtime.rs#L296-L302
  • rust/src/websocket.rs#L23-L46
  • rust/src/websocket.rs#L108-L111
  • rust/src/websocket.rs#L148-L166
  • rust/src/websocket.rs#L202-L205
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/src/http.rs` around lines 21 - 28, Remove the unsafe Send/Sync
implementations for HandlerBundle in rust/src/http.rs:21-28, keep PHP callback
execution thread-affine, and replace Tokio cross-thread movement with
channel-based handoff. Update the affected HTTP flow in
rust/src/http.rs:107-138, runtime paths in rust/src/runtime.rs:51-82 and
296-302, and websocket flows in rust/src/websocket.rs:23-46, 108-111, 148-166,
and 202-205 so HandlerBundle/ZendCallable remains on its owning thread; the
mutex must only serialize execution, not justify cross-thread transfer.

Comment thread rust/src/http.rs
Comment on lines +277 to +283
let response = match tokio::time::timeout(
request_timeout,
std::future::ready(crate::runtime::run_php_blocking(move || {
crate::php_api::call_on_http(http_handler, &method_c, &path_c, &raw_headers, &body)
})),
)
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C5 'time::timeout|future::ready|run_php_blocking|block_in_place' rust/src

Repository: watsonhaw5566/lychee-worker

Length of output: 10549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- http.rs relevant block ---'
sed -n '268,286p' rust/src/http.rs

echo
echo '--- runtime.rs run_php_blocking ---'
sed -n '58,82p' rust/src/runtime.rs

echo
echo '--- any other timeout+run_php_blocking patterns ---'
rg -n -C3 'timeout\(|run_php_blocking\(' rust/src | sed -n '1,220p'

echo
echo '--- evaluation-order probe (Python) ---'
python3 - <<'PY'
events = []

def run_php_blocking():
    events.append("run_php_blocking")
    return "php-result"

def ready(x):
    events.append(f"ready({x})")
    return ("future", x)

def timeout(d, fut):
    events.append(f"timeout({d}, {fut})")
    return "timeout-await"

# Mimic timeout(request_timeout, ready(run_php_blocking()))
result = timeout(5, ready(run_php_blocking()))
events.append(f"result={result}")
print("\n".join(events))
PY

Repository: watsonhaw5566/lychee-worker

Length of output: 6262


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '283,306p' rust/src/http.rs

Repository: watsonhaw5566/lychee-worker

Length of output: 914


Move the PHP call out of future::ready
run_php_blocking(...) is evaluated before tokio::time::timeout starts polling, so the deadline only wraps an already-completed ready future. A hung handler can still hold PHP_CALL_LOCK indefinitely, and this branch never reaches the 504 path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/src/http.rs` around lines 277 - 283, Update the timeout flow around
run_php_blocking in the HTTP response handling so the PHP call is executed by a
genuinely asynchronous future rather than being eagerly evaluated inside
std::future::ready. Ensure request_timeout begins applying before the blocking
handler runs, allowing hung handlers to reach the existing timeout/504 path and
not hold PHP_CALL_LOCK indefinitely.

Comment thread rust/src/http.rs
Comment on lines +293 to +299
"HTTP/1.1 504 Gateway Timeout\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
Content-Length: 19\r\n\
Connection: close\r\n\
\r\n\
Gateway Timeout\n"
.to_string()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the malformed 504 response and force connection closure.

Gateway Timeout\n is 16 bytes, not 19. Additionally, propagate a timeout flag to the keep-alive decision so this Connection: close response actually closes the socket.

Proposed content-length fix
-                 Content-Length: 19\r\n\
+                 Content-Length: 16\r\n\
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/src/http.rs` around lines 293 - 299, Correct the response body length in
the 504 Gateway Timeout response constructed in the HTTP handling flow, matching
Content-Length to the actual body bytes. Also propagate the timeout state into
the keep-alive decision so timeout responses honor the existing Connection:
close header and terminate the socket.

Comment thread rust/src/websocket.rs
Comment on lines 158 to +160
Message::Binary(data) => {
let payload = String::from_utf8(data).unwrap_or_default();
crate::php_api::call_on_ws_message(
ws_message_handler,
&conn_id,
&payload,
);
crate::runtime::run_php_blocking(|| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not silently replace invalid binary payloads with an empty message.

Use lossy conversion, explicitly reject non-UTF-8 frames, or expose bytes/base64 to PHP.

Minimal loss-aware fix
-                                let payload = String::from_utf8(data).unwrap_or_default();
+                                let payload = String::from_utf8_lossy(&data).into_owned();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Message::Binary(data) => {
let payload = String::from_utf8(data).unwrap_or_default();
crate::php_api::call_on_ws_message(
ws_message_handler,
&conn_id,
&payload,
);
crate::runtime::run_php_blocking(|| {
Message::Binary(data) => {
let payload = String::from_utf8_lossy(&data).into_owned();
crate::runtime::run_php_blocking(|| {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/src/websocket.rs` around lines 158 - 160, Update the Message::Binary
handling in the websocket message flow to avoid converting invalid UTF-8 data
into an empty string via unwrap_or_default. Use a loss-aware conversion,
explicitly reject invalid frames, or pass the raw bytes/base64 through to the
PHP block while preserving valid payload content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant