Skip to content

Commit 28f7db5

Browse files
feat: Default to "none singletons preloader"
Not loading dependencies with expiring connections is hard, so default to not loading any singletons at all in this package.
1 parent fcce7dc commit 28f7db5

5 files changed

Lines changed: 216 additions & 32 deletions

File tree

Classes/Package.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Netlogix\JobQueue\FastRabbit;
6+
7+
use Neos\Flow\Core\Bootstrap;
8+
use Neos\Flow\Monitor\FileMonitor;
9+
use Neos\Flow\ObjectManagement\CompileTimeObjectManager;
10+
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
11+
use Neos\Flow\ObjectManagement\Proxy\Compiler;
12+
use Neos\Flow\Package\Package as BasePackage;
13+
use Neos\Flow\SignalSlot\Dispatcher;
14+
use Netlogix\JobQueue\FastRabbit\SingletonPreloading\AllSingletonsPreloader;
15+
16+
final class Package extends BasePackage
17+
{
18+
public function boot(Bootstrap $bootstrap): void
19+
{
20+
$dispatcher = $bootstrap->getSignalSlotDispatcher();
21+
assert($dispatcher instanceof Dispatcher);
22+
23+
/**
24+
* @see Compiler::compiledClasses()
25+
* @see FileMonitor::emitFilesHaveChanged()
26+
* @see AllSingletonsPreloader::flush()
27+
*/
28+
$dispatcher->connect(
29+
signalClassName: FileMonitor::class,
30+
signalName: 'filesHaveChanged',
31+
slotClassNameOrObject: fn () => static::flushSingletonsPreloaderCache($bootstrap->getObjectManager())
32+
);
33+
}
34+
35+
private function flushSingletonsPreloaderCache(ObjectManagerInterface $objectManager): void
36+
{
37+
if ($objectManager instanceof CompileTimeObjectManager) {
38+
return;
39+
}
40+
$objectManager
41+
->get(AllSingletonsPreloader::CACHE)
42+
->flush();
43+
}
44+
}

Classes/SingletonPreloading/AllSingletonsPreloader.php

Lines changed: 104 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22

33
namespace Netlogix\JobQueue\FastRabbit\SingletonPreloading;
44

5-
use Neos\Flow\Core\Bootstrap;
5+
use Doctrine\DBAL\Connection;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Neos\Cache\Frontend\VariableFrontend;
68
use Neos\Flow\ObjectManagement\Configuration\Configuration;
79
use Neos\Flow\ObjectManagement\ObjectManager;
810
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
911
use Neos\Flow\Reflection\ReflectionService;
1012
use Neos\Flow\Annotations as Flow;
1113
use Throwable;
12-
1314
use Traversable;
1415

15-
use function array_filter;
16+
use function class_exists;
1617
use function is_a;
1718

1819
/**
@@ -29,54 +30,125 @@
2930
*/
3031
class AllSingletonsPreloader implements SingletonsPreloader
3132
{
33+
public const string CACHE = 'Netlogix.JobQueue.FakeQueue:SingletonPreloaderCache';
34+
3235
#[Flow\InjectConfiguration(path: 'AllSingletonsPreloader.ignoreClassNames', package: 'Netlogix.JobQueue.FastRabbit')]
3336
protected array $ignoreClassNames = [];
3437

35-
public function __construct(
36-
protected readonly ObjectManager $objectManager,
37-
protected readonly ReflectionService $reflectionService
38-
) {
39-
}
38+
#[Flow\Inject(name: AllSingletonsPreloader::CACHE, lazy: false)]
39+
protected VariableFrontend $cache;
40+
41+
#[Flow\Inject(lazy: false)]
42+
protected ObjectManager $objectManager;
43+
44+
#[Flow\Inject(lazy: false)]
45+
protected ReflectionService $reflectionService;
4046

4147
public function collect(): void
4248
{
43-
$objectManager = Bootstrap::$staticObjectManager;
44-
foreach ($this->getSingletonClassNames($objectManager) as $className) {
45-
try {
46-
$objectManager->get($className);
47-
} catch (Throwable $e) {
48-
// ignore
49-
}
49+
foreach ($this->getClassList() as $className => $buildInstance) {
50+
$this->preload(className: $className, buildInstance: $buildInstance);
5051
}
52+
$this->pauseExpiringObjects();
5153
}
5254

5355
/**
54-
* @return Traversable<string>
56+
* @return array<string, bool>
5557
*/
56-
public function getSingletonClassNames(ObjectManagerInterface $objectManager): Traversable
58+
protected function getClassList(): array
5759
{
58-
foreach (self::getSingletonClassNamesFromReflection($objectManager) as $className) {
59-
foreach ($this->ignoreClassNames as $ignoredClassName) {
60-
if (is_a($className, $ignoredClassName, true)) {
61-
continue;
62-
}
60+
if ($this->cache->has('classList')) {
61+
return $this->cache->get('classList');
62+
} else {
63+
$list = [... $this->buildClassList()];
64+
$this->cache->set('classList', $list);
65+
return $list;
66+
}
67+
}
68+
69+
protected function preload(string $className, bool $buildInstance): void
70+
{
71+
try {
72+
$buildInstance
73+
? $this->objectManager->get($className)
74+
: class_exists(class: $className, autoload: true);
75+
} catch (Throwable) {
76+
// ignore
77+
}
78+
}
79+
80+
protected function pauseExpiringObjects()
81+
{
82+
if ($this->objectManager->has(EntityManagerInterface::class)) {
83+
$this->objectManager
84+
->get(EntityManagerInterface::class)
85+
->getConnection()
86+
->close();
87+
}
88+
if ($this->objectManager->has(Connection::class)) {
89+
$this->objectManager
90+
->get(Connection::class)
91+
->close();
92+
}
93+
// TODO: There are other objects that might expire, for example ´
94+
}
95+
96+
/**
97+
* @return Traversable<string, bool>
98+
*/
99+
protected function buildClassList(): Traversable
100+
{
101+
foreach (self::getSingletonClassNamesFromReflection($this->objectManager) as $className => $buildInstance) {
102+
yield $className => $buildInstance && !$this->ignoreClassName($className);
103+
}
104+
}
105+
106+
protected function ignoreClassName(string $className): bool
107+
{
108+
foreach ($this->ignoreClassNames as $ignoredClassName) {
109+
if (is_a($className, $ignoredClassName, true)) {
110+
return true;
63111
}
64-
yield $className;
65112
}
113+
return false;
66114
}
67115

116+
/**
117+
* @return array<string, bool>
118+
*/
68119
#[Flow\CompileStatic]
69-
public static function getSingletonClassNamesFromReflection(ObjectManagerInterface $objectManager): array
120+
final public static function getSingletonClassNamesFromReflection(ObjectManagerInterface $objectManager): array
70121
{
71-
return array_filter(
72-
array: $objectManager->get(ReflectionService::class)->getAllClassNames(),
73-
callback: static function ($className) use ($objectManager): bool {
74-
try {
75-
return $objectManager->getScope($className) === Configuration::SCOPE_SINGLETON;
76-
} catch (\Exception $e) {
77-
return false;
122+
$reflection = $objectManager->get(ReflectionService::class);
123+
assert($reflection instanceof ReflectionService);
124+
$classNames = [];
125+
foreach ($reflection->getAllClassNames() as $className) {
126+
try {
127+
if ($objectManager->getScope($className) !== Configuration::SCOPE_SINGLETON) {
128+
/**
129+
* Only preload singletons
130+
*/
131+
$classNames[$className] = false;
132+
continue;
78133
}
134+
} catch (\Exception $e) {
135+
$classNames[$className] = false;
136+
continue;
79137
}
80-
);
138+
139+
$constructParameters = $reflection->getMethodParameters($className, '__construct');
140+
if (count($constructParameters)) {
141+
/**
142+
* Skip preloading for classes with constructor arguments because they are
143+
* likely to depend on stateful objects that, in one way or other, expire,
144+
* like database connections.
145+
*/
146+
$classNames[$className] = false;
147+
} else {
148+
$classNames[$className] = true;
149+
}
150+
}
151+
152+
return $classNames;
81153
}
82154
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Netlogix\JobQueue\FastRabbit\SingletonPreloading;
4+
5+
use Doctrine\DBAL\Connection;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Neos\Cache\Frontend\VariableFrontend;
8+
use Neos\Flow\ObjectManagement\Configuration\Configuration;
9+
use Neos\Flow\ObjectManagement\ObjectManager;
10+
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
11+
use Neos\Flow\Reflection\ReflectionService;
12+
use Neos\Flow\Annotations as Flow;
13+
use Throwable;
14+
use Traversable;
15+
16+
use function class_exists;
17+
use function is_a;
18+
19+
/**
20+
* Fetch all known singleton classes from the ObjectManager to have
21+
* all of them in the memory and ready to use.
22+
*
23+
* This should not be used "as is" because there's a high risk of
24+
* creating singletons with expiring connections, like, for example,
25+
* database connections.
26+
*
27+
* It's not enough to just exclude those database connections here
28+
* because there might be other singletons depending on those connections
29+
* through constructor injection, which triggers loading them anyway.
30+
*/
31+
final class NoneSingletonsPreloader implements SingletonsPreloader
32+
{
33+
public function collect(): void
34+
{
35+
}
36+
37+
}

Configuration/Caches.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Netlogix_JobQueue_FastRabbit_SingletonPreloaderCache:
2+
frontend: Neos\Cache\Frontend\VariableFrontend
3+
backend: Neos\Cache\Backend\SimpleFileBackend

Configuration/Objects.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'Netlogix\JobQueue\FastRabbit\SingletonPreloading\SingletonsPreloader':
2+
3+
# Don't create any singleton instances by default. This cautious setting completely
4+
# avoids creating "expiring" singleton instances, like doctrine DBAL connections,
5+
# during the preloading phase.
6+
7+
className: Netlogix\JobQueue\FastRabbit\SingletonPreloading\NoneSingletonsPreloader
8+
9+
10+
# Create "all" singletons. Those with constructor argument injection are excluded
11+
# to avoid the before-mentioned Doctrine DBAL, which comes through the EntityManager.
12+
#
13+
# As soon as injected properties are "lazy: false", which needs to be the case
14+
# for properly typed ones and which usually comes when switching from doc comments
15+
# to PHP 8 attributes, the loader can't easily decide which singleton depends on
16+
# an "expiring" object, so the "all" preloader might silently load doctrine DBAL.
17+
#
18+
# className: Netlogix\JobQueue\FastRabbit\SingletonPreloading\AllSingletonsPreloader
19+
20+
21+
'Netlogix.JobQueue.FakeQueue:SingletonPreloaderCache':
22+
className: Neos\Cache\Frontend\VariableFrontend
23+
scope: singleton
24+
factoryObjectName: Neos\Flow\Cache\CacheManager
25+
factoryMethodName: getCache
26+
arguments:
27+
1:
28+
value: Netlogix_JobQueue_FastRabbit_SingletonPreloaderCache

0 commit comments

Comments
 (0)