|
29 | 29 |
|
30 | 30 | /** |
31 | 31 | * Subprocess processor that executes jobs in isolated PHP processes. |
| 32 | + * |
| 33 | + * This processor spawns a new PHP process for each job, providing complete isolation |
| 34 | + * between jobs. This is useful for development environments where code changes need |
| 35 | + * to be reloaded without restarting the worker. |
| 36 | + * |
| 37 | + * Configuration options: |
| 38 | + * - `command`: Full command to execute (default: 'php bin/cake.php queue subprocess-runner') |
| 39 | + * - `timeout`: Maximum execution time in seconds (default: 300) |
| 40 | + * - `maxOutputSize`: Maximum output size in bytes (default: 1048576 = 1MB) |
| 41 | + * |
| 42 | + * Example configuration: |
| 43 | + * ``` |
| 44 | + * 'Queue' => [ |
| 45 | + * 'default' => [ |
| 46 | + * 'subprocess' => [ |
| 47 | + * 'command' => 'php bin/cake.php queue subprocess-runner', |
| 48 | + * 'timeout' => 60, |
| 49 | + * 'maxOutputSize' => 2097152, // 2MB |
| 50 | + * ], |
| 51 | + * ], |
| 52 | + * ], |
| 53 | + * ``` |
| 54 | + * |
32 | 55 | * Extends Processor to reuse event handling and processing logic (DRY principle). |
33 | 56 | */ |
34 | 57 | class SubprocessProcessor extends Processor |
35 | 58 | { |
36 | 59 | /** |
37 | 60 | * @param \Psr\Log\LoggerInterface $logger Logger instance |
38 | | - * @param array<string, mixed> $config Subprocess configuration |
| 61 | + * @param array<string, mixed> $config Subprocess configuration options |
39 | 62 | * @param \Cake\Core\ContainerInterface|null $container DI container instance |
40 | 63 | */ |
41 | 64 | public function __construct( |
@@ -188,63 +211,94 @@ protected function executeInSubprocess(array $jobData): array |
188 | 211 | throw new RuntimeException('Failed to create subprocess'); |
189 | 212 | } |
190 | 213 |
|
191 | | - $jobDataJson = json_encode($jobData); |
192 | | - if ($jobDataJson !== false) { |
193 | | - fwrite($pipes[0], $jobDataJson); |
194 | | - } |
| 214 | + try { |
| 215 | + $jobDataJson = json_encode($jobData); |
| 216 | + if ($jobDataJson !== false) { |
| 217 | + fwrite($pipes[0], $jobDataJson); |
| 218 | + } |
195 | 219 |
|
196 | | - fclose($pipes[0]); |
| 220 | + fclose($pipes[0]); |
197 | 221 |
|
198 | | - $output = ''; |
199 | | - $errorOutput = ''; |
200 | | - $startTime = time(); |
| 222 | + $output = ''; |
| 223 | + $errorOutput = ''; |
| 224 | + $startTime = time(); |
| 225 | + $maxOutputSize = $this->config['maxOutputSize'] ?? 1048576; // 1MB default |
201 | 226 |
|
202 | | - stream_set_blocking($pipes[1], false); |
203 | | - stream_set_blocking($pipes[2], false); |
| 227 | + stream_set_blocking($pipes[1], false); |
| 228 | + stream_set_blocking($pipes[2], false); |
204 | 229 |
|
205 | | - while (true) { |
206 | | - if ($timeout > 0 && (time() - $startTime) > $timeout) { |
207 | | - proc_terminate($process, 9); |
208 | | - fclose($pipes[1]); |
209 | | - fclose($pipes[2]); |
210 | | - proc_close($process); |
| 230 | + while (true) { |
| 231 | + if ($timeout > 0 && (time() - $startTime) > $timeout) { |
| 232 | + proc_terminate($process, 9); |
211 | 233 |
|
212 | | - return [ |
213 | | - 'success' => false, |
214 | | - 'error' => sprintf('Subprocess execution timeout after %d seconds', $timeout), |
215 | | - ]; |
216 | | - } |
| 234 | + return [ |
| 235 | + 'success' => false, |
| 236 | + 'error' => sprintf('Subprocess execution timeout after %d seconds', $timeout), |
| 237 | + ]; |
| 238 | + } |
217 | 239 |
|
218 | | - $read = [$pipes[1], $pipes[2]]; |
219 | | - $write = null; |
220 | | - $except = null; |
221 | | - $selectResult = stream_select($read, $write, $except, 1); |
| 240 | + $read = [$pipes[1], $pipes[2]]; |
| 241 | + $write = null; |
| 242 | + $except = null; |
| 243 | + $selectResult = stream_select($read, $write, $except, 1); |
222 | 244 |
|
223 | | - if ($selectResult === false) { |
224 | | - break; |
225 | | - } |
| 245 | + if ($selectResult === false) { |
| 246 | + return [ |
| 247 | + 'success' => false, |
| 248 | + 'error' => 'Stream select failed', |
| 249 | + ]; |
| 250 | + } |
| 251 | + |
| 252 | + if (in_array($pipes[1], $read)) { |
| 253 | + $chunk = fread($pipes[1], 8192); |
| 254 | + if ($chunk !== false) { |
| 255 | + if (strlen($output) + strlen($chunk) > $maxOutputSize) { |
| 256 | + proc_terminate($process, 9); |
226 | 257 |
|
227 | | - if (in_array($pipes[1], $read)) { |
228 | | - $chunk = fread($pipes[1], 8192); |
229 | | - if ($chunk !== false) { |
230 | | - $output .= $chunk; |
| 258 | + return [ |
| 259 | + 'success' => false, |
| 260 | + 'error' => sprintf('Subprocess output exceeded maximum size of %d bytes', $maxOutputSize), |
| 261 | + ]; |
| 262 | + } |
| 263 | + |
| 264 | + $output .= $chunk; |
| 265 | + } |
231 | 266 | } |
232 | | - } |
233 | 267 |
|
234 | | - if (in_array($pipes[2], $read)) { |
235 | | - $chunk = fread($pipes[2], 8192); |
236 | | - if ($chunk !== false) { |
237 | | - $errorOutput .= $chunk; |
| 268 | + if (in_array($pipes[2], $read)) { |
| 269 | + $chunk = fread($pipes[2], 8192); |
| 270 | + if ($chunk !== false) { |
| 271 | + if (strlen($errorOutput) + strlen($chunk) > $maxOutputSize) { |
| 272 | + proc_terminate($process, 9); |
| 273 | + |
| 274 | + return [ |
| 275 | + 'success' => false, |
| 276 | + 'error' => sprintf( |
| 277 | + 'Subprocess error output exceeded maximum size of %d bytes', |
| 278 | + $maxOutputSize, |
| 279 | + ), |
| 280 | + ]; |
| 281 | + } |
| 282 | + |
| 283 | + $errorOutput .= $chunk; |
| 284 | + } |
| 285 | + } |
| 286 | + |
| 287 | + if (feof($pipes[1]) && feof($pipes[2])) { |
| 288 | + break; |
238 | 289 | } |
239 | 290 | } |
| 291 | + } finally { |
| 292 | + // Always cleanup resources |
| 293 | + if (is_resource($pipes[1])) { |
| 294 | + fclose($pipes[1]); |
| 295 | + } |
240 | 296 |
|
241 | | - if (feof($pipes[1]) && feof($pipes[2])) { |
242 | | - break; |
| 297 | + if (is_resource($pipes[2])) { |
| 298 | + fclose($pipes[2]); |
243 | 299 | } |
244 | 300 | } |
245 | 301 |
|
246 | | - fclose($pipes[1]); |
247 | | - fclose($pipes[2]); |
248 | 302 | $exitCode = proc_close($process); |
249 | 303 |
|
250 | 304 | if ($exitCode !== 0 && empty($output)) { |
|
0 commit comments