mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(polling-tools): pass plan execution timeout to internal polling tool routes (#4884)
* fix(polling-tools): pass plan execution timeout to internal polling tool routes * address comments
This commit is contained in:
@@ -39,7 +39,12 @@ const MAX_IMAGE_BYTES = 25 * 1024 * 1024
|
||||
const MAX_IMAGE_JSON_BYTES = Math.ceil((MAX_IMAGE_BYTES * 4) / 3) + 256 * 1024
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 600
|
||||
/**
|
||||
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
|
||||
* `getMaxExecutionTimeout()` for the provider polling loop below. Next.js requires a
|
||||
* static literal for `maxDuration`, so this value must be kept in sync with that source.
|
||||
*/
|
||||
export const maxDuration = 5400
|
||||
|
||||
type ImageProvider = (typeof imageProviders)[number]
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sttToolContract } from '@/lib/api/contracts/tools/media/stt'
|
||||
import { getValidationErrorMessage, parseRequest, validationErrorResponse } from '@/lib/api/server'
|
||||
import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import {
|
||||
secureFetchWithPinnedIP,
|
||||
validateUrlWithDNS,
|
||||
@@ -25,7 +25,12 @@ const logger = createLogger('SttProxyAPI')
|
||||
const ELEVENLABS_STT_MODEL = 'scribe_v2'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 300 // 5 minutes for large files
|
||||
/**
|
||||
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
|
||||
* `getMaxExecutionTimeout()` for the transcript polling loop below. Next.js requires a
|
||||
* static literal for `maxDuration`, so this value must be kept in sync with that source.
|
||||
*/
|
||||
export const maxDuration = 5400
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId()
|
||||
@@ -629,7 +634,7 @@ async function transcribeWithAssemblyAI(
|
||||
let transcript: any
|
||||
let attempts = 0
|
||||
const pollIntervalMs = 5000
|
||||
const maxAttempts = Math.ceil(DEFAULT_EXECUTION_TIMEOUT_MS / pollIntervalMs)
|
||||
const maxAttempts = Math.ceil(getMaxExecutionTimeout() / pollIntervalMs)
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const statusResponse = await fetch(`https://api.assemblyai.com/v2/transcript/${id}`, {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { textractParseContract } from '@/lib/api/contracts/tools/media/document-parse'
|
||||
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import { validateS3BucketName } from '@/lib/core/security/input-validation'
|
||||
import {
|
||||
secureFetchWithPinnedIP,
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 300 // 5 minutes for large multi-page PDF processing
|
||||
/**
|
||||
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
|
||||
* `getMaxExecutionTimeout()` for the job polling loop below. Next.js requires a static
|
||||
* literal for `maxDuration`, so this value must be kept in sync with that source.
|
||||
*/
|
||||
export const maxDuration = 5400
|
||||
|
||||
const logger = createLogger('TextractParseAPI')
|
||||
|
||||
@@ -184,7 +189,7 @@ async function pollForJobCompletion(
|
||||
requestId: string
|
||||
): Promise<Record<string, unknown>> {
|
||||
const pollIntervalMs = 5000
|
||||
const maxPollTimeMs = DEFAULT_EXECUTION_TIMEOUT_MS
|
||||
const maxPollTimeMs = getMaxExecutionTimeout()
|
||||
const maxAttempts = Math.ceil(maxPollTimeMs / pollIntervalMs)
|
||||
|
||||
const getTarget = useAnalyzeDocument
|
||||
|
||||
@@ -28,7 +28,12 @@ const MAX_VIDEO_REFERENCE_IMAGE_BYTES = 25 * 1024 * 1024
|
||||
const MAX_VIDEO_JSON_BYTES = 2 * 1024 * 1024
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 600 // 10 minutes for video generation
|
||||
/**
|
||||
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
|
||||
* `getMaxExecutionTimeout()` for the provider polling loops below. Next.js requires a
|
||||
* static literal for `maxDuration`, so this value must be kept in sync with that source.
|
||||
*/
|
||||
export const maxDuration = 5400
|
||||
|
||||
async function readVideoResponseBuffer(response: Response, label: string): Promise<Buffer> {
|
||||
return readResponseToBufferWithLimit(response, {
|
||||
|
||||
+30
-20
@@ -5,7 +5,7 @@ import { randomFloat } from '@sim/utils/random'
|
||||
import { getBYOKKey } from '@/lib/api-key/byok'
|
||||
import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
|
||||
import { DEFAULT_EXECUTION_TIMEOUT_MS, getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import { getHostedKeyRateLimiter } from '@/lib/core/rate-limiter'
|
||||
import {
|
||||
secureFetchWithPinnedIP,
|
||||
@@ -879,6 +879,9 @@ export async function executeTool(
|
||||
options: ExecuteToolOptions = {}
|
||||
): Promise<ToolResponse> {
|
||||
const { skipPostProcess = false, executionContext, signal } = options
|
||||
// Fall back to the workflow execution's abort signal so plan-based execution timeouts
|
||||
// and cancellation propagate to tool fetches when the caller passes no explicit signal.
|
||||
const effectiveSignal = signal ?? executionContext?.abortSignal
|
||||
// Capture start time for precise timing
|
||||
const startTime = new Date()
|
||||
const startTimeISO = startTime.toISOString()
|
||||
@@ -972,7 +975,7 @@ export async function executeTool(
|
||||
executionContext,
|
||||
requestId,
|
||||
startTimeISO,
|
||||
signal
|
||||
effectiveSignal
|
||||
)
|
||||
} else {
|
||||
// For built-in tools, use the synchronous version
|
||||
@@ -1169,23 +1172,26 @@ export async function executeTool(
|
||||
// Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch)
|
||||
// Wrap with retry logic for hosted keys to handle rate limiting due to higher usage
|
||||
const result = hostedKeyInfo.isUsingHostedKey
|
||||
? await executeWithRetry(() => executeToolRequest(toolId, tool, contextParams, signal), {
|
||||
requestId,
|
||||
toolId,
|
||||
envVarName: hostedKeyInfo.envVarName!,
|
||||
executionContext,
|
||||
reacquireAfterRetriesExhausted: async () => {
|
||||
const reacquired = await reacquireHostedKey(
|
||||
tool,
|
||||
contextParams,
|
||||
executionContext,
|
||||
requestId
|
||||
)
|
||||
if (!reacquired) return null
|
||||
return () => executeToolRequest(toolId, tool, contextParams)
|
||||
},
|
||||
})
|
||||
: await executeToolRequest(toolId, tool, contextParams, signal)
|
||||
? await executeWithRetry(
|
||||
() => executeToolRequest(toolId, tool, contextParams, effectiveSignal),
|
||||
{
|
||||
requestId,
|
||||
toolId,
|
||||
envVarName: hostedKeyInfo.envVarName!,
|
||||
executionContext,
|
||||
reacquireAfterRetriesExhausted: async () => {
|
||||
const reacquired = await reacquireHostedKey(
|
||||
tool,
|
||||
contextParams,
|
||||
executionContext,
|
||||
requestId
|
||||
)
|
||||
if (!reacquired) return null
|
||||
return () => executeToolRequest(toolId, tool, contextParams, effectiveSignal)
|
||||
},
|
||||
}
|
||||
)
|
||||
: await executeToolRequest(toolId, tool, contextParams, effectiveSignal)
|
||||
|
||||
// Apply post-processing if available and not skipped
|
||||
let finalResult = result
|
||||
@@ -1576,7 +1582,11 @@ async function executeToolRequest(
|
||||
try {
|
||||
if (isInternalRoute) {
|
||||
const controller = new AbortController()
|
||||
const timeout = requestParams.timeout || DEFAULT_EXECUTION_TIMEOUT_MS
|
||||
// With a caller/execution abort signal present, the plan-based timeout bounds the call and
|
||||
// this only acts as a ceiling; without one, keep the tighter default as the hang safety net.
|
||||
const timeout =
|
||||
requestParams.timeout ||
|
||||
(signal ? getMaxExecutionTimeout() : DEFAULT_EXECUTION_TIMEOUT_MS)
|
||||
const timeoutId = setTimeout(
|
||||
() => controller.abort(`timeout:internal_tool_fetch:${timeout}ms`),
|
||||
timeout
|
||||
|
||||
Reference in New Issue
Block a user