Skip to content

Commit c47803c

Browse files
committed
Add timestamps to both Flag and Gate
This is an improvement on [tompave#195](tompave#195). It adds the inserted_at and updated_at timestamps to the Gate structs, as well as a `last_modified_at` on the Flag itself which is the max of all gates' updated_at timestamps.
1 parent 26c18e5 commit c47803c

10 files changed

Lines changed: 398 additions & 159 deletions

File tree

lib/fun_with_flags/flag.ex

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,29 @@ defmodule FunWithFlags.Flag do
77

88
alias FunWithFlags.Gate
99

10-
defstruct [name: nil, gates: []]
11-
@type t :: %FunWithFlags.Flag{name: atom, gates: [FunWithFlags.Gate.t]}
12-
@typep options :: Keyword.t
10+
defstruct name: nil, gates: [], last_modified_at: nil
1311

12+
@type t :: %FunWithFlags.Flag{
13+
name: atom,
14+
gates: [FunWithFlags.Gate.t()],
15+
last_modified_at: DateTime.t() | nil
16+
}
17+
@typep options :: Keyword.t()
1418

1519
@doc false
1620
def new(name, gates \\ []) when is_atom(name) do
17-
%__MODULE__{name: name, gates: gates}
21+
last_modified_at =
22+
gates
23+
|> Enum.map(& &1.updated_at)
24+
|> Enum.reject(&is_nil/1)
25+
|> case do
26+
[] -> nil
27+
dates -> Enum.max(dates)
28+
end
29+
30+
%__MODULE__{name: name, gates: gates, last_modified_at: last_modified_at}
1831
end
1932

20-
2133
@doc false
2234
@spec enabled?(t, options) :: boolean
2335
def enabled?(flag, options \\ [])

lib/fun_with_flags/gate.ex

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,17 @@ defmodule FunWithFlags.Gate do
1616
defexception [:message]
1717
end
1818

19+
defstruct [:type, :for, :enabled, inserted_at: nil, updated_at: nil]
1920

20-
defstruct [:type, :for, :enabled]
21-
@type t :: %FunWithFlags.Gate{type: atom, for: (nil | String.t), enabled: boolean}
22-
@typep options :: Keyword.t
21+
@type t :: %FunWithFlags.Gate{
22+
type: atom,
23+
for: nil | String.t(),
24+
enabled: boolean,
25+
inserted_at: DateTime.t() | nil,
26+
updated_at: DateTime.t() | nil
27+
}
28+
29+
@typep options :: Keyword.t()
2330

2431
@doc false
2532
@spec new(atom, boolean | float) :: t

lib/fun_with_flags/store/persistent/ecto/record.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule FunWithFlags.Store.Persistent.Ecto.Record do
1313
field :gate_type, :string
1414
field :target, :string
1515
field :enabled, :boolean
16+
timestamps(type: :utc_datetime)
1617
end
1718

1819
@fields [:flag_name, :gate_type, :target, :enabled]

lib/fun_with_flags/store/serializer/ecto.ex

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,24 @@ defmodule FunWithFlags.Store.Serializer.Ecto do
2626
def deserialize_gate(_flag_name, _record), do: nil
2727

2828

29-
defp do_deserialize_gate(%Record{gate_type: "boolean", enabled: enabled}) do
30-
%Gate{type: :boolean, for: nil, enabled: enabled}
29+
defp do_deserialize_gate(%Record{gate_type: "boolean", enabled: enabled} = record) do
30+
%Gate{type: :boolean, for: nil, enabled: enabled, inserted_at: record.inserted_at, updated_at: record.updated_at}
3131
end
3232

33-
defp do_deserialize_gate(%Record{gate_type: "actor", enabled: enabled, target: target}) do
34-
%Gate{type: :actor, for: target, enabled: enabled}
33+
defp do_deserialize_gate(%Record{gate_type: "actor", enabled: enabled, target: target} = record) do
34+
%Gate{type: :actor, for: target, enabled: enabled, inserted_at: record.inserted_at, updated_at: record.updated_at}
3535
end
3636

37-
defp do_deserialize_gate(%Record{gate_type: "group", enabled: enabled, target: target}) do
38-
%Gate{type: :group, for: target, enabled: enabled}
37+
defp do_deserialize_gate(%Record{gate_type: "group", enabled: enabled, target: target} = record) do
38+
%Gate{type: :group, for: target, enabled: enabled, inserted_at: record.inserted_at, updated_at: record.updated_at}
3939
end
4040

41-
defp do_deserialize_gate(%Record{gate_type: "percentage", target: "time/" <> ratio_s}) do
42-
%Gate{type: :percentage_of_time, for: parse_float(ratio_s), enabled: true}
41+
defp do_deserialize_gate(%Record{gate_type: "percentage", target: "time/" <> ratio_s} = record) do
42+
%Gate{type: :percentage_of_time, for: parse_float(ratio_s), enabled: true, inserted_at: record.inserted_at, updated_at: record.updated_at}
4343
end
4444

45-
defp do_deserialize_gate(%Record{gate_type: "percentage", target: "actors/" <> ratio_s}) do
46-
%Gate{type: :percentage_of_actors, for: parse_float(ratio_s), enabled: true}
45+
defp do_deserialize_gate(%Record{gate_type: "percentage", target: "actors/" <> ratio_s} = record) do
46+
%Gate{type: :percentage_of_actors, for: parse_float(ratio_s), enabled: true, inserted_at: record.inserted_at, updated_at: record.updated_at}
4747
end
4848

4949
def to_atom(atm) when is_atom(atm), do: atm
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule FunWithFlags.Dev.EctoRepo.Migrations.CreateFeatureFlagsTable do
2+
use Ecto.Migration
3+
4+
# This migration assumes the default table name of "fun_with_flags_toggles"
5+
# is being used. If you have overridden that via configuration, you should
6+
# change this migration accordingly.
7+
8+
def change do
9+
alter table(:fun_with_flags_toggles) do
10+
add :inserted_at, :utc_datetime, null: false, default: fragment("CURRENT_TIMESTAMP")
11+
add :updated_at, :utc_datetime, null: false, default: fragment("CURRENT_TIMESTAMP")
12+
end
13+
end
14+
end

test/fun_with_flags/notifications/phoenix_pubsub_test.exs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,17 +169,21 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do
169169
setup do
170170
name = unique_atom()
171171
gate = %Gate{type: :boolean, enabled: true}
172-
stored_flag = %Flag{name: name, gates: [gate]}
172+
expected_stored_flag = %Flag{name: name, gates: [gate]}
173173

174174
gate2 = %Gate{type: :boolean, enabled: false}
175175
cached_flag = %Flag{name: name, gates: [gate2]}
176176

177-
{:ok, ^stored_flag} = Config.persistence_adapter.put(name, gate)
177+
{:ok, stored_flag} = Config.persistence_adapter.put(name, gate)
178+
assert drop_timestamps(stored_flag) == expected_stored_flag
179+
178180
assert_with_retries(fn ->
179-
{:ok, ^cached_flag} = Cache.put(cached_flag)
181+
{:ok, put} = Cache.put(cached_flag)
182+
assert drop_timestamps(put) == drop_timestamps(cached_flag)
180183
end)
181184

182-
assert {:ok, ^stored_flag} = Config.persistence_adapter.get(name)
185+
{:ok, persisted} = Config.persistence_adapter.get(name)
186+
assert drop_timestamps(persisted) == expected_stored_flag
183187
assert {:ok, ^cached_flag} = Cache.get(name)
184188

185189
wait_until_pubsub_is_ready!()
@@ -190,7 +194,7 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do
190194
# This should be in `setup` but in there it produces a compiler warning because
191195
# the two variables will never match (duh).
192196
test "verify test setup", %{cached_flag: cached_flag, stored_flag: stored_flag} do
193-
refute match? ^stored_flag, cached_flag
197+
refute match? ^stored_flag, drop_timestamps(cached_flag)
194198
end
195199

196200

@@ -203,7 +207,8 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do
203207
Phoenix.PubSub.broadcast!(client, channel, message)
204208

205209
assert_with_retries(fn ->
206-
assert {:ok, ^cached_flag} = Cache.get(name)
210+
assert {:ok, put} = Cache.get(name)
211+
assert drop_timestamps(put) == drop_timestamps(cached_flag)
207212
end)
208213
end
209214

@@ -220,8 +225,17 @@ defmodule FunWithFlags.Notifications.PhoenixPubSubTest do
220225
Phoenix.PubSub.broadcast!(client, channel, message)
221226

222227
assert_with_retries(fn ->
223-
assert {:ok, ^stored_flag} = Cache.get(name)
228+
assert {:ok, result} = Cache.get(name)
229+
assert drop_timestamps(result) == drop_timestamps(stored_flag)
224230
end)
225231
end
226232
end
233+
234+
defp drop_timestamps(%FunWithFlags.Gate{} = gate) do
235+
%{gate | inserted_at: nil, updated_at: nil}
236+
end
237+
238+
defp drop_timestamps(%FunWithFlags.Flag{} = flag) do
239+
%{flag | last_modified_at: nil, gates: Enum.map(flag.gates, &drop_timestamps/1)}
240+
end
227241
end

test/fun_with_flags/simple_store_test.exs

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,22 @@ defmodule FunWithFlags.SimpleStoreTest do
2626
assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name)
2727

2828
SimpleStore.put(name, gate)
29-
assert {:ok, %Flag{name: ^name, gates: [^gate]}} = SimpleStore.lookup(name)
29+
{:ok, result} = SimpleStore.lookup(name)
30+
assert %Flag{name: ^name} = result
31+
assert [persisted_gate] = result.gates
32+
assert drop_timestamps(persisted_gate) == gate
3033

3134
gate2 = %Gate{gate | enabled: false}
3235
SimpleStore.put(name, gate2)
33-
assert {:ok, %Flag{name: ^name, gates: [^gate2]}} = SimpleStore.lookup(name)
34-
refute match? {:ok, %Flag{name: ^name, gates: [^gate]}}, SimpleStore.lookup(name)
36+
{:ok, result2} = SimpleStore.lookup(name)
37+
assert %Flag{name: ^name} = result2
38+
assert [persisted_gate2] = result2.gates
39+
assert drop_timestamps(persisted_gate2) == gate2
3540
end
3641

3742
test "put() returns the tuple {:ok, %Flag{}}", %{name: name, gate: gate, flag: flag} do
38-
assert {:ok, ^flag} = SimpleStore.put(name, gate)
43+
{:ok, result} = SimpleStore.put(name, gate)
44+
assert drop_timestamps(result) == flag
3945
end
4046

4147
@tag :telemetry
@@ -91,27 +97,45 @@ defmodule FunWithFlags.SimpleStoreTest do
9197
SimpleStore.put(name, bool_gate)
9298
SimpleStore.put(name, group_gate)
9399
{:ok, flag} = SimpleStore.lookup(name)
94-
assert %Flag{name: ^name, gates: [^bool_gate, ^group_gate]} = flag
100+
assert %Flag{name: ^name} = flag
101+
assert [persisted_bool, persisted_group] = flag.gates
102+
assert drop_timestamps(persisted_bool) == bool_gate
103+
assert drop_timestamps(persisted_group) == group_gate
95104

96-
{:ok, name: name, bool_gate: bool_gate, group_gate: group_gate}
105+
{:ok, name: name, bool_gate: persisted_bool, group_gate: persisted_group}
97106
end
98107

99108
test "delete(flag_name, gate) can change the value of a flag", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do
100-
assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = SimpleStore.lookup(name)
109+
{:ok, flag} = SimpleStore.lookup(name)
110+
assert %Flag{name: ^name} = flag
111+
assert length(flag.gates) == 2
101112

102113
SimpleStore.delete(name, bool_gate)
103-
assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.lookup(name)
114+
{:ok, flag2} = SimpleStore.lookup(name)
115+
assert %Flag{name: ^name} = flag2
116+
assert [remaining_gate] = flag2.gates
117+
assert drop_timestamps(remaining_gate) == drop_timestamps(group_gate)
118+
104119
SimpleStore.delete(name, group_gate)
105120
assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name)
106121
end
107122

108123
test "delete(flag_name, gate) returns the tuple {:ok, %Flag{}}", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do
109-
assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.delete(name, bool_gate)
124+
{:ok, result} = SimpleStore.delete(name, bool_gate)
125+
assert %Flag{name: ^name} = result
126+
assert [remaining_gate] = result.gates
127+
assert drop_timestamps(remaining_gate) == drop_timestamps(group_gate)
110128
end
111129

112130
test "deleting is safe and idempotent", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do
113-
assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.delete(name, bool_gate)
114-
assert {:ok, %Flag{name: ^name, gates: [^group_gate]}} = SimpleStore.delete(name, bool_gate)
131+
{:ok, result1} = SimpleStore.delete(name, bool_gate)
132+
assert [g1] = result1.gates
133+
assert drop_timestamps(g1) == drop_timestamps(group_gate)
134+
135+
{:ok, result2} = SimpleStore.delete(name, bool_gate)
136+
assert [g2] = result2.gates
137+
assert drop_timestamps(g2) == drop_timestamps(group_gate)
138+
115139
assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.delete(name, group_gate)
116140
assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.delete(name, group_gate)
117141
end
@@ -169,13 +193,18 @@ defmodule FunWithFlags.SimpleStoreTest do
169193
SimpleStore.put(name, bool_gate)
170194
SimpleStore.put(name, group_gate)
171195
{:ok, flag} = SimpleStore.lookup(name)
172-
assert %Flag{name: ^name, gates: [^bool_gate, ^group_gate]} = flag
196+
assert %Flag{name: ^name} = flag
197+
assert [persisted_bool, persisted_group] = flag.gates
198+
assert drop_timestamps(persisted_bool) == bool_gate
199+
assert drop_timestamps(persisted_group) == group_gate
173200

174-
{:ok, name: name, bool_gate: bool_gate, group_gate: group_gate}
201+
{:ok, name: name, bool_gate: persisted_bool, group_gate: persisted_group}
175202
end
176203

177-
test "delete(flag_name) will reset all the flag gates", %{name: name, bool_gate: bool_gate, group_gate: group_gate} do
178-
assert {:ok, %Flag{name: ^name, gates: [^bool_gate, ^group_gate]}} = SimpleStore.lookup(name)
204+
test "delete(flag_name) will reset all the flag gates", %{name: name} do
205+
{:ok, flag} = SimpleStore.lookup(name)
206+
assert %Flag{name: ^name} = flag
207+
assert length(flag.gates) == 2
179208

180209
SimpleStore.delete(name)
181210
assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name)
@@ -246,7 +275,10 @@ defmodule FunWithFlags.SimpleStoreTest do
246275

247276
assert {:ok, %Flag{name: ^name, gates: []}} = SimpleStore.lookup(name)
248277
SimpleStore.put(name, gate)
249-
assert {:ok, %Flag{name: ^name, gates: [^gate]}} = SimpleStore.lookup(name)
278+
{:ok, result} = SimpleStore.lookup(name)
279+
assert %Flag{name: ^name} = result
280+
assert [persisted_gate] = result.gates
281+
assert drop_timestamps(persisted_gate) == gate
250282
end
251283

252284
@tag :telemetry
@@ -327,12 +359,14 @@ defmodule FunWithFlags.SimpleStoreTest do
327359
{:ok, result} = SimpleStore.all_flags()
328360
assert 3 = length(result)
329361

362+
result_without_timestamps = Enum.map(result, &drop_timestamps/1)
363+
330364
for flag <- [
331365
%Flag{name: name1, gates: [g_1a, g_1b, g_1c]},
332366
%Flag{name: name2, gates: [g_2a, g_2b]},
333367
%Flag{name: name3, gates: [g_3a]}
334368
] do
335-
assert flag in result
369+
assert flag in result_without_timestamps
336370
end
337371
end
338372

@@ -531,4 +565,12 @@ defmodule FunWithFlags.SimpleStoreTest do
531565
end
532566
end
533567
end
568+
569+
defp drop_timestamps(%Gate{} = gate) do
570+
%{gate | inserted_at: nil, updated_at: nil}
571+
end
572+
573+
defp drop_timestamps(%Flag{} = flag) do
574+
%{flag | last_modified_at: nil, gates: Enum.map(flag.gates, &drop_timestamps/1)}
575+
end
534576
end

0 commit comments

Comments
 (0)