This commit is contained in:
ray zhou
2026-06-29 14:51:55 +08:00
parent 225fb2bd28
commit 2dd9f17da9
319 changed files with 29461 additions and 9412 deletions

View File

@@ -0,0 +1,92 @@
<!-- 5511b223-7953-4aa5-bd6e-94f0d6c56b21 -->
---
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_BUSYrelease(),删除手写 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](app/logic/OtaCheckLogic.php) 中的锁实现(`withDeviceFirmwareLock` / `acquireLock` / `releaseLock`)。
- 业务流程(先查后建/复用、§7.5 幂等)、错误码 `SERVICE_BUSY`、TTL/重试参数语义保持不变。
- 单测/集成测试结构不变(仍依赖 docker `redis`,跳过守卫不变)。
## 为什么可行
- `php82` = 8.2.24,满足 symfony/lock 7.4PHP ≥ 8.2)。
- symfony `RedisStore` 接受原生 `\Redis`,可由 `support\Redis::connection()->client()` 取得(见 `vendor/illuminate/redis/Connections/Connection.php:81``client()`)。
- symfony `Lock` 自带唯一 token + 所有者校验的 `release()`,可删除手写 Lua 释放脚本。
```mermaid
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.json) / `composer.lock` / `vendor`
### 2. 新增 `app/support/OtaLockFactory.php`
- 职责:封装 symfony 锁与 phpredis 客户端的装配,返回可用的 `LockInterface`
- 每次调用基于「当前」连接构建 `RedisStore`(不缓存客户端,规避 webman 连接池跨请求换连接导致的陈旧句柄):
```php
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](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`
```php
$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](composer.json) 的 `"php": ">=8.1"` 提升为 `>=8.2` 以与 symfony/lock 对齐(非必需,运行时已是 8.2)。
- 备选方案:不新增 `OtaLockFactory`,把装配内联为 `OtaCheckLogic` 私有方法;本方案选独立 support 类以隔离基础设施、便于复用与替换 Store。
- 阻塞策略:沿用 `acquire(false)` 有界重试而非 `acquire(true)` 无限阻塞,避免阻塞 webman worker并保留 `SERVICE_BUSY` 行为。