Skip to content

Commit 74d3fcc

Browse files
authored
doc: Add mention about subscribed services (#277)
Add an explanation about proxies and subscribed services and add a test to show-case the issue within our test suite.
1 parent 1ad1d10 commit 74d3fcc

9 files changed

+313
-37
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This library supports the parallelization of Symfony Console commands.
1212
- [Batches](#batches)
1313
- [Configuration](#configuration)
1414
- [Hooks](#hooks)
15+
- [Subscribed Services](#subscribed-services)
1516
- [Differences with Amphp/ReactPHP](#differences-with-amphpreactphp)
1617
- [Contribute](#contribute)
1718
- [Upgrade](#upgrade)
@@ -267,6 +268,17 @@ The library supports several process hooks which can be configured via
267268
*: When using the `Parallelization` trait, those hooks can be directly configured by overriding the corresponding method.
268269

269270

271+
## Subscribed Services
272+
273+
You should be using [subscribed services] or proxies. Indeed, you may otherwise end up with the issue that the service
274+
initially injected in the command may end up being different than the one used by the container. This is because upon
275+
error, the `ResetServiceErrorHandler` error handler is used which resets the container when an item fails. As a result,
276+
if the service is not directly fetched from the container (to get a fresh instance if the container resets), you will
277+
end up using an obsolete service.
278+
279+
A common symptom of this issue is to run into a closed entity manager issue.
280+
281+
270282
## Differences with Amphp/ReactPHP
271283

272284
If you came across this library and wonder what the differences are with [Amphp] or [ReactPHP] or other potential
@@ -322,6 +334,7 @@ All contents of this package are licensed under the [MIT license].
322334
[Composer]: https://getcomposer.org
323335
[Bernhard Schussek]: http://webmozarts.com
324336
[ReactPHP]: https://reactphp.org/
337+
[subscribed services]: https://symfony.com/doc/current/service_container/service_subscribers_locators.html#service-subscriber-trait
325338
[Théo Fidry]: http://webmozarts.com
326339
[The Community Contributors]: https://github.com/webmozarts/console-parallelization/graphs/contributors
327340
[issue tracker]: https://github.com/webmozarts/console-parallelization/issues

bin/console

-6
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,4 @@ error_reporting(E_ALL);
1919
ini_set('display_errors', '1');
2020

2121
$application = new Application(new Kernel());
22-
$application->add(new ImportMoviesCommand());
23-
$application->add(new ImportUnknownMoviesCountCommand());
24-
$application->add(new LegacyCommand());
25-
$application->add(new NoSubProcessCommand());
26-
$application->add(new AbsoluteScriptPathCommand());
27-
2822
$application->run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Webmozarts Console Parallelization package.
5+
*
6+
* (c) Webmozarts GmbH <office@webmozarts.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Webmozarts\Console\Parallelization\Fixtures\Command;
15+
16+
use LogicException;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
use Symfony\Contracts\Service\Attribute\SubscribedService;
20+
use Symfony\Contracts\Service\ServiceSubscriberInterface;
21+
use Symfony\Contracts\Service\ServiceSubscriberTrait;
22+
use UnexpectedValueException;
23+
use Webmozarts\Console\Parallelization\ErrorHandler\ErrorHandler;
24+
use Webmozarts\Console\Parallelization\ErrorHandler\ResetServiceErrorHandler;
25+
use Webmozarts\Console\Parallelization\Fixtures\Counter;
26+
use Webmozarts\Console\Parallelization\Input\ParallelizationInput;
27+
use Webmozarts\Console\Parallelization\ParallelCommand;
28+
use Webmozarts\Console\Parallelization\ParallelExecutorFactory;
29+
use function array_map;
30+
use function range;
31+
use function strval;
32+
33+
final class SubscribedServiceCommand extends ParallelCommand implements ServiceSubscriberInterface
34+
{
35+
use ServiceSubscriberTrait;
36+
37+
private bool $threwOnce = false;
38+
39+
public function __construct()
40+
{
41+
parent::__construct('subscribed-service');
42+
}
43+
44+
protected function initialize(InputInterface $input, OutputInterface $output): void
45+
{
46+
// Ensure this command cannot be run with child processes. The purpose of this command
47+
// is to test that the service counter is properly reset, doing this with child processes
48+
// would require to persist its state across processes which would be needlessly complicated.
49+
$input->setOption(ParallelizationInput::MAIN_PROCESS_OPTION, true);
50+
}
51+
52+
/**
53+
* @return list<string>
54+
*/
55+
protected function fetchItems(InputInterface $input, OutputInterface $output): array
56+
{
57+
return array_map(
58+
strval(...),
59+
range(0, 3),
60+
);
61+
}
62+
63+
protected function getItemName(?int $count): string
64+
{
65+
return 1 === $count ? 'item' : 'items';
66+
}
67+
68+
protected function configureParallelExecutableFactory(
69+
ParallelExecutorFactory $parallelExecutorFactory,
70+
InputInterface $input,
71+
OutputInterface $output,
72+
): ParallelExecutorFactory {
73+
return $parallelExecutorFactory
74+
->withBatchSize(2)
75+
->withRunAfterLastCommand($this->checkCounter(...));
76+
}
77+
78+
protected function runSingleCommand(string $item, InputInterface $input, OutputInterface $output): void
79+
{
80+
$counter = $this->counter();
81+
82+
if ($counter->getCount() >= 2 && !$this->threwOnce) {
83+
$this->threwOnce = true;
84+
85+
throw new UnexpectedValueException('3rd item reached.');
86+
}
87+
88+
$counter->increment();
89+
}
90+
91+
private function checkCounter(): void
92+
{
93+
$counter = $this->counter();
94+
$count = $counter->getCount();
95+
96+
if ($count >= 2) {
97+
throw new LogicException('The Counter service was not correctly reset.');
98+
}
99+
}
100+
101+
#[SubscribedService]
102+
private function counter(): Counter
103+
{
104+
return $this->container->get(__METHOD__);
105+
}
106+
107+
protected function createErrorHandler(InputInterface $input, OutputInterface $output): ErrorHandler
108+
{
109+
return ResetServiceErrorHandler::forContainer($this->getContainer());
110+
}
111+
}

tests/Fixtures/Counter.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Webmozarts Console Parallelization package.
5+
*
6+
* (c) Webmozarts GmbH <office@webmozarts.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Webmozarts\Console\Parallelization\Fixtures;
15+
16+
final class Counter
17+
{
18+
private int $count = 0;
19+
20+
public function increment(): void
21+
{
22+
++$this->count;
23+
}
24+
25+
public function getCount(): int
26+
{
27+
return $this->count;
28+
}
29+
}

tests/Integration/BareKernel.php

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Webmozarts Console Parallelization package.
5+
*
6+
* (c) Webmozarts GmbH <office@webmozarts.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Webmozarts\Console\Parallelization\Integration;
15+
16+
use Symfony\Component\Config\Loader\LoaderInterface;
17+
use Symfony\Component\Console\Logger\ConsoleLogger;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Definition;
20+
use Symfony\Component\EventDispatcher\EventDispatcher;
21+
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
22+
use Symfony\Component\HttpKernel\Kernel as HttpKernel;
23+
24+
final class BareKernel extends HttpKernel
25+
{
26+
public function __construct()
27+
{
28+
parent::__construct('dev', true);
29+
}
30+
31+
/**
32+
* @return BundleInterface[]
33+
*/
34+
public function registerBundles(): array
35+
{
36+
return [];
37+
}
38+
39+
public function registerContainerConfiguration(LoaderInterface $loader): void
40+
{
41+
}
42+
43+
public function getCacheDir(): string
44+
{
45+
return __DIR__.'/var/cache/BareKernel';
46+
}
47+
48+
public function getLogDir(): string
49+
{
50+
return __DIR__.'/var/log/BareKernel';
51+
}
52+
53+
protected function build(ContainerBuilder $container): void
54+
{
55+
$eventDispatcherDefinition = new Definition(
56+
EventDispatcher::class,
57+
[],
58+
);
59+
$eventDispatcherDefinition->setPublic(true);
60+
61+
$loggerDefinition = new Definition(
62+
ConsoleLogger::class,
63+
[],
64+
);
65+
$loggerDefinition->setPublic(true);
66+
67+
$container->addDefinitions([
68+
'event_dispatcher' => $eventDispatcherDefinition,
69+
'logger' => $loggerDefinition,
70+
]);
71+
}
72+
}

tests/Integration/Kernel.php

+7-27
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@
1313

1414
namespace Webmozarts\Console\Parallelization\Integration;
1515

16+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
1617
use Symfony\Component\Config\Loader\LoaderInterface;
17-
use Symfony\Component\Console\Logger\ConsoleLogger;
18-
use Symfony\Component\DependencyInjection\ContainerBuilder;
19-
use Symfony\Component\DependencyInjection\Definition;
20-
use Symfony\Component\EventDispatcher\EventDispatcher;
2118
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
2219
use Symfony\Component\HttpKernel\Kernel as HttpKernel;
2320

@@ -33,40 +30,23 @@ public function __construct()
3330
*/
3431
public function registerBundles(): array
3532
{
36-
return [];
33+
return [
34+
new FrameworkBundle(),
35+
];
3736
}
3837

3938
public function registerContainerConfiguration(LoaderInterface $loader): void
4039
{
40+
$loader->load(__DIR__.'/services.php');
4141
}
4242

4343
public function getCacheDir(): string
4444
{
45-
return __DIR__.'/var/cache';
45+
return __DIR__.'/var/cache/Kernel';
4646
}
4747

4848
public function getLogDir(): string
4949
{
50-
return __DIR__.'/var/log';
51-
}
52-
53-
protected function build(ContainerBuilder $container): void
54-
{
55-
$eventDispatcherDefinition = new Definition(
56-
EventDispatcher::class,
57-
[],
58-
);
59-
$eventDispatcherDefinition->setPublic(true);
60-
61-
$loggerDefinition = new Definition(
62-
ConsoleLogger::class,
63-
[],
64-
);
65-
$loggerDefinition->setPublic(true);
66-
67-
$container->addDefinitions([
68-
'event_dispatcher' => $eventDispatcherDefinition,
69-
'logger' => $loggerDefinition,
70-
]);
50+
return __DIR__.'/var/log/Kernel';
7151
}
7252
}

tests/Integration/ParallelizationIntegrationTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,16 @@ class ParallelizationIntegrationTest extends TestCase
4848

4949
protected function setUp(): void
5050
{
51-
$this->importMoviesCommand = (new Application(new Kernel()))->add(new ImportMoviesCommand());
51+
$this->importMoviesCommand = (new Application(new BareKernel()))->add(new ImportMoviesCommand());
5252
$this->importMoviesCommandTester = new CommandTester($this->importMoviesCommand);
5353

54-
$this->importUnknownMoviesCountCommand = (new Application(new Kernel()))->add(new ImportUnknownMoviesCountCommand());
54+
$this->importUnknownMoviesCountCommand = (new Application(new BareKernel()))->add(new ImportUnknownMoviesCountCommand());
5555
$this->importUnknownMoviesCountCommandTester = new CommandTester($this->importUnknownMoviesCountCommand);
5656

57-
$this->noSubProcessCommand = (new Application(new Kernel()))->add(new NoSubProcessCommand());
57+
$this->noSubProcessCommand = (new Application(new BareKernel()))->add(new NoSubProcessCommand());
5858
$this->noSubProcessCommandTester = new CommandTester($this->noSubProcessCommand);
5959

60-
$this->legacyCommand = (new Application(new Kernel()))->add(new LegacyCommand());
60+
$this->legacyCommand = (new Application(new BareKernel()))->add(new LegacyCommand());
6161
$this->legacyCommandTester = new CommandTester($this->legacyCommand);
6262
}
6363

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Webmozarts Console Parallelization package.
5+
*
6+
* (c) Webmozarts GmbH <office@webmozarts.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Webmozarts\Console\Parallelization\Integration;
15+
16+
use PHPUnit\Framework\Attributes\CoversNothing;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Bundle\FrameworkBundle\Console\Application;
19+
use Symfony\Component\Console\Tester\CommandTester;
20+
21+
/**
22+
* @internal
23+
*/
24+
#[CoversNothing]
25+
class SubscribedServicesIntegrationTest extends TestCase
26+
{
27+
private CommandTester $subscribedServiceCommandTester;
28+
29+
protected function setUp(): void
30+
{
31+
$this->subscribedServiceCommandTester = new CommandTester(
32+
(new Application(new Kernel()))->get('subscribed-service')
33+
);
34+
}
35+
36+
public function test_it_can_a_command_with_subscribed_services(): void
37+
{
38+
$commandTester = $this->subscribedServiceCommandTester;
39+
40+
$commandTester->execute(
41+
['command' => 'subscribed-service'],
42+
['interactive' => true],
43+
);
44+
45+
$commandTester->assertCommandIsSuccessful();
46+
}
47+
}

0 commit comments

Comments
 (0)