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

Optionally trigger pcntl_signal_dispatch #96

Merged
merged 8 commits into from
Jul 3, 2017
Merged

Optionally trigger pcntl_signal_dispatch #96

merged 8 commits into from
Jul 3, 2017

Conversation

fritz-gerneth
Copy link
Contributor

Currently there is no way to gracefully shut down a running projection when any OS signals are received. Setting the projection is stopping state via a custom CLI script is not always an option (e.g. when projections and others are managed by supervisord).

I think how the different signals should be handled should be left to the programmer, I have included a short example for reference below. In any case, to support this feature the projection loop at least needs to trigger the signal dispatch function.

Some remarks on my approach:

  • declare(ticks=1) is not required anymore to my knowledge, it got replaced by pcntl_signal_dispatch
  • I made this argument optional, defaulting to false to remain fully backwards compatible
  • The location of the triggerPcntlSignalDispatch call within the loop has quite a few implications. It might even make sense to trigger this before usleep.
        $pcntlCallback = function () use ($name) {
            echo "Attempting graceful shutdown of projection " . $name . PHP_EOL;
            $this->currentProjection->stop();
        };
        pcntl_signal(SIGTERM, $pcntlCallback);
        pcntl_signal(SIGINT, $pcntlCallback);
        pcntl_signal(SIGQUIT, $pcntlCallback);

      $this->currentProjection->start();

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.07%) to 92.497% when pulling b0c4eef on funct:pcntl_signals into 1f9702d on prooph:master.

@@ -164,6 +170,7 @@ public function __construct(
$this->persistBlockSize = $persistBlockSize;
$this->sleep = $sleep;
$this->status = ProjectionStatus::IDLE();
$this->triggerPcntlSignalDispatch = extension_loaded('pcntl') && $triggerPcntlSignalDispatch;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch the order of the checks so that if $triggerPcntlSignalDispatch = false PHP does not check if extension is loaded

@@ -151,6 +161,7 @@ public function __construct(
$this->persistBlockSize = $persistBlockSize;
$this->sleep = $sleep;
$this->status = ProjectionStatus::IDLE();
$this->triggerPcntlSignalDispatch = extension_loaded('pcntl') && $triggerPcntlSignalDispatch;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.8%) to 91.743% when pulling 33b410b on funct:pcntl_signals into 1f9702d on prooph:master.

@codeliner codeliner requested a review from prolic June 29, 2017 11:38
@codeliner
Copy link
Member

@prolic can you take a look? You are the expert in that area.

Also a test and docs update would be nice, especially regarding $pcntlCallback

Copy link
Member

@prolic prolic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

besides some small remarks, it definitely needs docs and a small sample in the docs on how usage might look like

@@ -35,6 +35,10 @@

final class PdoEventStoreReadModelProjector implements ReadModelProjector
{
const OPTION_PCNTL_DISPATCH = 'trigger_pcntl_dispatch';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public const

@@ -35,6 +35,10 @@

final class PdoEventStoreReadModelProjector implements ReadModelProjector
{
const OPTION_PCNTL_DISPATCH = 'trigger_pcntl_dispatch';

const DEFAULT_PCNTL_DISPATCH = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public const

*/
private $triggerPcntlSignalDispatch;

/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new consts missing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow, could you ellaborate? :)

@@ -87,7 +87,8 @@ public function createProjection(
$options[PdoEventStoreProjector::OPTION_LOCK_TIMEOUT_MS] ?? PdoEventStoreProjector::DEFAULT_LOCK_TIMEOUT_MS,
$options[PdoEventStoreProjector::OPTION_CACHE_SIZE] ?? PdoEventStoreProjector::DEFAULT_CACHE_SIZE,
$options[PdoEventStoreProjector::OPTION_PERSIST_BLOCK_SIZE] ?? PdoEventStoreProjector::DEFAULT_PERSIST_BLOCK_SIZE,
$options[PdoEventStoreProjector::OPTION_SLEEP] ?? PdoEventStoreProjector::DEFAULT_SLEEP
$options[PdoEventStoreProjector::OPTION_SLEEP] ?? PdoEventStoreProjector::DEFAULT_SLEEP,
$options[PdoEventStoreReadModelProjector::OPTION_PCNTL_DISPATCH] ?? PdoEventStoreReadModelProjector::DEFAULT_PCNTL_DISPATCH
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong cost usage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow, could you ellaborate? :)

@@ -87,7 +87,8 @@ public function createProjection(
$options[PdoEventStoreProjector::OPTION_LOCK_TIMEOUT_MS] ?? PdoEventStoreProjector::DEFAULT_LOCK_TIMEOUT_MS,
$options[PdoEventStoreProjector::OPTION_CACHE_SIZE] ?? PdoEventStoreProjector::DEFAULT_CACHE_SIZE,
$options[PdoEventStoreProjector::OPTION_PERSIST_BLOCK_SIZE] ?? PdoEventStoreProjector::DEFAULT_PERSIST_BLOCK_SIZE,
$options[PdoEventStoreProjector::OPTION_SLEEP] ?? PdoEventStoreProjector::DEFAULT_SLEEP
$options[PdoEventStoreProjector::OPTION_SLEEP] ?? PdoEventStoreProjector::DEFAULT_SLEEP,
$options[PdoEventStoreReadModelProjector::OPTION_PCNTL_DISPATCH] ?? PdoEventStoreReadModelProjector::DEFAULT_PCNTL_DISPATCH
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong const usage

@@ -87,7 +87,8 @@ public function createProjection(
$options[PdoEventStoreProjector::DEFAULT_LOCK_TIMEOUT_MS] ?? PdoEventStoreProjector::DEFAULT_LOCK_TIMEOUT_MS,
$options[PdoEventStoreProjector::OPTION_CACHE_SIZE] ?? PdoEventStoreProjector::DEFAULT_CACHE_SIZE,
$options[PdoEventStoreProjector::OPTION_PERSIST_BLOCK_SIZE] ?? PdoEventStoreProjector::DEFAULT_PERSIST_BLOCK_SIZE,
$options[PdoEventStoreProjector::OPTION_SLEEP] ?? PdoEventStoreProjector::DEFAULT_SLEEP
$options[PdoEventStoreProjector::OPTION_SLEEP] ?? PdoEventStoreProjector::DEFAULT_SLEEP,
$options[PdoEventStoreReadModelProjector::OPTION_PCNTL_DISPATCH] ?? PdoEventStoreReadModelProjector::DEFAULT_PCNTL_DISPATCH
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong const usage

@@ -498,6 +505,8 @@ public function run(bool $keepRunning = true): void

$this->eventCounter = 0;

$this->triggerPcntlSignalDispatch();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be the best place for that function call

@prolic
Copy link
Member

prolic commented Jun 29, 2017

@fritz-gerneth do you have any idea how to add a test for this ?

@prolic prolic self-assigned this Jun 29, 2017
@prolic
Copy link
Member

prolic commented Jun 29, 2017 via email

@fritz-gerneth
Copy link
Contributor Author

Certainly can add some documentation. Any prefered location or just create a new file "Using from CLI"?

For testing - unfortunately I don't have any idea that does not require dirty hacks. The two problems I see:

  • How to assert invocation of a core function (without rootkit, namespacing tricks) when doing a UT
  • How spawn a new process running the projection from a test and send send a kill signal as an integration test. Technically we sure can spawn a new child process and kill it. But pcntl functions are known to cause a bit of a headache when child processes install signal handlers (e.g. this comment)
  • How to test for extension loaded / not loaded

I sure could change this to remove the dependency on pcntl_signal_dispatch and replace it with some generic lifecycle callback that can be passed to the constructor instead. I find this quite obfuscated though :)

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.8%) to 91.743% when pulling 1c98bde on funct:pcntl_signals into 1f9702d on prooph:master.

@@ -38,6 +38,10 @@

final class PdoEventStoreProjector implements Projector
{
public const OPTION_PCNTL_DISPATCH = 'trigger_pcntl_dispatch';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's useful for all implementations, not specific to pdo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about that too, they definitely belong there and are helpful for others. This would require a new release of the event-store and update the minimum version required in this package though (else the constants in the projection managers are not defined). If you prefer this I certainly can draft this up.

@prolic
Copy link
Member

prolic commented Jun 29, 2017

@fritz-gerneth A unit test is not possible, I agree, but we could add an integration test. So spin up an in-memory event-store, run some test projection on it, send a kill signal and see if it stopped gracefully.

hint:

$pid = exec('php test-projector.php &', $output);
posix_kill($pid, SIGUSR1);

@prolic
Copy link
Member

prolic commented Jun 30, 2017 via email

* Created process inherits env variables from this process.
* Script returns with non-standard code SIGUSR1 from the handler and -1 else
*/
$projectionProcess = proc_open($command, $descriptorSpec, $pipes);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turned out to be harder than I thought and brought me down a road I certainly don't want to head to :)

Problem with exec is that it waits for the command to finish and doesn't return the pid. We could resolve this by sending the command to the background (php ... &) or return the pid (php ... & echo $!;). But still then, it doesn't return if the output is not redirected to /dev/null (see comment in documentation). SO I opted for the proc* family instead.

The test itself is rather simple - we return a non-standard exit code in our callback instead of the default one. Disabling the flag on the projection makes this test fail.

Copy link
Member

@prolic prolic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like that test-case, just some notes.

@@ -151,6 +161,7 @@ public function __construct(
$this->persistBlockSize = $persistBlockSize;
$this->sleep = $sleep;
$this->status = ProjectionStatus::IDLE();
$this->triggerPcntlSignalDispatch = $triggerPcntlSignalDispatch && extension_loaded('pcntl');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if $triggerPcntlSignalDispatch is set to true but extension is missing, we should maybe throw an exception instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would limit portability of projections across systems (e.g. when a library defines a projection with this flag enabled, while the user's system doesn't have it enabled). Personally I prefer the degraded functionality approach, but it's your call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* @test
*/
public function it_dispatches_pcntl_signals_when_enabled(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move these tests to the AbstractReadModelProjectorTest in the event-store repo. This way the test can be reused across all event-store implementations. If you think InMemoryEventStore is not worth adding this feature, we simply skip the tests for it.


require __DIR__ . '/../../vendor/autoload.php';

$readModel = new class() implements \Prooph\EventStore\Projection\ReadModel {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import Prooph\EventStore\Projection\ReadModel

@prolic
Copy link
Member

prolic commented Jun 30, 2017 via email

@prolic
Copy link
Member

prolic commented Jul 1, 2017

Require https://github.com/prooph/event-store/releases/tag/v7.2.0 and update according to changes made there.

Copy link
Member

@prolic prolic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nearly finished :)

@@ -164,6 +170,7 @@ public function __construct(
$this->persistBlockSize = $persistBlockSize;
$this->sleep = $sleep;
$this->status = ProjectionStatus::IDLE();
$this->triggerPcntlSignalDispatch = $triggerPcntlSignalDispatch && extension_loaded('pcntl');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -857,4 +866,11 @@ private function prepareStreamPositions(): void

$this->streamPositions = array_merge($streamPositions, $this->streamPositions);
}

private function triggerPcntlSignalDispatch(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this private function and call it directly. It has only a single usage.

@@ -151,6 +161,7 @@ public function __construct(
$this->persistBlockSize = $persistBlockSize;
$this->sleep = $sleep;
$this->status = ProjectionStatus::IDLE();
$this->triggerPcntlSignalDispatch = $triggerPcntlSignalDispatch && extension_loaded('pcntl');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -807,4 +816,11 @@ private function prepareStreamPositions(): void

$this->streamPositions = array_merge($streamPositions, $this->streamPositions);
}

private function triggerPcntlSignalDispatch(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. we don't need it as method.

@@ -0,0 +1,74 @@
<?php

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docheader missing

@@ -0,0 +1,45 @@
<?php

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docheader missing

@coveralls
Copy link

Coverage Status

Coverage decreased (-1.0%) to 91.612% when pulling 5432dc9 on funct:pcntl_signals into 1f9702d on prooph:master.

@prolic prolic merged commit 6ad9281 into prooph:master Jul 3, 2017
@prolic
Copy link
Member

prolic commented Jul 3, 2017

thanks a lot! 👍

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

Successfully merging this pull request may close these issues.

4 participants