Skip to content

Commit

Permalink
Merge pull request #3 from zumba/alias-strategy
Browse files Browse the repository at this point in the history
Add an alias strategy for primary index.
  • Loading branch information
cjsaylor committed Mar 4, 2016
2 parents 684c1d9 + cfa3932 commit 2103e94
Show file tree
Hide file tree
Showing 8 changed files with 428 additions and 88 deletions.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ $client->search([
<?php

$client = new \Elasticsearch\Client();
$indexRotator = new IndexRotator($client, 'pizza_shops');
$indexRotator = new \Zumba\ElasticsearchRotator\IndexRotator($client, 'pizza_shops');
// Build your index here
$newlyBuiltIndexName = 'my_new_built_index_name';
$indexRotator->copyPrimaryIndexToSecondary();
Expand Down Expand Up @@ -97,3 +97,36 @@ class MySearchIndex {

}
```

## Using Strategies

You can now customize the strategy of getting/setting the primary index. By default, the `ConfigurationStrategy` is employed,
however we have also included an `AliasStrategy`. The main difference is when `setPrimaryIndex` is called, instead of creating an entry
in the configuration index, it adds an alias (specified by `alias_name` option) on the specified index and deletes all other aliases
for the old primary indices (specified by `index_pattern`).

#### Using the `AliasStrategy`

```php
<?php

$client = new \Elasticsearch\Client();
$indexRotator = new \Zumba\ElasticsearchRotator\IndexRotator($client, 'pizza_shops');
$aliasStrategy = $indexRotator->strategyFactory(IndexRotator::STRATEGY_ALIAS, [
'alias_name' => 'pizza_shops',
'index_pattern' => 'pizza_shops_*'
]);
// Build your index here
$newlyBuiltIndexName = 'pizza_shops_1234102874';
$indexRotator->copyPrimaryIndexToSecondary();
$indexRotator->setPrimaryIndex($newlyBuiltIndexName);

// Now that the alias is set, you can search on that alias instead of having to call `getPrimaryIndex`.
$client->search([
'index' => 'pizza_shops',
'type' => 'shop',
'body' => [] //...
])
```

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.
5 changes: 5 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory>./src</directory>
</whitelist>
</filter>
</phpunit>
34 changes: 34 additions & 0 deletions src/Common/PrimaryIndexStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Zumba\ElasticsearchRotator\Common;

use Elasticsearch\Client;
use Psr\Log\LoggerInterface;

interface PrimaryIndexStrategy
{
/**
* Constructor.
*
* @param \Elasticsearch\Client $engine
* @param \Psr\Log\LoggerInterface $logger
* @param array $options Options specific to this strategy
*/
public function __construct(Client $engine, LoggerInterface $logger, array $options = []);

/**
* Get the primary index name for this configuration.
*
* @return string
* @throws \ElasticsearchRotator\Exceptions\MissingPrimaryException
*/
public function getPrimaryIndex();

/**
* Get the primary index name for this configuration.
*
* @return string
* @throws \ElasticsearchRotator\Exceptions\MissingPrimaryException
*/
public function setPrimaryIndex($name);
}
81 changes: 81 additions & 0 deletions src/ConfigurationIndex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Zumba\ElasticsearchRotator;

use Elasticsearch\Client;
use Psr\Log\LoggerInterface;

class ConfigurationIndex
{
const INDEX_NAME_CONFIG = '.%s_configuration';
const TYPE_CONFIGURATION = 'configuration';
const PRIMARY_ID = 'primary';

/**
* Constructor.
*
* @param \Elasticsearch\Client $engine [description]
* @param \Psr\Log\LoggerInterface $logger [description]
* @param string $prefix
*/
public function __construct(Client $engine, LoggerInterface $logger, $prefix)
{
$this->engine = $engine;
$this->logger = $logger;
$this->configurationIndexName = sprintf(ConfigurationIndex::INDEX_NAME_CONFIG, $prefix);
}

/**
* Determines if the configured configuration index is available.
*
* @return boolean
*/
public function isConfigurationIndexAvailable()
{
return $this->engine->indices()->exists(['index' => $this->configurationIndexName]);
}

/**
* Create the index needed to store the primary index name.
*
* @return void
*/
public function createCurrentIndexConfiguration()
{
if ($this->isConfigurationIndexAvailable()) {
return;
}
$this->engine->indices()->create([
'index' => $this->configurationIndexName,
'body' => static::$elasticSearchConfigurationMapping
]);
$this->logger->debug('Configuration index created.', [
'index' => $this->configurationIndexName
]);
}

/**
* Delete an entry from the configuration index.
*
* @param string $id
* @return array
*/
public function deleteConfigurationEntry($id)
{
return $this->engine->delete([
'index' => $this->configurationIndexName,
'type' => static::TYPE_CONFIGURATION,
'id' => $id
]);
}

/**
* String representation of this class is the index name.
*
* @return string
*/
public function __toString()
{
return $this->configurationIndexName;
}
}
133 changes: 46 additions & 87 deletions src/IndexRotator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Elasticsearch\Client;
use Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy;

class IndexRotator
{
const INDEX_NAME_CONFIG = '.%s_configuration';
const TYPE_CONFIGURATION = 'configuration';
const SECONDARY_NAME_ONLY = 0;
const SECONDARY_INCLUDE_ID = 1;
const PRIMARY_ID = 'primary';
const RETRY_TIME_COPY = 500000;
const MAX_RETRY_COUNT = 5;
const STRATEGY_CONFIGURATION = 'Zumba\ElasticsearchRotator\Strategy\ConfigurationStrategy';
const STRATEGY_ALIAS = 'Zumba\ElasticsearchRotator\Strategy\AliasStrategy';
const DEFAULT_PRIMARY_INDEX_STRATEGY = self::STRATEGY_CONFIGURATION;

/**
* Elasticsearch client instance.
Expand All @@ -24,18 +25,18 @@ class IndexRotator
private $engine;

/**
* Prefix identifier for this index.
* Configuration index name for this index.
*
* @var string
* @var \Zumba\ElasticsearchRotator\ConfigurationIndex
*/
private $prefix;
private $configurationIndex;

/**
* Configuration index name for this index.
* Strategy to employ when working on primary index.
*
* @var string
* @var \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy
*/
private $configurationIndexName;
private $primaryIndexStrategy;

/**
* Mapping for configuration index.
Expand All @@ -62,13 +63,31 @@ class IndexRotator
public function __construct(\Elasticsearch\Client $engine, $prefix, LoggerInterface $logger = null)
{
$this->engine = $engine;
$this->prefix = $prefix;
if ($logger !== null) {
$this->logger = $logger;
} else {
$this->logger = new NullLogger();
}
$this->configurationIndexName = sprintf(static::INDEX_NAME_CONFIG, $this->prefix);
$this->logger = $logger ?: new NullLogger();
$this->configurationIndex = new ConfigurationIndex($this->engine, $this->logger, $prefix);
$this->setPrimaryIndexStrategy($this->strategyFactory(static::DEFAULT_PRIMARY_INDEX_STRATEGY, [
'configuration_index' => $this->configurationIndex
]));
}

/**
* Instantiate a specific strategy.
*
* @param string $strategyClass Fully qualified class name for a strategy.
* @param array $options Options specific to the strategy
* @return \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy
*/
public function strategyFactory($strategyClass, array $options = []) {
return new $strategyClass($this->engine, $this->logger, $options);
}

/**
* Set the primary index strategy.
*
* @param \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy $strategy
*/
public function setPrimaryIndexStrategy(PrimaryIndexStrategy $strategy) {
$this->primaryIndexStrategy = $strategy;
}

/**
Expand All @@ -79,23 +98,8 @@ public function __construct(\Elasticsearch\Client $engine, $prefix, LoggerInterf
*/
public function getPrimaryIndex()
{
if (!$this->engine->indices()->exists(['index' => $this->configurationIndexName])) {
$this->logger->error('Primary index configuration index not available.');
throw new Exception\MissingPrimaryIndex('Primary index configuration index not available.');
}
$primaryPayload = [
'index' => $this->configurationIndexName,
'type' => static::TYPE_CONFIGURATION,
'id' => static::PRIMARY_ID,
'preference' => '_primary'
];
try {
$primary = $this->engine->get($primaryPayload);
} catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) {
$this->logger->error('Primary index does not exist.');
throw new Exception\MissingPrimaryIndex('Primary index not available.');
}
return $primary['_source']['name'];
return $this->primaryIndexStrategy->getPrimaryIndex();

}

/**
Expand All @@ -106,19 +110,7 @@ public function getPrimaryIndex()
*/
public function setPrimaryIndex($name)
{
if (!$this->engine->indices()->exists(['index' => $this->configurationIndexName])) {
$this->createCurrentIndexConfiguration();
}
$this->engine->index([
'index' => $this->configurationIndexName,
'type' => static::TYPE_CONFIGURATION,
'id' => static::PRIMARY_ID,
'body' => [
'name' => $name,
'timestamp' => time()
]
]);
$this->logger->debug('Primary index set.', compact('name'));
$this->primaryIndexStrategy->setPrimaryIndex($name);
}

/**
Expand All @@ -130,9 +122,7 @@ public function setPrimaryIndex($name)
*/
public function copyPrimaryIndexToSecondary($retryCount = 0)
{
if (!$this->engine->indices()->exists(['index' => $this->configurationIndexName])) {
$this->createCurrentIndexConfiguration();
}
$this->configurationIndex->createCurrentIndexConfiguration();
try {
$primaryName = $this->getPrimaryIndex();
} catch (\Elasticsearch\Common\Exceptions\ServerErrorResponseException $e) {
Expand All @@ -144,8 +134,8 @@ public function copyPrimaryIndexToSecondary($retryCount = 0)
return $this->copyPrimaryIndexToSecondary($retryCount++);
}
$id = $this->engine->index([
'index' => $this->configurationIndexName,
'type' => static::TYPE_CONFIGURATION,
'index' => (string)$this->configurationIndex,
'type' => ConfigurationIndex::TYPE_CONFIGURATION,
'body' => [
'name' => $primaryName,
'timestamp' => time()
Expand All @@ -170,14 +160,14 @@ public function getSecondaryIndices(\DateTime $olderThan = null, $disposition =
$olderThan = new \DateTime();
}
$params = [
'index' => $this->configurationIndexName,
'type' => static::TYPE_CONFIGURATION,
'index' => (string)$this->configurationIndex,
'type' => ConfigurationIndex::TYPE_CONFIGURATION,
'body' => [
'query' => [
'bool' => [
'must_not' => [
'term' => [
'_id' => static::PRIMARY_ID
'_id' => ConfigurationIndex::PRIMARY_ID
]
],
'filter' => [
Expand Down Expand Up @@ -231,51 +221,20 @@ public function deleteSecondaryIndices(\DateTime $olderThan = null)
if ($this->engine->indices()->exists(['index' => $indexToDelete['index']])) {
$results[$indexToDelete['index']] = [
'index' => $this->engine->indices()->delete(['index' => $indexToDelete['index']]),
'config' => $this->deleteConfigurationEntry($indexToDelete['configuration_id'])
'config' => $this->configurationIndex->deleteConfigurationEntry($indexToDelete['configuration_id'])
];
$this->logger->debug('Deleted secondary index.', compact('indexToDelete'));
} else {
$results[$indexToDelete] = [
'index' => null,
'config' => $this->deleteConfigurationEntry($indexToDelete['configuration_id'])
'config' => $this->configurationIndex->deleteConfigurationEntry($indexToDelete['configuration_id'])
];
$this->logger->debug('Index not found to delete.', compact('indexToDelete'));
}
}
return $results;
}

/**
* Delete an entry from the configuration index.
*
* @param string $id
* @return array
*/
private function deleteConfigurationEntry($id)
{
return $this->engine->delete([
'index' => $this->configurationIndexName,
'type' => static::TYPE_CONFIGURATION,
'id' => $id
]);
}

/**
* Create the index needed to store the primary index name.
*
* @return void
*/
private function createCurrentIndexConfiguration()
{
$this->engine->indices()->create([
'index' => $this->configurationIndexName,
'body' => static::$elasticSearchConfigurationMapping
]);
$this->logger->debug('Configuration index created.', [
'index' => $this->configurationIndexName
]);
}

/**
* Determines if the combined filter in query DSL is supported.
*
Expand Down
Loading

0 comments on commit 2103e94

Please sign in to comment.