-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuffer.ex
More file actions
310 lines (242 loc) · 9.79 KB
/
Copy pathbuffer.ex
File metadata and controls
310 lines (242 loc) · 9.79 KB
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
defmodule Buffer do
@moduledoc """
A simple data buffer that can be added directly to a supervision tree.
"""
alias Buffer.{Server, Stream}
@supervisor_fields [:name, :partitioner, :partitions]
################################
# Public API
################################
@doc false
@spec child_spec(keyword()) :: map()
def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
end
@doc """
Starts a `Buffer` process linked to the current process.
## Options
* `:flush_callback` - The function invoked to flush a buffer. Must have an arity of 2
where the first arg is a list of items and the second arg is a keyword list of flush
opts. This option is required.
* `:name` - The name of the buffer. Must be an atom or a `:via` tuple. This option is
required.
* `:buffer_timeout` - The maximum time (in ms) allowed between flushes of the buffer.
Defaults to `:infinity`
* `:flush_meta` - Any term to be included in the flush opts under the `:meta` key.
* `:jitter_rate` - The rate at which limits are jittered between partitions. Limits are not
jittered by default.
* `:max_length` - The maximum number of items allowed in the buffer before being flushed.
By default, this limit is `:infinity`.
* `:max_size` - The maximum size (in bytes) of the buffer before being flushed. By default,
this limit is `:infinity`.
* `:ordering` - The order in which buffered items are returned. The options are `:fifo`
(first-in-first-out) and `:lifo` (last-in-first-out). Defaults to `:fifo`. Using `:lifo`
provides a small performance improvement by skipping the list reversal.
* `:partitioner` - The method by which items are inserted into different partitions. The
options are `:rotating` and `:random` and the former is the default.
* `:partitions` - The number of buffer partitions.
* `:size_callback` - The function invoked to determine item size. Must have an arity of 1
where the only arg is an inserted item and must return a non-negative integer representing
the size of the item. Default size callback is `byte_size` (predicated by `term_to_binary`
if applicable).
"""
@spec start_link(keyword()) :: Supervisor.on_start()
def start_link(opts) do
opts = Keyword.put_new(opts, :name, __MODULE__)
with {:ok, partitions} <- validate_partitions(opts),
{:ok, partitioner} <- validate_partitioner(opts),
{:ok, _} = result <- do_start_link(opts) do
partitioner = build_partitioner(partitions, partitioner)
name = Keyword.get(opts, :name)
put_buffer(name, partitioner, partitions)
result
end
end
@doc """
Lazily chunks an enumerable based on `Buffer` flush conditions.
## Options
* `:max_length` - The maximum number of items in a chunk. By default, this limit is `:infinity`.
* `:max_size` - The maximum size (in bytes) of the items in a chunk. By default, this limit is
`:infinity`.
* `:size_callback` - The function invoked to determine the size of an item. Default size callback
is `byte_size` (predicated by `term_to_binary` if applicable).
"""
@spec chunk(Enumerable.t(), keyword()) :: {:ok, Enumerable.t()} | {:error, atom()}
defdelegate chunk(enum, opts \\ []), to: Stream
@doc """
Lazily chunks an enumerable based on `Buffer` flush conditions and raises an `ArgumentError`
with invalid options.
For information on options, see `chunk/2`.
"""
@spec chunk!(Enumerable.t(), keyword()) :: Enumerable.t()
defdelegate chunk!(enum, opts \\ []), to: Stream
@doc """
Dumps the contents of the given `Buffer` to a list, bypassing a flush
callback and resetting the buffer.
## Options
* `:partition` - The specific partition to dump. Defaults to `:all`.
* `:timeout` - The timeout for the GenServer call in milliseconds. Defaults to `5000`.
"""
@spec dump(GenServer.server(), keyword()) :: {:ok, list()} | {:error, atom()}
def dump(buffer, opts \\ []) do
timeout = Keyword.get(opts, :timeout, 5000)
with {:ok, {_, parts}} <- fetch_buffer(buffer),
{:ok, part} <- validate_partition(opts, parts) do
case part do
:all -> {:ok, Enum.reduce(1..parts, [], &(&2 ++ do_dump_part(buffer, &1 - 1, timeout)))}
part -> {:ok, do_dump_part(buffer, part, timeout)}
end
end
end
@doc """
Flushes the given `Buffer`, regardless of whether or not the flush conditions
have been met.
## Options
* `:async` - Whether or not the flush will be async. Defaults to `true`.
* `:partition` - The specific partition to flush. Defaults to `:all`.
* `:timeout` - The timeout for the GenServer call in milliseconds. Defaults to `5000`.
"""
@spec flush(GenServer.server(), keyword()) :: :ok | {:error, atom()}
def flush(buffer, opts \\ []) do
with {:ok, {_, parts}} <- fetch_buffer(buffer),
{:ok, part} <- validate_partition(opts, parts) do
case part do
:all -> Enum.each(1..parts, &do_flush_part(buffer, &1 - 1, opts))
part -> do_flush_part(buffer, part, opts)
end
end
end
@doc """
Returns information about the given `Buffer`.
## Options
* `:partition` - The specific partition to return info for. Defaults to `:all`.
* `:timeout` - The timeout for the GenServer call in milliseconds. Defaults to `5000`.
"""
@spec info(GenServer.server(), keyword()) :: {:ok, list()} | {:error, atom()}
def info(buffer, opts \\ []) do
timeout = Keyword.get(opts, :timeout, 5000)
with {:ok, {_, parts}} <- fetch_buffer(buffer),
{:ok, part} <- validate_partition(opts, parts) do
case part do
:all -> {:ok, Enum.map(1..parts, &do_info_part(buffer, &1 - 1, timeout))}
part -> {:ok, [do_info_part(buffer, part, timeout)]}
end
end
end
@doc """
Inserts the given item into the given `Buffer`.
## Options
* `timeout` - The timeout for the GenServer call in milliseconds. Defaults to `5000`.
"""
@spec insert(GenServer.server(), term(), timeout()) :: :ok | {:error, atom()}
def insert(buffer, item, timeout \\ 5000) do
with {:ok, {partitioner, _}} <- fetch_buffer(buffer) do
do_insert(buffer, partitioner, item, timeout)
end
end
@doc """
Inserts a batch of items into the given `Buffer` and returns the number of items inserted.
## Options
* `:safe_flush` - Whether or not to flush immediately after exceeding a buffer limit.
Defaults to `true`. If set to `false`, all items in the batch will be inserted
regardless of flush conditions being met. Afterwards, if a limit has been exceeded,
the buffer will be flushed async.
* `:timeout` - The timeout for the GenServer call in milliseconds. Defaults to `5000`.
"""
@spec insert_batch(GenServer.server(), Enumerable.t(), keyword()) ::
{:ok, non_neg_integer()} | {:error, atom()}
def insert_batch(buffer, items, opts \\ []) do
with {:ok, {partitioner, _}} <- fetch_buffer(buffer) do
{:ok, do_insert_batch(buffer, partitioner, items, opts)}
end
end
################################
# Private API
################################
defguardp is_valid_part(part, parts) when part == :all or (part >= 0 and part < parts)
defp validate_partitions(opts) do
case Keyword.get(opts, :partitions, 1) do
parts when is_integer(parts) and parts > 0 -> {:ok, parts}
_ -> {:error, :invalid_partitions}
end
end
defp validate_partitioner(opts) do
case Keyword.get(opts, :partitioner, :rotating) do
partitioner when partitioner in [:random, :rotating] -> {:ok, partitioner}
_ -> {:error, :invalid_partitioner}
end
end
defp validate_partition(opts, partitions) do
case Keyword.get(opts, :partition, :all) do
part when is_valid_part(part, partitions) -> {:ok, part}
_ -> {:error, :invalid_partition}
end
end
defp do_start_link(opts) do
{sup_opts, buffer_opts} = Keyword.split(opts, @supervisor_fields)
with_args = fn [opts], part -> [Keyword.put(opts, :partition, part)] end
child_spec = {Server, buffer_opts}
sup_opts
|> Keyword.merge(with_arguments: with_args, child_spec: child_spec)
|> PartitionSupervisor.start_link()
end
defp build_partitioner(1, _), do: fn -> 0 end
defp build_partitioner(partitions, :random) do
fn -> :rand.uniform(partitions) - 1 end
end
defp build_partitioner(partitions, :rotating) do
atomics_ref = :atomics.new(1, [])
fn ->
case :atomics.add_get(atomics_ref, 1, 1) do
part when part > partitions ->
:atomics.put(atomics_ref, 1, 0)
0
part ->
part - 1
end
end
end
defp put_buffer(buffer, partitioner, partitions) do
buffer
|> build_key()
|> :persistent_term.put({partitioner, partitions})
end
defp fetch_buffer(buffer) do
buffer
|> build_key()
|> :persistent_term.get(nil)
|> case do
nil -> {:error, :not_found}
buffer -> {:ok, buffer}
end
end
defp build_key(buffer), do: {__MODULE__, buffer}
defp do_dump_part(buffer, partition, timeout) do
buffer
|> buffer_partition_name(partition)
|> Server.dump(timeout)
end
defp do_flush_part(buffer, partition, opts) do
buffer
|> buffer_partition_name(partition)
|> Server.flush(opts)
end
defp do_info_part(buffer, partition, timeout) do
buffer
|> buffer_partition_name(partition)
|> Server.info(timeout)
end
defp do_insert(buffer, partitioner, item, timeout) do
buffer
|> buffer_partition_name(partitioner.())
|> Server.insert(item, timeout)
end
defp do_insert_batch(buffer, partitioner, items, opts) do
buffer
|> buffer_partition_name(partitioner.())
|> Server.insert_batch(items, opts)
end
defp buffer_partition_name(buffer, partition) do
{:via, PartitionSupervisor, {buffer, partition}}
end
end