Skip to content

Commit 3ae55bc

Browse files
committed
ACPT-1666: Fix race conditions in _resetState methods
1 parent 5100c9b commit 3ae55bc

File tree

5 files changed

+374
-0
lines changed

5 files changed

+374
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\ObjectManager\Resetter;
9+
10+
use Magento\Framework\App\ObjectManager;
11+
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
12+
use Magento\Framework\ObjectManagerInterface;
13+
use WeakMap;
14+
use WeakReference;
15+
16+
/**
17+
* Class that keeps track of the instances that need to be reset, and resets them
18+
*/
19+
class Resetter implements ResetterInterface
20+
{
21+
/** @var WeakMap instances to be reset after request */
22+
private WeakMap $resetAfterWeakMap;
23+
24+
/**
25+
* @var array
26+
*
27+
*/
28+
private array $classList = [
29+
//phpcs:disable Magento2.PHP.LiteralNamespaces
30+
'Magento\Framework\GraphQl\Query\Fields' => true,
31+
'Magento\Store\Model\Store' => [
32+
"_baseUrlCache" => [],
33+
"_configCache" => null,
34+
"_configCacheBaseNodes" => [],
35+
"_dirCache" => [],
36+
"_urlCache" => []
37+
]
38+
];
39+
40+
/**
41+
* @var array
42+
*/
43+
private array $reflectionCache = [];
44+
45+
/**
46+
* Constructor
47+
*
48+
* @return void
49+
* @phpcs:disable Magento2.Functions.DiscouragedFunction
50+
*/
51+
public function __construct()
52+
{
53+
if (\file_exists(BP . '/app/etc/reset.php')) {
54+
// phpcs:ignore Magento2.Security.IncludeFile.FoundIncludeFile
55+
$this->classList = array_replace($this->classList, (require BP . '/app/etc/reset.php'));
56+
}
57+
$this->resetAfterWeakMap = new WeakMap;
58+
}
59+
60+
/**
61+
* Add instance to be reset later
62+
*
63+
* @param object $instance
64+
* @return void
65+
*/
66+
public function addInstance(object $instance) : void
67+
{
68+
if ($instance instanceof ResetAfterRequestInterface
69+
|| isset($this->classList[\get_class($instance)])
70+
) {
71+
$this->resetAfterWeakMap[$instance] = true;
72+
}
73+
}
74+
75+
/**
76+
* Reset state for all instances that we've created
77+
*
78+
* @return void
79+
* @throws \ReflectionException
80+
*/
81+
public function _resetState(): void
82+
{
83+
/* Note: We force garbage collection to clean up cyclic referenced objects before _resetState()
84+
* This is to prevent calling _resetState() on objects that will be destroyed by garbage collector. */
85+
gc_collect_cycles();
86+
$weakMapSorter = ObjectManager::getInstance()
87+
->create(WeakMapSorter::class, ['weakmap' => $this->resetAfterWeakMap]);
88+
$weakMapSorter->_resetState();
89+
$temporaryWeakReferenceList = [];
90+
foreach ($this->resetAfterWeakMap as $weakMapObject => $value) {
91+
$temporaryWeakReferenceList[] = WeakReference::create($weakMapObject);
92+
unset($weakMapObject);
93+
unset($value);
94+
}
95+
foreach ($temporaryWeakReferenceList as $weakReference) {
96+
$instance = $weakReference->get();
97+
if (!$instance) {
98+
continue;
99+
}
100+
if (!$instance instanceof ResetAfterRequestInterface) {
101+
$this->resetStateWithReflection($instance);
102+
}
103+
}
104+
unset($temporaryWeakReferenceList);
105+
}
106+
107+
/**
108+
* @inheritDoc
109+
* @phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedFunction
110+
*/
111+
public function setObjectManager(ObjectManagerInterface $objectManager) : void
112+
{
113+
}
114+
115+
/**
116+
* Gets weak map
117+
*
118+
* @return WeakMap
119+
*/
120+
public function getWeakMap() : WeakMap
121+
{
122+
throw new \RuntimeException("Not implemented\n");
123+
}
124+
125+
/**
126+
* State reset without reflection
127+
*
128+
* @param object $instance
129+
* @return void
130+
* @throws \ReflectionException
131+
*/
132+
private function resetStateWithReflection(object $instance)
133+
{
134+
$className = \get_class($instance);
135+
136+
$reflectionClass = $this->reflectionCache[$className]
137+
?? $this->reflectionCache[$className] = new \ReflectionClass($className);
138+
139+
foreach ($reflectionClass->getProperties() as $property) {
140+
$type = $property->getType()?->getName();
141+
if (empty($type) && preg_match('/@var\s+([^\s]+)/', $property->getDocComment(), $matches)) {
142+
$type = $matches[1];
143+
if (\str_contains($type, '[]')) {
144+
$type = 'array';
145+
}
146+
}
147+
148+
$name = $property->getName();
149+
if (!in_array($type, ['bool', 'array', 'null', 'true', 'false'], true)) {
150+
continue;
151+
}
152+
$value = $this->classList[$className][$name] ??
153+
match ($type) {
154+
'bool' => false,
155+
'true' => true,
156+
'false' => false,
157+
'array' => [],
158+
'null' => null,
159+
};
160+
$property->setAccessible(true);
161+
$property->setValue($instance, $value);
162+
$property->setAccessible(false);
163+
}
164+
}
165+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\ObjectManager\Resetter;
9+
10+
/**
11+
* Factory that creates Resetter based on environment variable.
12+
*/
13+
class ResetterFactory
14+
{
15+
/**
16+
* @var string
17+
*/
18+
private static string $resetterClassName = Resetter::class;
19+
20+
/**
21+
* Create resseter factory
22+
*
23+
* @return ResetterInterface
24+
* @phpcs:disable Magento2.Functions.StaticFunction
25+
*/
26+
public static function create() : ResetterInterface
27+
{
28+
return new static::$resetterClassName;
29+
}
30+
31+
/**
32+
* Sets resetter class name
33+
*
34+
* @param string $resetterClassName
35+
* @return void
36+
* @phpcs:disable Magento2.Functions.StaticFunction
37+
*/
38+
public static function setResetterClassName($resetterClassName) : void
39+
{
40+
static::$resetterClassName = $resetterClassName;
41+
}
42+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\ObjectManager\Resetter;
9+
10+
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
11+
use Magento\Framework\ObjectManagerInterface;
12+
use WeakMap;
13+
14+
/**
15+
* Interface that keeps track of the instances that need to be reset, and resets them
16+
*/
17+
interface ResetterInterface extends ResetAfterRequestInterface
18+
{
19+
/**
20+
* Adds instance
21+
*
22+
* @param object $instance
23+
* @return void
24+
*/
25+
public function addInstance(object $instance) : void;
26+
27+
/**
28+
* Sets object manager
29+
*
30+
* @param ObjectManagerInterface $objectManager
31+
* @return void
32+
*/
33+
public function setObjectManager(ObjectManagerInterface $objectManager) : void;
34+
35+
/**
36+
* Gets weak map
37+
*
38+
* @return WeakMap
39+
*/
40+
public function getWeakMap() : WeakMap;
41+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\ObjectManager\Resetter;
9+
10+
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
11+
use WeakReference;
12+
13+
/**
14+
* Data class used for hold reference and sort value
15+
*/
16+
class SortableReferenceObject implements ResetAfterRequestInterface
17+
{
18+
/**
19+
* @param WeakReference $reference
20+
* @param int $sort
21+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
22+
* @phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedFunction
23+
*/
24+
public function __construct(readonly WeakReference $reference, readonly int $sort)
25+
{
26+
}
27+
28+
/**
29+
* Gets sorted object
30+
*
31+
* @return int
32+
*/
33+
public function getSort() : int
34+
{
35+
return $this->sort;
36+
}
37+
38+
/**
39+
* State reset
40+
*
41+
* @return void
42+
*/
43+
public function _resetState(): void
44+
{
45+
$object = $this->reference->get();
46+
if (!$object || !($object instanceof ResetAfterRequestInterface)) {
47+
return;
48+
}
49+
$object->_resetState();
50+
}
51+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\ObjectManager\Resetter;
9+
10+
use Magento\Framework\App\ObjectManager;
11+
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
12+
use WeakReference;
13+
use WeakMap;
14+
15+
/**
16+
* Sorts a WeakMap into an ordered array of WeakReference and reset them in order.
17+
*/
18+
class WeakMapSorter implements ResetAfterRequestInterface
19+
{
20+
21+
private const DEFAULT_SORT_VALUE = 5000;
22+
23+
private const MAX_SORT_VALUE = 9000;
24+
25+
/**
26+
* @var SortableReferenceObject[]
27+
*/
28+
private array $sortableReferenceList = [];
29+
30+
/**
31+
* Constructor
32+
*
33+
* @param WeakMap $weakmap
34+
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
35+
*/
36+
public function __construct (WeakMap $weakmap, array $resettableArgs)
37+
{
38+
foreach ($weakmap as $weakMapObject => $value) {
39+
if (!$weakMapObject || !($weakMapObject instanceof ResetAfterRequestInterface)) {
40+
continue;
41+
}
42+
$sortValue = $this->getSortValueOfObject($weakMapObject, $resettableArgs);
43+
$weakReference = WeakReference::create($weakMapObject);
44+
$this->sortableReferenceList[] = new SortableReferenceObject($weakReference, $sortValue);
45+
}
46+
usort(
47+
$this->sortableReferenceList,
48+
fn(SortableReferenceObject $a, SortableReferenceObject $b) => $a->getSort() - $b->getSort()
49+
);
50+
}
51+
52+
/**
53+
* @param object $object
54+
* @return int
55+
*/
56+
public function getSortValueOfObject(object $object, array $resettableArgs) : int
57+
{
58+
if (!in_array($object, $resettableArgs)) {
59+
return static::DEFAULT_SORT_VALUE;
60+
}
61+
$args = ObjectManager::getInstance()->get(\Magento\Framework\ObjectManager\Config\Config::class)->getArguments(get_class($object));
62+
63+
return self::MAX_SORT_VALUE;
64+
}
65+
66+
/**
67+
* @inheritDoc
68+
*/
69+
public function _resetState(): void
70+
{
71+
foreach ($this->sortableReferenceList as $sortableReferenceObject) {
72+
$sortableReferenceObject->_resetState();
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)