Files
cursor/plans/symfony-lock-distributed-lock-5511b223.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

4.6 KiB
Raw Permalink Blame History


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 中的锁实现(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:81client())。
  • 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 行为。