Skip to content

Conversation

@huangdijia
Copy link
Contributor

@huangdijia huangdijia commented Nov 22, 2025

Summary

  • Refactor database server address tracing to use WeakMap for caching server information
  • Extract server address and port directly from DSN at PDO creation time
  • Simplify EventHandleListener by removing complex fallback logic

Changes

This PR optimizes the database server address tracing mechanism:

  1. DbConnectionAspect.php:

    • Added WeakMap to cache server address/port information keyed by PDO instance
    • Intercepts createPdoConnection to extract host and port from DSN using regex
    • Modified getPdo and getPdoForSelect handling to retrieve cached server info from WeakMap
    • Removed dependency on PDO::ATTR_CONNECTION_STATUS which was less reliable
  2. EventHandleListener.php:

    • Removed complex logic for determining server port from database configuration
    • Eliminated read/write PDO detection and config fallback logic
    • Simplified server address/port retrieval to direct Context lookups with sensible defaults

Benefits

  • More reliable: Captures actual connection details from DSN at creation time rather than inferring later
  • Simpler code: Removes ~20 lines of complex fallback logic
  • Better performance: WeakMap provides O(1) lookup without memory leaks (auto-cleanup when PDO is garbage collected)
  • Consistent data: Server information is captured once at connection creation and reused

Test Plan

  • Verify database queries still trace correctly
  • Confirm server.address and server.port are properly captured in spans
  • Test with both read and write connections
  • Ensure no memory leaks from WeakMap usage

Summary by CodeRabbit

发布说明

  • Bug Fixes

    • 改进了数据库连接信息的追踪方式,通过缓存机制更可靠地捕获服务器地址和端口。
  • Performance

    • 优化了数据库服务器连接详情的查询逻辑,简化了配置读取流程以提升追踪性能。

✏️ Tip: You can customize this high-level summary in your review settings.

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.
@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

总体说明

此 PR 改进了数据库连接追踪机制,通过引入 WeakMap 缓存在 PDO 连接首次创建时存储主机和端口信息,后续查询操作直接从缓存读取而非动态计算。DbConnectionAspect 新增构造函数并扩展目标方法列表,EventHandleListener 改用上下文常量查询。

文件变更

工作单元 / 文件 变更摘要
DbConnectionAspect 缓存机制
src/sentry/src/Tracing/Aspect/DbConnectionAspect.php
新增 WeakMap 类型的 $serverCache 属性用于缓存 PDO 连接的服务器地址和端口;添加构造函数初始化缓存;扩展 $classes 数组以包含 Hyperf\Database\Connectors\Connector::createPdoConnection 方法;在 createPdoConnection 拦截点解析 DSN 提取主机和端口并缓存;后续 PDO 操作改为从缓存读取而非直接访问 PDO 属性。
查询监听器常量查询
src/sentry/src/Tracing/Listener/EventHandleListener.php
移除动态计算数据库服务器地址和端口的逻辑,改为直接从上下文常量 TRACE_DB_SERVER_ADDRESS(默认 'localhost')和 TRACE_DB_SERVER_PORT(默认 3306)读取值。

序列图

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: 追踪完成
Loading

代码审查工作量估算

🎯 3 (中等复杂) | ⏱️ ~20 分钟

  • WeakMap 缓存机制的正确性需验证,尤其是 PDO 生命周期与缓存一致性的关系
  • DSN 解析逻辑需审查以确保支持各种 DSN 格式(MySQL、PostgreSQL 等)
  • 上下文常量的定义和初始化流程需确认,包括默认值的设定
  • 两个文件间的数据流转需确保完整性,缓存写入与读取的配对

可能关联的 PR

诗歌

🐰 缓存妙计驱寒霜,WeakMap 映射主与港,

一次拦截存周详,后来查询无烦忧,

踪迹轻灵似月光,追踪数据齐欢唱~ ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确总结了拉取请求的主要变化:使用WeakMap优化数据库服务器地址跟踪。标题清晰、简洁,直接反映了代码重构的核心目标。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/db-server-address-tracing

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.

❤️ Share

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

@huangdijia huangdijia merged commit 4122445 into main Nov 22, 2025
21 of 23 checks passed
@huangdijia huangdijia deleted the refactor/db-server-address-tracing branch November 22, 2025 06:37
huangdijia added a commit that referenced this pull request Nov 22, 2025
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>
Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between f05bed1 and 260cfef.

📒 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+)即可。

Comment on lines 51 to +57
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']);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

WeakMap 访问方式存在潜在 fatal 风险,且 Context key 语义仅记录首个连接的 server 信息

这里有两个点需要注意:

  1. 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 没有对应缓存,也只会静默跳过,不会中断请求。

  1. 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 -5

Length of output: 54


🏁 Script executed:

#!/bin/bash
# 查找 serverCache 初始化和使用位置
rg -n "serverCache" -A 2 -B 2 --type php

Length of output: 1581


🏁 Script executed:

#!/bin/bash
# 查找所有 getPdo 相关方法调用
rg -n "getPdo|getPdoForSelect" --type php -C 3

Length of output: 691


🏁 Script executed:

#!/bin/bash
# 查找 WeakMap 的使用
rg -n "WeakMap" --type php -C 2

Length of output: 8885


🏁 Script executed:

#!/bin/bash
# 查看 Context::getOrSet 的实现和使用
rg -n "getOrSet" --type php -C 3

Length of output: 6913


🏁 Script executed:

#!/bin/bash
# 查找 createPdoConnection 方法
rg -n "createPdoConnection" --type php -C 5

Length of output: 2096


🏁 Script executed:

#!/bin/bash
# 检查 Constants::TRACE_DB_SERVER 相关常量
rg -n "TRACE_DB_SERVER" --type php

Length of output: 810


🏁 Script executed:

#!/bin/bash
# 查看 Tracing/Aspect 目录结构
find . -path "*/Tracing/Aspect/*" -type f -name "*.php" 2>/dev/null | head -20

Length of output: 1022


🏁 Script executed:

#!/bin/bash
# 在数据库连接相关代码中搜索 isset 使用 WeakMap 的模式
rg -n "isset.*\[" --type php -C 2 | grep -A 2 -B 2 -i weak

Length 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
fi

Length 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
fi

Length 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 php

Length of output: 404


🏁 Script executed:

#!/bin/bash
# 查看 Hyperf Context 实现,确认 getOrSet 行为
rg -n "class Context" --type php -A 20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 检查是否有其他 setOrGet 调用使用 self::class
rg -n 'getOrSet\(self::class' --type php

Length 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
fi

Length of output: 247


🏁 Script executed:

#!/bin/bash
# 查看是否存在处理多个 PDO 的逻辑
rg -n "readPdo|writePdo|read_pdo|write_pdo" --type php -C 3

Length 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
fi

Length 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
fi

Length of output: 1218


🏁 Script executed:

#!/bin/bash
# 查看 Context 的实现,特别是 getOrSet 是否会缓存
find . -type f -name "*.php" -path "*/Context*" | head -10

Length of output: 691


🏁 Script executed:

#!/bin/bash
# 查看与 PDO 相关的多个实例处理
rg -n "private.*pdo|protected.*pdo|public.*pdo" --type php -C 1

Length 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
fi

Length of output: 1982


🏁 Script executed:

#!/bin/bash
# 检查是否使用了多个数据库连接
rg -n "Connection|connection" src/sentry/src/Tracing/Aspect/ --type php

Length of output: 3695


🏁 Script executed:

#!/bin/bash
# 验证 WeakMap 的访问方式
rg -n 'WeakMap|offsetExists' --type php -C 3

Length 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
fi

Length 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/null

Length of output: 156


🏁 Script executed:

#!/bin/bash
# 确定文件位置并读取
find . -type f -name "DbConnectionAspect.php" -print0 | xargs -0 cat

Length of output: 1982


🏁 Script executed:

#!/bin/bash
# 检查整个 Tracing 目录
find . -path "*/Tracing/*" -type f -name "*.php" | head -20

Length of output: 1068


🏁 Script executed:

#!/bin/bash
# 列出 src 目录结构
find . -maxdepth 4 -type d | grep -i tracing | head -20

Length of output: 412


🏁 Script executed:

#!/bin/bash
# 最终查找
find . -type f -name "*DbConnection*" 2>/dev/null

Length of output: 112


🏁 Script executed:

#!/bin/bash
# 在当前目录搜索
pwd
ls -la
find . -maxdepth 3 -type d 2>/dev/null | head -20

Length 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 -20

Length 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 -10

Length of output: 555


🏁 Script executed:

#!/bin/bash
# 显示当前目录和深度
pwd
find . -maxdepth 1 -type f | head -20

Length 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 -10

Length 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 -30

Length 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_EOF

Length 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/null

Length 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 -10

Length of output: 144


🏁 Script executed:

#!/bin/bash
# 展示目录树
find . -maxdepth 2 -type d | sort | head -30

Length of output: 391


🏁 Script executed:

#!/bin/bash
# 统计 PHP 文件
find . -type f -name "*.php" | wc -l
echo "---"
find . -type f -name "*.php" -path "*Tracing*" | head -20

Length of output: 1078


🏁 Script executed:

#!/bin/bash
# 用 -type f 确保是文件
find . -type f -name "*.php" -exec grep -l "WeakMap\|serverCache\|DbConnectionAspect" {} \; | head -20

Length of output: 1189


🏁 Script executed:

#!/bin/bash
# 直接读取
if [ -d "src" ]; then
  find src -type f -name "*.php" | grep -i db | head -10
fi

Length of output: 279


🏁 Script executed:

#!/bin/bash
# 详细列出 src 下的结构
if [ -d "src" ]; then
  ls -R src | head -100
fi

Length 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
fi

Length 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
done

Length 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.

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.

2 participants