Skip to content

MultiplexRpcTransporter strict timeout . - #43

Open
xuanyanwow wants to merge 2 commits into
mainfrom
transporter
Open

MultiplexRpcTransporter strict timeout . #43
xuanyanwow wants to merge 2 commits into
mainfrom
transporter

Conversation

@xuanyanwow

@xuanyanwow xuanyanwow commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes
    • Improved error messages by including the relevant exception class when available.
    • Improved multiplexed transport timeout handling so reads consistently respect the configured time limit.
    • Prevented successive message reads from exceeding the overall timeout.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes add optional server class prefixes to exception messages and update multiplexed RPC reads to track and apply the remaining configured timeout across header and body reads.

Changes

Server exception messages

Layer / File(s) Summary
Exception message context
src/Exception/ServerException.php
ServerException prefixes the computed message with the optional $error['data']['class'] value.

Multiplexed RPC read timeout

Layer / File(s) Summary
Remaining timeout propagation
src/Transporter/MultiplexRpcTransporter.php
receive() recalculates the remaining timeout for header and body reads, while readBytes() converts the supplied float timeout for stream_select().

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • friendsofhyperf/jet#40: Introduced the multiplexed RPC stream handling whose timeout behavior is updated here.
🚥 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 The title is related to the main change, which tightens timeout handling in MultiplexRpcTransporter.
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 transporter

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.2.5)

PHPStan was skipped because the user-provided config is missing the required paths: directive.


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: 2

🤖 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 `@src/Exception/ServerException.php`:
- Around line 27-30: Update the exception message construction in
ServerException to extract data before accessing class, verify data is an array,
and only prepend class when it is a non-empty string. Preserve the existing
fallback message when data or class is invalid.

In `@src/Transporter/MultiplexRpcTransporter.php`:
- Around line 65-68: Update readBytes() to compute a single deadline when the
read begins, then recalculate the remaining seconds and microseconds before
every stream_select() iteration, including after partial fread() results. Ensure
the configured timeout is not reset between reads and add a regression test
covering trickled partial data that verifies receive() respects the overall
deadline.
🪄 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 Plus

Run ID: b969db55-0ee4-4688-919f-04cc8bed5cb8

📥 Commits

Reviewing files that changed from the base of the PR and between 44e80fc and 07d64ed.

📒 Files selected for processing (2)
  • src/Exception/ServerException.php
  • src/Transporter/MultiplexRpcTransporter.php

Comment on lines +27 to +30
$class = $error['data']['class'] ?? '';
if ($class) {
$message = $class . ': ' . $message;
}

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

php -r '
$cases = [
    ["message" => "failure", "data" => "malformed"],
    ["message" => "failure", "data" => ["class" => ["unexpected"]]],
];

foreach ($cases as $error) {
    try {
        $class = $error["data"]["class"] ?? "";
        echo $class . ": " . $error["message"] . PHP_EOL;
    } catch (Throwable $e) {
        echo get_class($e) . ": " . $e->getMessage() . PHP_EOL;
    }
}
'

Repository: friendsofhyperf/jet

Length of output: 255


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== file =="
cat -n src/Exception/ServerException.php | sed -n '1,80p'

echo "== composer / PHP versions =="
if [ -f composer.json ]; then
  python3 - <<'PY'
import json
p=json.load(open('composer.json'))
print(json.dumps(p.get('require', {}).get('php'),'ignore'), indent=2)
print('composer require php:', p.get('require', {}).get('php'))
PY
fi

echo "== usage of ServerException =="
rg -n "new ServerException|ServerException::|data\]\s*=>|class\]\s*=>|server.*error" src -S || true

echo "== behavioral PHP probe =="
php -r '
error_reporting(E_ALL);
set_error_handler(function($severity, $message, $file, $line) {
    throw new \ErrorException($message, 0, $severity, $file, $line);
});
$cases = [
    ["message" => "failure", "data" => "malformed"],
    ["message" => "failure", "data" => ["class" => ["unexpected"]]],
    ["message" => "failure", "data" => ["class" => ["array"]], "data" => ["class" => ["array"]]],
];
try {
php -r '"'"'
set_error_handler(function($severity, $message, $file, $line) { throw new Exception("error",$severity); });
$cases = [
    ["message"=> "failure", "data"=> "malformed"],
    ["message"=> "failure", "data"=> ["class"=> ["unexpected"]]],
    ["message"=> "failure", "data"=> ["class"=> 123]],
];
foreach ($cases as $error) {
    try {
        $class = $error["data"]["class"] ?? "";
        echo $class . ": " . $error["message"] . PHP_EOL;
    } catch (Throwable $e) {
        echo get_class($e) . ": " . $e->getMessage() . PHP_EOL;
    }
}
'"'"'
except (Throwable $e) {
    echo get_class($e) . ": " . $e->getMessage() . " at line " . $e->getLine() . PHP_EOL;
}
'

Repository: friendsofhyperf/jet

Length of output: 1566


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== composer dependencies =="
if [ -f composer.json ]; then
  python3 - <<'PY'
import json
p=json.load(open('composer.json'))
print("php:", p.get('require', {}).get('php'))
print("require:", p.get('require', {}))
PY
fi
echo "== relevant usage/tests mentions =="
rg -n "ServerException|rpc|error|data\]\s*=>" . -S --glob '!vendor/**' --glob '!node_modules/**' | head -n 200

echo "== PHP runtime probe with type conversion/strictness =="
php -r '
error_reporting(E_ALL);
set_error_handler(function($severity, $message, $file, $line) {
    if (PHP_VERSION_ID >= 80400 && in_array($severity, [E_WARNING, E_DEPRECATED], true)) {
        throw new ErrorException($message, 0, $severity, $file, $line);
    }
    throw new Exception($message, $severity);
});

$cases = [
    ["message" => "failure", "data" => "malformed"],
    ["message" => "failure", "data" => ["class" => ["unexpected"]]],
    ["message" => "failure", "data" => ["class" => 123]],
];
foreach ($cases as $error) {
    try {
        $class = $error["data"]["class"] ?? "";
        echo get_class(new \Exception($class . ": " . $error["message"])) . PHP_EOL;
    } catch (Throwable $e) {
        echo "[" . get_class($e) . "] " . $e->getMessage() . " at line " . $e->getLine() . PHP_EOL;
    }
}
'

Repository: friendsofhyperf/jet

Length of output: 6493


Validate data['class'] before using it in the exception message.

?? only provides a default for missing keys; it does not prevent offset access on non-array data. Also, non-string class values still enter the concatenation and can fire conversion warnings/errors during ServerException construction. Extract data first, check it is an array, and only use non-empty string class values.

🤖 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 `@src/Exception/ServerException.php` around lines 27 - 30, Update the exception
message construction in ServerException to extract data before accessing class,
verify data is an array, and only prepend class when it is a non-empty string.
Preserve the existing fallback message when data or class is invalid.

Comment on lines +65 to +68
$timeoutSeconds = (int) $timeout;
$timeoutMicroseconds = (int) (($timeout - $timeoutSeconds) * 1000000);

$selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds);

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 | ⚡ Quick win

Preserve the deadline across partial reads.

$timeout is reused for every stream_select() call inside readBytes(). If fread() returns partial data, a peer can repeatedly reset the full wait period and keep receive() blocked beyond the configured timeout. Compute a deadline once per readBytes() call and pass the remaining duration on each iteration; add a regression test for trickled partial reads.

Proposed fix
 private function readBytes(int $length, float $timeout): string
 {
     $buffer = '';
+    $deadline = microtime(true) + $timeout;

     while (strlen($buffer) < $length) {
         $read = [$this->client];
         $write = null;
         $except = null;

-        $timeoutSeconds = (int) $timeout;
-        $timeoutMicroseconds = (int) (($timeout - $timeoutSeconds) * 1000000);
+        $remaining = $deadline - microtime(true);
+        if ($remaining <= 0) {
+            throw new RecvFailedException('Receive timeout.');
+        }
+        $timeoutSeconds = (int) $remaining;
+        $timeoutMicroseconds = (int) (($remaining - $timeoutSeconds) * 1000000);

         $selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds);
📝 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
$timeoutSeconds = (int) $timeout;
$timeoutMicroseconds = (int) (($timeout - $timeoutSeconds) * 1000000);
$selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds);
$deadline = microtime(true) + $timeout;
$remaining = $deadline - microtime(true);
if ($remaining <= 0) {
throw new RecvFailedException('Receive timeout.');
}
$timeoutSeconds = (int) $remaining;
$timeoutMicroseconds = (int) (($remaining - $timeoutSeconds) * 1000000);
$selected = stream_select($read, $write, $except, $timeoutSeconds, $timeoutMicroseconds);
🤖 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 `@src/Transporter/MultiplexRpcTransporter.php` around lines 65 - 68, Update
readBytes() to compute a single deadline when the read begins, then recalculate
the remaining seconds and microseconds before every stream_select() iteration,
including after partial fread() results. Ensure the configured timeout is not
reset between reads and add a regression test covering trickled partial data
that verifies receive() respects the overall deadline.

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