diff --git a/.env.dist b/.env.dist index 983c80104..54e74965c 100644 --- a/.env.dist +++ b/.env.dist @@ -51,6 +51,7 @@ NNTP_SSLENABLED_A=${NNTP_SSLENABLED_A} NNTP_SOCKET_TIMEOUT_A=${NNTP_SOCKET_TIMEOUT_A} NN_MULTIPROCESSING_MAX_CHILD_TIME=${NN_MULTIPROCESSING_MAX_CHILD_TIME} +NN_CONCURRENCY_TIMEOUT=${NN_CONCURRENCY_TIMEOUT} ADMIN_USER=${ADMIN_USER} ADMIN_PASS=${ADMIN_PASS} diff --git a/.env.example b/.env.example index fbc0331b7..63cb47e80 100644 --- a/.env.example +++ b/.env.example @@ -65,6 +65,7 @@ NNTP_SSLENABLED_A=false NNTP_SOCKET_TIMEOUT_A=120 NN_MULTIPROCESSING_MAX_CHILD_TIME=1800 +NN_CONCURRENCY_TIMEOUT= ADMIN_USER= ADMIN_PASS= diff --git a/app/Providers/ConcurrencyServiceProvider.php b/app/Providers/ConcurrencyServiceProvider.php new file mode 100644 index 000000000..7fe1c83a4 --- /dev/null +++ b/app/Providers/ConcurrencyServiceProvider.php @@ -0,0 +1,31 @@ +app->afterResolving(ConcurrencyManager::class, function (ConcurrencyManager $manager): void { + $manager->extend('process', function (): TimeoutAwareProcessDriver { + $configuredTimeout = config('nntmux.concurrency_timeout'); + $timeout = (int) ($configuredTimeout ?? config('nntmux.multiprocessing_max_child_time', 1800)); + + return new TimeoutAwareProcessDriver( + app(ProcessFactory::class), + $timeout + ); + }); + }); + } +} diff --git a/app/Services/Concurrency/TimeoutAwareProcessDriver.php b/app/Services/Concurrency/TimeoutAwareProcessDriver.php new file mode 100644 index 000000000..7b8984248 --- /dev/null +++ b/app/Services/Concurrency/TimeoutAwareProcessDriver.php @@ -0,0 +1,72 @@ +processFactory->pool(function (Pool $pool) use ($tasks, $command) { + foreach (Arr::wrap($tasks) as $key => $task) { + $pool->as((string) $key) + ->timeout($this->timeout) + ->path(base_path()) + ->env([ + 'LARAVEL_INVOKABLE_CLOSURE' => base64_encode( + serialize(new SerializableClosure($task)) + ), + ]) + ->command($command); + } + })->start()->wait(); + + return $results->collect()->mapWithKeys(function ($result, $key) { + if ($result->failed()) { + throw new Exception('Concurrent process failed with exit code ['.$result->exitCode().']. Message: '.$result->errorOutput()); + } + + $output = $result->output(); + + if (($pos = strpos($output, "\x1f\x8b")) !== false) { + $output = substr($output, 0, $pos); + } + + $decodedResult = json_decode($output, true); + + if (! $decodedResult['successful']) { + throw new $decodedResult['exception']( + ...(! empty(array_filter($decodedResult['parameters'])) + ? $decodedResult['parameters'] + : [$decodedResult['message']]) + ); + } + + return [$key => unserialize($decodedResult['result'])]; + })->all(); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 39a68f6cc..83dd49fad 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -3,6 +3,7 @@ use App\Providers\AdditionalProcessingServiceProvider; use App\Providers\AppServiceProvider; use App\Providers\CategorizationServiceProvider; +use App\Providers\ConcurrencyServiceProvider; use App\Providers\ForumServiceProvider; use App\Providers\HorizonServiceProvider; use App\Providers\ProcessingServiceProvider; @@ -14,6 +15,7 @@ return [ AdditionalProcessingServiceProvider::class, AppServiceProvider::class, CategorizationServiceProvider::class, + ConcurrencyServiceProvider::class, ForumServiceProvider::class, HorizonServiceProvider::class, ProcessingServiceProvider::class, diff --git a/config/nntmux.php b/config/nntmux.php index e6af1efe2..e7c4e0b82 100644 --- a/config/nntmux.php +++ b/config/nntmux.php @@ -16,6 +16,7 @@ return [ 'admin_email' => env('ADMIN_EMAIL', 'admin@example.com'), 'crc_token' => env('CRC_TOKEN', null), 'multiprocessing_max_child_time' => env('NN_MULTIPROCESSING_MAX_CHILD_TIME', 1800), + 'concurrency_timeout' => env('NN_CONCURRENCY_TIMEOUT'), 'stream_fork_output' => env('STREAM_FORK_OUTPUT', false), 'purge_inactive_users' => env('PURGE_INACTIVE_USERS', false), 'purge_inactive_users_days' => env('PURGE_INACTIVE_USERS_DAYS', 180),