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

[9.x] Introducing Signal Traps 🚦 #43933

Merged
merged 24 commits into from
Sep 2, 2022
Merged

[9.x] Introducing Signal Traps 🚦 #43933

merged 24 commits into from
Sep 2, 2022

Conversation

nunomaduro
Copy link
Member

@nunomaduro nunomaduro commented Aug 30, 2022

Documentation: laravel/docs#8178.

Many operating systems allow signals to be sent to running processes. As an example, when typing Control + C on MacOS, a SIGINT will initiate an Artisan command shutdown.

Artisan commands shutdowns may lead to unexpected state, as the user may have defined an Artisan command that takes more than expected to run, and ending the command without any sort of clean up may leave temporary files, temporary resources, and more.

To mitigate this issue, this pull request introduces Signal Traps on Laravel, a new concept that allows you to catch process signals and execute code when they occur:

Artisan::command('export:sales', function () {
    $this->continue = true;

    $this->trap(SIGINT, function () {
        // End the command's job gracefully ...
        // E.g: clean created files, etc ...

        $this->continue = false;
    });

    while($this->continue) {
        dump('Exporting batch of sales...');
    }
});

With this technique, Laravel developers can perform any clean-up tasks / or potentially revert operations when an Artisan command is ended unexpectedly. The given API, sits on top of a new trap method, on the base Command class of Laravel.

Now, because is very common wanting to subscribe to multiple signals at the same time, you may pass an array of signals in those cases:

$this->trap([SIGTERM, SIGINT], function () {
    ExportSales::markAsNotReady();


    throw new Exception('Command ended unexpectedly via signal');
}

In addition, traps get untrap once the command is "done". This is useful, and intentionally behavior, as commands may get called within commands, or within "queue:jobs":

# Use Case 1: Command "1" is still running, command "2" is done:
Artisan::command('1', function () {
    $this->trap(SIGINT, function () {
        dump('trap #1');
    });

    Artisan::call('2');

    sleep(10); // Control + C here ...
});

Artisan::command('2', function () {
    $this->trap(SIGINT, function () {
        dump('trap #2');
    });
});

// Displays: 
// #1
# Use Case 2: Command "2" and command "1" are still running;
Artisan::command('1', function () {
    $this->trap(SIGINT, function () {
        dump('trap #1');
    });

    Artisan::call('2');
});

Artisan::command('2', function () {
    $this->trap(SIGINT, function () {
        dump('trap #2');
    });

    sleep(10); // Control + C here ...
});

// Displays: 
// #2
// #1
# Use Case 3: Command "1" is still running, and command "2" is done, on a dispatched job.

dispatch(function () {
    Artisan::call('1');
});

Artisan::command('1', function () {
    $this->trap(SIGINT, function () {
        dump('trap #1');
    });

    Artisan::call('2');

    sleep(10); // Control + C here ...
});

Artisan::command('2', function () {
    $this->trap(SIGINT, function () {
        dump('trap #2');
    });
});

// Displays: 
// #1

Finally, two important things that not be trivial at first glance:

  1. by defining a trap method, you intentionally override PHP's native behavior for that signal. For example, if you define a trap for SIGINT, PHP won't end the process as it would do natively, therefore is up to the user to end the process gracefully or not.
  2. Traps won't be run on non-UNIX platforms / or platforms that don't support the pcntl extension - in fact, trying to use SIGINT on an Windows environment, results on the following error:

Screenshot 2022-09-01 at 12 05 25

Package developers should use extension_loaded('pcntl') before even calling traps, as their users may use Windows.


For future development considerations:

  1. As mentioned in point 1) above, signals that typically would shut down the process, won't anymore. While this may be expected, it may not also be expected for some users. Therefore, if people get confused by this, we can future develop an API as the following:
Artisan::command('2', function () {
    $this->trap(SIGINT, function () {
        $this->quit(); //  this...
    })->shouldQuit(); // or this...
});

@nunomaduro nunomaduro self-assigned this Aug 31, 2022
@nunomaduro nunomaduro changed the title [9.x] Adds Signal Traps to Artisan [9.x] Introducing Signal Traps Aug 31, 2022
@nunomaduro nunomaduro changed the title [9.x] Introducing Signal Traps [9.x] Introducing Signal Traps 🚦 Sep 1, 2022
@nunomaduro nunomaduro marked this pull request as ready for review September 1, 2022 10:51
@taylorotwell taylorotwell merged commit b539903 into 9.x Sep 2, 2022
@taylorotwell taylorotwell deleted the feat/traps branch September 2, 2022 14:37
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.

7 participants