Skip to content

Commit 2103e94

Browse files
committed
Merge pull request #3 from zumba/alias-strategy
Add an alias strategy for primary index.
2 parents 684c1d9 + cfa3932 commit 2103e94

8 files changed

+428
-88
lines changed

README.md

+34-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ $client->search([
4646
<?php
4747

4848
$client = new \Elasticsearch\Client();
49-
$indexRotator = new IndexRotator($client, 'pizza_shops');
49+
$indexRotator = new \Zumba\ElasticsearchRotator\IndexRotator($client, 'pizza_shops');
5050
// Build your index here
5151
$newlyBuiltIndexName = 'my_new_built_index_name';
5252
$indexRotator->copyPrimaryIndexToSecondary();
@@ -97,3 +97,36 @@ class MySearchIndex {
9797

9898
}
9999
```
100+
101+
## Using Strategies
102+
103+
You can now customize the strategy of getting/setting the primary index. By default, the `ConfigurationStrategy` is employed,
104+
however we have also included an `AliasStrategy`. The main difference is when `setPrimaryIndex` is called, instead of creating an entry
105+
in the configuration index, it adds an alias (specified by `alias_name` option) on the specified index and deletes all other aliases
106+
for the old primary indices (specified by `index_pattern`).
107+
108+
#### Using the `AliasStrategy`
109+
110+
```php
111+
<?php
112+
113+
$client = new \Elasticsearch\Client();
114+
$indexRotator = new \Zumba\ElasticsearchRotator\IndexRotator($client, 'pizza_shops');
115+
$aliasStrategy = $indexRotator->strategyFactory(IndexRotator::STRATEGY_ALIAS, [
116+
'alias_name' => 'pizza_shops',
117+
'index_pattern' => 'pizza_shops_*'
118+
]);
119+
// Build your index here
120+
$newlyBuiltIndexName = 'pizza_shops_1234102874';
121+
$indexRotator->copyPrimaryIndexToSecondary();
122+
$indexRotator->setPrimaryIndex($newlyBuiltIndexName);
123+
124+
// Now that the alias is set, you can search on that alias instead of having to call `getPrimaryIndex`.
125+
$client->search([
126+
'index' => 'pizza_shops',
127+
'type' => 'shop',
128+
'body' => [] //...
129+
])
130+
```
131+
132+
Since the alias (`pizza_shops`) is mapped to the primary index (`pizza_shops_1234102874`), you can use the alias directly in your client application rather than having to call `getPrimaryIndex()` on the `IndexRotator`. That being said, calling `getPrimaryIndex` won't return the alias, but rather the index that it is aliasing. The secondary entries in the configuration index are still used and reference the actual index names, since the alias can be updated at any time and there wouldn't be a reference to remove the old one.

phpunit.xml

+5
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@
1414
<directory suffix="Test.php">./tests</directory>
1515
</testsuite>
1616
</testsuites>
17+
<filter>
18+
<whitelist addUncoveredFilesFromWhitelist="true">
19+
<directory>./src</directory>
20+
</whitelist>
21+
</filter>
1722
</phpunit>

src/Common/PrimaryIndexStrategy.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Zumba\ElasticsearchRotator\Common;
4+
5+
use Elasticsearch\Client;
6+
use Psr\Log\LoggerInterface;
7+
8+
interface PrimaryIndexStrategy
9+
{
10+
/**
11+
* Constructor.
12+
*
13+
* @param \Elasticsearch\Client $engine
14+
* @param \Psr\Log\LoggerInterface $logger
15+
* @param array $options Options specific to this strategy
16+
*/
17+
public function __construct(Client $engine, LoggerInterface $logger, array $options = []);
18+
19+
/**
20+
* Get the primary index name for this configuration.
21+
*
22+
* @return string
23+
* @throws \ElasticsearchRotator\Exceptions\MissingPrimaryException
24+
*/
25+
public function getPrimaryIndex();
26+
27+
/**
28+
* Get the primary index name for this configuration.
29+
*
30+
* @return string
31+
* @throws \ElasticsearchRotator\Exceptions\MissingPrimaryException
32+
*/
33+
public function setPrimaryIndex($name);
34+
}

src/ConfigurationIndex.php

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace Zumba\ElasticsearchRotator;
4+
5+
use Elasticsearch\Client;
6+
use Psr\Log\LoggerInterface;
7+
8+
class ConfigurationIndex
9+
{
10+
const INDEX_NAME_CONFIG = '.%s_configuration';
11+
const TYPE_CONFIGURATION = 'configuration';
12+
const PRIMARY_ID = 'primary';
13+
14+
/**
15+
* Constructor.
16+
*
17+
* @param \Elasticsearch\Client $engine [description]
18+
* @param \Psr\Log\LoggerInterface $logger [description]
19+
* @param string $prefix
20+
*/
21+
public function __construct(Client $engine, LoggerInterface $logger, $prefix)
22+
{
23+
$this->engine = $engine;
24+
$this->logger = $logger;
25+
$this->configurationIndexName = sprintf(ConfigurationIndex::INDEX_NAME_CONFIG, $prefix);
26+
}
27+
28+
/**
29+
* Determines if the configured configuration index is available.
30+
*
31+
* @return boolean
32+
*/
33+
public function isConfigurationIndexAvailable()
34+
{
35+
return $this->engine->indices()->exists(['index' => $this->configurationIndexName]);
36+
}
37+
38+
/**
39+
* Create the index needed to store the primary index name.
40+
*
41+
* @return void
42+
*/
43+
public function createCurrentIndexConfiguration()
44+
{
45+
if ($this->isConfigurationIndexAvailable()) {
46+
return;
47+
}
48+
$this->engine->indices()->create([
49+
'index' => $this->configurationIndexName,
50+
'body' => static::$elasticSearchConfigurationMapping
51+
]);
52+
$this->logger->debug('Configuration index created.', [
53+
'index' => $this->configurationIndexName
54+
]);
55+
}
56+
57+
/**
58+
* Delete an entry from the configuration index.
59+
*
60+
* @param string $id
61+
* @return array
62+
*/
63+
public function deleteConfigurationEntry($id)
64+
{
65+
return $this->engine->delete([
66+
'index' => $this->configurationIndexName,
67+
'type' => static::TYPE_CONFIGURATION,
68+
'id' => $id
69+
]);
70+
}
71+
72+
/**
73+
* String representation of this class is the index name.
74+
*
75+
* @return string
76+
*/
77+
public function __toString()
78+
{
79+
return $this->configurationIndexName;
80+
}
81+
}

src/IndexRotator.php

+46-87
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55
use Psr\Log\LoggerInterface;
66
use Psr\Log\NullLogger;
77
use Elasticsearch\Client;
8+
use Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy;
89

910
class IndexRotator
1011
{
11-
const INDEX_NAME_CONFIG = '.%s_configuration';
12-
const TYPE_CONFIGURATION = 'configuration';
1312
const SECONDARY_NAME_ONLY = 0;
1413
const SECONDARY_INCLUDE_ID = 1;
15-
const PRIMARY_ID = 'primary';
1614
const RETRY_TIME_COPY = 500000;
1715
const MAX_RETRY_COUNT = 5;
16+
const STRATEGY_CONFIGURATION = 'Zumba\ElasticsearchRotator\Strategy\ConfigurationStrategy';
17+
const STRATEGY_ALIAS = 'Zumba\ElasticsearchRotator\Strategy\AliasStrategy';
18+
const DEFAULT_PRIMARY_INDEX_STRATEGY = self::STRATEGY_CONFIGURATION;
1819

1920
/**
2021
* Elasticsearch client instance.
@@ -24,18 +25,18 @@ class IndexRotator
2425
private $engine;
2526

2627
/**
27-
* Prefix identifier for this index.
28+
* Configuration index name for this index.
2829
*
29-
* @var string
30+
* @var \Zumba\ElasticsearchRotator\ConfigurationIndex
3031
*/
31-
private $prefix;
32+
private $configurationIndex;
3233

3334
/**
34-
* Configuration index name for this index.
35+
* Strategy to employ when working on primary index.
3536
*
36-
* @var string
37+
* @var \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy
3738
*/
38-
private $configurationIndexName;
39+
private $primaryIndexStrategy;
3940

4041
/**
4142
* Mapping for configuration index.
@@ -62,13 +63,31 @@ class IndexRotator
6263
public function __construct(\Elasticsearch\Client $engine, $prefix, LoggerInterface $logger = null)
6364
{
6465
$this->engine = $engine;
65-
$this->prefix = $prefix;
66-
if ($logger !== null) {
67-
$this->logger = $logger;
68-
} else {
69-
$this->logger = new NullLogger();
70-
}
71-
$this->configurationIndexName = sprintf(static::INDEX_NAME_CONFIG, $this->prefix);
66+
$this->logger = $logger ?: new NullLogger();
67+
$this->configurationIndex = new ConfigurationIndex($this->engine, $this->logger, $prefix);
68+
$this->setPrimaryIndexStrategy($this->strategyFactory(static::DEFAULT_PRIMARY_INDEX_STRATEGY, [
69+
'configuration_index' => $this->configurationIndex
70+
]));
71+
}
72+
73+
/**
74+
* Instantiate a specific strategy.
75+
*
76+
* @param string $strategyClass Fully qualified class name for a strategy.
77+
* @param array $options Options specific to the strategy
78+
* @return \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy
79+
*/
80+
public function strategyFactory($strategyClass, array $options = []) {
81+
return new $strategyClass($this->engine, $this->logger, $options);
82+
}
83+
84+
/**
85+
* Set the primary index strategy.
86+
*
87+
* @param \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy $strategy
88+
*/
89+
public function setPrimaryIndexStrategy(PrimaryIndexStrategy $strategy) {
90+
$this->primaryIndexStrategy = $strategy;
7291
}
7392

7493
/**
@@ -79,23 +98,8 @@ public function __construct(\Elasticsearch\Client $engine, $prefix, LoggerInterf
7998
*/
8099
public function getPrimaryIndex()
81100
{
82-
if (!$this->engine->indices()->exists(['index' => $this->configurationIndexName])) {
83-
$this->logger->error('Primary index configuration index not available.');
84-
throw new Exception\MissingPrimaryIndex('Primary index configuration index not available.');
85-
}
86-
$primaryPayload = [
87-
'index' => $this->configurationIndexName,
88-
'type' => static::TYPE_CONFIGURATION,
89-
'id' => static::PRIMARY_ID,
90-
'preference' => '_primary'
91-
];
92-
try {
93-
$primary = $this->engine->get($primaryPayload);
94-
} catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) {
95-
$this->logger->error('Primary index does not exist.');
96-
throw new Exception\MissingPrimaryIndex('Primary index not available.');
97-
}
98-
return $primary['_source']['name'];
101+
return $this->primaryIndexStrategy->getPrimaryIndex();
102+
99103
}
100104

101105
/**
@@ -106,19 +110,7 @@ public function getPrimaryIndex()
106110
*/
107111
public function setPrimaryIndex($name)
108112
{
109-
if (!$this->engine->indices()->exists(['index' => $this->configurationIndexName])) {
110-
$this->createCurrentIndexConfiguration();
111-
}
112-
$this->engine->index([
113-
'index' => $this->configurationIndexName,
114-
'type' => static::TYPE_CONFIGURATION,
115-
'id' => static::PRIMARY_ID,
116-
'body' => [
117-
'name' => $name,
118-
'timestamp' => time()
119-
]
120-
]);
121-
$this->logger->debug('Primary index set.', compact('name'));
113+
$this->primaryIndexStrategy->setPrimaryIndex($name);
122114
}
123115

124116
/**
@@ -130,9 +122,7 @@ public function setPrimaryIndex($name)
130122
*/
131123
public function copyPrimaryIndexToSecondary($retryCount = 0)
132124
{
133-
if (!$this->engine->indices()->exists(['index' => $this->configurationIndexName])) {
134-
$this->createCurrentIndexConfiguration();
135-
}
125+
$this->configurationIndex->createCurrentIndexConfiguration();
136126
try {
137127
$primaryName = $this->getPrimaryIndex();
138128
} catch (\Elasticsearch\Common\Exceptions\ServerErrorResponseException $e) {
@@ -144,8 +134,8 @@ public function copyPrimaryIndexToSecondary($retryCount = 0)
144134
return $this->copyPrimaryIndexToSecondary($retryCount++);
145135
}
146136
$id = $this->engine->index([
147-
'index' => $this->configurationIndexName,
148-
'type' => static::TYPE_CONFIGURATION,
137+
'index' => (string)$this->configurationIndex,
138+
'type' => ConfigurationIndex::TYPE_CONFIGURATION,
149139
'body' => [
150140
'name' => $primaryName,
151141
'timestamp' => time()
@@ -170,14 +160,14 @@ public function getSecondaryIndices(\DateTime $olderThan = null, $disposition =
170160
$olderThan = new \DateTime();
171161
}
172162
$params = [
173-
'index' => $this->configurationIndexName,
174-
'type' => static::TYPE_CONFIGURATION,
163+
'index' => (string)$this->configurationIndex,
164+
'type' => ConfigurationIndex::TYPE_CONFIGURATION,
175165
'body' => [
176166
'query' => [
177167
'bool' => [
178168
'must_not' => [
179169
'term' => [
180-
'_id' => static::PRIMARY_ID
170+
'_id' => ConfigurationIndex::PRIMARY_ID
181171
]
182172
],
183173
'filter' => [
@@ -231,51 +221,20 @@ public function deleteSecondaryIndices(\DateTime $olderThan = null)
231221
if ($this->engine->indices()->exists(['index' => $indexToDelete['index']])) {
232222
$results[$indexToDelete['index']] = [
233223
'index' => $this->engine->indices()->delete(['index' => $indexToDelete['index']]),
234-
'config' => $this->deleteConfigurationEntry($indexToDelete['configuration_id'])
224+
'config' => $this->configurationIndex->deleteConfigurationEntry($indexToDelete['configuration_id'])
235225
];
236226
$this->logger->debug('Deleted secondary index.', compact('indexToDelete'));
237227
} else {
238228
$results[$indexToDelete] = [
239229
'index' => null,
240-
'config' => $this->deleteConfigurationEntry($indexToDelete['configuration_id'])
230+
'config' => $this->configurationIndex->deleteConfigurationEntry($indexToDelete['configuration_id'])
241231
];
242232
$this->logger->debug('Index not found to delete.', compact('indexToDelete'));
243233
}
244234
}
245235
return $results;
246236
}
247237

248-
/**
249-
* Delete an entry from the configuration index.
250-
*
251-
* @param string $id
252-
* @return array
253-
*/
254-
private function deleteConfigurationEntry($id)
255-
{
256-
return $this->engine->delete([
257-
'index' => $this->configurationIndexName,
258-
'type' => static::TYPE_CONFIGURATION,
259-
'id' => $id
260-
]);
261-
}
262-
263-
/**
264-
* Create the index needed to store the primary index name.
265-
*
266-
* @return void
267-
*/
268-
private function createCurrentIndexConfiguration()
269-
{
270-
$this->engine->indices()->create([
271-
'index' => $this->configurationIndexName,
272-
'body' => static::$elasticSearchConfigurationMapping
273-
]);
274-
$this->logger->debug('Configuration index created.', [
275-
'index' => $this->configurationIndexName
276-
]);
277-
}
278-
279238
/**
280239
* Determines if the combined filter in query DSL is supported.
281240
*

0 commit comments

Comments
 (0)