Skip to content
Open
20 changes: 20 additions & 0 deletions apps/sim/executor/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ export const DEFAULTS = {
MAX_LOOP_ITERATIONS: 1000,
MAX_WORKFLOW_DEPTH: 10,
EXECUTION_TIME: 0,
/**
* Maximum number of iteration outputs to retain in memory during loop execution.
* Older iterations are discarded to prevent memory exhaustion in long-running loops.
* The final aggregated results will contain only the most recent iterations.
*/
MAX_STORED_ITERATION_OUTPUTS: 100,
/**
* Maximum size in bytes for iteration outputs before triggering truncation.
* This is an approximate estimate based on JSON serialization.
*/
MAX_ITERATION_OUTPUTS_SIZE_BYTES: 50 * 1024 * 1024, // 50MB
/**
* Maximum number of block logs to retain in memory during execution.
* Older logs are discarded to prevent memory exhaustion in long-running workflows.
*/
MAX_BLOCK_LOGS: 500,
/**
* Maximum size in bytes for block logs before triggering truncation.
*/
MAX_BLOCK_LOGS_SIZE_BYTES: 100 * 1024 * 1024, // 100MB
TOKENS: {
PROMPT: 0,
COMPLETION: 0,
Expand Down
54 changes: 53 additions & 1 deletion apps/sim/executor/execution/block-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class BlockExecutor {
let blockLog: BlockLog | undefined
if (!isSentinel) {
blockLog = this.createBlockLog(ctx, node.id, block, node)
ctx.blockLogs.push(blockLog)
this.addBlockLogWithMemoryLimit(ctx, blockLog)
this.callOnBlockStart(ctx, node, block)
}

Expand Down Expand Up @@ -658,4 +658,56 @@ export class BlockExecutor {

executionOutput.content = fullContent
}

/**
* Adds a block log to the execution context with memory management.
* Prevents unbounded memory growth by:
* 1. Limiting the number of stored logs (MAX_BLOCK_LOGS)
* 2. Checking estimated memory size (MAX_BLOCK_LOGS_SIZE_BYTES)
*
* When limits are exceeded, older logs are discarded to make room for newer ones.
*/
private addBlockLogWithMemoryLimit(ctx: ExecutionContext, blockLog: BlockLog): void {
ctx.blockLogs.push(blockLog)

// Check log count limit
if (ctx.blockLogs.length > DEFAULTS.MAX_BLOCK_LOGS) {
const discardCount = ctx.blockLogs.length - DEFAULTS.MAX_BLOCK_LOGS
ctx.blockLogs = ctx.blockLogs.slice(discardCount)
logger.warn('Block logs exceeded count limit, discarding older logs', {
discardedCount: discardCount,
retainedCount: ctx.blockLogs.length,
maxAllowed: DEFAULTS.MAX_BLOCK_LOGS,
})
}

// Periodically check memory size (every 50 logs to avoid frequent serialization)
if (ctx.blockLogs.length % 50 === 0) {
const estimatedSize = this.estimateBlockLogsSize(ctx.blockLogs)
if (estimatedSize > DEFAULTS.MAX_BLOCK_LOGS_SIZE_BYTES) {
const halfLength = Math.floor(ctx.blockLogs.length / 2)
const discardCount = Math.max(halfLength, 1)
ctx.blockLogs = ctx.blockLogs.slice(discardCount)
logger.warn('Block logs exceeded memory limit, discarding older logs', {
estimatedSizeBytes: estimatedSize,
maxSizeBytes: DEFAULTS.MAX_BLOCK_LOGS_SIZE_BYTES,
discardedCount: discardCount,
retainedCount: ctx.blockLogs.length,
})
}
}
}

/**
* Estimates the memory size of block logs in bytes.
* Returns a value exceeding the limit on serialization failure to trigger cleanup.
*/
private estimateBlockLogsSize(logs: BlockLog[]): number {
try {
return JSON.stringify(logs).length * 2
} catch {
// Return a value that exceeds the limit to trigger cleanup on serialization failure
return DEFAULTS.MAX_BLOCK_LOGS_SIZE_BYTES + 1
}
}
}
69 changes: 68 additions & 1 deletion apps/sim/executor/orchestrators/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export class LoopOrchestrator {
}

if (iterationResults.length > 0) {
scope.allIterationOutputs.push(iterationResults)
this.addIterationOutputsWithMemoryLimit(scope, iterationResults, loopId)
}

scope.currentIterationOutputs.clear()
Expand Down Expand Up @@ -462,4 +462,71 @@ export class LoopOrchestrator {
return []
}
}

/**
* Adds iteration outputs to the loop scope with memory management.
* Prevents unbounded memory growth by:
* 1. Limiting the number of stored iterations (MAX_STORED_ITERATION_OUTPUTS)
* 2. Checking estimated memory size (MAX_ITERATION_OUTPUTS_SIZE_BYTES)
*
* When limits are exceeded, older iterations are discarded to make room for newer ones.
* This ensures long-running loops don't cause memory exhaustion while still providing
* access to recent iteration results.
*/
private addIterationOutputsWithMemoryLimit(
scope: LoopScope,
iterationResults: NormalizedBlockOutput[],
loopId: string
): void {
scope.allIterationOutputs.push(iterationResults)

// Check iteration count limit
if (scope.allIterationOutputs.length > DEFAULTS.MAX_STORED_ITERATION_OUTPUTS) {
const discardCount = scope.allIterationOutputs.length - DEFAULTS.MAX_STORED_ITERATION_OUTPUTS
scope.allIterationOutputs = scope.allIterationOutputs.slice(discardCount)
logger.warn('Loop iteration outputs exceeded count limit, discarding older iterations', {
loopId,
iteration: scope.iteration,
discardedCount: discardCount,
retainedCount: scope.allIterationOutputs.length,
maxAllowed: DEFAULTS.MAX_STORED_ITERATION_OUTPUTS,
})
}

// Periodically check memory size limit (every 10 iterations to avoid frequent serialization)
if (scope.allIterationOutputs.length % 10 === 0) {
const estimatedSize = this.estimateObjectSize(scope.allIterationOutputs)
if (estimatedSize > DEFAULTS.MAX_ITERATION_OUTPUTS_SIZE_BYTES) {
// Discard oldest half of iterations when memory limit exceeded
const halfLength = Math.floor(scope.allIterationOutputs.length / 2)
const discardCount = Math.max(halfLength, 1)
scope.allIterationOutputs = scope.allIterationOutputs.slice(discardCount)
logger.warn('Loop iteration outputs exceeded memory limit, discarding older iterations', {
loopId,
iteration: scope.iteration,
estimatedSizeBytes: estimatedSize,
maxSizeBytes: DEFAULTS.MAX_ITERATION_OUTPUTS_SIZE_BYTES,
discardedCount: discardCount,
retainedCount: scope.allIterationOutputs.length,
})
}
}
}

/**
* Estimates the memory size of an object in bytes.
* This is an approximation based on JSON serialization size.
* Actual memory usage may vary due to object overhead and references.
*/
private estimateObjectSize(obj: unknown): number {
try {
// Use JSON.stringify length as a rough estimate
// Multiply by 2 for UTF-16 encoding overhead in JS strings
return JSON.stringify(obj).length * 2
} catch {
// If serialization fails (circular refs, etc.), return a value that exceeds
// the limit to trigger cleanup as a safety measure
return DEFAULTS.MAX_ITERATION_OUTPUTS_SIZE_BYTES + 1
}
}
}
4 changes: 4 additions & 0 deletions helm/sim/templates/networkpolicy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ spec:
ports:
- protocol: TCP
port: 443
# Allow custom egress rules
{{- with .Values.networkPolicy.egress }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}

{{- if .Values.postgresql.enabled }}
Expand Down