Skip to content

Commit a2451ef

Browse files
authored
feat(locks): add no-op for locking without redis to allow deployments without redis (#2703)
* feat(locks): add no-op for locking without redis to allow deployments without redis * ack PR comments, fixed worklfow block color
1 parent 6a262f3 commit a2451ef

File tree

4 files changed

+10
-85
lines changed

4 files changed

+10
-85
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/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ function getBlockIconAndColor(
164164
return { icon: ParallelTool.icon, bgColor: ParallelTool.bgColor }
165165
}
166166
if (lowerType === 'workflow') {
167-
return { icon: WorkflowIcon, bgColor: '#705335' }
167+
return { icon: WorkflowIcon, bgColor: '#6366F1' }
168168
}
169169

170170
// Look up from block registry (model maps to agent)

apps/sim/blocks/blocks/workflow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const WorkflowBlock: BlockConfig = {
3232
description:
3333
'This is a core workflow block. Execute another workflow as a block in your workflow. Enter the input variable to pass to the child workflow.',
3434
category: 'blocks',
35-
bgColor: '#705335',
35+
bgColor: '#6366F1',
3636
icon: WorkflowIcon,
3737
subBlocks: [
3838
{

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

Lines changed: 8 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,24 @@ 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+
return true // No-op when Redis unavailable; idempotency layer handles duplicates
13893
}
13994

14095
const result = await redis.set(lockKey, value, 'EX', expirySeconds, 'NX')
14196
return result === 'OK'
14297
}
14398

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-
15799
/**
158100
* Release a distributed lock safely.
159101
* Only releases if the caller owns the lock (value matches).
160102
* Returns true if lock was released, false if not owned or already expired.
161-
* Requires Redis - throws if Redis is not available.
103+
*
104+
* When Redis is not available, returns true (no-op) since no lock was held.
162105
*/
163106
export async function releaseLock(lockKey: string, value: string): Promise<boolean> {
164107
const redis = getRedisClient()
165108
if (!redis) {
166-
throw new Error('Redis not available for distributed locking')
109+
return true // No-op when Redis unavailable; no lock was actually held
167110
}
168111

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

0 commit comments

Comments
 (0)