Skip to content

Commit 5ad14a6

Browse files
authored
Merge pull request #79 from WTTJ/feat/add-conditional-deploy-to-replicas
feat: added "if" option to replica for conditional deployment
2 parents c74c89c + 6586b65 commit 5ad14a6

8 files changed

+186
-14
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## v0.8.3 - 2024-10-18
4+
5+
New `if` option for replicas which decides if they should be updated or not.
6+
7+
- **If not provided**, the replica will be updated (so no impact on existing configurations)
8+
- Must be `nil|true|false` or the name (atom) of a arity-0 func which returns a boolean
9+
- If provided, the replica will be updated only if the value is `true` or the function returns `true`
10+
11+
Useful for Algolia's A/B testing which requires replicas and only deploy them in production.
12+
313
## v0.8.2 - 2024-09-16
414

515
- Upgrading all dependencies

CONTRIBUTING.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Then run `mix test` to run the tests.
2525
## CI/CD
2626

2727
We use CircleCI to:
28+
2829
- Run the code_analysis (format/credo)
2930
- Check for vulnerabilities
3031
- Run the tests

lib/algoliax/exceptions.ex

+13
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ defmodule Algoliax.MissingIndexNameError do
3131
end
3232
end
3333

34+
defmodule Algoliax.InvalidReplicaConfigurationError do
35+
@moduledoc "Raise when a replica has an invalid configuration"
36+
37+
defexception [:message]
38+
39+
@impl true
40+
def exception(%{index_name: index_name, error: error}) do
41+
%__MODULE__{
42+
message: "Invalid configuration for replica #{index_name}: #{error}"
43+
}
44+
end
45+
end
46+
3447
defmodule Algoliax.AlgoliaApiError do
3548
@moduledoc "Raise Algolia API error"
3649

lib/algoliax/resources/index.ex

+48-10
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,28 @@ defmodule Algoliax.Resources.Index do
2424
end
2525

2626
def replicas_names(module, settings, replica_index) do
27-
settings
28-
|> replicas_settings()
27+
module
28+
|> replicas_settings(settings)
2929
|> Enum.map(fn replica_settings ->
3030
index_name(module, replica_settings)
3131
|> Enum.at(replica_index)
3232
end)
3333
end
3434

35-
def replicas_settings(settings) do
36-
replicas = Keyword.get(settings, :replicas, [])
37-
38-
Enum.map(replicas, fn replica ->
35+
def replicas_settings(module, settings) do
36+
settings
37+
|> Keyword.get(:replicas, [])
38+
|> Enum.filter(fn replica -> should_be_updated?(module, replica) end)
39+
|> Enum.map(fn replica ->
3940
case Keyword.get(replica, :inherit, true) do
4041
true ->
4142
replica_algolia_settings = algolia_settings(replica)
42-
primary_algolia_setttings = algolia_settings(settings)
43+
primary_algolia_settings = algolia_settings(settings)
4344

4445
Keyword.put(
4546
replica,
4647
:algolia,
47-
Keyword.merge(primary_algolia_setttings, replica_algolia_settings)
48+
Keyword.merge(primary_algolia_settings, replica_algolia_settings)
4849
)
4950

5051
false ->
@@ -53,6 +54,43 @@ defmodule Algoliax.Resources.Index do
5354
end)
5455
end
5556

57+
defp should_be_updated?(module, replica) do
58+
index_name = Keyword.get(replica, :index_name, nil)
59+
60+
error_message =
61+
"`if` must be `nil|true|false` or be the name of a 0-arity func which returns a boolean."
62+
63+
value = Keyword.get(replica, :if, nil)
64+
65+
cond do
66+
# No config, defaults to true
67+
is_nil(value) ->
68+
true
69+
70+
# Boolean, use this value
71+
value == true || value == false ->
72+
value
73+
74+
# Name of a 0-arity func
75+
is_atom(value) ->
76+
if module.__info__(:functions) |> Keyword.get(value) == 0 do
77+
apply(module, value, []) == true
78+
else
79+
raise Algoliax.InvalidReplicaConfigurationError, %{
80+
index_name: index_name,
81+
error: error_message
82+
}
83+
end
84+
85+
# Any other value, raise an error
86+
true ->
87+
raise Algoliax.InvalidReplicaConfigurationError, %{
88+
index_name: index_name,
89+
error: error_message
90+
}
91+
end
92+
end
93+
5694
def get_settings(module, settings) do
5795
index_name(module, settings)
5896
|> Enum.map(fn index_name ->
@@ -80,8 +118,8 @@ defmodule Algoliax.Resources.Index do
80118
end
81119

82120
def configure_replicas(module, settings) do
83-
settings
84-
|> replicas_settings()
121+
module
122+
|> replicas_settings(settings)
85123
|> Enum.map(fn replica_settings ->
86124
configure_index(module, replica_settings)
87125
end)

mix.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Algoliax.MixProject do
22
use Mix.Project
33

44
@source_url "https://github.com/WTTJ/algoliax"
5-
@version "0.8.2"
5+
@version "0.8.3"
66

77
def project do
88
[

test/algoliax/replica_test.exs

+39-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule AlgoliaxTest.ReplicaTest do
33

44
alias Algoliax.Schemas.PeopleWithReplicas
55
alias Algoliax.Schemas.PeopleWithReplicasMultipleIndexes
6+
alias Algoliax.Schemas.PeopleWithInvalidReplicas
67

78
setup do
89
Algoliax.SettingsStore.set_settings(:algoliax_people_replicas, %{})
@@ -76,7 +77,11 @@ defmodule AlgoliaxTest.ReplicaTest do
7677
body: %{
7778
"searchableAttributes" => ["full_name"],
7879
"attributesForFaceting" => ["age"],
79-
"replicas" => ["algoliax_people_replicas_asc_en", "algoliax_people_replicas_desc_en"]
80+
"replicas" => [
81+
"algoliax_people_replicas_asc_en",
82+
"algoliax_people_replicas_desc_en",
83+
"algoliax_people_replicas_not_skipped_en"
84+
]
8085
}
8186
})
8287

@@ -85,7 +90,11 @@ defmodule AlgoliaxTest.ReplicaTest do
8590
body: %{
8691
"searchableAttributes" => ["full_name"],
8792
"attributesForFaceting" => ["age"],
88-
"replicas" => ["algoliax_people_replicas_asc_fr", "algoliax_people_replicas_desc_fr"]
93+
"replicas" => [
94+
"algoliax_people_replicas_asc_fr",
95+
"algoliax_people_replicas_desc_fr",
96+
"algoliax_people_replicas_not_skipped_fr"
97+
]
8998
}
9099
})
91100

@@ -124,6 +133,24 @@ defmodule AlgoliaxTest.ReplicaTest do
124133
"ranking" => ["desc(age)"]
125134
}
126135
})
136+
137+
assert_request("PUT", %{
138+
path: ~r/algoliax_people_replicas_not_skipped_en/,
139+
body: %{
140+
"searchableAttributes" => ["age"],
141+
"attributesForFaceting" => ["age"],
142+
"ranking" => ["asc(age)"]
143+
}
144+
})
145+
146+
assert_request("PUT", %{
147+
path: ~r/algoliax_people_replicas_not_skipped_fr/,
148+
body: %{
149+
"searchableAttributes" => ["age"],
150+
"attributesForFaceting" => ["age"],
151+
"ranking" => ["asc(age)"]
152+
}
153+
})
127154
end
128155

129156
test "save_object/1" do
@@ -524,5 +551,15 @@ defmodule AlgoliaxTest.ReplicaTest do
524551
body: %{"facetQuery" => "2"}
525552
})
526553
end
554+
555+
test "should raise error on invalid replica config" do
556+
assert_raise(
557+
Algoliax.InvalidReplicaConfigurationError,
558+
"Invalid configuration for replica algoliax_people_replicas_asc: `if` must be `nil|true|false` or be the name of a 0-arity func which returns a boolean.",
559+
fn ->
560+
PeopleWithInvalidReplicas.configure_index()
561+
end
562+
)
563+
end
527564
end
528565
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
defmodule Algoliax.Schemas.PeopleWithInvalidReplicas do
2+
@moduledoc false
3+
4+
use Algoliax.Indexer,
5+
index_name: :algoliax_people_replicas,
6+
object_id: :reference,
7+
algolia: [
8+
attributes_for_faceting: ["age"],
9+
searchable_attributes: ["full_name"],
10+
custom_ranking: ["desc(update_at)"]
11+
],
12+
replicas: [
13+
[
14+
index_name: :algoliax_people_replicas_asc,
15+
inherit: true,
16+
algolia: [
17+
searchable_attributes: ["age"],
18+
ranking: ["asc(age)"]
19+
],
20+
if: :ok
21+
]
22+
]
23+
24+
defstruct reference: nil, last_name: nil, first_name: nil, age: nil
25+
26+
def build_object(people) do
27+
%{
28+
first_name: people.first_name,
29+
last_name: people.last_name,
30+
age: people.age,
31+
updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix(),
32+
full_name: Map.get(people, :first_name, "") <> " " <> Map.get(people, :last_name, ""),
33+
nickname: Map.get(people, :first_name, "") |> String.downcase()
34+
}
35+
end
36+
end

test/support/schemas/people_with_replicas_multiple_indexes.ex

+38-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,41 @@ defmodule Algoliax.Schemas.PeopleWithReplicasMultipleIndexes do
2121
[
2222
index_name: [:algoliax_people_replicas_desc_en, :algoliax_people_replicas_desc_fr],
2323
inherit: false,
24-
algolia: [ranking: ["desc(age)"]]
24+
algolia: [ranking: ["desc(age)"]],
25+
if: true
26+
],
27+
[
28+
index_name: [:algoliax_people_replicas_skipped_en, :algoliax_people_replicas_skipped_fr],
29+
inherit: true,
30+
algolia: [
31+
searchable_attributes: ["age"],
32+
ranking: ["asc(age)"]
33+
],
34+
if: :do_not_deploy
35+
],
36+
[
37+
index_name: [
38+
:algoliax_people_replicas_skipped_too_en,
39+
:algoliax_people_replicas_skipped_too_fr
40+
],
41+
inherit: true,
42+
algolia: [
43+
searchable_attributes: ["age"],
44+
ranking: ["asc(age)"]
45+
],
46+
if: false
47+
],
48+
[
49+
index_name: [
50+
:algoliax_people_replicas_not_skipped_en,
51+
:algoliax_people_replicas_not_skipped_fr
52+
],
53+
inherit: true,
54+
algolia: [
55+
searchable_attributes: ["age"],
56+
ranking: ["asc(age)"]
57+
],
58+
if: :do_deploy
2559
]
2660
]
2761

@@ -37,4 +71,7 @@ defmodule Algoliax.Schemas.PeopleWithReplicasMultipleIndexes do
3771
nickname: Map.get(people, :first_name, "") |> String.downcase()
3872
}
3973
end
74+
75+
def do_not_deploy, do: false
76+
def do_deploy, do: true
4077
end

0 commit comments

Comments
 (0)