Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11.x] delegate ProcessDriver@defer() to ProcessDriver@run() method #52807

Merged
merged 1 commit into from
Sep 16, 2024

Conversation

rodrigopedra
Copy link
Contributor

Closes #52790

Current implementation of ProcessDriver::defer() includes output redirection and background on the command array:

public function defer(Closure|array $tasks): DeferredCallback
{
$php = $this->phpBinary();
$artisan = $this->artisanBinary();
return defer(function () use ($tasks, $php, $artisan) {
foreach (Arr::wrap($tasks) as $task) {
$this->processFactory->path(base_path())->env([
'LARAVEL_INVOKABLE_CLOSURE' => serialize(new SerializableClosure($task)),
])->run([
$php,
$artisan,
'invoke-serialized-closure 2>&1 &',
]);
}
});
}

This ends building up this command line:

"/usr/bin/php" "artisan" "invoke-serialized-closure 2>&1 &"

As the third-part is quoted, when building the command, Artisan ends up looking for a command called literally invoke-serialized-closure 2>&1 &, including the 2>&1 & as the command name, which it fails to find.

This PR

  • Delegates the ProcessDriver@defer() to use the ProcessDriver@run() method

  • Same approach is used by the ForkDriver:

    public function defer(Closure|array $tasks): DeferredCallback
    {
    return defer(fn () => $this->run($tasks));
    }

  • I chose this approach as, from the method's docblock description, and from previous implementation (the & at the end), the deferred tasks should be run in the background and thus the implementation would be almost the same as the ProcessDriver::run() method (minus the output processing).

No tests were added, as this is not dependent on user's parameters, and I have no idea how to write a test to prevent it from happening in the future.

@taylorotwell
Copy link
Member

This isn't totally the same. The invoke-serialized-closure 2>&1 & is what puts the process in the background.

@rodrigopedra
Copy link
Contributor Author

I am on mobile right now, so it is hard to link to source, but on symfony/process documentation it says the Process@start() method executes the process on background.

I tried without the wait() with the issue's code, but as soon the artisan process dies it doesn't spawn the background processes without the wait() call.

@rodrigopedra
Copy link
Contributor Author

@rodrigopedra
Copy link
Contributor Author

It is actually not in background, but asynchronously, that might be why it kills the child process as soon as the calling process dies.

On the issue's code this was working as expected. Although the artisan calling code waits for the child's to end.

But the main issue is that passing the command as an array it quotes everything, I even tested passing the 2>&1 & as separate array items, but then, as they get quoted, artisan thinks they are an argument and complains either that it expects a single argument (when splitting 2>&1, and &), or that it couldn't unserialize the code (when passing these as a single separate argument).

I didn't test calling the command as a string, maybe it could work. But unfortunately I am out-of-office right now.

@rodrigopedra
Copy link
Contributor Author

One more thing, even if I the & at the end sends the process to the background, depending on how proc_open() works (symfony's docs says it uses it to run the processes) the child processes might get killed when the calling process ends.

On Linux, at least, it might be needed to pair it with a command like nohup to detach the child process from the main process, if the goal is to truly send a process to the background, and have it to be executed independently, regardless if the calling command ends.

@taylorotwell
Copy link
Member

The current code works exactly how I expect it to on my machine. 😅

What OS are you on?

@rodrigopedra
Copy link
Contributor Author

Anyway, with this PR's code, the deferred closure gets executed after the calling artisan command ends, as I thought it was the expected behavior.

I couldn't find in the docs (neither in Laravel's nor in Symfony's) a way to pass additional shell instructions like 2>&1, or & to a process

@rodrigopedra
Copy link
Contributor Author

Linux (openSUSE Leap 15.6)

@rodrigopedra
Copy link
Contributor Author

Did you check the logs output? On the CLI no errors are shown

@rodrigopedra
Copy link
Contributor Author

@taylorotwell

laravel-2024-09-16_12.02.49.mp4

Just got back at my office, recorded a screencast showing the behavior

@taylorotwell
Copy link
Member

For what it's worth, we already use the 2>&1 syntax in the console scheduler:

CleanShot 2024-09-16 at 10 08 47@2x

@taylorotwell
Copy link
Member

Will go ahead and merge this - though it will keep the FPM worker around while the deferred stuff executes which wasn't totally my intention.

@taylorotwell taylorotwell merged commit 79fbf4a into laravel:11.x Sep 16, 2024
63 checks passed
@rodrigopedra
Copy link
Contributor Author

@taylorotwell I saw this on the codebase, but the Console/Scheduling component executes the command using Process::fromShellCommandline() instead of passing the command as an array, and thus, the 2>&1 & doesn't get quoted.

protected function execute($container)
{
return Process::fromShellCommandline(
$this->buildCommand(), base_path(), null, null, null
)->run();
}

Maybe we can change the Concurrency\ProcessDriver to use the same approach. If you want to give it a try, I can send a new PR.

I guess it is better to truly send the deferred commands to the background, so it doesn't keep an FPM worker busy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug] Exception when try to run Concurrency::defer function
2 participants