4.6 KiB
4.6 KiB
todos:
- id: "dep" content: "在 php82 容器执行 composer require symfony/lock:7.4.9" status: pending
- id: "factory" content: "新增 app/support/OtaLockFactory.php 封装 symfony LockFactory + RedisStore(用 support\Redis::connection()->client())" status: pending
- id: "refactor" content: "重构 OtaCheckLogic 锁方法:acquire(false) 重试+SERVICE_BUSY,release(),删除手写 SET/Lua 与 support\Redis 依赖" status: pending
- id: "verify" content: "运行 phpunit + php -l + 完成门禁,确认全绿" status: pending isProject: false
用 symfony/lock 7.4.9 替换 OTA 分布式锁
目标与范围
- 仅替换 app/logic/OtaCheckLogic.php 中的锁实现(
withDeviceFirmwareLock/acquireLock/releaseLock)。 - 业务流程(先查后建/复用、§7.5 幂等)、错误码
SERVICE_BUSY、TTL/重试参数语义保持不变。 - 单测/集成测试结构不变(仍依赖 docker
redis,跳过守卫不变)。
为什么可行
php82= 8.2.24,满足 symfony/lock 7.4(PHP ≥ 8.2)。- symfony
RedisStore接受原生\Redis,可由support\Redis::connection()->client()取得(见vendor/illuminate/redis/Connections/Connection.php:81的client())。 - symfony
Lock自带唯一 token + 所有者校验的release(),可删除手写 Lua 释放脚本。
flowchart LR
Logic[OtaCheckLogic] -->|create resource| Factory[OtaLockFactory]
Factory -->|RedisStore| RawRedis["support Redis connection client (\\Redis)"]
Logic -->|"acquire(false) 重试 / release()"| LockObj[Symfony Lock]
改动点
1. 依赖
- 在
php82容器内执行(dev-environment 规则,禁止宿主机 php/composer):docker exec -w /app/www/ai-device/ota php82 composer require symfony/lock:7.4.9 - 结果会更新 composer.json /
composer.lock/vendor。
2. 新增 app/support/OtaLockFactory.php
- 职责:封装 symfony 锁与 phpredis 客户端的装配,返回可用的
LockInterface。 - 每次调用基于「当前」连接构建
RedisStore(不缓存客户端,规避 webman 连接池跨请求换连接导致的陈旧句柄):
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\RedisStore;
use support\Redis;
final class OtaLockFactory
{
/** 基于当前 redis 连接创建一个带 TTL 的命名分布式锁 */
public static function create(string $resource, float $ttlSeconds): LockInterface
{
$store = new RedisStore(Redis::connection()->client());
return (new LockFactory($store))->createLock($resource, $ttlSeconds);
}
}
3. 重构 app/logic/OtaCheckLogic.php
- 删除
use support\Redis;;新增use Symfony\Component\Lock\LockInterface;与use app\support\OtaLockFactory;。 - 保留常量
LOCK_KEY_PREFIX/LOCK_TTL_SECONDS/LOCK_MAX_RETRY/LOCK_RETRY_INTERVAL_US。 withDeviceFirmwareLock:
$lock = OtaLockFactory::create(self::LOCK_KEY_PREFIX . $deviceId . ':' . $firmwareId, (float) self::LOCK_TTL_SECONDS);
$this->acquireOrFail($lock);
try {
return $criticalSection();
} finally {
$this->releaseQuietly($lock);
}
acquireOrFail(LockInterface $lock):循环LOCK_MAX_RETRY次调用$lock->acquire(false),成功 return;否则usleep(LOCK_RETRY_INTERVAL_US);用尽后Log::warning+throw new BusinessException(OtaErrorCode::SERVICE_BUSY)。releaseQuietly(LockInterface $lock):try { $lock->release(); } catch (Throwable) { Log::warning(...) }(释放失败不影响主流程,TTL 兜底)。- 删除手写 token 生成与 Lua 释放脚本(
acquireLock/releaseLock旧实现)。
验证
docker exec -w /app/www/ai-device/ota php82 ./vendor/bin/phpunit(含OtaCheckLogicTest真连 redis 验证加锁/幂等)。- 对改动文件容器内
php -l。 - 运行完成门禁
~/.cursor/skills/slot-backend-completion-report/scripts/report.sh。
备注 / 取舍
- 可选:将 composer.json 的
"php": ">=8.1"提升为>=8.2以与 symfony/lock 对齐(非必需,运行时已是 8.2)。 - 备选方案:不新增
OtaLockFactory,把装配内联为OtaCheckLogic私有方法;本方案选独立 support 类以隔离基础设施、便于复用与替换 Store。 - 阻塞策略:沿用
acquire(false)有界重试而非acquire(true)无限阻塞,避免阻塞 webman worker,并保留SERVICE_BUSY行为。