Skip to content

Commit a0283f8

Browse files
committed
feat(locks): add no-op for locking without redis to allow deployments without redis
1 parent 5145ce1 commit a0283f8

File tree

2 files changed

+12
-83
lines changed

2 files changed

+12
-83
lines changed

apps/sim/app/api/webhooks/trigger/[path]/route.test.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ import {
1414
} from '@/app/api/__test-utils__/utils'
1515

1616
const {
17-
hasProcessedMessageMock,
18-
markMessageAsProcessedMock,
19-
closeRedisConnectionMock,
20-
acquireLockMock,
2117
generateRequestHashMock,
2218
validateSlackSignatureMock,
2319
handleWhatsAppVerificationMock,
@@ -28,10 +24,6 @@ const {
2824
processWebhookMock,
2925
executeMock,
3026
} = vi.hoisted(() => ({
31-
hasProcessedMessageMock: vi.fn().mockResolvedValue(false),
32-
markMessageAsProcessedMock: vi.fn().mockResolvedValue(true),
33-
closeRedisConnectionMock: vi.fn().mockResolvedValue(undefined),
34-
acquireLockMock: vi.fn().mockResolvedValue(true),
3527
generateRequestHashMock: vi.fn().mockResolvedValue('test-hash-123'),
3628
validateSlackSignatureMock: vi.fn().mockResolvedValue(true),
3729
handleWhatsAppVerificationMock: vi.fn().mockResolvedValue(null),
@@ -73,13 +65,6 @@ vi.mock('@/background/logs-webhook-delivery', () => ({
7365
logsWebhookDelivery: {},
7466
}))
7567

76-
vi.mock('@/lib/redis', () => ({
77-
hasProcessedMessage: hasProcessedMessageMock,
78-
markMessageAsProcessed: markMessageAsProcessedMock,
79-
closeRedisConnection: closeRedisConnectionMock,
80-
acquireLock: acquireLockMock,
81-
}))
82-
8368
vi.mock('@/lib/webhooks/utils', () => ({
8469
handleWhatsAppVerification: handleWhatsAppVerificationMock,
8570
handleSlackChallenge: handleSlackChallengeMock,
@@ -201,9 +186,6 @@ describe('Webhook Trigger API Route', () => {
201186
workspaceId: 'test-workspace-id',
202187
})
203188

204-
hasProcessedMessageMock.mockResolvedValue(false)
205-
markMessageAsProcessedMock.mockResolvedValue(true)
206-
acquireLockMock.mockResolvedValue(true)
207189
handleWhatsAppVerificationMock.mockResolvedValue(null)
208190
processGenericDeduplicationMock.mockResolvedValue(null)
209191
processWebhookMock.mockResolvedValue(new Response('Webhook processed', { status: 200 }))

apps/sim/lib/core/config/redis.ts

Lines changed: 12 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -61,54 +61,6 @@ export function getRedisClient(): Redis | null {
6161
}
6262
}
6363

64-
/**
65-
* Check if Redis is ready for commands.
66-
* Use for health checks only - commands should be sent regardless (ioredis queues them).
67-
*/
68-
export function isRedisConnected(): boolean {
69-
return globalRedisClient?.status === 'ready'
70-
}
71-
72-
/**
73-
* Get Redis connection status for diagnostics.
74-
*/
75-
export function getRedisStatus(): string {
76-
return globalRedisClient?.status ?? 'not initialized'
77-
}
78-
79-
const MESSAGE_ID_PREFIX = 'processed:'
80-
const MESSAGE_ID_EXPIRY = 60 * 60 * 24 * 7
81-
82-
/**
83-
* Check if a message has been processed (for idempotency).
84-
* Requires Redis - throws if Redis is not available.
85-
*/
86-
export async function hasProcessedMessage(key: string): Promise<boolean> {
87-
const redis = getRedisClient()
88-
if (!redis) {
89-
throw new Error('Redis not available for message deduplication')
90-
}
91-
92-
const result = await redis.exists(`${MESSAGE_ID_PREFIX}${key}`)
93-
return result === 1
94-
}
95-
96-
/**
97-
* Mark a message as processed (for idempotency).
98-
* Requires Redis - throws if Redis is not available.
99-
*/
100-
export async function markMessageAsProcessed(
101-
key: string,
102-
expirySeconds: number = MESSAGE_ID_EXPIRY
103-
): Promise<void> {
104-
const redis = getRedisClient()
105-
if (!redis) {
106-
throw new Error('Redis not available for message deduplication')
107-
}
108-
109-
await redis.set(`${MESSAGE_ID_PREFIX}${key}`, '1', 'EX', expirySeconds)
110-
}
111-
11264
/**
11365
* Lua script for safe lock release.
11466
* Only deletes the key if the value matches (ownership verification).
@@ -125,7 +77,10 @@ end
12577
/**
12678
* Acquire a distributed lock using Redis SET NX.
12779
* Returns true if lock acquired, false if already held.
128-
* Requires Redis - throws if Redis is not available.
80+
*
81+
* When Redis is not available, returns true (lock "acquired") to allow
82+
* single-replica deployments to function without Redis. In multi-replica
83+
* deployments without Redis, the idempotency layer prevents duplicate processing.
12984
*/
13085
export async function acquireLock(
13186
lockKey: string,
@@ -134,36 +89,28 @@ export async function acquireLock(
13489
): Promise<boolean> {
13590
const redis = getRedisClient()
13691
if (!redis) {
137-
throw new Error('Redis not available for distributed locking')
92+
logger.warn(
93+
'Redis not available - distributed locking disabled. For multi-replica deployments, configure REDIS_URL.',
94+
{ lockKey }
95+
)
96+
return true // Allow operation to proceed; idempotency layer handles duplicates
13897
}
13998

14099
const result = await redis.set(lockKey, value, 'EX', expirySeconds, 'NX')
141100
return result === 'OK'
142101
}
143102

144-
/**
145-
* Get the value of a lock key.
146-
* Requires Redis - throws if Redis is not available.
147-
*/
148-
export async function getLockValue(key: string): Promise<string | null> {
149-
const redis = getRedisClient()
150-
if (!redis) {
151-
throw new Error('Redis not available')
152-
}
153-
154-
return redis.get(key)
155-
}
156-
157103
/**
158104
* Release a distributed lock safely.
159105
* Only releases if the caller owns the lock (value matches).
160106
* Returns true if lock was released, false if not owned or already expired.
161-
* Requires Redis - throws if Redis is not available.
107+
*
108+
* When Redis is not available, returns true (no-op) since no lock was held.
162109
*/
163110
export async function releaseLock(lockKey: string, value: string): Promise<boolean> {
164111
const redis = getRedisClient()
165112
if (!redis) {
166-
throw new Error('Redis not available for distributed locking')
113+
return true // No-op when Redis unavailable; no lock was actually held
167114
}
168115

169116
const result = await redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, value)

0 commit comments

Comments
 (0)