Skip to content

[9.x] Introducing Signal Traps 馃殾 - #43933

Merged
taylorotwell merged 24 commits into
9.xfrom
feat/traps
Sep 2, 2022
Merged

[9.x] Introducing Signal Traps 馃殾#43933
taylorotwell merged 24 commits into
9.xfrom
feat/traps

Conversation

@nunomaduro

@nunomaduro nunomaduro commented Aug 30, 2022

Copy link
Copy Markdown
Member

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...
});

Comment thread src/Illuminate/Console/Signals.php
@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
Comment thread src/Illuminate/Console/Signals.php
Comment thread src/Illuminate/Console/Signals.php Outdated
@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