/
actor.ex
167 lines (125 loc) · 4.08 KB
/
actor.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
defprotocol FunWithFlags.Actor do
@moduledoc ~S"""
Implement this protocol to provide actors.
Actor gates allows you to enable or disable a flag for one or more entities.
For example, in web applications it's common to use a `%User{}` struct or
equivalent as an actor, or perhaps the data used to represent the current
country for an HTTP request.
This can be useful to showcase a work-in-progress feature to someone, to
gradually rollout a functionality by country, or to dynamically disable some
features in some contexts (e.g. a deploy introduces a critical error that
only happens in one specific country).
Actor gates take precedence over the others, both when they're enabled and
when they're disabled. They can be considered as toggle overrides.
In order to be used as an actor, an entity must implement
the `FunWithFlags.Actor` protocol. This can be implemented for custom structs
or literally any other type.
## Examples
This protocol is typically implemented for some application structure.
defmodule MyApp.User do
defstruct [:id, :name]
end
defimpl FunWithFlags.Actor, for: MyApp.User do
def id(%{id: id}) do
"user:#{id}"
end
end
bruce = %User{id: 1, name: "Bruce"}
alfred = %User{id: 2, name: "Alfred"}
FunWithFlags.Actor.id(bruce)
"user:1"
FunWithFlags.Actor.id(alfred)
"user:2"
FunWithFlags.enable(:batmobile, for_actor: bruce)
but it can also be implemented for the builtin types:
defimpl FunWithFlags.Actor, for: Map do
def id(%{actor_id: actor_id}) do
"map:#{actor_id}"
end
def id(map) do
map
|> inspect()
|> (&:crypto.hash(:md5, &1)).()
|> Base.encode16
|> (&"map:#{&1}").()
end
end
defimpl FunWithFlags.Actor, for: BitString do
def id(str) do
"string:#{str}"
end
end
FunWithFlags.Actor.id(%{actor_id: "bar"})
"map:bar"
FunWithFlags.Actor.id(%{foo: "bar"})
"map:E0BB5BA6873E3AC34B0B6928190C1F2B"
FunWithFlags.Actor.id("foobar")
"string:foobar"
FunWithFlags.disable(:foobar, for_actor: %{actor_id: "just a map"})
FunWithFlags.enable(:foobar, for_actor: "just a string")
Actor identifiers must be globally unique binaries. Since supporting multiple
kinds of actors is a common requirement, all the examples use the common
technique of namespacing the IDs:
defimpl FunWithFlags.Actor, for: MyApp.User do
def id(user) do
"user:#{user.id}"
end
end
defimpl FunWithFlags.Actor, for: MyApp.Country do
def id(country) do
"country:#{country.iso3166}"
end
end
"""
@doc """
Should return a globally unique binary.
## Example
iex> FunWithFlags.Actor.id(%FunWithFlags.TestUser{id: 313})
"user:313"
"""
@spec id(term) :: binary
def id(actor)
end
defmodule FunWithFlags.Actor.Percentage do
@moduledoc false
alias FunWithFlags.Actor
# Combine an actor id and a flag name to get
# a score. The flag name must be included to
# ensure that the same actors get different
# scores for different flags, but with
# deterministic and predictable results.
#
@spec score(term, atom) :: float
def score(actor, flag_name) do
blob = Actor.id(actor) <> to_string(flag_name)
_actor_score(blob)
end
# first 16 bits:
# 2 ** 16 = 65_536
#
# %_ratio : 1.0 = 16_bits : 65_536
#
defp _actor_score(string) do
<<score :: size(16), _rest :: binary>> = :crypto.hash(:sha256, string)
score / 65_536
end
# To verify that the distribution is uniform
#
def distributions(count, flag_name) do
key_fun = fn(i) ->
a = %{actor_id: i}
score = FunWithFlags.Actor.Percentage.score(a, flag_name)
round(score * 100)
end
1..count
|> Enum.group_by(key_fun)
|> Enum.map(fn({perc, items}) ->
{perc, length(items)}
end)
|> Enum.sort()
|> Enum.each(fn({perc, count}) ->
IO.puts("#{perc} - #{count}")
end)
nil
end
end