-
-
Notifications
You must be signed in to change notification settings - Fork 27
refactor: optimize database server address tracing with WeakMap #1027
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This change improves the database server address tracing mechanism by: - Using WeakMap to cache server address and port information from DSN at PDO creation time - Extracting server information directly from DSN in createPdoConnection - Removing complex fallback logic from EventHandleListener that relied on config and read/write PDO detection - Simplifying server address/port retrieval to direct Context lookups The new approach is more efficient and reliable, capturing the actual connection details at the earliest point rather than inferring them later.
总体说明此 PR 改进了数据库连接追踪机制,通过引入 WeakMap 缓存在 PDO 连接首次创建时存储主机和端口信息,后续查询操作直接从缓存读取而非动态计算。DbConnectionAspect 新增构造函数并扩展目标方法列表,EventHandleListener 改用上下文常量查询。 文件变更
序列图sequenceDiagram
participant App as 应用
participant Connector as Connector<br/>(拦截点)
participant Aspect as DbConnectionAspect
participant Cache as serverCache<br/>(WeakMap)
participant Listener as EventHandleListener
App->>Connector: createPdoConnection(DSN,...)
Connector->>Aspect: 拦截 createPdoConnection
Aspect->>Aspect: 解析 DSN 提取 host/port
Aspect->>Cache: 缓存 [PDO → {host,port}]
Cache-->>Aspect: 已缓存
Aspect-->>Connector: 继续执行
Connector-->>App: 返回 PDO
App->>App: 执行 SQL 查询
App->>Listener: 处理查询事件
Listener->>Cache: 查询 PDO 对应的 host/port
Cache-->>Listener: 返回缓存值
Listener->>Listener: 设置 server.address/port
Listener-->>App: 追踪完成
代码审查工作量估算🎯 3 (中等复杂) | ⏱️ ~20 分钟
可能关联的 PR
诗歌
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.1.32)At least one path must be specified to analyse. 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. Comment |
This change improves the database server address tracing mechanism by: - Using WeakMap to cache server address and port information from DSN at PDO creation time - Extracting server information directly from DSN in createPdoConnection - Removing complex fallback logic from EventHandleListener that relied on config and read/write PDO detection - Simplifying server address/port retrieval to direct Context lookups The new approach is more efficient and reliable, capturing the actual connection details at the earliest point rather than inferring them later. Co-authored-by: Deeka Wong <8337659+huangdijia@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/sentry/src/Tracing/Listener/EventHandleListener.php (1)
196-197: 从 Context 读取 DB server.address/server.port 的方式合理,但默认端口可能偏 MySQL 场景这里直接从
Context读取Constants::TRACE_DB_SERVER_ADDRESS/PORT并提供默认值,和 Redis 那段的做法保持一致,整体设计没问题。不过默认端口硬编码为3306,在非 MySQL 驱动(例如 PostgreSQL、SQLServer)且上游未成功写入TRACE_DB_SERVER_PORT时,会把跨度标成3306,可能产生误导。如果后续要支持多种驱动,建议考虑:
- 要么根据
$event->connection->getDriverName()选择更合适的默认端口;- 要么当 Context 中没有端口信息时不设置
server.port,让下游显式感知“端口未知”。src/sentry/src/Tracing/Aspect/DbConnectionAspect.php (1)
40-48: DSN 正则对格式要求较严格,可能遗漏合法 DSN 形态当前正则:
$pattern = '/host=([^;]+);port=(\d+);?/';假定 DSN 中:
- 一定同时包含
host=与port=;- 并且顺序固定为
host=...;port=...;。在 Hyperf 默认 MySQL DSN 下通常没问题,但如果用户自定义 DSN 时:
- 省略
port(走驱动默认端口);- 或者调整参数顺序(如
port=...;host=...;dbname=...);就不会命中该正则,导致该 PDO 不会被写入
$serverCache,后续所有查询都只能依赖默认的localhost:3306,信息会不够准确。如果你希望兼容更多 DSN 变体,建议后续考虑:
- 放宽正则,只要求匹配
host=,port=可选;- 或者不依赖顺序,改为对
substr($dsn, strpos($dsn, ':') + 1)使用parse_str风格解析(按;切分后再按=拆键值)。目前实现功能上是可工作的,这一点算是增强健壮性的改进点。
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/sentry/src/Tracing/Aspect/DbConnectionAspect.php(1 hunks)src/sentry/src/Tracing/Listener/EventHandleListener.php(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/sentry/src/Tracing/Listener/EventHandleListener.php (1)
src/sentry/src/Constants.php (1)
Constants(14-43)
src/sentry/src/Tracing/Aspect/DbConnectionAspect.php (1)
src/sentry/src/Constants.php (1)
Constants(14-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
- GitHub Check: Test on PHP 8.1 with Swoole 6.0.2
- GitHub Check: Test on PHP 8.1 with Swoole 6.1.2
- GitHub Check: Test on PHP 8.1 with Swoole 5.1.8
- GitHub Check: Test on PHP 8.2 with Swoole 6.0.2
- GitHub Check: Test on PHP 8.2 with Swoole 5.1.8
- GitHub Check: Test on PHP 8.3 with Swoole 5.1.8
- GitHub Check: Test on PHP 8.3 with Swoole 6.0.2
- GitHub Check: Test on PHP 8.3 with Swoole 6.1.2
- GitHub Check: Test on PHP 8.2 with Swoole 6.1.2
- GitHub Check: Test on PHP 8.2 with Swoole 6.1.2
- GitHub Check: Test on PHP 8.2 with Swoole 5.1.8
- GitHub Check: Test on PHP 8.2 with Swoole 6.0.2
- GitHub Check: Test on PHP 8.1 with Swoole 6.1.2
- GitHub Check: Test on PHP 8.1 with Swoole 6.0.2
- GitHub Check: Test on PHP 8.1 with Swoole 5.1.8
- GitHub Check: Test on PHP 8.3 with Swoole 6.1.2
- GitHub Check: Test on PHP 8.3 with Swoole 5.1.8
- GitHub Check: Test on PHP 8.3 with Swoole 6.0.2
🔇 Additional comments (1)
src/sentry/src/Tracing/Aspect/DbConnectionAspect.php (1)
24-35: 使用 WeakMap 做 PDO -> server 信息缓存的总体设计可取把 PDO 实例作为 WeakMap 的 key,可以做到:
- O(1) 查找对应的 host/port;
- 当 PDO 被回收时自动释放缓存,避免长期请求或连接池场景下的显性内存管理问题。
构造函数里初始化 WeakMap 也清晰直观,整体结构上没有明显问题。只需要确保组件最低 PHP 版本已经覆盖 WeakMap(PHP 8+)即可。
| Context::getOrSet(self::class, function () use ($pdo) { | ||
| $connectionStatus = $pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS); | ||
| [$host] = explode(' ', $connectionStatus); | ||
| $server = $this->serverCache[$pdo] ?? null; | ||
|
|
||
| Context::set(Constants::TRACE_DB_SERVER_ADDRESS, $host); | ||
| if (is_array($server)) { | ||
| Context::set(Constants::TRACE_DB_SERVER_ADDRESS, $server['host']); | ||
| Context::set(Constants::TRACE_DB_SERVER_PORT, $server['port']); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
WeakMap 访问方式存在潜在 fatal 风险,且 Context key 语义仅记录首个连接的 server 信息
这里有两个点需要注意:
- WeakMap 访问方式可能抛
Error
$server = $this->serverCache[$pdo] ?? null;在 PHP 中,对 WeakMap 使用 $map[$obj] 访问一个尚未存在的 key,会抛出 Error: Object not contained in WeakMap,即使你用 ?? null 包了一层也无法避免。只要出现“未经过 createPdoConnection 写入缓存、但走到了 getPdo/getPdoForSelect 分支”的情况(例如某些自定义连接器或未来扩展),就可能导致运行时 fatal。
建议改为先通过 isset()/offsetExists() 检查,再读取值,例如:
- Context::getOrSet(self::class, function () use ($pdo) {
- $server = $this->serverCache[$pdo] ?? null;
-
- if (is_array($server)) {
- Context::set(Constants::TRACE_DB_SERVER_ADDRESS, $server['host']);
- Context::set(Constants::TRACE_DB_SERVER_PORT, $server['port']);
- }
-
- return true;
- });
+ Context::getOrSet(self::class, function () use ($pdo) {
+ $server = null;
+ if (isset($this->serverCache[$pdo])) {
+ $server = $this->serverCache[$pdo];
+ }
+
+ if (is_array($server)) {
+ Context::set(Constants::TRACE_DB_SERVER_ADDRESS, $server['host']);
+ Context::set(Constants::TRACE_DB_SERVER_PORT, $server['port']);
+ }
+
+ return true;
+ });这样即便某个 PDO 没有对应缓存,也只会静默跳过,不会中断请求。
- Context::getOrSet(self::class, ...) 只会在当前协程第一次调用时执行闭包
Context::getOrSet(self::class, ...) 使用 self::class 作为 key,意味着:
- 当前协程内第一次走到
getPdo/getPdoForSelect时会从 WeakMap 读取一份 server 信息并写入TRACE_DB_SERVER_*; - 后续即使在同一协程内切换到其他 PDO(例如读写分离的 write PDO 或另一条连接),闭包也不会再次执行,依然使用第一次记录下来的 host/port。
如果你认为“一次请求只关心首个 DB server”是可接受的行为,那现在的实现是 OK 的;如果希望在读写分离或多库场景下精确标记每次查询的实际 server 信息,后续可以考虑:
- 用更细粒度的 Context key(例如包含 connectionName 或 DSN);
- 或者放弃
getOrSet,在每次getPdo*时覆盖写入当前 PDO 对应的 host/port。
第一点属于需要优先修复的稳定性问题,第二点则是行为语义上的设计权衡。
需要验证此评审意见中的两个关键点。让我检查代码上下文和PHP WeakMap行为。
🏁 Script executed:
#!/bin/bash
# 查看 DbConnectionAspect.php 文件的完整内容
find . -type f -name "DbConnectionAspect.php" | head -5Length of output: 54
🏁 Script executed:
#!/bin/bash
# 查找 serverCache 初始化和使用位置
rg -n "serverCache" -A 2 -B 2 --type phpLength of output: 1581
🏁 Script executed:
#!/bin/bash
# 查找所有 getPdo 相关方法调用
rg -n "getPdo|getPdoForSelect" --type php -C 3Length of output: 691
🏁 Script executed:
#!/bin/bash
# 查找 WeakMap 的使用
rg -n "WeakMap" --type php -C 2Length of output: 8885
🏁 Script executed:
#!/bin/bash
# 查看 Context::getOrSet 的实现和使用
rg -n "getOrSet" --type php -C 3Length of output: 6913
🏁 Script executed:
#!/bin/bash
# 查找 createPdoConnection 方法
rg -n "createPdoConnection" --type php -C 5Length of output: 2096
🏁 Script executed:
#!/bin/bash
# 检查 Constants::TRACE_DB_SERVER 相关常量
rg -n "TRACE_DB_SERVER" --type phpLength of output: 810
🏁 Script executed:
#!/bin/bash
# 查看 Tracing/Aspect 目录结构
find . -path "*/Tracing/Aspect/*" -type f -name "*.php" 2>/dev/null | head -20Length of output: 1022
🏁 Script executed:
#!/bin/bash
# 在数据库连接相关代码中搜索 isset 使用 WeakMap 的模式
rg -n "isset.*\[" --type php -C 2 | grep -A 2 -B 2 -i weakLength of output: 0
🏁 Script executed:
#!/bin/bash
# 查看 DbConnectionAspect.php 的前 100 行
if [ -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" ]; then
head -100 src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
fiLength of output: 1982
🏁 Script executed:
#!/bin/bash
# 查看 DbConnectionAspect.php 第 40-70 行
if [ -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" ]; then
sed -n '40,70p' src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
fiLength of output: 919
🏁 Script executed:
#!/bin/bash
# 查看整个 getPdo 或 getPdoForSelect 方法
ast-grep --pattern $'function getPdo*($_) {
$$$
}'Length of output: 0
🏁 Script executed:
#!/bin/bash
# 查看完整 class DbConnectionAspect
ast-grep --pattern $'class DbConnectionAspect {
$$$
}'Length of output: 0
🏁 Script executed:
#!/bin/bash
# 统计一个请求中是否会调用多次 getPdo
rg -n "->getPdo" --type phpLength of output: 404
🏁 Script executed:
#!/bin/bash
# 查看 Hyperf Context 实现,确认 getOrSet 行为
rg -n "class Context" --type php -A 20Length of output: 0
🏁 Script executed:
#!/bin/bash
# 检查是否有其他 setOrGet 调用使用 self::class
rg -n 'getOrSet\(self::class' --type phpLength of output: 123
🏁 Script executed:
#!/bin/bash
# 查看此文件是否有多个 Context 调用
if [ -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" ]; then
grep -n "Context::" src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
fiLength of output: 247
🏁 Script executed:
#!/bin/bash
# 查看是否存在处理多个 PDO 的逻辑
rg -n "readPdo|writePdo|read_pdo|write_pdo" --type php -C 3Length of output: 447
🏁 Script executed:
#!/bin/bash
# 找出文件的总行数和周围代码
if [ -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" ]; then
wc -l src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
echo "---"
sed -n '1,100p' src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
fiLength of output: 2042
🏁 Script executed:
#!/bin/bash
# 查看 getPdo/getPdoForSelect 的完整实现
if [ -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" ]; then
sed -n '30,100p' src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
fiLength of output: 1218
🏁 Script executed:
#!/bin/bash
# 查看 Context 的实现,特别是 getOrSet 是否会缓存
find . -type f -name "*.php" -path "*/Context*" | head -10Length of output: 691
🏁 Script executed:
#!/bin/bash
# 查看与 PDO 相关的多个实例处理
rg -n "private.*pdo|protected.*pdo|public.*pdo" --type php -C 1Length of output: 1001
🏁 Script executed:
#!/bin/bash
# 查看整个 DbConnectionAspect 文件
if [ -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" ]; then
cat src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
fiLength of output: 1982
🏁 Script executed:
#!/bin/bash
# 检查是否使用了多个数据库连接
rg -n "Connection|connection" src/sentry/src/Tracing/Aspect/ --type phpLength of output: 3695
🏁 Script executed:
#!/bin/bash
# 验证 WeakMap 的访问方式
rg -n 'WeakMap|offsetExists' --type php -C 3Length of output: 12697
🏁 Script executed:
#!/bin/bash
# 在整个 src/sentry 找 serverCache 的定义
find src/sentry -name "*.php" -type f -exec grep -l "serverCache" {} \;Length of output: 52
🏁 Script executed:
#!/bin/bash
# 查看 DbConnectionAspect 的全部内容和注释
if [ -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" ]; then
nl -ba src/sentry/src/Tracing/Aspect/DbConnectionAspect.php | head -200
fiLength of output: 40
🏁 Script executed:
#!/bin/bash
# 快速检查文件是否存在
ls -la src/sentry/src/Tracing/Aspect/DbConnectionAspect.php 2>&1 || echo "File not found, searching..."
find . -name "DbConnectionAspect.php" 2>/dev/nullLength of output: 156
🏁 Script executed:
#!/bin/bash
# 确定文件位置并读取
find . -type f -name "DbConnectionAspect.php" -print0 | xargs -0 catLength of output: 1982
🏁 Script executed:
#!/bin/bash
# 检查整个 Tracing 目录
find . -path "*/Tracing/*" -type f -name "*.php" | head -20Length of output: 1068
🏁 Script executed:
#!/bin/bash
# 列出 src 目录结构
find . -maxdepth 4 -type d | grep -i tracing | head -20Length of output: 412
🏁 Script executed:
#!/bin/bash
# 最终查找
find . -type f -name "*DbConnection*" 2>/dev/nullLength of output: 112
🏁 Script executed:
#!/bin/bash
# 在当前目录搜索
pwd
ls -la
find . -maxdepth 3 -type d 2>/dev/null | head -20Length of output: 2326
🏁 Script executed:
#!/bin/bash
# 查找所有 PHP 文件
find . -type f -name "*.php" 2>/dev/null | wc -l
find . -type f -name "*.php" 2>/dev/null | grep -i "db\|connection\|aspect" | head -20Length of output: 1005
🏁 Script executed:
#!/bin/bash
# 简单查找
fd "DbConnectionAspect" 2>/dev/null || echo "Not found with fd"
rg "DbConnectionAspect" --files 2>/dev/null | head -5 || echo "Not found with rg"Length of output: 52
🏁 Script executed:
#!/bin/bash
# 检查是否需要克隆或已有源代码
if [ -d ".git" ]; then
echo "Git repository found"
git remote -v
git branch -a
fi
ls -la .Length of output: 2282
🏁 Script executed:
#!/bin/bash
# 尝试查找相关文件
find . -name "*.php" -type f 2>/dev/null | head -5
find . -path "*src*" -name "*.php" -type f 2>/dev/null | head -10Length of output: 555
🏁 Script executed:
#!/bin/bash
# 显示当前目录和深度
pwd
find . -maxdepth 1 -type f | head -20Length of output: 332
🏁 Script executed:
#!/bin/bash
# 最简单的验证
test -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" && echo "File exists" || echo "File not found, looking..."
find . -iname "*dbconnection*.php" 2>/dev/null
find . -iname "*.php" -path "*tracing*" 2>/dev/null | head -10Length of output: 799
🏁 Script executed:
#!/bin/bash
# 查看是否在 .git 中或是否是初始化的克隆
if [ -f ".gitmodules" ]; then
cat .gitmodules
fi
if [ -f "README.md" ]; then
head -20 README.md
fi
ls -la | head -30Length of output: 3339
🏁 Script executed:
#!/bin/bash
# 使用 Python 来查找和分析 PHP 文件
python3 << 'PYTHON_EOF'
import os
import re
# 查找相关文件
for root, dirs, files in os.walk('.'):
for file in files:
if 'DbConnection' in file and file.endswith('.php'):
filepath = os.path.join(root, file)
print(f"Found: {filepath}")
with open(filepath, 'r', errors='ignore') as f:
content = f.read()
print(f"File size: {len(content)} bytes")
# 查找 serverCache 和相关内容
if 'serverCache' in content:
print("Contains serverCache")
if 'WeakMap' in content:
print("Contains WeakMap")
PYTHON_EOFLength of output: 208
🏁 Script executed:
#!/bin/bash
# 使用绝对路径搜索
grep -r "DbConnectionAspect" . 2>/dev/null | head -5 || echo "Not found"
grep -r "serverCache" . 2>/dev/null | head -5 || echo "Not found"Length of output: 1005
🏁 Script executed:
#!/bin/bash
# 检查是否有 vendor 或外部库
find . -maxdepth 3 -type d -name "vendor" 2>/dev/null
find . -maxdepth 3 -type d -name "Tracing" 2>/dev/nullLength of output: 8
🏁 Script executed:
#!/bin/bash
# 直接搜索关键字
rg "WeakMap|serverCache" -t php 2>/dev/null | head -10 || echo "Pattern not found"Length of output: 698
🏁 Script executed:
#!/bin/bash
# 检查输入是否可用
cat /dev/null
echo "System ready"
pwd
ls | head -10Length of output: 144
🏁 Script executed:
#!/bin/bash
# 展示目录树
find . -maxdepth 2 -type d | sort | head -30Length of output: 391
🏁 Script executed:
#!/bin/bash
# 统计 PHP 文件
find . -type f -name "*.php" | wc -l
echo "---"
find . -type f -name "*.php" -path "*Tracing*" | head -20Length of output: 1078
🏁 Script executed:
#!/bin/bash
# 用 -type f 确保是文件
find . -type f -name "*.php" -exec grep -l "WeakMap\|serverCache\|DbConnectionAspect" {} \; | head -20Length of output: 1189
🏁 Script executed:
#!/bin/bash
# 直接读取
if [ -d "src" ]; then
find src -type f -name "*.php" | grep -i db | head -10
fiLength of output: 279
🏁 Script executed:
#!/bin/bash
# 详细列出 src 下的结构
if [ -d "src" ]; then
ls -R src | head -100
fiLength of output: 1246
🏁 Script executed:
#!/bin/bash
# 如果 src 不存在,检查其他可能的位置
if [ ! -d "src" ]; then
ls -la
find . -maxdepth 2 -type f -name "*.php" 2>/dev/null | head -10
fiLength of output: 0
🏁 Script executed:
#!/bin/bash
# 最后尝试:
for dir in . ./src ./app ./lib ./vendor; do
if [ -d "$dir" ]; then
find "$dir" -type f -name "*.php" 2>/dev/null | head -5 && break
fi
doneLength of output: 164
🏁 Script executed:
#!/bin/bash
# 检查是否有源文件在特定位置
test -f "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" && cat "src/sentry/src/Tracing/Aspect/DbConnectionAspect.php" || find . -name "DbConnectionAspect.php" -type f 2>/dev/null | xargs cat 2>/dev/null || echo "File not found in repository"Length of output: 1982
🏁 Script executed:
#!/bin/bash
# 如果文件查找失败,至少检查当前上下文中的信息
echo "Repository appears to be uninitialized or path structure differs"
echo "Proceeding with web search for PHP WeakMap behavior verification"Length of output: 129
🌐 Web query:
PHP WeakMap array access unset key Error behavior
💡 Result:
Short answer: WeakMap is an internal container class that implements ArrayAccess, so you must use object keys (not scalar/string keys) and its ArrayAccess rules are different from arrays — reading a missing key throws an Error, using a non-object key throws a TypeError, and you must call unset($wm[$obj]) or $wm->offsetUnset($obj) to remove an entry. See the RFC and the manual for the exact behaviours. [1][2]
Key points and examples:
- WeakMap implements ArrayAccess (offsetGet/offsetSet/offsetExists/offsetUnset). You use it like $wm[$obj] = $value and unset($wm[$obj]). [2]
- Using a non-object as a key (e.g. $wm['foo']) throws a TypeError. [1][2]
- Reading a non-existent key with $wm[$obj] throws an Error (unlike arrays which return null/warning or isset checks). Use isset($wm[$obj]) or offsetExists() to check before reading. [1][2]
- unset($wm[$obj]) (or $wm->offsetUnset($obj)) removes the entry; if the key object is garbage-collected the map also automatically drops the entry. [2]
References:
[1] PHP RFC: container/offset behaviour and WeakMap RFC notes.
[2] PHP manual: WeakMap class and its offsetGet/offsetSet/offsetUnset pages.
🌐 Web query:
PHP WeakMap isset offsetExists difference
💡 Result:
Short answer
- WeakMap implements ArrayAccess, so isset($wm[$obj]) calls WeakMap::offsetExists($obj). [1][2]
- However, isset semantics for arrays treat null as "not set". For ArrayAccess objects the behavior of isset() depends entirely on offsetExists() implementation — so if a WeakMap value is null, isset($wm[$obj]) will return whatever WeakMap::offsetExists() returns (i.e. whether the key is present). To avoid ambiguity, call $wm->offsetExists($obj) directly when you need an explicit presence check. [1][2]
References
- WeakMap (PHP manual). [1]
- ArrayAccess::offsetExists (PHP manual) — explains isset()/empty() interaction. [2]
🌐 Web query:
Hyperf framework Context getOrSet coroutine cache behavior
💡 Result:
Short answer — how Context::getOrSet behaves in Hyperf:
- Context is coroutine-local: each coroutine has its own storage; data set in one coroutine is not visible to other coroutines (child coroutines do not automatically inherit the parent's context). [1][2]
- Context::getOrSet(key, valueOrCallable) will:
- return the existing value for key from the current coroutine's context if present; otherwise
- set the key to the provided value (or to the result of the provided callable) and return that value. Implementations in the codebase use this pattern to lazily create/shared-per-coroutine objects (e.g., tracer instantiation via a closure passed to Context::getOrSet). [3][1]
- In non-coroutine environments Hyperf provides a shared fallback context copy (i.e., non-coroutine requests share the same context copy). [1][2]
If you want links to the exact source file or an example showing passing a closure to getOrSet, tell me your Hyperf version and I’ll fetch the precise implementation lines.
Sources: Hyperf context package and docs; community write-up on RPC/context coroutine isolation; release notes mentioning getOrSet usage. [1][2][3]
🌐 Web query:
PDO multiple connections same request coroutine
💡 Result:
Short answer: do not share a single PDO instance across coroutines. Open a connection per coroutine (or — better — borrow one from a connection pool and return it when done). Use Swoole/OpenSwoole's PDOPool or a Channel-based pool and store the borrowed PDO in the coroutine context so the same connection is reused within that request's coroutine and released via defer. This avoids race conditions and too-many-connections issues.
Why and how (concise):
- Coroutines are concurrent execution contexts; one PDO object can't be safely used by multiple coroutines simultaneously. Use pooling instead. [1][2].
- Use Swoole/OpenSwoole PDOPool or implement a pool with Coroutine Channel; call get() at start of request, put() (or rely on Coroutine::defer) at end. [2][3].
- Optionally store the PDO in the coroutine context (Context::set / Coroutine::getContext) so all code in the same coroutine reuses it. [4].
Minimal pattern (pseudo-PHP):
- create pool at worker start
- on request coroutine:
- $pdo = $pool->get();
- Coroutine::defer(fn() => $pool->put($pdo));
- Context::set('pdo', $pdo); // optional for library code
- use $pdo for queries
References:
- OpenSwoole — common questions (no sharing; use pools). [1]
- OpenSwoole PDOPool docs (PDOPool API and example). [2]
- Swoole connection-pool / Channel example (pooling pattern). [3]
- Workerman example storing connection in coroutine context and using defer to return to pool. [4]
If you want, tell me which runtime you're using (Swoole, OpenSwoole, RoadRunner, Workerman, plain FPM) and I’ll give a concrete code example for that environment.
修复 WeakMap 访问的 Error 风险
第一个问题经验证属实且为关键问题。PHP WeakMap 对不存在的 key 直接访问会抛出 Error,即使使用 ?? null 也无法避免,因为 ?? 操作符在短路评估前会先执行左侧的表达式。建议改用 isset() 检查:
Context::getOrSet(self::class, function () use ($pdo) {
$server = null;
if (isset($this->serverCache[$pdo])) {
$server = $this->serverCache[$pdo];
}
if (is_array($server)) {
Context::set(Constants::TRACE_DB_SERVER_ADDRESS, $server['host']);
Context::set(Constants::TRACE_DB_SERVER_PORT, $server['port']);
}
return true;
});这样即便某个 PDO 对象未经 createPdoConnection 缓存(例如通过自定义连接器创建),也只会静默跳过,不会中断请求。
第二个问题关于 Context::getOrSet(self::class, ...) 的语义是设计权衡而非缺陷——若当前实现"一次请求仅需记录首个数据库服务器信息"是可接受的需求,则现有实现 OK;若需支持读写分离或多库场景精确标记每次查询的实际服务器信息,可考虑后续使用更细粒度的 Context key。
🤖 Prompt for AI Agents
In src/sentry/src/Tracing/Aspect/DbConnectionAspect.php around lines 51 to 57,
accessing $this->serverCache[$pdo] with the null coalescing operator can throw
an Error for WeakMap keys that don't exist; replace that access with an isset()
guard: initialize $server = null, use if (isset($this->serverCache[$pdo])) {
$server = $this->serverCache[$pdo]; } then keep the is_array($server) block to
set Context::TRACE_DB_SERVER_ADDRESS and PORT, and ensure the closure returns
true; this prevents WeakMap key access errors for PDO instances not cached.
Summary
Changes
This PR optimizes the database server address tracing mechanism:
DbConnectionAspect.php:
createPdoConnectionto extract host and port from DSN using regexgetPdoandgetPdoForSelecthandling to retrieve cached server info from WeakMapPDO::ATTR_CONNECTION_STATUSwhich was less reliableEventHandleListener.php:
Benefits
Test Plan
Summary by CodeRabbit
发布说明
Bug Fixes
Performance
✏️ Tip: You can customize this high-level summary in your review settings.