Skip to content

Commit 948e6cb

Browse files
committed
MAGE-1218 Add mock scenario for repeated patch application attempts
1 parent a43fda7 commit 948e6cb

File tree

2 files changed

+120
-28
lines changed

2 files changed

+120
-28
lines changed

Setup/Patch/Data/RebuildReplicasPatch.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
namespace Algolia\AlgoliaSearch\Setup\Patch\Data;
44

5+
use Algolia\AlgoliaSearch\Exceptions\AlgoliaException;
6+
use Algolia\AlgoliaSearch\Exceptions\ExceededRetriesException;
57
use Algolia\AlgoliaSearch\Helper\ConfigHelper;
68
use Algolia\AlgoliaSearch\Helper\Entity\ProductHelper;
79
use Algolia\AlgoliaSearch\Registry\ReplicaState;
810
use Algolia\AlgoliaSearch\Service\Product\ReplicaManager;
911
use Magento\Framework\App\Area;
1012
use Magento\Framework\App\State as AppState;
1113
use Magento\Framework\Exception\LocalizedException;
14+
use Magento\Framework\Exception\NoSuchEntityException;
1215
use Magento\Framework\Setup\ModuleDataSetupInterface;
1316
use Magento\Framework\Setup\Patch\DataPatchInterface;
1417
use Magento\Framework\Setup\Patch\PatchInterface;
@@ -66,17 +69,37 @@ public function apply(): PatchInterface
6669

6770
$storeIds = array_keys($this->storeManager->getStores());
6871
// Delete all replicas before resyncing in case of incorrect replica assignments
69-
foreach ($storeIds as $storeId) {
70-
$this->replicaManager->deleteReplicasFromAlgolia($storeId);
71-
}
7272

73-
foreach ($storeIds as $storeId) {
74-
$this->replicaState->setChangeState(ReplicaState::REPLICA_STATE_CHANGED, $storeId); // avoids latency
75-
$this->replicaManager->syncReplicasToAlgolia($storeId, $this->productHelper->getIndexSettings($storeId));
73+
try {
74+
foreach ($storeIds as $storeId) {
75+
$this->retryDeleteReplica($storeId);
76+
}
77+
78+
foreach ($storeIds as $storeId) {
79+
$this->replicaState->setChangeState(ReplicaState::REPLICA_STATE_CHANGED, $storeId); // avoids latency
80+
$this->replicaManager->syncReplicasToAlgolia($storeId, $this->productHelper->getIndexSettings($storeId));
81+
}
82+
}
83+
catch (AlgoliaException $e) {
84+
$this->logger->error("Could not rebuild replicas - a full reindex will be required.");
7685
}
7786

7887
$this->moduleDataSetup->getConnection()->endSetup();
7988

8089
return $this;
8190
}
91+
92+
protected function retryDeleteReplica(int $storeId, int $maxRetries = 3, int $interval = 5)
93+
{
94+
for ($tries = $maxRetries - 1; $tries >= 0; $tries--) {
95+
try {
96+
$this->replicaManager->deleteReplicasFromAlgolia($storeId);
97+
return;
98+
} catch (AlgoliaException $e) {
99+
$this->logger->warning(__("Unable to delete replicas, %1 tries remaining: %2", $tries, $e->getMessage()));
100+
sleep($interval);
101+
}
102+
}
103+
throw new ExceededRetriesException('Unable to delete old replica indices after $maxRetries retries.');
104+
}
82105
}

Test/Integration/Product/ReplicaIndexingTest.php

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
use Algolia\AlgoliaSearch\Model\IndicesConfigurator;
1414
use Algolia\AlgoliaSearch\Registry\ReplicaState;
1515
use Algolia\AlgoliaSearch\Service\IndexNameFetcher;
16+
use Algolia\AlgoliaSearch\Service\Product\ReplicaManager;
1617
use Algolia\AlgoliaSearch\Service\Product\SortingTransformer;
1718
use Algolia\AlgoliaSearch\Service\StoreNameFetcher;
1819
use Algolia\AlgoliaSearch\Test\Integration\TestCase;
1920
use Algolia\AlgoliaSearch\Validator\VirtualReplicaValidatorFactory;
21+
use Magento\Framework\App\State as AppState;
22+
use Magento\Framework\Setup\ModuleDataSetupInterface;
2023
use Magento\Store\Model\StoreManagerInterface;
24+
use PHPUnit\Framework\MockObject\MockObject;
25+
use Psr\Log\LoggerInterface;
2126

2227
class ReplicaIndexingTest extends TestCase
2328
{
@@ -28,6 +33,8 @@ class ReplicaIndexingTest extends TestCase
2833

2934
protected ?string $indexName = null;
3035

36+
protected ?int $patchRetries = 3;
37+
3138
protected function setUp(): void
3239
{
3340
parent::setUp();
@@ -245,7 +252,7 @@ public function testReplicaDelete(): void
245252
}
246253

247254
/**
248-
* Test
255+
* Test failure to clear index replica setting
249256
* @magentoConfigFixture current_store algoliasearch_instant/instant/is_instant_enabled 1
250257
*/
251258
public function testReplicaDeleteUnreliable(): void
@@ -260,7 +267,7 @@ public function testReplicaDeleteUnreliable(): void
260267

261268
$this->assertEquals(count($sorting), count($replicas));
262269

263-
$this->getCrippledReplicaManager()->deleteReplicasFromAlgolia(1);
270+
$this->getMustPrevalidateMockReplicaManager()->deleteReplicasFromAlgolia(1);
264271

265272
$newSettings = $this->algoliaHelper->getSettings($primaryIndexName);
266273
$this->assertArrayNotHasKey('replicas', $newSettings);
@@ -269,6 +276,33 @@ public function testReplicaDeleteUnreliable(): void
269276
}
270277
}
271278

279+
/**
280+
* @magentoConfigFixture current_store algoliasearch_instant/instant/is_instant_enabled 1
281+
*/
282+
public function testReplicaRebuildPatch(): void
283+
{
284+
$primaryIndexName = $this->indexName;
285+
$sorting = $this->populateReplicas(1);
286+
$currentSettings = $this->algoliaHelper->getSettings($primaryIndexName);
287+
$this->assertArrayHasKey('replicas', $currentSettings);
288+
$replicas = $currentSettings['replicas'];
289+
290+
$this->assertTrue($this->configHelper->credentialsAreConfigured());
291+
292+
$patch = new \Algolia\AlgoliaSearch\Setup\Patch\Data\RebuildReplicasPatch(
293+
$this->objectManager->get(ModuleDataSetupInterface::class),
294+
$this->objectManager->get(StoreManagerInterface::class),
295+
$this->getTroublesomePatchReplicaManager(),
296+
$this->objectManager->get(ProductHelper::class),
297+
$this->objectManager->get(AppState::class),
298+
$this->objectManager->get(ReplicaState::class),
299+
$this->configHelper,
300+
$this->objectManager->get(LoggerInterface::class)
301+
);
302+
303+
$patch->apply();
304+
}
305+
272306
protected function extractIndexFromReplicaSetting(string $setting): string {
273307
return preg_replace('/^virtual\((.*)\)$/', '$1', $setting);
274308
}
@@ -279,14 +313,56 @@ protected function extractIndexFromReplicaSetting(string $setting): string {
279313
* This aims to reproduce this potential scenario by not disassociating the replica
280314
*
281315
*/
282-
protected function getCrippledReplicaManager(): ReplicaManagerInterface
316+
protected function getMustPrevalidateMockReplicaManager(): ReplicaManagerInterface
283317
{
284-
$mockedClass = \Algolia\AlgoliaSearch\Service\Product\ReplicaManager::class;
285318
$mockedMethod = 'clearReplicasSettingInAlgolia';
319+
320+
$mock = $this->getMockReplicaManager([
321+
$mockedMethod => function(...$params) {
322+
//DO NOTHING
323+
return;
324+
}
325+
]);
326+
$mock->expects($this->once())->method($mockedMethod);
327+
return $mock;
328+
}
329+
330+
/**
331+
* This mock is to recreate the scenario where a patch tries to apply up to 3 times but the replicas
332+
* are never detached which throws a replica delete error until the last attempt which should succeed
333+
*/
334+
protected function getTroublesomePatchReplicaManager(): ReplicaManager
335+
{
336+
$mock = $this->getMockReplicaManager([
337+
'clearReplicasSettingInAlgolia' => null,
338+
'deleteReplicas' => null
339+
]);
340+
$mock
341+
->expects($this->exactly($this->patchRetries))
342+
->method('clearReplicasSettingInAlgolia')
343+
->willReturnCallback(function(...$params) use ($mock) {
344+
if (--$this->patchRetries) return;
345+
$originalMethod = new \ReflectionMethod(ReplicaManager::class, 'clearReplicasSettingInAlgolia');
346+
$originalMethod->invoke($mock, ...$params);
347+
});
348+
$mock
349+
->expects($this->any())
350+
->method('deleteReplicas')
351+
->willReturnCallback(function(array $replicasToDelete, ...$params) use ($mock) {
352+
$originalMethod = new \ReflectionMethod(ReplicaManager::class, 'deleteReplicas');
353+
$originalMethod->invoke($mock, $replicasToDelete, false, false);
354+
});
355+
356+
return $mock;
357+
}
358+
359+
protected function getMockReplicaManager($mockedMethods = array()): MockObject & ReplicaManager
360+
{
361+
$mockedClass = ReplicaManager::class;
286362
$mockedReplicaManager = $this->getMockBuilder($mockedClass)
287363
->setConstructorArgs([
288-
$this->objectManager->get(ConfigHelper::class),
289-
$this->objectManager->get(AlgoliaHelper::class),
364+
$this->configHelper,
365+
$this->algoliaHelper,
290366
$this->objectManager->get(ReplicaState::class),
291367
$this->objectManager->get(VirtualReplicaValidatorFactory::class),
292368
$this->objectManager->get(IndexNameFetcher::class),
@@ -295,23 +371,16 @@ protected function getCrippledReplicaManager(): ReplicaManagerInterface
295371
$this->objectManager->get(StoreManagerInterface::class),
296372
$this->objectManager->get(Logger::class)
297373
])
298-
->onlyMethods([$mockedMethod])
374+
->onlyMethods(array_keys($mockedMethods))
299375
->getMock();
300-
$mockedReplicaManager
301-
->expects($this->once())
302-
->method($mockedMethod)
303-
->willReturnCallback(
304-
function (...$params)
305-
use ($mockedClass, $mockedMethod, $mockedReplicaManager)
306-
{
307-
// DO NOTHING
308-
return;
309-
310-
// If aiming to test a throttle on retry invoke after a specified number of failures
311-
//$originalMethod = new \ReflectionMethod($mockedClass, $mockedMethod);
312-
//return $originalMethod->invoke($mockedReplicaManager, ...$params);
313-
}
314-
);
376+
377+
foreach ($mockedMethods as $method => $callback) {
378+
if (!$callback) continue;
379+
$mockedReplicaManager
380+
->method($method)
381+
->willReturnCallback($callback);
382+
}
383+
315384
return $mockedReplicaManager;
316385
}
317386

0 commit comments

Comments
 (0)