Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches:
- master
pull_request:
branches:
- master

jobs:
test:
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,28 @@ $indices->putAlias('books', new Alias(
$aliases = $indices->getAliases('books');
```

Apply multiple alias changes atomically when switching between versioned indexes:

```php
use DirectoryTree\OpenSearchAdapter\Indices\Alias;
use DirectoryTree\OpenSearchAdapter\Indices\AliasActions;

$actions = (new AliasActions)
->remove('books_blue', 'books')
->add('books_green', new Alias('books', isWriteIndex: true));

$indices->updateAliases($actions);
```

An old physical index can be removed in the same atomic operation:

```php
$actions = (new AliasActions)
->remove('books_blue', 'books')
->add('books_green', new Alias('books', isWriteIndex: true))
->removeIndex('books_retired');
```

## Raw Responses

Search response objects expose the original OpenSearch payload through `raw()`:
Expand Down
14 changes: 14 additions & 0 deletions src/Indices/Alias.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ class Alias
* @param string $name The alias name.
* @param array<string, mixed>|null $filter
* @param string|null $routing The optional alias routing value.
* @param bool|null $isWriteIndex Whether this is the alias write index.
*/
public function __construct(
protected string $name,
protected ?array $filter = null,
protected ?string $routing = null,
protected ?bool $isWriteIndex = null,
) {}

/**
Expand Down Expand Up @@ -46,6 +48,14 @@ public function routing(): ?string
return $this->routing;
}

/**
* Determine whether this is the alias write index.
*/
public function isWriteIndex(): ?bool
{
return $this->isWriteIndex;
}

/**
* Get the OpenSearch alias body payload.
*
Expand All @@ -63,6 +73,10 @@ public function toArray(): array
$body['filter'] = $this->filter;
}

if (! is_null($this->isWriteIndex)) {
$body['is_write_index'] = $this->isWriteIndex;
}

return $body;
}
}
78 changes: 78 additions & 0 deletions src/Indices/AliasActions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace DirectoryTree\OpenSearchAdapter\Indices;

/**
* Builds an atomic OpenSearch alias update payload.
*
* @see https://docs.opensearch.org/latest/api-reference/alias/aliases-api/
*/
class AliasActions
{
/**
* The alias actions.
*
* @var array<int, array<string, array<string, mixed>>>
*/
protected array $actions = [];

/**
* Add an alias to an index.
*/
public function add(string $index, Alias $alias): static
{
$this->actions[] = [
'add' => [
'index' => $index,
'alias' => $alias->name(),
...$alias->toArray(),
],
];

return $this;
}

/**
* Remove an alias from an index.
*/
public function remove(string $index, string $alias): static
{
$this->actions[] = [
'remove' => compact('index', 'alias'),
];

return $this;
}

/**
* Remove an index as part of the atomic alias update.
*/
public function removeIndex(string $index): static
{
$this->actions[] = [
'remove_index' => compact('index'),
];

return $this;
}

/**
* Get the configured alias actions.
*
* @return array<int, array<string, array<string, mixed>>>
*/
public function actions(): array
{
return $this->actions;
}

/**
* Get the OpenSearch update aliases body payload.
*
* @return array{actions: array<int, array<string, array<string, mixed>>>}
*/
public function toArray(): array
{
return ['actions' => $this->actions];
}
}
15 changes: 14 additions & 1 deletion src/Indices/IndexManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ public function getAliases(string $index): array
$results[$name] = new Alias(
$name,
$parameters['filter'] ?? null,
$parameters['routing'] ?? null
$parameters['routing'] ?? null,
$parameters['is_write_index'] ?? null,
);
}

Expand Down Expand Up @@ -167,4 +168,16 @@ public function deleteAlias(string $index, string $aliasName): static

return $this;
}

/**
* Atomically apply multiple alias actions.
*/
public function updateAliases(AliasActions $actions): static
{
$this->indices->updateAliases([
'body' => $actions->toArray(),
]);

return $this;
}
}
7 changes: 7 additions & 0 deletions src/Indices/IndexManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,11 @@ public function putAlias(string $index, Alias $alias): static;
* @see https://docs.opensearch.org/latest/api-reference/index-apis/alias/
*/
public function deleteAlias(string $index, string $aliasName): static;

/**
* Atomically apply multiple alias actions.
*
* @see https://docs.opensearch.org/latest/api-reference/alias/aliases-api/
*/
public function updateAliases(AliasActions $actions): static;
}
85 changes: 84 additions & 1 deletion src/Testing/Fakes/FakeIndexManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace DirectoryTree\OpenSearchAdapter\Testing\Fakes;

use DirectoryTree\OpenSearchAdapter\Indices\Alias;
use DirectoryTree\OpenSearchAdapter\Indices\AliasActions;
use DirectoryTree\OpenSearchAdapter\Indices\IndexBlueprint;
use DirectoryTree\OpenSearchAdapter\Indices\IndexManagerInterface;
use DirectoryTree\OpenSearchAdapter\Indices\Mapping;
Expand Down Expand Up @@ -70,6 +71,13 @@ class FakeIndexManager implements IndexManagerInterface
*/
protected array $aliases = [];

/**
* The atomic alias updates.
*
* @var array<int, AliasActions>
*/
protected array $aliasUpdates = [];

/**
* The deleted aliases.
*
Expand Down Expand Up @@ -153,7 +161,15 @@ public function putSettings(string $index, Settings $settings): static
public function delete(string $index): static
{
$this->deleted[] = $index;
$this->existing = array_values(array_diff($this->existing, [$index]));

$this->existing = array_values(
array_diff($this->existing, [$index])
);

$this->aliases = array_values(array_filter(
$this->aliases,
fn (array $operation) => $operation['index'] !== $index,
));

return $this;
}
Expand Down Expand Up @@ -181,6 +197,8 @@ public function getAliases(string $index): array
*/
public function putAlias(string $index, Alias $alias): static
{
$this->removeAlias($index, $alias->name());

$this->aliases[] = compact('index', 'alias');

return $this;
Expand All @@ -196,6 +214,49 @@ public function deleteAlias(string $index, string $aliasName): static
'alias' => $aliasName,
];

$this->removeAlias($index, $aliasName);

return $this;
}

/**
* Atomically apply multiple alias actions.
*/
public function updateAliases(AliasActions $actions): static
{
$this->aliasUpdates[] = $actions;

foreach ($actions->actions() as $action) {
if ($parameters = $action['add'] ?? null) {
$alias = new Alias(
$parameters['alias'],
$parameters['filter'] ?? null,
$parameters['routing'] ?? null,
$parameters['is_write_index'] ?? null,
);

$this->removeAlias($parameters['index'], $parameters['alias']);

$this->aliases[] = [
'index' => $parameters['index'],
'alias' => $alias,
];
}

if ($parameters = $action['remove'] ?? null) {
$this->deletedAliases[] = [
'index' => $parameters['index'],
'alias' => $parameters['alias'],
];

$this->removeAlias($parameters['index'], $parameters['alias']);
}

if ($parameters = $action['remove_index'] ?? null) {
$this->delete($parameters['index']);
}
}

return $this;
}

Expand Down Expand Up @@ -300,4 +361,26 @@ public function assertAliasDeleted(string $index, string $alias): static

return $this;
}

/**
* Assert that the given atomic alias update was performed.
*/
public function assertAliasesUpdated(AliasActions $actions): static
{
PHPUnit::assertContainsEquals($actions, $this->aliasUpdates);

return $this;
}

/**
* Remove an alias from the fake's current state.
*/
protected function removeAlias(string $index, string $alias): void
{
$this->aliases = array_values(
array_filter($this->aliases, fn (array $operation) => (
$operation['index'] !== $index || $operation['alias']->name() !== $alias
))
);
}
}
35 changes: 35 additions & 0 deletions tests/Integration/OpenSearchAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use DirectoryTree\OpenSearchAdapter\Documents\DocumentManager;
use DirectoryTree\OpenSearchAdapter\Documents\DocumentRouting;
use DirectoryTree\OpenSearchAdapter\Indices\Alias;
use DirectoryTree\OpenSearchAdapter\Indices\AliasActions;
use DirectoryTree\OpenSearchAdapter\Indices\IndexBlueprint;
use DirectoryTree\OpenSearchAdapter\Indices\IndexManager;
use DirectoryTree\OpenSearchAdapter\Indices\Mapping;
Expand Down Expand Up @@ -168,6 +169,40 @@
}
});

it('atomically switches an alias between physical indexes', function (): void {
$client = openSearchClient();
$indices = new IndexManager($client);

$prefix = sprintf('adapter_integration_%s', bin2hex(random_bytes(4)));
$blue = $prefix.'_blue';
$green = $prefix.'_green';
$alias = $prefix.'_alias';

try {
$indices->create(new IndexBlueprint($blue));
$indices->create(new IndexBlueprint($green));
$indices->putAlias($blue, new Alias($alias, isWriteIndex: true));

$indices->updateAliases(
(new AliasActions)
->remove($blue, $alias)
->add($green, new Alias($alias, isWriteIndex: true)),
);

expect($indices->getAliases($blue))->not->toHaveKey($alias)
->and($indices->getAliases($green))->toHaveKey($alias)
->and($indices->getAliases($green)[$alias]->isWriteIndex())->toBeTrue();
} finally {
if ($indices->exists($blue)) {
$indices->delete($blue);
}

if ($indices->exists($green)) {
$indices->delete($green);
}
}
});

/**
* Create an OpenSearch client for integration tests.
*/
Expand Down
Loading
Loading