(functionName: string, args: unknown[])
| 136 | } |
| 137 | |
| 138 | private async executeCall(functionName: string, args: unknown[]): Promise<unknown> { |
| 139 | let tempDirectory: string | undefined; |
| 140 | |
| 141 | try { |
| 142 | tempDirectory = await createSecureTempDirectory('promptfoo-worker-'); |
| 143 | const requestFile = await writeSecureTempFile( |
| 144 | tempDirectory, |
| 145 | 'request.json', |
| 146 | safeJsonStringify(args) as string, |
| 147 | ); |
| 148 | const responseFile = await writeSecureTempFile(tempDirectory, 'response.json', ''); |
| 149 | |
| 150 | // Send CALL command with function name |
| 151 | // Note: PythonShell.send() adds newline automatically in 'text' mode |
| 152 | // Using pipe (|) delimiter to avoid conflicts with Windows drive letters (C:) |
| 153 | const command = `CALL|${functionName}|${requestFile}|${responseFile}`; |
| 154 | this.process!.send(command); |
| 155 | |
| 156 | // Wait for DONE |
| 157 | await new Promise<unknown>((resolve, reject) => { |
| 158 | this.pendingRequest = { responseFile, resolve, reject }; |
| 159 | }); |
| 160 | |
| 161 | // Read response with exponential backoff retry. |
| 162 | // Python verifies file readability before sending DONE, but OS-level delays may still occur. |
| 163 | let responseData: string | undefined; |
| 164 | let lastError: unknown; |
| 165 | |
| 166 | // Exponential backoff: 1ms, 2ms, 4ms, 8ms, 16ms, 32ms, 64ms, 128ms, 256ms, 512ms, 1024ms, 2048ms, 4096ms, 5000ms (capped)... |
| 167 | // Total max wait: ~18 seconds (handles severe filesystem delays) |
| 168 | for (let attempt = 0, delay = 1; attempt < 16; attempt++, delay = Math.min(delay * 2, 5000)) { |
| 169 | try { |
| 170 | responseData = await fs.readFile(responseFile, 'utf-8'); |
| 171 | if (attempt > 0) { |
| 172 | logger.debug(`Response file read succeeded on attempt ${attempt + 1}`); |
| 173 | } |
| 174 | break; |
| 175 | } catch (error: unknown) { |
| 176 | lastError = error; |
| 177 | if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { |
| 178 | // File doesn't exist yet, wait and retry with exponential backoff. |
| 179 | await new Promise((resolve) => setTimeout(resolve, delay)); |
| 180 | continue; |
| 181 | } |
| 182 | // Non-ENOENT error, don't retry. |
| 183 | throw error; |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | // If we exhausted all retries, throw with debugging info |
| 188 | if (!responseData) { |
| 189 | try { |
| 190 | const files = await fs.readdir(tempDirectory); |
| 191 | logger.error( |
| 192 | `Failed to read response file after 16 attempts (~18s). Expected: ${path.basename(responseFile)}, Found in temporary directory: ${files.join(', ')}`, |
| 193 | ); |
| 194 | } catch { |
| 195 | logger.error( |
no test coverage detected