Skip to content

Commit b232a0c

Browse files
committed
MAGE-1218 Add replica detach validation
1 parent 2f766a3 commit b232a0c

File tree

5 files changed

+216
-14
lines changed

5 files changed

+216
-14
lines changed

Api/Product/ReplicaManagerInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function syncReplicasToAlgolia(int $storeId, array $primaryIndexSettings)
3131
/**
3232
* Delete the replica indices on a store index
3333
* @param int $storeId
34-
* @param bool $unused Defaults to false - if true identifies any straggler indices and deletes those, otherwise deletes the replicas it knows aobut
34+
* @param bool $unused Defaults to false - if true identifies any straggler indices and deletes those, otherwise deletes the replicas it knows about
3535
* @return void
3636
*
3737
* @throws LocalizedException

Service/Product/ReplicaManager.php

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
class ReplicaManager implements ReplicaManagerInterface
4242
{
4343
public const ALGOLIA_SETTINGS_KEY_REPLICAS = 'replicas';
44+
public const ALGOLIA_SETTINGS_KEY_PRIMARY = 'primary';
4445

4546
protected const _DEBUG = true;
4647

@@ -260,7 +261,7 @@ protected function setReplicasOnPrimaryIndex(int $storeId): array
260261
$setReplicasTaskId = $this->algoliaHelper->getLastTaskId();
261262
$this->algoliaHelper->waitLastTask($indexName, $setReplicasTaskId);
262263
$this->clearAlgoliaReplicaSettingCache($indexName);
263-
$this->deleteIndices($replicasToDelete);
264+
$this->deleteReplicas($replicasToDelete);
264265

265266
if (self::_DEBUG) {
266267
$this->logger->log(
@@ -354,21 +355,74 @@ protected function getBareIndexNameFromReplicaSetting(string $replicaSetting): s
354355
}
355356

356357
/**
357-
* @param array $replicasToDelete
358-
* @param bool $waitLastTask
358+
* Delete replica indices
359+
*
360+
* @param array $replicasToDelete - which replicas to delete
361+
* @param bool $waitLastTask - wait until deleting next replica (default: false)
362+
* @param bool $prevalidate - verify replica is not attached to a primary index before attempting to delete (default: false)
359363
* @return void
360364
* @throws AlgoliaException
365+
* @throws ExceededRetriesException
361366
*/
362-
protected function deleteIndices(array $replicasToDelete, bool $waitLastTask = false): void
367+
protected function deleteReplicas(array $replicasToDelete, bool $waitLastTask = false, bool $prevalidate = false): void
363368
{
364369
foreach ($replicasToDelete as $deletedReplica) {
365-
$this->algoliaHelper->deleteIndex($deletedReplica);
370+
$this->deleteReplica($deletedReplica, $prevalidate);
366371
if ($waitLastTask) {
367372
$this->algoliaHelper->waitLastTask($deletedReplica);
368373
}
369374
}
370375
}
371376

377+
/**
378+
* @throws AlgoliaException
379+
* @throws ExceededRetriesException
380+
*/
381+
protected function deleteReplica(string $replicaIndexName, bool $precheck = false): void
382+
{
383+
if ($precheck) {
384+
$settings = $this->algoliaHelper->getSettings($replicaIndexName);
385+
if (isset($settings[self::ALGOLIA_SETTINGS_KEY_PRIMARY])) {
386+
$this->detachReplica($settings[self::ALGOLIA_SETTINGS_KEY_PRIMARY], $replicaIndexName);
387+
}
388+
}
389+
390+
$this->algoliaHelper->deleteIndex($replicaIndexName);
391+
}
392+
393+
/**
394+
* Detach a single replica from its primary index
395+
*
396+
* @throws ExceededRetriesException
397+
* @throws AlgoliaException
398+
*/
399+
protected function detachReplica(string $primaryIndexName, string $replicaIndexName): void
400+
{
401+
$settings = $this->algoliaHelper->getSettings($primaryIndexName);
402+
if (!isset($settings[self::ALGOLIA_SETTINGS_KEY_REPLICAS])) {
403+
return;
404+
}
405+
$newReplicas = $this->removeReplicaFromReplicaSetting($settings[self::ALGOLIA_SETTINGS_KEY_REPLICAS], $replicaIndexName);
406+
$this->algoliaHelper->setSettings($primaryIndexName, [ self::ALGOLIA_SETTINGS_KEY_REPLICAS => $newReplicas]);
407+
$this->algoliaHelper->waitLastTask($primaryIndexName);
408+
}
409+
410+
/**
411+
* Remove a single replica from the replica setting array
412+
* (Can feature virtual or standard)
413+
*/
414+
protected function removeReplicaFromReplicaSetting(array $replicaSetting, string $replicaToRemove): array
415+
{
416+
return array_filter(
417+
$replicaSetting,
418+
function ($replicaIndexSetting) use ($replicaToRemove) {
419+
$escaped = preg_quote($replicaToRemove);
420+
$regex = '/^' . $escaped . '|virtual\(' . $escaped . '\)$/';
421+
return !preg_match($regex, $replicaToRemove);
422+
}
423+
);
424+
}
425+
372426
/**
373427
* Apply ranking settings to the added replica indices
374428
* @param int $storeId
@@ -449,7 +503,7 @@ public function deleteReplicasFromAlgolia(int $storeId, bool $unused = false): v
449503
$this->clearReplicasSettingInAlgolia($primaryIndexName);
450504
}
451505

452-
$this->deleteIndices($replicasToDelete);
506+
$this->deleteReplicas($replicasToDelete, true, true);
453507

454508
if ($unused) {
455509
$this->clearUnusedReplicaIndicesCache($storeId);

Test/Integration/Product/ReplicaIndexingTest.php

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@
55
use Algolia\AlgoliaSearch\Api\Product\ReplicaManagerInterface;
66
use Algolia\AlgoliaSearch\Exceptions\AlgoliaException;
77
use Algolia\AlgoliaSearch\Exceptions\ExceededRetriesException;
8+
use Algolia\AlgoliaSearch\Helper\AlgoliaHelper;
89
use Algolia\AlgoliaSearch\Helper\ConfigHelper;
910
use Algolia\AlgoliaSearch\Helper\Entity\ProductHelper;
11+
use Algolia\AlgoliaSearch\Helper\Logger;
1012
use Algolia\AlgoliaSearch\Model\Indexer\Product as ProductIndexer;
1113
use Algolia\AlgoliaSearch\Model\IndicesConfigurator;
14+
use Algolia\AlgoliaSearch\Registry\ReplicaState;
15+
use Algolia\AlgoliaSearch\Service\IndexNameFetcher;
16+
use Algolia\AlgoliaSearch\Service\Product\SortingTransformer;
17+
use Algolia\AlgoliaSearch\Service\StoreNameFetcher;
1218
use Algolia\AlgoliaSearch\Test\Integration\TestCase;
19+
use Algolia\AlgoliaSearch\Validator\VirtualReplicaValidatorFactory;
20+
use Magento\Store\Model\StoreManagerInterface;
1321

1422
class ReplicaIndexingTest extends TestCase
1523
{
@@ -195,23 +203,70 @@ public function testReplicaRebuild(): void
195203
public function testReplicaSync(): void
196204
{
197205
$primaryIndexName = $this->getIndexName('default');
206+
207+
// Make one replica virtual
198208
$this->mockSortUpdate('created_at', 'desc', ['virtualReplica' => 1]);
199209

200-
$sorting = $this->objectManager->get(\Algolia\AlgoliaSearch\Service\Product\SortingTransformer::class)->getSortingIndices(1, null, null, true);
210+
$sorting = $this->populateReplicas(1);
201211

202-
$cmd = $this->objectManager->get(\Algolia\AlgoliaSearch\Console\Command\ReplicaSyncCommand::class);
212+
$currentSettings = $this->algoliaHelper->getSettings($primaryIndexName);
213+
$this->assertArrayHasKey('replicas', $currentSettings);
214+
$replicas = $currentSettings['replicas'];
203215

204-
$this->mockProperty($cmd, 'output', \Symfony\Component\Console\Output\OutputInterface::class);
216+
$this->assertEquals(count($sorting), count($replicas));
217+
$this->assertSortToReplicaConfigParity($primaryIndexName, $sorting, $replicas);
218+
}
205219

206-
$cmd->syncReplicas();
207-
$this->algoliaHelper->waitLastTask();
220+
/**
221+
* @magentoConfigFixture current_store algoliasearch_instant/instant/is_instant_enabled 1
222+
*/
223+
public function testReplicaDelete(): void
224+
{
225+
$primaryIndexName = $this->indexName;
226+
227+
// Make one replica virtual
228+
$this->mockSortUpdate('price', 'asc', ['virtualReplica' => 1]);
229+
230+
$sorting = $this->populateReplicas(1);
208231

209232
$currentSettings = $this->algoliaHelper->getSettings($primaryIndexName);
210233
$this->assertArrayHasKey('replicas', $currentSettings);
211234
$replicas = $currentSettings['replicas'];
212235

213236
$this->assertEquals(count($sorting), count($replicas));
214-
$this->assertSortToReplicaConfigParity($primaryIndexName, $sorting, $replicas);
237+
238+
$this->replicaManager->deleteReplicasFromAlgolia(1);
239+
240+
$newSettings = $this->algoliaHelper->getSettings($primaryIndexName);
241+
$this->assertArrayNotHasKey('replicas', $newSettings);
242+
foreach ($replicas as $replica) {
243+
$this->assertIndexNotExists($this->extractIndexFromReplicaSetting($replica));
244+
}
245+
}
246+
247+
protected function extractIndexFromReplicaSetting(string $setting): string {
248+
return preg_replace('/^virtual\((.*)\)$/', '$1', $setting);
249+
}
250+
251+
/**
252+
* Populate replica indices for test based on store id and return sorting configuration used
253+
*
254+
* @throws AlgoliaException
255+
* @throws ExceededRetriesException
256+
* @throws \ReflectionException
257+
*/
258+
protected function populateReplicas(int $storeId): array
259+
{
260+
$sorting = $this->objectManager->get(\Algolia\AlgoliaSearch\Service\Product\SortingTransformer::class)->getSortingIndices($storeId, null, null, true);
261+
262+
$cmd = $this->objectManager->get(\Algolia\AlgoliaSearch\Console\Command\ReplicaSyncCommand::class);
263+
264+
$this->mockProperty($cmd, 'output', \Symfony\Component\Console\Output\OutputInterface::class);
265+
266+
$cmd->syncReplicas();
267+
$this->algoliaHelper->waitLastTask();
268+
269+
return $sorting;
215270
}
216271

217272
protected function assertSortToReplicaConfigParity(string $primaryIndexName, array $sorting, array $replicas): void
@@ -242,7 +297,14 @@ protected function assertReplicaIndexExists(string $primaryIndexName, string $re
242297
return $replicaSettings;
243298
}
244299

245-
protected function assertReplicaRanking(array $replicaSettings, string $rankingKey, string $sort) {
300+
protected function assertIndexNotExists($indexName): void
301+
{
302+
$indexSettings = $this->algoliaHelper->getSettings($indexName);
303+
$this->assertCount(0, $indexSettings, "Settings found for index that should not exist");
304+
}
305+
306+
protected function assertReplicaRanking(array $replicaSettings, string $rankingKey, string $sort): void
307+
{
246308
$this->assertArrayHasKey($rankingKey, $replicaSettings);
247309
$this->assertEquals($sort, reset($replicaSettings[$rankingKey]));
248310
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Test\Unit\Service;
4+
5+
use Algolia\AlgoliaSearch\Helper\AlgoliaHelper;
6+
use Algolia\AlgoliaSearch\Helper\ConfigHelper;
7+
use Algolia\AlgoliaSearch\Helper\Logger;
8+
use Algolia\AlgoliaSearch\Registry\ReplicaState;
9+
use Algolia\AlgoliaSearch\Service\IndexNameFetcher;
10+
use Algolia\AlgoliaSearch\Service\Product\ReplicaManager;
11+
use Algolia\AlgoliaSearch\Service\Product\SortingTransformer;
12+
use Algolia\AlgoliaSearch\Service\StoreNameFetcher;
13+
use Algolia\AlgoliaSearch\Validator\VirtualReplicaValidatorFactory;
14+
use Magento\Store\Model\StoreManagerInterface;
15+
use PHPUnit\Framework\TestCase;
16+
17+
class ReplicaManagerTest extends TestCase
18+
{
19+
protected ?ReplicaManager $replicaManager;
20+
21+
public function setUp(): void
22+
{
23+
$configHelper = $this->createMock(ConfigHelper::class);
24+
$algoliaHelper = $this->createMock(AlgoliaHelper::class);
25+
$replicaState = $this->createMock(ReplicaState::class);
26+
$virtualReplicaValidatorFactory = $this->createMock(VirtualReplicaValidatorFactory::class);
27+
$indexNameFetcher = $this->createMock(IndexNameFetcher::class);
28+
$storeNameFetcher = $this->createMock(StoreNameFetcher::class);
29+
$sortingTransformer = $this->createMock(SortingTransformer::class);
30+
$storeManager = $this->createMock(StoreManagerInterface::class);
31+
$logger = $this->createMock(Logger::class);
32+
33+
$this->replicaManager = new ReplicaManagerTestable(
34+
$configHelper,
35+
$algoliaHelper,
36+
$replicaState,
37+
$virtualReplicaValidatorFactory,
38+
$indexNameFetcher,
39+
$storeNameFetcher,
40+
$sortingTransformer,
41+
$storeManager,
42+
$logger
43+
);
44+
}
45+
46+
public function testVirtualReplicaSettingRemove(): void
47+
{
48+
$replicaSetting = [
49+
'virtual(replica1)',
50+
'virtual(replica2)',
51+
'virtual(replica3)'
52+
];
53+
$replicaToRemove = 'replica2';
54+
55+
$newReplicas = $this->replicaManager->removeReplicaFromReplicaSetting($replicaSetting, $replicaToRemove);
56+
57+
$this->assertNotContains($replicaToRemove, $newReplicas);
58+
}
59+
60+
public function testStandardReplicaSettingRemove(): void
61+
{
62+
$replicaSetting = [
63+
'replica1',
64+
'replica2',
65+
'replica3'
66+
];
67+
$replicaToRemove = 'replica2';
68+
69+
$newReplicas = $this->replicaManager->removeReplicaFromReplicaSetting($replicaSetting, $replicaToRemove);
70+
71+
$this->assertNotContains($replicaToRemove, $newReplicas);
72+
}
73+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Test\Unit\Service;
4+
5+
use Algolia\AlgoliaSearch\Service\Product\ReplicaManager;
6+
7+
class ReplicaManagerTestable extends ReplicaManager
8+
{
9+
public function removeReplicaFromReplicaSetting(...$params): array
10+
{
11+
return parent::removeReplicaFromReplicaSetting(...$params);
12+
}
13+
}

0 commit comments

Comments
 (0)