├── .github └── workflows │ └── action.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── docs ├── disruption.png └── rotation.png ├── phpunit.xml ├── src ├── Common │ └── PrimaryIndexStrategy.php ├── ConfigurationIndex.php ├── Exception │ ├── MissingPrimaryIndex.php │ └── PrimaryIndexCopyFailure.php ├── IndexRotator.php └── Strategy │ ├── AliasStrategy.php │ └── ConfigurationStrategy.php └── tests ├── IndexRotatorTest.php └── Strategy └── AliasStrategyTest.php /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: PHP Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | name: Php Build & Test 14 | 15 | strategy: 16 | matrix: 17 | php-version: [7] 18 | os: [ubuntu-latest] 19 | es-version: [5.3.2] 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | 27 | - name: Use PHP ${{ matrix.php-version }} 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php-version }} 31 | extensions: curl 32 | env: 33 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Get composer cache directory 36 | id: composercache 37 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 38 | 39 | - name: Cache dependencies 40 | uses: actions/cache@v2 41 | with: 42 | path: ${{ steps.composercache.outputs.dir }} 43 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 44 | restore-keys: ${{ runner.os }}-composer- 45 | 46 | - name: Install dependencies 47 | run: | 48 | composer install --prefer-dist 49 | 50 | - name: Runs Elasticsearch ${{ matrix.es-version }} 51 | uses: getong/elasticsearch-action@v1.2 52 | with: 53 | elasticsearch version: '${{ matrix.es-version }}' 54 | host port: 9200 55 | container port: 9200 56 | host node port: 9300 57 | node port: 9300 58 | discovery type: 'single-node' 59 | 60 | - name: Run test suite 61 | run: ./vendor/bin/phpunit --coverage-text 62 | env: 63 | ELASTICSEARCH_URL: http://localhost:9200 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Zumba ® License (MIT) 2 | 3 | Permission is hereby granted by **Zumba Fitness, LLC**, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch Index Rotator 2 | 3 | A library to enable you to safely rotate indexes with no downtime to end users. 4 | 5 | [![Build Status](https://travis-ci.org/zumba/elasticsearch-index-rotator.svg?branch=master)](https://travis-ci.org/zumba/elasticsearch-index-rotator) 6 | 7 | ### Why would I use this? 8 | 9 | In many situations, Elasticsearch is used as an ephemeral datastore used to take structured or relational data and make it fast to search on that data. Often this is achieved via scheduled jobs that read data from a permanent datastore (such as MySQL or Postgres) and translate it into an Elasticsearch index. 10 | 11 | In many cases, rebuilding an index requires a clean slate so that the entire index is rebuilt. How do you do this without interrupting end users searching on that index? The answer is a rotating index. 12 | 13 | ![User search disrupted by rebuild](docs/disruption.png) 14 | 15 | > Here the user's search is fully disrupted when the index is first removed, and only partially available while the index is being rebuilt. While the index is being rebuilt, users get incomplete data. 16 | 17 | ![User search contiguous](docs/rotation.png) 18 | 19 | > Here the user's search is never disrupted because we construct a new index and after it is built/settled, we change the what index to search by the client. 20 | 21 | ## Installation 22 | 23 | ``` 24 | composer require zumba/elasticsearch-index-rotate 25 | ``` 26 | 27 | Elasticsearch Index Rotator supports multiple versions of ElasticSearch server and uses the [official elasticsearch](https://packagist.org/packages/elasticsearch/elasticsearch) library for execute the commands. 28 | On your application, make sure you include this package as well and specify the version supported by your Elasticsearch server. See the library documentation for the versions. 29 | 30 | ## Usage 31 | 32 | #### Example Search 33 | 34 | ```php 35 | search([ 40 | 'index' => $indexRotator->getPrimaryIndex(), // Get the current primary! 41 | 'type' => 'shop', 42 | 'body' => [] //... 43 | ]); 44 | ``` 45 | 46 | #### Example Build 47 | 48 | ```php 49 | copyPrimaryIndexToSecondary(); 56 | $indexRotator->setPrimaryIndex($newlyBuiltIndexName); 57 | // optionally remove the old index right now 58 | $indexRotator->deleteSecondaryIndices(); 59 | ``` 60 | 61 | #### All together 62 | 63 | ```php 64 | client = $client; 75 | } 76 | 77 | public function search($params) { 78 | $indexRotator = new IndexRotator($this->client, static::INDEX_PREFIX); 79 | return $client->search([ 80 | 'index' => $indexRotator->getPrimaryIndex(), // Get the current primary! 81 | 'type' => 'shop', 82 | 'body' => $params 83 | ]); 84 | } 85 | 86 | public function rebuildIndex() { 87 | $indexRotator = new IndexRotator($client, static::INDEX_PREFIX); 88 | $newlyBuiltIndexName = $this->buildIndex($client); 89 | $indexRotator->copyPrimaryIndexToSecondary(); 90 | $indexRotator->setPrimaryIndex($newlyBuiltIndexName); 91 | // optionally remove the old index right now 92 | $indexRotator->deleteSecondaryIndices(); 93 | } 94 | 95 | private function buildIndex(\Elasticsearch\Client $client) { 96 | $newIndex = static::INDEX_PREFIX . '_' . time(); 97 | // get data and build index for `$newIndex` 98 | return $newIndex; 99 | } 100 | 101 | } 102 | ``` 103 | 104 | ## Using Strategies 105 | 106 | You can now customize the strategy of getting/setting the primary index. By default, the `ConfigurationStrategy` is employed, 107 | however we have also included an `AliasStrategy`. The main difference is when `setPrimaryIndex` is called, instead of creating an entry 108 | in the configuration index, it adds an alias (specified by `alias_name` option) on the specified index and deletes all other aliases 109 | for the old primary indices (specified by `index_pattern`). 110 | 111 | #### Using the `AliasStrategy` 112 | 113 | ```php 114 | strategyFactory(IndexRotator::STRATEGY_ALIAS, [ 119 | 'alias_name' => 'pizza_shops', 120 | 'index_pattern' => 'pizza_shops_*' 121 | ]); 122 | // Build your index here 123 | $newlyBuiltIndexName = 'pizza_shops_1234102874'; 124 | $indexRotator->copyPrimaryIndexToSecondary(); 125 | $indexRotator->setPrimaryIndex($newlyBuiltIndexName); 126 | 127 | // Now that the alias is set, you can search on that alias instead of having to call `getPrimaryIndex`. 128 | $client->search([ 129 | 'index' => 'pizza_shops', 130 | 'type' => 'shop', 131 | 'body' => [] //... 132 | ]) 133 | ``` 134 | 135 | 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. 136 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zumba/elasticsearch-index-rotate", 3 | "description": "Safely rotate indexes with no downtime to end users.", 4 | "keywords": ["elasticsearch"], 5 | "authors": [ 6 | { 7 | "name": "Chris Saylor", 8 | "email": "cjsaylor@gmail.com" 9 | } 10 | ], 11 | "license": "MIT", 12 | "require": { 13 | "php": ">=7.0", 14 | "elasticsearch/elasticsearch": "^5.0", 15 | "psr/log": "^1.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Zumba\\ElasticsearchRotator\\": "src/" 20 | } 21 | }, 22 | "suggest": { 23 | "monolog/monolog": "Allows more advanced logging of the application flow" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "4.8.23", 27 | "zumba/elasticsearchunit": "^1.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/disruption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zumba/elasticsearch-index-rotator/e47ff6b06f2caae2c05adf10edf4df7884451030/docs/disruption.png -------------------------------------------------------------------------------- /docs/rotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zumba/elasticsearch-index-rotator/e47ff6b06f2caae2c05adf10edf4df7884451030/docs/rotation.png -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Common/PrimaryIndexStrategy.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'configuration' => [ 22 | 'properties' => [ 23 | 'name' => ['type' => 'string', 'index' => 'not_analyzed'], 24 | 'timestamp' => ['type' => 'date'] 25 | ] 26 | ] 27 | ] 28 | ]; 29 | 30 | /** 31 | * Constructor. 32 | * 33 | * @param \Elasticsearch\Client $engine [description] 34 | * @param \Psr\Log\LoggerInterface $logger [description] 35 | * @param string $prefix 36 | */ 37 | public function __construct(Client $engine, LoggerInterface $logger, $prefix) 38 | { 39 | $this->engine = $engine; 40 | $this->logger = $logger; 41 | $this->configurationIndexName = sprintf(ConfigurationIndex::INDEX_NAME_CONFIG, $prefix); 42 | } 43 | 44 | /** 45 | * Determines if the configured configuration index is available. 46 | * 47 | * @return boolean 48 | */ 49 | public function isConfigurationIndexAvailable() 50 | { 51 | return $this->engine->indices()->exists(['index' => $this->configurationIndexName]); 52 | } 53 | 54 | /** 55 | * Create the index needed to store the primary index name. 56 | * 57 | * @return void 58 | */ 59 | public function createCurrentIndexConfiguration() 60 | { 61 | if ($this->isConfigurationIndexAvailable()) { 62 | return; 63 | } 64 | $this->engine->indices()->create([ 65 | 'index' => $this->configurationIndexName, 66 | 'body' => static::$elasticSearchConfigurationMapping 67 | ]); 68 | $this->logger->debug('Configuration index created.', [ 69 | 'index' => $this->configurationIndexName 70 | ]); 71 | } 72 | 73 | /** 74 | * Delete an entry from the configuration index. 75 | * 76 | * @param string $id 77 | * @return array 78 | */ 79 | public function deleteConfigurationEntry($id) 80 | { 81 | return $this->engine->delete([ 82 | 'index' => $this->configurationIndexName, 83 | 'type' => static::TYPE_CONFIGURATION, 84 | 'id' => $id 85 | ]); 86 | } 87 | 88 | /** 89 | * String representation of this class is the index name. 90 | * 91 | * @return string 92 | */ 93 | public function __toString() 94 | { 95 | return $this->configurationIndexName; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Exception/MissingPrimaryIndex.php: -------------------------------------------------------------------------------- 1 | engine = $engine; 51 | $this->logger = $logger ?: new NullLogger(); 52 | $this->configurationIndex = new ConfigurationIndex($this->engine, $this->logger, $prefix); 53 | $this->setPrimaryIndexStrategy($this->strategyFactory(static::DEFAULT_PRIMARY_INDEX_STRATEGY, [ 54 | 'configuration_index' => $this->configurationIndex 55 | ])); 56 | } 57 | 58 | /** 59 | * Instantiate a specific strategy. 60 | * 61 | * @param string $strategyClass Fully qualified class name for a strategy. 62 | * @param array $options Options specific to the strategy 63 | * @return \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy 64 | */ 65 | public function strategyFactory($strategyClass, array $options = []) 66 | { 67 | return new $strategyClass($this->engine, $this->logger, $options); 68 | } 69 | 70 | /** 71 | * Set the primary index strategy. 72 | * 73 | * @param \Zumba\ElasticsearchRotator\Common\PrimaryIndexStrategy $strategy 74 | */ 75 | public function setPrimaryIndexStrategy(PrimaryIndexStrategy $strategy) 76 | { 77 | $this->primaryIndexStrategy = $strategy; 78 | } 79 | 80 | /** 81 | * Get the primary index name for this configuration. 82 | * 83 | * @return string 84 | * @throws \ElasticsearchRotator\Exceptions\MissingPrimaryException 85 | */ 86 | public function getPrimaryIndex() 87 | { 88 | return $this->primaryIndexStrategy->getPrimaryIndex(); 89 | } 90 | 91 | /** 92 | * Sets the primary index for searches using this configuration. 93 | * 94 | * @param string $name Index name for the primary index to use. 95 | * @return void 96 | */ 97 | public function setPrimaryIndex($name) 98 | { 99 | $this->primaryIndexStrategy->setPrimaryIndex($name); 100 | } 101 | 102 | /** 103 | * Copy the primary index to a secondary index. 104 | * 105 | * @param integer $retryCount Recursive retry count for retrying the operation of this method. 106 | * @return string ID of the newly created secondary entry. 107 | * @throws \Zumba\ElasticsearchRotator\Exception\PrimaryIndexCopyFailure 108 | */ 109 | public function copyPrimaryIndexToSecondary($retryCount = 0) 110 | { 111 | $this->configurationIndex->createCurrentIndexConfiguration(); 112 | try { 113 | $primaryName = $this->getPrimaryIndex(); 114 | } catch (\Elasticsearch\Common\Exceptions\ServerErrorResponseException $e) { 115 | $this->logger->debug('Unable to get primary index.', json_decode($e->getMessage(), true)); 116 | usleep(static::RETRY_TIME_COPY); 117 | if ($retryCount > static::MAX_RETRY_COUNT) { 118 | throw new Exception\PrimaryIndexCopyFailure('Unable to copy primary to secondary index.'); 119 | } 120 | return $this->copyPrimaryIndexToSecondary($retryCount++); 121 | } 122 | $id = $this->engine->index([ 123 | 'index' => (string)$this->configurationIndex, 124 | 'type' => ConfigurationIndex::TYPE_CONFIGURATION, 125 | 'body' => [ 126 | 'name' => $primaryName, 127 | 'timestamp' => time() 128 | ] 129 | ])['_id']; 130 | $this->logger->debug('Secondary entry created.', compact('id')); 131 | return $id; 132 | } 133 | 134 | /** 135 | * Retrieve a list of all secondary indexes (rotated from) that are older than provided date (or ES date math) 136 | * 137 | * Note, if date is not provided, it will find all secondary indexes. 138 | * 139 | * @param string $olderThan 140 | * @param integer $disposition Controls the return style (defaults to name only) 141 | * @return array 142 | */ 143 | public function getSecondaryIndices(\DateTime $olderThan = null, $disposition = self::SECONDARY_NAME_ONLY) 144 | { 145 | if ($olderThan === null) { 146 | $olderThan = new \DateTime(); 147 | } 148 | $params = [ 149 | 'index' => (string)$this->configurationIndex, 150 | 'type' => ConfigurationIndex::TYPE_CONFIGURATION, 151 | 'body' => [ 152 | 'query' => [ 153 | 'bool' => [ 154 | 'must_not' => [ 155 | 'term' => [ 156 | '_id' => ConfigurationIndex::PRIMARY_ID 157 | ] 158 | ], 159 | 'filter' => [ 160 | 'range' => [ 161 | 'timestamp' => [ 162 | 'lt' => $olderThan->format('U') 163 | ] 164 | ] 165 | ] 166 | ] 167 | ], 168 | 'sort' => ['_doc' => 'asc'] 169 | ] 170 | ]; 171 | // This section is to support deprecated feature set for ES 1.x. 172 | // It may be removed in future versions of this library when ES 1.x is sufficiently unsupported. 173 | if (!$this->doesSupportCombinedQueryFilter()) { 174 | $this->logger->notice('Using deprecated query format due to elasticsearch version <= 1.x.'); 175 | unset($params['body']['query']['bool']['filter']); 176 | $params['body']['filter']['range']['timestamp']['lt'] = $olderThan->format('U'); 177 | } 178 | $results = $this->engine->search($params); 179 | if ($results['hits']['total'] == 0) { 180 | return []; 181 | } 182 | $mapper = $disposition === static::SECONDARY_INCLUDE_ID ? 183 | function($entry) { 184 | return [ 185 | 'index' => $entry['_source']['name'], 186 | 'configuration_id' => $entry['_id'] 187 | ]; 188 | } : 189 | function($entry) { 190 | return $entry['_source']['name']; 191 | }; 192 | return array_map($mapper, $results['hits']['hits']); 193 | } 194 | 195 | /** 196 | * Remove any secondary index older that provided date. 197 | * 198 | * If no date is provided, will remove all secondary indices. 199 | * 200 | * @param \DateTime $olderThan 201 | * @return array Results of the bulk operation. 202 | */ 203 | public function deleteSecondaryIndices(\DateTime $olderThan = null) 204 | { 205 | $results = []; 206 | foreach ($this->getSecondaryIndices($olderThan, static::SECONDARY_INCLUDE_ID) as $indexToDelete) { 207 | if ($this->engine->indices()->exists(['index' => $indexToDelete['index']])) { 208 | $results[$indexToDelete['index']] = [ 209 | 'index' => $this->engine->indices()->delete(['index' => $indexToDelete['index']]), 210 | 'config' => $this->configurationIndex->deleteConfigurationEntry($indexToDelete['configuration_id']) 211 | ]; 212 | $this->logger->debug('Deleted secondary index.', compact('indexToDelete')); 213 | } else { 214 | $results[$indexToDelete] = [ 215 | 'index' => null, 216 | 'config' => $this->configurationIndex->deleteConfigurationEntry($indexToDelete['configuration_id']) 217 | ]; 218 | $this->logger->debug('Index not found to delete.', compact('indexToDelete')); 219 | } 220 | } 221 | return $results; 222 | } 223 | 224 | /** 225 | * Determines if the combined filter in query DSL is supported. 226 | * 227 | * @return boolean 228 | */ 229 | private function doesSupportCombinedQueryFilter() 230 | { 231 | return version_compare($this->engine->info()['version']['number'], '2.0.0', '>='); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/Strategy/AliasStrategy.php: -------------------------------------------------------------------------------- 1 | engine = $engine; 24 | $this->logger = $logger ?: new NullLogger(); 25 | if (empty($options['alias_name'])) { 26 | throw new \DomainException('Alias name must be specified.'); 27 | } 28 | if (empty($options['index_pattern'])) { 29 | throw new \DomainException('Index pattern must be specified.'); 30 | } 31 | $this->options = $options; 32 | } 33 | 34 | /** 35 | * Get the primary index name for this configuration. 36 | * 37 | * @return string 38 | * @throws \ElasticsearchRotator\Exceptions\MissingPrimaryException 39 | */ 40 | public function getPrimaryIndex() 41 | { 42 | try { 43 | $indexMeta = $this->engine->indices()->get([ 44 | 'index' => $this->options['alias_name'] 45 | ]); 46 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 47 | throw new Exception\MissingPrimaryIndex('Primary index configuration index not available.'); 48 | } 49 | return key($indexMeta); 50 | } 51 | 52 | /** 53 | * Sets the primary index for searches using this configuration. 54 | * 55 | * @param string $name Index name for the primary index to use. 56 | * @return void 57 | */ 58 | public function setPrimaryIndex($name) 59 | { 60 | $this->logger->debug(sprintf('Setting primary index to %s.', $name)); 61 | $params = [ 62 | 'body' => [ 63 | 'actions' => [ 64 | [ 65 | 'remove' => [ 66 | 'index' => $this->options['index_pattern'], 67 | 'alias' => $this->options['alias_name'] 68 | ], 69 | ], 70 | [ 71 | 'add' => [ 72 | 'index' => $name, 73 | 'alias' => $this->options['alias_name'] 74 | ] 75 | ] 76 | ] 77 | ] 78 | ]; 79 | try { 80 | $this->engine->indices()->updateAliases($params); 81 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 82 | $this->logger->debug('No aliases matched the pattern. Retrying without the removal of old indices.'); 83 | array_shift($params['body']['actions']); 84 | $this->engine->indices()->updateAliases($params); 85 | } 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Strategy/ConfigurationStrategy.php: -------------------------------------------------------------------------------- 1 | engine = $engine; 28 | $this->logger = $logger; 29 | $this->options = $options; 30 | } 31 | 32 | /** 33 | * Get the primary index name for this configuration. 34 | * 35 | * @return string 36 | * @throws \ElasticsearchRotator\Exceptions\MissingPrimaryException 37 | */ 38 | public function getPrimaryIndex() 39 | { 40 | if (!$this->options['configuration_index']->isConfigurationIndexAvailable()) { 41 | $this->logger->error('Primary index configuration index not available.'); 42 | throw new Exception\MissingPrimaryIndex('Primary index configuration index not available.'); 43 | } 44 | $primaryPayload = [ 45 | 'index' => (string)$this->options['configuration_index'], 46 | 'type' => ConfigurationIndex::TYPE_CONFIGURATION, 47 | 'id' => ConfigurationIndex::PRIMARY_ID, 48 | 'preference' => '_primary' 49 | ]; 50 | try { 51 | $primary = $this->engine->get($primaryPayload); 52 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 53 | $this->logger->error('Primary index does not exist.'); 54 | throw new Exception\MissingPrimaryIndex('Primary index not available.'); 55 | } 56 | return $primary['_source']['name']; 57 | } 58 | 59 | /** 60 | * Sets the primary index for searches using this configuration. 61 | * 62 | * @param string $name Index name for the primary index to use. 63 | * @return void 64 | */ 65 | public function setPrimaryIndex($name) 66 | { 67 | $this->options['configuration_index']->createCurrentIndexConfiguration(); 68 | $this->engine->index([ 69 | 'index' => (string)$this->options['configuration_index'], 70 | 'type' => ConfigurationIndex::TYPE_CONFIGURATION, 71 | 'id' => ConfigurationIndex::PRIMARY_ID, 72 | 'body' => [ 73 | 'name' => $name, 74 | 'timestamp' => time() 75 | ] 76 | ]); 77 | $this->logger->debug('Primary index set.', compact('name')); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/IndexRotatorTest.php: -------------------------------------------------------------------------------- 1 | indexRotator = new IndexRotator($this->getElasticSearchConnector()->getConnection(), 'config_test'); 14 | parent::setUp(); 15 | } 16 | 17 | public function getElasticSearchConnector() 18 | { 19 | if (empty($this->connection)) { 20 | $clientBuilder = \Elasticsearch\ClientBuilder::create(); 21 | if (getenv('ES_TEST_HOST')) { 22 | $clientBuilder->setHosts([getenv('ES_TEST_HOST')]); 23 | } 24 | $this->connection = new Connector($clientBuilder->build()); 25 | } 26 | return $this->connection; 27 | } 28 | 29 | public function getElasticSearchDataSet() 30 | { 31 | $dataSet = new DataSet($this->getElasticSearchConnector()); 32 | $dataSet->setFixture([ 33 | '.config_test_configuration' => [ 34 | 'configuration' => [ 35 | [ 36 | 'id' => 'primary', 37 | 'name' => 'some_index_1', 38 | 'timestamp' => time() 39 | ], 40 | [ 41 | 'id' => 'somesecondary2', 42 | 'name' => 'some_index_3', 43 | 'timestamp' => (new \DateTime('2015-02-01'))->format('U') 44 | ], 45 | [ 46 | 'id' => 'somesecondary1', 47 | 'name' => 'some_index_2', 48 | 'timestamp' => (new \DateTime('2015-01-15'))->format('U') 49 | ], 50 | ] 51 | ], 52 | 'some_index_1' => [], 53 | 'some_index_2' => [], 54 | 'some_index_3' => [], 55 | ]); 56 | $dataSet->setMappings([ 57 | '.config_test_configuration' => ConfigurationIndex::$elasticSearchConfigurationMapping['mappings'] 58 | ]); 59 | return $dataSet; 60 | } 61 | 62 | public function testGetPrimaryIndex() 63 | { 64 | $this->assertEquals('some_index_1', $this->indexRotator->getPrimaryIndex()); 65 | } 66 | 67 | /** 68 | * @expectedException Zumba\ElasticsearchRotator\Exception\MissingPrimaryIndex 69 | */ 70 | public function testFailingToRetreivePrimaryIndex() { 71 | // Remove the fixtured primary index. 72 | $this->elasticSearchTearDown(); 73 | $this->indexRotator->getPrimaryIndex(); 74 | } 75 | 76 | public function testSetPrimaryIndex() 77 | { 78 | $this->indexRotator->setPrimaryIndex('some_index_2'); 79 | $this->assertEquals('some_index_2', $this->indexRotator->getPrimaryIndex()); 80 | } 81 | 82 | public function testCopyPrimaryIndexToSecondary() 83 | { 84 | $id = $this->indexRotator->copyPrimaryIndexToSecondary(); 85 | $this->assertNotEquals('primary', $id); 86 | $results = $this->getElasticSearchConnector()->getConnection()->get([ 87 | 'index' => '.config_test_configuration', 88 | 'type' => 'configuration', 89 | 'id' => $id 90 | ]); 91 | $this->assertEquals('some_index_1', $results['_source']['name']); 92 | } 93 | 94 | /** 95 | * @dataProvider secondaryIndexConditionProvider 96 | */ 97 | public function testGetSecondaryIndices($olderThan, $expectedIndices) 98 | { 99 | $results = $this->indexRotator->getSecondaryIndices($olderThan); 100 | $this->assertEmpty(array_diff($results, $expectedIndices)); 101 | } 102 | 103 | /** 104 | * @dataProvider secondaryIndexConditionProvider 105 | */ 106 | public function testGetSecondaryIndicesIncludeIds($olderThan, $expectedIndices, $expectedConfigurationIds) 107 | { 108 | $results = $this->indexRotator->getSecondaryIndices($olderThan, IndexRotator::SECONDARY_INCLUDE_ID); 109 | $this->assertEmpty(array_diff(array_column($results, 'index'), $expectedIndices)); 110 | $this->assertEmpty(array_diff(array_column($results, 'configuration_id'), $expectedConfigurationIds)); 111 | } 112 | 113 | /** 114 | * @dataProvider secondaryIndexConditionProvider 115 | */ 116 | public function testDeleteSecondaryIndices($olderThan, $expectedToDelete) 117 | { 118 | $results = $this->indexRotator->deleteSecondaryIndices($olderThan); 119 | $this->assertEmpty(array_diff(array_keys($results), $expectedToDelete)); 120 | foreach ($results as $result) { 121 | $this->assertEquals(['acknowledged' => true], $result['index']); 122 | $this->assertTrue($result['config']['found']); 123 | } 124 | } 125 | 126 | public function secondaryIndexConditionProvider() 127 | { 128 | return [ 129 | 'all' => [null, ['some_index_3', 'some_index_2'], ['somesecondary2', 'somesecondary1']], 130 | 'older than 2015-02-01' => [new \DateTime('2015-02-01'), ['some_index_2'], ['somesecondary1']], 131 | 'older than 2015-01-01' => [new \DateTime('2015-01-01'), [], []] 132 | ]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Strategy/AliasStrategyTest.php: -------------------------------------------------------------------------------- 1 | aliasStrategy = new AliasStrategy($this->getElasticSearchConnector()->getConnection(), null, [ 13 | 'alias_name' => 'some_alias', 14 | 'index_pattern' => 'some_index_*' 15 | ]); 16 | } 17 | 18 | public function getElasticSearchConnector() 19 | { 20 | if (empty($this->connection)) { 21 | $clientBuilder = \Elasticsearch\ClientBuilder::create(); 22 | if (getenv('ES_TEST_HOST')) { 23 | $clientBuilder->setHosts([getenv('ES_TEST_HOST')]); 24 | } 25 | $this->connection = new Connector($clientBuilder->build()); 26 | } 27 | return $this->connection; 28 | } 29 | 30 | public function getElasticSearchDataSet() 31 | { 32 | $dataSet = new DataSet($this->getElasticSearchConnector()); 33 | $dataSet->setFixture([ 34 | 'some_index_1' => [], 35 | 'some_index_2' => [], 36 | ]); 37 | return $dataSet; 38 | } 39 | 40 | public function testGetPrimaryIndex() 41 | { 42 | // Assume an alias already exists. 43 | $this->getElasticSearchConnector()->getConnection()->indices()->updateAliases([ 44 | 'body' => [ 45 | 'actions' => [[ 46 | 'add' => [ 47 | 'index' => 'some_index_1', 48 | 'alias' => 'some_alias' 49 | ] 50 | ]] 51 | ] 52 | ]); 53 | $this->assertEquals('some_index_1', $this->aliasStrategy->getPrimaryIndex()); 54 | } 55 | 56 | /** 57 | * @expectedException Zumba\ElasticsearchRotator\Exception\MissingPrimaryIndex 58 | */ 59 | public function testFailingToRetreivePrimaryIndex() { 60 | $this->aliasStrategy->getPrimaryIndex(); 61 | } 62 | 63 | public function testSetPrimaryIndex() 64 | { 65 | $this->aliasStrategy->setPrimaryIndex('some_index_2'); 66 | $this->assertEquals('some_index_2', $this->aliasStrategy->getPrimaryIndex()); 67 | } 68 | } 69 | --------------------------------------------------------------------------------