|
6 | 6 | */ |
7 | 7 |
|
8 | 8 | import Redis from 'ioredis'; |
| 9 | +import path from 'node:path'; |
| 10 | +import fs from 'node:fs'; |
| 11 | +import proper-lockfile from 'proper-lockfile'; |
9 | 12 |
|
10 | 13 | // 生成唯一 token |
11 | 14 | function generateToken(): string { |
@@ -60,13 +63,9 @@ export class RedisLockManager { |
60 | 63 | await this.redis.ping(); |
61 | 64 | redisAvailable = true; |
62 | 65 | } catch { |
63 | | - // Redis 不可用,優雅降級 - 回傳 no-op lock |
64 | | - console.warn('[RedisLock] Redis unavailable, using no-op lock (allow concurrent)'); |
65 | | - return async () => {}; // No-op release |
66 | | - } |
67 | | - |
68 | | - if (!redisAvailable) { |
69 | | - return async () => {}; |
| 66 | + // Redis 不可用,使用 file lock fallback |
| 67 | + console.warn('[RedisLock] Redis unavailable, using file lock fallback'); |
| 68 | + return this.createFileLock(key, ttl); |
70 | 69 | } |
71 | 70 |
|
72 | 71 | let attempts = 0; |
@@ -131,6 +130,45 @@ export class RedisLockManager { |
131 | 130 | private sleep(ms: number): Promise<void> { |
132 | 131 | return new Promise(resolve => setTimeout(resolve, ms)); |
133 | 132 | } |
| 133 | + |
| 134 | + /** |
| 135 | + * 建立 file lock(Redis 不可用時的 fallback) |
| 136 | + */ |
| 137 | + private createFileLock(key: string, ttl?: number): () => Promise<void> { |
| 138 | + const lockPath = path.join('/tmp', `.memory-lock-${key}.lock`); |
| 139 | + const lockTTL = (ttl || this.defaultTTL) / 1000; // proper-lockfile 用秒 |
| 140 | + |
| 141 | + // 確保目錄存在 |
| 142 | + const dir = path.dirname(lockPath); |
| 143 | + if (!fs.existsSync(dir)) { |
| 144 | + fs.mkdirSync(dir, { recursive: true }); |
| 145 | + } |
| 146 | + |
| 147 | + // 同步取得 file lock |
| 148 | + try { |
| 149 | + proper_lockfile.lockSync(lockPath, { |
| 150 | + retries: { |
| 151 | + retries: 10, |
| 152 | + minTimeout: 1000, |
| 153 | + maxTimeout: 30000, |
| 154 | + }, |
| 155 | + stale: lockTTL, |
| 156 | + }); |
| 157 | + console.log(`[RedisLock] Acquired file lock for ${key}`); |
| 158 | + } catch (err) { |
| 159 | + console.warn(`[RedisLock] Failed to acquire file lock: ${err}`); |
| 160 | + } |
| 161 | + |
| 162 | + // 回傳 release function |
| 163 | + return async () => { |
| 164 | + try { |
| 165 | + await proper_lockfile.unlock(lockPath); |
| 166 | + console.log(`[RedisLock] Released file lock for ${key}`); |
| 167 | + } catch (err) { |
| 168 | + console.warn(`[RedisLock] Failed to release file lock: ${err}`); |
| 169 | + } |
| 170 | + }; |
| 171 | + } |
134 | 172 | } |
135 | 173 |
|
136 | 174 | /** |
|
0 commit comments