Skip to content

Commit 7b7586d

Browse files
authored
fix(memory-util): fixed unbounded array of gmail/outlook pollers causing high memory util, added missing db indexes/removed unused ones, auto-disable schedules/webhooks after 10 consecutive failures (#2115)
* fix(memory-util): fixed unbounded array of gmail/outlook pollers causing high memory util, added missing db indexes/removed unused ones, auto-disable schedules/webhooks after 10 consecutive failures * ack PR comments * ack
1 parent d413bcd commit 7b7586d

File tree

12 files changed

+8145
-180
lines changed

12 files changed

+8145
-180
lines changed

apps/sim/app/api/webhooks/[id]/route.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
55
import { getSession } from '@/lib/auth'
66
import { createLogger } from '@/lib/logs/console/logger'
77
import { getUserEntityPermissions } from '@/lib/permissions/utils'
8+
import { validateInteger } from '@/lib/security/input-validation'
89
import { generateRequestId } from '@/lib/utils'
910

1011
const logger = createLogger('WebhookAPI')
@@ -95,7 +96,15 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
9596
}
9697

9798
const body = await request.json()
98-
const { path, provider, providerConfig, isActive } = body
99+
const { path, provider, providerConfig, isActive, failedCount } = body
100+
101+
if (failedCount !== undefined) {
102+
const validation = validateInteger(failedCount, 'failedCount', { min: 0 })
103+
if (!validation.isValid) {
104+
logger.warn(`[${requestId}] ${validation.error}`)
105+
return NextResponse.json({ error: validation.error }, { status: 400 })
106+
}
107+
}
99108

100109
let resolvedProviderConfig = providerConfig
101110
if (providerConfig) {
@@ -172,6 +181,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
172181
hasProviderUpdate: provider !== undefined,
173182
hasConfigUpdate: providerConfig !== undefined,
174183
hasActiveUpdate: isActive !== undefined,
184+
hasFailedCountUpdate: failedCount !== undefined,
175185
})
176186

177187
// Update the webhook
@@ -185,6 +195,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
185195
? resolvedProviderConfig
186196
: webhooks[0].webhook.providerConfig,
187197
isActive: isActive !== undefined ? isActive : webhooks[0].webhook.isActive,
198+
failedCount: failedCount !== undefined ? failedCount : webhooks[0].webhook.failedCount,
188199
updatedAt: new Date(),
189200
})
190201
.where(eq(webhook.id, id))

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-webhook-info.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { useCallback } from 'react'
1+
import { useCallback, useEffect, useState } from 'react'
2+
import { createLogger } from '@/lib/logs/console/logger'
23
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
34
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
45

6+
const logger = createLogger('useWebhookInfo')
7+
58
/**
69
* Return type for the useWebhookInfo hook
710
*/
@@ -12,16 +15,30 @@ export interface UseWebhookInfoReturn {
1215
webhookProvider: string | undefined
1316
/** The webhook path */
1417
webhookPath: string | undefined
18+
/** Whether the webhook is disabled */
19+
isDisabled: boolean
20+
/** The webhook ID if it exists in the database */
21+
webhookId: string | undefined
22+
/** Function to reactivate a disabled webhook */
23+
reactivateWebhook: (webhookId: string) => Promise<void>
1524
}
1625

1726
/**
1827
* Custom hook for managing webhook information for a block
1928
*
2029
* @param blockId - The ID of the block
30+
* @param workflowId - The current workflow ID
2131
* @returns Webhook configuration status and details
2232
*/
23-
export function useWebhookInfo(blockId: string): UseWebhookInfoReturn {
33+
export function useWebhookInfo(blockId: string, workflowId: string): UseWebhookInfoReturn {
2434
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
35+
const [webhookStatus, setWebhookStatus] = useState<{
36+
isDisabled: boolean
37+
webhookId: string | undefined
38+
}>({
39+
isDisabled: false,
40+
webhookId: undefined,
41+
})
2542

2643
const isWebhookConfigured = useSubBlockStore(
2744
useCallback(
@@ -55,9 +72,82 @@ export function useWebhookInfo(blockId: string): UseWebhookInfoReturn {
5572
)
5673
)
5774

75+
const fetchWebhookStatus = useCallback(async () => {
76+
if (!workflowId || !blockId || !isWebhookConfigured) {
77+
setWebhookStatus({ isDisabled: false, webhookId: undefined })
78+
return
79+
}
80+
81+
try {
82+
const params = new URLSearchParams({
83+
workflowId,
84+
blockId,
85+
})
86+
87+
const response = await fetch(`/api/webhooks?${params}`, {
88+
cache: 'no-store',
89+
headers: { 'Cache-Control': 'no-cache' },
90+
})
91+
92+
if (!response.ok) {
93+
setWebhookStatus({ isDisabled: false, webhookId: undefined })
94+
return
95+
}
96+
97+
const data = await response.json()
98+
const webhooks = data.webhooks || []
99+
100+
if (webhooks.length > 0) {
101+
const webhook = webhooks[0].webhook
102+
setWebhookStatus({
103+
isDisabled: !webhook.isActive,
104+
webhookId: webhook.id,
105+
})
106+
} else {
107+
setWebhookStatus({ isDisabled: false, webhookId: undefined })
108+
}
109+
} catch (error) {
110+
logger.error('Error fetching webhook status:', error)
111+
setWebhookStatus({ isDisabled: false, webhookId: undefined })
112+
}
113+
}, [workflowId, blockId, isWebhookConfigured])
114+
115+
useEffect(() => {
116+
fetchWebhookStatus()
117+
}, [fetchWebhookStatus])
118+
119+
const reactivateWebhook = useCallback(
120+
async (webhookId: string) => {
121+
try {
122+
const response = await fetch(`/api/webhooks/${webhookId}`, {
123+
method: 'PATCH',
124+
headers: {
125+
'Content-Type': 'application/json',
126+
},
127+
body: JSON.stringify({
128+
isActive: true,
129+
failedCount: 0,
130+
}),
131+
})
132+
133+
if (response.ok) {
134+
await fetchWebhookStatus()
135+
} else {
136+
logger.error('Failed to reactivate webhook')
137+
}
138+
} catch (error) {
139+
logger.error('Error reactivating webhook:', error)
140+
}
141+
},
142+
[fetchWebhookStatus]
143+
)
144+
58145
return {
59146
isWebhookConfigured,
60147
webhookProvider,
61148
webhookPath,
149+
isDisabled: webhookStatus.isDisabled,
150+
webhookId: webhookStatus.webhookId,
151+
reactivateWebhook,
62152
}
63153
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 82 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ const getDisplayValue = (value: unknown): string => {
139139
const firstMessage = value[0]
140140
if (!firstMessage?.content || firstMessage.content.trim() === '') return '-'
141141
const content = firstMessage.content.trim()
142-
// Show first 50 characters of the first message content
143142
return content.length > 50 ? `${content.slice(0, 50)}...` : content
144143
}
145144

@@ -326,7 +325,6 @@ const SubBlockRow = ({
326325
? (workflowMap[rawValue]?.name ?? null)
327326
: null
328327

329-
// Hydrate MCP server ID to name using TanStack Query
330328
const { data: mcpServers = [] } = useMcpServers(workspaceId || '')
331329
const mcpServerDisplayName = useMemo(() => {
332330
if (subBlock?.type !== 'mcp-server-selector' || typeof rawValue !== 'string') {
@@ -362,7 +360,6 @@ const SubBlockRow = ({
362360

363361
const names = rawValue
364362
.map((a) => {
365-
// Prioritize ID lookup (source of truth) over stored name
366363
if (a.variableId) {
367364
const variable = workflowVariables.find((v: any) => v.id === a.variableId)
368365
return variable?.name
@@ -450,7 +447,14 @@ export const WorkflowBlock = memo(function WorkflowBlock({
450447
currentWorkflow.blocks
451448
)
452449

453-
const { isWebhookConfigured, webhookProvider, webhookPath } = useWebhookInfo(id)
450+
const {
451+
isWebhookConfigured,
452+
webhookProvider,
453+
webhookPath,
454+
isDisabled: isWebhookDisabled,
455+
webhookId,
456+
reactivateWebhook,
457+
} = useWebhookInfo(id, currentWorkflowId)
454458

455459
const {
456460
scheduleInfo,
@@ -746,7 +750,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({
746750
config.category !== 'triggers' && type !== 'starter' && !displayTriggerMode
747751
const hasContentBelowHeader = subBlockRows.length > 0 || shouldShowDefaultHandles
748752

749-
// Count rows based on block type and whether default handles section is shown
750753
const defaultHandlesRow = shouldShowDefaultHandles ? 1 : 0
751754

752755
let rowsCount = 0
@@ -857,101 +860,62 @@ export const WorkflowBlock = memo(function WorkflowBlock({
857860
</span>
858861
</div>
859862
<div className='relative z-10 flex flex-shrink-0 items-center gap-2'>
860-
{isWorkflowSelector && childWorkflowId && (
861-
<>
862-
{typeof childIsDeployed === 'boolean' ? (
863-
<Tooltip.Root>
864-
<Tooltip.Trigger asChild>
865-
<Badge
866-
variant='outline'
867-
className={!childIsDeployed || childNeedsRedeploy ? 'cursor-pointer' : ''}
868-
style={{
869-
borderColor: !childIsDeployed
870-
? '#EF4444'
871-
: childNeedsRedeploy
872-
? '#FF6600'
873-
: '#22C55E',
874-
color: !childIsDeployed
875-
? '#EF4444'
876-
: childNeedsRedeploy
877-
? '#FF6600'
878-
: '#22C55E',
879-
}}
880-
onClick={(e) => {
881-
e.stopPropagation()
882-
if (
883-
(!childIsDeployed || childNeedsRedeploy) &&
884-
childWorkflowId &&
885-
!isDeploying
886-
) {
887-
deployWorkflow(childWorkflowId)
888-
}
889-
}}
890-
>
891-
{isDeploying
892-
? 'Deploying...'
893-
: !childIsDeployed
894-
? 'undeployed'
895-
: childNeedsRedeploy
896-
? 'redeploy'
897-
: 'deployed'}
898-
</Badge>
899-
</Tooltip.Trigger>
900-
{(!childIsDeployed || childNeedsRedeploy) && (
901-
<Tooltip.Content>
902-
<span className='text-sm'>
903-
{!childIsDeployed ? 'Click to deploy' : 'Click to redeploy'}
904-
</span>
905-
</Tooltip.Content>
906-
)}
907-
</Tooltip.Root>
908-
) : (
909-
<Badge variant='outline' style={{ visibility: 'hidden' }}>
910-
deployed
911-
</Badge>
912-
)}
913-
</>
914-
)}
863+
{isWorkflowSelector &&
864+
childWorkflowId &&
865+
typeof childIsDeployed === 'boolean' &&
866+
(!childIsDeployed || childNeedsRedeploy) && (
867+
<Tooltip.Root>
868+
<Tooltip.Trigger asChild>
869+
<Badge
870+
variant='outline'
871+
className='cursor-pointer'
872+
style={{
873+
borderColor: !childIsDeployed ? '#EF4444' : '#FF6600',
874+
color: !childIsDeployed ? '#EF4444' : '#FF6600',
875+
}}
876+
onClick={(e) => {
877+
e.stopPropagation()
878+
if (childWorkflowId && !isDeploying) {
879+
deployWorkflow(childWorkflowId)
880+
}
881+
}}
882+
>
883+
{isDeploying ? 'Deploying...' : !childIsDeployed ? 'undeployed' : 'redeploy'}
884+
</Badge>
885+
</Tooltip.Trigger>
886+
<Tooltip.Content>
887+
<span className='text-sm'>
888+
{!childIsDeployed ? 'Click to deploy' : 'Click to redeploy'}
889+
</span>
890+
</Tooltip.Content>
891+
</Tooltip.Root>
892+
)}
915893
{!isEnabled && <Badge>disabled</Badge>}
916894

917-
{type === 'schedule' && (
918-
<>
919-
{shouldShowScheduleBadge ? (
920-
<Tooltip.Root>
921-
<Tooltip.Trigger asChild>
922-
<Badge
923-
variant='outline'
924-
className={scheduleInfo?.isDisabled ? 'cursor-pointer' : ''}
925-
style={{
926-
borderColor: scheduleInfo?.isDisabled ? '#FF6600' : '#22C55E',
927-
color: scheduleInfo?.isDisabled ? '#FF6600' : '#22C55E',
928-
}}
929-
onClick={(e) => {
930-
e.stopPropagation()
931-
if (scheduleInfo?.id) {
932-
if (scheduleInfo.isDisabled) {
933-
reactivateSchedule(scheduleInfo.id)
934-
} else {
935-
disableSchedule(scheduleInfo.id)
936-
}
937-
}
938-
}}
939-
>
940-
{scheduleInfo?.isDisabled ? 'disabled' : 'scheduled'}
941-
</Badge>
942-
</Tooltip.Trigger>
943-
{scheduleInfo?.isDisabled && (
944-
<Tooltip.Content>
945-
<span className='text-sm'>Click to reactivate</span>
946-
</Tooltip.Content>
947-
)}
948-
</Tooltip.Root>
949-
) : (
950-
<Badge variant='outline' style={{ visibility: 'hidden' }}>
951-
scheduled
895+
{type === 'schedule' && shouldShowScheduleBadge && scheduleInfo?.isDisabled && (
896+
<Tooltip.Root>
897+
<Tooltip.Trigger asChild>
898+
<Badge
899+
variant='outline'
900+
className='cursor-pointer'
901+
style={{
902+
borderColor: '#FF6600',
903+
color: '#FF6600',
904+
}}
905+
onClick={(e) => {
906+
e.stopPropagation()
907+
if (scheduleInfo?.id) {
908+
reactivateSchedule(scheduleInfo.id)
909+
}
910+
}}
911+
>
912+
disabled
952913
</Badge>
953-
)}
954-
</>
914+
</Tooltip.Trigger>
915+
<Tooltip.Content>
916+
<span className='text-sm'>Click to reactivate</span>
917+
</Tooltip.Content>
918+
</Tooltip.Root>
955919
)}
956920

957921
{showWebhookIndicator && (
@@ -982,6 +946,27 @@ export const WorkflowBlock = memo(function WorkflowBlock({
982946
</Tooltip.Content>
983947
</Tooltip.Root>
984948
)}
949+
950+
{isWebhookConfigured && isWebhookDisabled && webhookId && (
951+
<Tooltip.Root>
952+
<Tooltip.Trigger asChild>
953+
<Badge
954+
variant='outline'
955+
className='cursor-pointer'
956+
style={{ borderColor: '#FF6600', color: '#FF6600' }}
957+
onClick={(e) => {
958+
e.stopPropagation()
959+
reactivateWebhook(webhookId)
960+
}}
961+
>
962+
disabled
963+
</Badge>
964+
</Tooltip.Trigger>
965+
<Tooltip.Content>
966+
<span className='text-sm'>Click to reactivate</span>
967+
</Tooltip.Content>
968+
</Tooltip.Root>
969+
)}
985970
{/* {isActive && (
986971
<div className='mr-[2px] ml-2 flex h-[16px] w-[16px] items-center justify-center'>
987972
<div

0 commit comments

Comments
 (0)