├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .vimrc
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── doc
├── custom_stylesheet.css
└── overview.edoc
├── microbenchmark.erl
├── microbenchmark.escript
├── rebar.config
├── rebar.lock
├── src
├── deigma.app.src
├── deigma.erl
├── deigma_app.erl
├── deigma_event_window.erl
├── deigma_event_window_sup.erl
├── deigma_proc_reg.erl
├── deigma_sup.erl
└── deigma_util.erl
└── test
└── deigma_SUITE.erl
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: build
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | jobs:
11 | ci:
12 | name: >
13 | Run checks and tests over ${{matrix.otp_vsn}} and ${{matrix.os}}
14 | runs-on: ${{matrix.os}}
15 | container:
16 | image: erlang:${{matrix.otp_vsn}}
17 | strategy:
18 | matrix:
19 | otp_vsn: ['22.0', '22.1', '22.2', '22.3',
20 | '23.0', '23.1', '23.2', '23.3',
21 | '24.0', '24.1', '24.2', '24.3']
22 | os: [ubuntu-latest]
23 | steps:
24 | - uses: actions/checkout@v2
25 | - run: RUNNING_ON_CI=yes make check ci_test
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .rebar3
2 | _*
3 | .eunit
4 | *.o
5 | *.beam
6 | *.plt
7 | *.swp
8 | *.swo
9 | .erlang.cookie
10 | ebin
11 | log
12 | erl_crash.dump
13 | .rebar
14 | logs
15 | _build
16 | .idea
17 | *.iml
18 | rebar3.crashdump
19 | doc
20 |
--------------------------------------------------------------------------------
/.vimrc:
--------------------------------------------------------------------------------
1 | auto BufNewFile,BufRead *.config setlocal ft=erlang
2 | auto FileType erlang setlocal expandtab softtabstop=4 shiftwidth=4
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 | ## [1.2.0] - 2021-05-13
8 | ### Added
9 | - OTP 24 to CI targets
10 | ### Changed
11 | - CI from Travis to GitHub Actions
12 | ### Removed
13 | - compatibility with OTP 19
14 | - compatibility with OTP 20
15 | - compatibility with OTP 21
16 |
17 | ## [1.1.1] - 2020-05-26
18 | ### Fixed
19 | - outdated README
20 |
21 | ## [1.1.0] - 2020-05-26
22 | ### Removed
23 | - compatibility with OTP 18
24 |
25 | ## [1.0.3] - 2019-11-11
26 | ### Changed
27 | - generated documentation as to (tentatively) make it prettier
28 |
29 | ## [1.0.2] - 2019-01-19
30 | ### Fixed
31 | - unwarranted import of rebar3_hex plugin in library consumers
32 |
33 | ## [1.0.1] - 2018-06-17
34 | ### Fixed
35 | - OTP 21 compatibility
36 |
37 | ## [1.0.0] - 2018-06-17
38 | ### Added
39 | - continuous sampling of arbitrary event types within explicit categories
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2022 Guilherme Andrade
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REBAR3_URL=https://s3.amazonaws.com/rebar3/rebar3
2 |
3 | ifeq ($(wildcard rebar3),rebar3)
4 | REBAR3 = $(CURDIR)/rebar3
5 | endif
6 |
7 | ifdef RUNNING_ON_CI
8 | REBAR3 = ./rebar3
9 | else
10 | REBAR3 ?= $(shell test -e `which rebar3` 2>/dev/null && which rebar3 || echo "./rebar3")
11 | endif
12 |
13 | ifeq ($(REBAR3),)
14 | REBAR3 = $(CURDIR)/rebar3
15 | endif
16 |
17 | .PHONY: all build clean check dialyzer xref test ci_test cover console microbenchmark doc publish
18 |
19 | .NOTPARALLEL: check test
20 |
21 | all: build
22 |
23 | build: $(REBAR3)
24 | @$(REBAR3) compile
25 |
26 | $(REBAR3):
27 | wget $(REBAR3_URL) || curl -Lo rebar3 $(REBAR3_URL)
28 | @chmod a+x rebar3
29 |
30 | clean: $(REBAR3)
31 | @$(REBAR3) clean
32 |
33 | check: dialyzer xref
34 |
35 | dialyzer: $(REBAR3)
36 | @$(REBAR3) dialyzer
37 |
38 | xref: $(REBAR3)
39 | @$(REBAR3) xref
40 |
41 | test: $(REBAR3)
42 | @$(REBAR3) as test ct
43 |
44 | ci_test: $(REBAR3)
45 | @$(REBAR3) as ci_test ct
46 |
47 | cover: test
48 | @$(REBAR3) as test cover
49 |
50 | console: $(REBAR3)
51 | @$(REBAR3) as development shell --apps deigma
52 |
53 | microbenchmark: $(REBAR3)
54 | @$(REBAR3) as development shell --script microbenchmark.escript
55 |
56 | doc: $(REBAR3)
57 | @$(REBAR3) edoc
58 |
59 | README.md: doc
60 | # non-portable dirty hack follows (pandoc 2.1.1 used)
61 | # gfm: "github-flavoured markdown"
62 | pandoc --from html --to gfm doc/overview-summary.html -o README.md
63 | @tail -n +11 <"README.md" >"README.md_"
64 | @head -n -13 <"README.md_" >"README.md"
65 | @rm "README.md_"
66 |
67 | publish: $(REBAR3)
68 | @$(REBAR3) hex publish
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # deigma
2 |
3 | **This library is not under active maintenance; if you'd like to perform
4 | maintenance yourself, feel free to open an issue requesting access.**
5 |
6 | [](https://hex.pm/packages/deigma)
7 | [](https://github.com/g-andrade/deigma/actions?query=workflow%3Abuild)
8 |
9 | `deigma` is an event sampler for Erlang/OTP and Elixir.
10 |
11 | It performs sampling of reported events within continuous one second
12 | windows\[\*\].
13 |
14 | The sampling percentage is steadily adjusted so that the events that
15 | seep through are representative of what's happening in the system while
16 | not exceeding specified rate limits.
17 |
18 | The sampling percentage is also exposed in the context of each reported
19 | event, so that whichever other component that later receives the samples
20 | can perform reasonable guesses of the original population properties
21 | with limited information.
22 |
23 | \[\*\] As far as the [monotonic
24 | clock](http://erlang.org/doc/apps/erts/time_correction.html#Erlang_Monotonic_Time)
25 | resolution goes.
26 |
27 | #### Example (Erlang)
28 |
29 | There's a heavy duty web service; we want to report metrics on inbound
30 | HTTP requests to a [StatsD](https://github.com/etsy/statsd) service over
31 | UDP while minimising the risk of dropped datagrams due to an excessive
32 | amount of them.
33 |
34 | For this, we can downsample the reported metrics while determining the
35 | real sampling percentage using `deigma`.
36 |
37 | ##### 1\. Start a deigma instance
38 |
39 | ``` erlang
40 | Category = metrics,
41 | {ok, _Pid} = deigma:start(Category).
42 | ```
43 |
44 | ##### 2\. Sample events
45 |
46 | ``` erlang
47 | Category = metrics,
48 | EventType = http_request,
49 |
50 | case deigma:ask(Category, EventType) of
51 | {sample, SamplingPercentage} ->
52 | your_metrics:report(counter, EventType, +1, SamplingPercentage);
53 | {drop, _SamplingPercentage} ->
54 | ok
55 | end.
56 | ```
57 |
58 | - [`Category`](#categories) must be an atom
59 | - [`EventType`](#event-windows) can be any term
60 | - `SamplingPercentage` is a floating point number between 0.0 and 1.0
61 | representing the percentage of events that were sampled during the
62 | last 1000 milliseconds, **including** the event reported just now.
63 | - The rate limit defaults to 100 `EventType` occurences per second
64 | within a `Category`; it can be [overridden](#rate-limiting).
65 | - The function invoked each time an event gets registered can also be
66 | [customized](#custom-event-functions-and-serializability).
67 |
68 | #### Example (Elixir)
69 |
70 | Same scenario as in the Erlang example.
71 |
72 | ##### 1\. Start a deigma instance
73 |
74 | ``` elixir
75 | category = :metrics
76 | {:ok, _pid} = :deigma.start(category)
77 | ```
78 |
79 | ##### 2\. Sample events
80 |
81 | ``` elixir
82 | category = :metrics
83 | event_type = :http_request
84 |
85 | case :deigma.ask(category, event_type) do
86 | {:sample, sampling_percentage} ->
87 | YourMetrics.report(:counter, event_type, +1, sampling_percentage)
88 | {:drop, _sampling_percentage} ->
89 | :ok
90 | end
91 | ```
92 |
93 | #### Documentation and Reference
94 |
95 | Documentation and reference are hosted on
96 | [HexDocs](https://hexdocs.pm/deigma/).
97 |
98 | #### Tested setup
99 |
100 | - Erlang/OTP 22 or higher
101 | - rebar3
102 |
103 | #### Categories
104 |
105 | Each `Category` represents an independent group of events and is managed
106 | separately; categories can be launched under your own supervision tree
107 | (using `:child_spec/1` or `:start_link/1`) as well as under the `deigma`
108 | application (using `:start/1`).
109 |
110 | Categories launched under `deigma` can be stopped using `:stop/1`.
111 |
112 | #### Event windows
113 |
114 | Within the context of each `Category`, each distinct `EventType` will be
115 | handled under dedicated event windows that are owned by independent
116 | processes.
117 |
118 | These processes are created on demand as new `EventType` values get
119 | sampled, and stopped after 1000 milliseconds of inactivity.
120 |
121 | #### Rate limiting
122 |
123 | Each time a new event is reported, the rate limit is applied according
124 | to how many events were sampled so far during the previous 1000
125 | milliseconds; if the limit has been or is about to be exceeded, the
126 | event gets dropped.
127 |
128 | The default rate limit is set to 100 `EventType` occurrences per second
129 | within a `Category`. It can be overridden using `:ask` options:
130 |
131 | ``` erlang
132 | Category = metrics,
133 | EventType = http_request,
134 | MaxRate = 50,
135 |
136 | case deigma:ask(Category, EventType, [{max_rate, MaxRate}]) of
137 | {sample, SamplingPercentage} ->
138 | your_metrics:report(counter, EventType, +1, SamplingPercentage);
139 | {drop, _SamplingPercentage} ->
140 | ok
141 | end.
142 | ```
143 |
144 | #### Custom event functions and serializability
145 |
146 | The function invoked upon an event getting registered, within an event
147 | window, can be customized.
148 |
149 | This is useful if you need serializability when handling sampling
150 | decisions and percentages, at the expense of increasing the risk of the
151 | event window becoming a performance bottleneck.
152 |
153 | ``` erlang
154 | Category = metrics,
155 | EventType = http_request,
156 |
157 | deigma:ask(
158 | Category, EventType,
159 | fun (Timestamp, sample, SamplingPercentage) ->
160 | your_metrics:report(counter, EventType, +1, SamplingPercentage);
161 | (_Timestamp, drop, _SamplingPercentage) ->
162 | ok
163 | end).
164 | ```
165 |
166 | - `Timestamp` is the [monotonic
167 | timestamp](http://erlang.org/doc/man/erlang.html#monotonic_time-0),
168 | in native units, at which the event was registered
169 |
170 | In this scenario, whatever your function returns (or throws) will be
171 | what `deigma:ask` returns (or throws.)
172 |
173 | #### License
174 |
175 | MIT License
176 |
177 | Copyright (c) 2018-2022 Guilherme Andrade
178 |
179 | Permission is hereby granted, free of charge, to any person obtaining a
180 | copy of this software and associated documentation files (the
181 | "Software"), to deal in the Software without restriction, including
182 | without limitation the rights to use, copy, modify, merge, publish,
183 | distribute, sublicense, and/or sell copies of the Software, and to
184 | permit persons to whom the Software is furnished to do so, subject to
185 | the following conditions:
186 |
187 | The above copyright notice and this permission notice shall be included
188 | in all copies or substantial portions of the Software.
189 |
190 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
191 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
192 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
193 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
194 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
195 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
196 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
197 |
198 |
--------------------------------------------------------------------------------
/doc/custom_stylesheet.css:
--------------------------------------------------------------------------------
1 | /* copied and modified from standard EDoc style sheet */
2 | body {
3 | font-family: Verdana, Arial, Helvetica, sans-serif;
4 | margin-left: .25in;
5 | margin-right: .2in;
6 | margin-top: 0.2in;
7 | margin-bottom: 0.2in;
8 | color: #010d2c;
9 | background-color: #f9f9f9;
10 | max-width: 1024px;
11 | }
12 | h1,h2 {
13 | }
14 | div.navbar, h2.indextitle, h3.function, h3.typedecl {
15 | background-color: #e8e8e8;
16 | }
17 | div.navbar, h2.indextitle {
18 | background-image: linear-gradient(to right, #e8e8e8, #f9f9f9);
19 | }
20 | div.navbar {
21 | padding: 0.2em;
22 | border-radius: 3px;
23 | }
24 | h2.indextitle {
25 | padding: 0.4em;
26 | border-radius: 3px;
27 | }
28 | h3.function, h3.typedecl {
29 | display: inline;
30 | }
31 | div.spec {
32 | margin-left: 2em;
33 | background-color: #eeeeee;
34 | border-radius: 3px;
35 | }
36 | a.module {
37 | text-decoration:none
38 | }
39 | a.module:hover {
40 | background-color: #eeeeee;
41 | }
42 | ul.definitions {
43 | list-style-type: none;
44 | }
45 | ul.index {
46 | list-style-type: none;
47 | background-color: #eeeeee;
48 | }
49 |
50 | /*
51 | * Minor style tweaks
52 | */
53 | ul {
54 | list-style-type: disc;
55 | }
56 | table {
57 | border-collapse: collapse;
58 | }
59 | td {
60 | padding: 3
61 | }
62 |
63 | /*
64 | * Extra style tweaks
65 | */
66 | code, pre {
67 | background-color: #ececec;
68 | font: Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;
69 | }
70 | code {
71 | padding-left: 2px;
72 | padding-right: 2px;
73 | }
74 | pre {
75 | color: #00000;
76 | padding: 5px;
77 | border: 0.5px dotted grey;
78 | border-radius: 2px;
79 | }
80 |
--------------------------------------------------------------------------------
/doc/overview.edoc:
--------------------------------------------------------------------------------
1 | @title deigma
2 | @doc
3 |
4 | This library is not under active maintenance; if you'd like to perform maintenance yourself, feel free to open an issue requesting access.
5 |
6 |
7 |
8 |
9 |
10 |
11 | `deigma' is an event sampler for Erlang/OTP and Elixir.
12 |
13 | It performs sampling of reported events within continuous one second windows[*].
14 |
15 | The sampling percentage is steadily adjusted so that the events that seep through are representative
16 | of what's happening in the system while not exceeding specified rate limits.
17 |
18 | The sampling percentage is also exposed in the context of each reported event, so that whichever
19 | other component that later receives the samples can perform reasonable guesses of the
20 | original population properties with limited information.
21 |
22 | [*] As far as the monotonic clock resolution goes.
24 |
25 |
Example (Erlang)
26 |
27 | There's a heavy duty web service; we want to report metrics on inbound HTTP requests
28 | to a StatsD service
29 | over UDP while minimising the risk of dropped datagrams due to an excessive amount of them.
30 |
31 | For this, we can downsample the reported metrics while determining the real sampling percentage
32 | using `deigma'.
33 |
34 | 1. Start a deigma instance
35 |
36 |
37 | Category = metrics,
38 | {ok, _Pid} = deigma:start(Category).
39 |
40 |
41 | 2. Sample events
42 |
43 |
44 | Category = metrics,
45 | EventType = http_request,
46 |
47 | case deigma:ask(Category, EventType) of
48 | {sample, SamplingPercentage} ->
49 | your_metrics:report(counter, EventType, +1, SamplingPercentage);
50 | {drop, _SamplingPercentage} ->
51 | ok
52 | end.
53 |
54 |
55 |
56 | - `Category' must be an atom
57 | - `EventType' can be any term
58 | - `SamplingPercentage' is a floating point number between 0.0 and 1.0 representing
59 | the percentage of events that were sampled during the last 1000 milliseconds,
60 | including the event reported just now.
61 |
62 | - The rate limit defaults to 100 `EventType' occurences per second within a
63 | `Category'; it can be overridden.
64 |
65 | - The function invoked each time an event gets registered can also be
66 | customized.
67 |
68 |
69 |
70 | Example (Elixir)
71 |
72 | Same scenario as in the Erlang example.
73 |
74 | 1. Start a deigma instance
75 |
76 |
77 | category = :metrics
78 | {:ok, _pid} = :deigma.start(category)
79 |
80 |
81 | 2. Sample events
82 |
83 |
84 | category = :metrics
85 | event_type = :http_request
86 |
87 | case :deigma.ask(category, event_type) do
88 | {:sample, sampling_percentage} ->
89 | YourMetrics.report(:counter, event_type, +1, sampling_percentage)
90 | {:drop, _sampling_percentage} ->
91 | :ok
92 | end
93 |
94 |
95 | Documentation and Reference
96 |
97 | Documentation and reference are hosted on HexDocs.
98 |
99 | Tested setup
100 |
101 |
102 | - Erlang/OTP 22 or higher
103 | - rebar3
104 |
105 |
106 | Categories
107 |
108 | Each `Category' represents an independent group of events and is managed separately;
109 | categories can be launched under your own supervision tree (using `:child_spec/1' or
110 | `:start_link/1') as well as under the `deigma' application (using `:start/1').
111 |
112 | Categories launched under `deigma' can be stopped using `:stop/1'.
113 |
114 | Event windows
115 |
116 | Within the context of each `Category', each distinct `EventType' will be handled
117 | under dedicated event windows that are owned by independent processes.
118 |
119 | These processes are created on demand as new `EventType' values get sampled,
120 | and stopped after 1000 milliseconds of inactivity.
121 |
122 | Rate limiting
123 |
124 | Each time a new event is reported, the rate limit is applied according to
125 | how many events were sampled so far during the previous 1000 milliseconds;
126 | if the limit has been or is about to be exceeded, the event gets dropped.
127 |
128 | The default rate limit is set to 100 `EventType' occurrences per second within
129 | a `Category'. It can be overridden using `:ask' options:
130 |
131 |
132 | Category = metrics,
133 | EventType = http_request,
134 | MaxRate = 50,
135 |
136 | case deigma:ask(Category, EventType, [{max_rate, MaxRate}]) of
137 | {sample, SamplingPercentage} ->
138 | your_metrics:report(counter, EventType, +1, SamplingPercentage);
139 | {drop, _SamplingPercentage} ->
140 | ok
141 | end.
142 |
143 |
144 | Custom event functions and serializability
145 |
146 | The function invoked upon an event getting registered, within
147 | an event window, can be customized.
148 |
149 | This is useful if you need serializability when handling sampling decisions
150 | and percentages, at the expense of increasing the risk of the event
151 | window becoming a performance bottleneck.
152 |
153 |
154 | Category = metrics,
155 | EventType = http_request,
156 |
157 | deigma:ask(
158 | Category, EventType,
159 | fun (Timestamp, sample, SamplingPercentage) ->
160 | your_metrics:report(counter, EventType, +1, SamplingPercentage);
161 | (_Timestamp, drop, _SamplingPercentage) ->
162 | ok
163 | end).
164 |
165 |
166 |
167 | - `Timestamp' is the monotonic timestamp, in native units,
169 | at which the event was registered
170 |
171 |
172 | In this scenario, whatever your function returns (or throws) will be what
173 | `deigma:ask' returns (or throws.)
174 |
175 | License
176 |
177 | MIT License
178 |
179 | Copyright (c) 2018-2022 Guilherme Andrade
180 |
181 | Permission is hereby granted, free of charge, to any person obtaining a copy
182 | of this software and associated documentation files (the "Software"), to deal
183 | in the Software without restriction, including without limitation the rights
184 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
185 | copies of the Software, and to permit persons to whom the Software is
186 | furnished to do so, subject to the following conditions:
187 |
188 | The above copyright notice and this permission notice shall be included in all
189 | copies or substantial portions of the Software.
190 |
191 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
192 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
193 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
194 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
195 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
196 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
197 | SOFTWARE.
198 |
199 | @end
200 |
--------------------------------------------------------------------------------
/microbenchmark.erl:
--------------------------------------------------------------------------------
1 | microbenchmark.escript
--------------------------------------------------------------------------------
/microbenchmark.escript:
--------------------------------------------------------------------------------
1 | -module(microbenchmark).
2 | -mode(compile).
3 |
4 | -export([main/1]).
5 |
6 | -define(NR_OF_WORKERS, 100).
7 |
8 | main([]) ->
9 | Category = microbenchmarking,
10 | NrOfWorkers = ?NR_OF_WORKERS,
11 | NrOfCalls = 4000000,
12 | {ok, _} = application:ensure_all_started(deigma),
13 | {ok, _} = application:ensure_all_started(sasl),
14 | {ok, _} = deigma:start(wowow),
15 | do_it(Category, NrOfWorkers, NrOfCalls).
16 |
17 | do_it(Category, NrOfWorkers, NrOfCalls) ->
18 | NrOfCallsPerWorker = NrOfCalls div NrOfWorkers,
19 | Parent = self(),
20 | Pids = [spawn(fun () -> run_worker(Category, Nr, Parent, NrOfCallsPerWorker) end)
21 | || Nr <- lists:seq(1, NrOfWorkers)],
22 | WithMonitors = [{Pid, monitor(process, Pid)} || Pid <- Pids],
23 | io:format("running benchmarks... (~p calls using ~p workers)~n",
24 | [NrOfCalls, NrOfWorkers]),
25 | wait_for_workers(WithMonitors, []).
26 |
27 | wait_for_workers([], ResultAcc) ->
28 | UniqueDeigmaResults = lists:usort( lists:flatten([maps:keys(M) || M <- ResultAcc]) ),
29 | lists:foreach(
30 | fun (DeigmaResult) ->
31 | Counts = [maps:get(DeigmaResult, M, 0) || M <- ResultAcc],
32 | TotalCount = trunc(lists:sum(Counts)),
33 | io:format("achieved an average of ~p '~p' results per second~n",
34 | [TotalCount, DeigmaResult])
35 | end,
36 | UniqueDeigmaResults),
37 | erlang:halt();
38 | wait_for_workers(WithMonitors, ResultAcc) ->
39 | receive
40 | {worker_result, Pid, Result} ->
41 | {value, {Pid, Monitor}, UpdatedWithMonitors} = lists:keytake(Pid, 1, WithMonitors),
42 | demonitor(Monitor, [flush]),
43 | UpdatedResultsAcc = [Result | ResultAcc],
44 | wait_for_workers(UpdatedWithMonitors, UpdatedResultsAcc);
45 | {'DOWN', _Ref, process, _Pid, Reason} ->
46 | error(Reason)
47 | end.
48 |
49 | run_worker(Category, Nr, Parent, NrOfCalls) ->
50 | run_worker_loop(Category, Nr, Parent, NrOfCalls,
51 | erlang:monotonic_time(), 0, #{}).
52 |
53 | run_worker_loop(_Category, _Nr, Parent, NrOfCalls, StartTs,
54 | Count, CountPerResult) when Count =:= NrOfCalls ->
55 | EndTs = erlang:monotonic_time(),
56 | TimeElapsed = EndTs - StartTs,
57 | NativeTimeRatio = erlang:convert_time_unit(1, seconds, native),
58 | SecondsElapsed = TimeElapsed / NativeTimeRatio,
59 | AdjustedCountPerResult =
60 | maps:map(
61 | fun (_Result, Count) ->
62 | Count / SecondsElapsed
63 | end,
64 | CountPerResult),
65 | Parent ! {worker_result, self(), AdjustedCountPerResult};
66 | run_worker_loop(Category, Nr, Parent, NrOfCalls, StartTs, Count, CountPerResult) ->
67 | ActorId = abs(erlang:monotonic_time()) rem ?NR_OF_WORKERS,
68 | {Result, SamplingPercentage} = deigma:ask(wowow, 1),
69 | %_ = (Count rem 10000 =:= 10) andalso io:format("Stats (~p): ~p~n", [Nr, Stats]),
70 | UpdatedCountPerResult = maps_increment(Result, +1, CountPerResult),
71 | run_worker_loop(Category, Nr, Parent, NrOfCalls, StartTs, Count + 1,
72 | UpdatedCountPerResult).
73 |
74 | maps_increment(Key, Incr, Map) ->
75 | maps_update_with(
76 | Key,
77 | fun (Value) -> Value + Incr end,
78 | Incr, Map).
79 |
80 | ask_handler(Decision, SamplingPercentage) ->
81 | {Decision, SamplingPercentage}.
82 |
83 | maps_update_with(Key, Fun, Init, Map) ->
84 | maps:update_with(Key, Fun, Init, Map).
85 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | {erl_opts,
2 | [debug_info,
3 | warn_export_all,
4 | warn_export_vars,
5 | warn_missing_spec,
6 | warn_obsolete_guards,
7 | warn_shadow_vars,
8 | warn_unused_import,
9 | warnings_as_errors
10 | ]}.
11 |
12 | {deps,
13 | []}.
14 |
15 | {minimum_otp_vsn, "22"}.
16 |
17 | {dialyzer,
18 | [{plt_include_all_deps, true},
19 | {warnings,
20 | [unmatched_returns,
21 | error_handling
22 | %underspecs
23 | ]}
24 | ]}.
25 |
26 | {xref_checks,
27 | [undefined_function_calls,
28 | undefined_functions,
29 | locals_not_used,
30 | exports_not_used,
31 | deprecated_function_calls,
32 | deprecated_functions
33 | ]}.
34 |
35 | {project_plugins,
36 | [{rebar3_hex, "6.10.3"}
37 | ]}.
38 |
39 | {profiles,
40 | [{development,
41 | [{erl_opts,
42 | [nowarn_missing_spec,
43 | nowarnings_as_errors]}
44 | ]},
45 |
46 | {test,
47 | [{erl_opts,
48 | [debug_info,
49 | nowarn_export_all,
50 | nowarn_missing_spec,
51 | nowarnings_as_errors]}
52 | ]},
53 |
54 | {ci_test,
55 | [{erl_opts,
56 | [debug_info,
57 | nowarn_export_all,
58 | nowarn_missing_spec,
59 | nowarnings_as_errors,
60 | {d, 'RUNNING_ON_CI'}]}
61 | ]}
62 | ]}.
63 |
64 | {cover_enabled, true}.
65 |
66 | {edoc_opts,
67 | [{stylesheet_file, "doc/custom_stylesheet.css"}
68 | ]}.
69 |
--------------------------------------------------------------------------------
/rebar.lock:
--------------------------------------------------------------------------------
1 | [].
2 |
--------------------------------------------------------------------------------
/src/deigma.app.src:
--------------------------------------------------------------------------------
1 | {application, deigma,
2 | [{description, "Continuous event sampler"},
3 | {vsn, "git"},
4 | {registered, []},
5 | {mod, {deigma_app, []}},
6 | {applications,
7 | [kernel,
8 | stdlib
9 | ]},
10 | {env,[]},
11 | {modules, []},
12 |
13 | {licenses, ["MIT"]},
14 | {links, [{"GitHub", "https://github.com/g-andrade/deigma"},
15 | {"GitLab", "https://gitlab.com/g-andrade/deigma"}
16 | ]}
17 | ]}.
18 |
--------------------------------------------------------------------------------
/src/deigma.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | -module(deigma).
22 | -behaviour(supervisor).
23 |
24 | %% ------------------------------------------------------------------
25 | %% API Function Exports
26 | %% ------------------------------------------------------------------
27 |
28 | -export(
29 | [start_link/1,
30 | child_spec/1,
31 | start/1,
32 | stop/1,
33 | ask/2,
34 | ask/3,
35 | ask/4
36 | ]).
37 |
38 | -ignore_xref(
39 | [start_link/1,
40 | child_spec/1,
41 | start/1,
42 | stop/1,
43 | ask/2,
44 | ask/3,
45 | ask/4
46 | ]).
47 |
48 | %% ------------------------------------------------------------------
49 | %% supervisor Function Exports
50 | %% ------------------------------------------------------------------
51 |
52 | -export(
53 | [init/1
54 | ]).
55 |
56 | %% ------------------------------------------------------------------
57 | %% Record and Type Definitions
58 | %% ------------------------------------------------------------------
59 |
60 | -type ask_opt() ::
61 | {max_rate, non_neg_integer() | infinity}.
62 | -export_type([ask_opt/0]).
63 |
64 | %% ------------------------------------------------------------------
65 | %% API Function Definitions
66 | %% ------------------------------------------------------------------
67 |
68 | %% @doc Start a deigma instance named `Category' under your own supervisor
69 | %%
70 | %%
71 | %% - `Category' must be an atom
72 | %%
73 | %%
74 | %% @see child_spec/1
75 | %% @see start/1
76 | -spec start_link(Category) -> {ok, pid()} | {error, term()}
77 | when Category :: atom().
78 | start_link(Category) ->
79 | Server = deigma_util:proc_name(?MODULE, Category),
80 | supervisor:start_link({local,Server}, ?MODULE, [Category]).
81 |
82 | %% @doc Declare a deigma instance named `Category' under your own supervisor
83 | %%
84 | %%
85 | %% - `Category' must be an atom
86 | %%
87 | %%
88 | %% @see start_link/2
89 | %% @see start/1
90 | -spec child_spec(Category) -> supervisor:child_spec()
91 | when Category :: atom().
92 | child_spec(Category) ->
93 | #{ id => {deigma, Category},
94 | start => {?MODULE, start_link, [Category]},
95 | type => supervisor
96 | }.
97 |
98 | %% @doc Start a deigma instance named `Category'
99 | %%
100 | %%
101 | %% - `Category' must be an atom
102 | %%
103 | %%
104 | %% @see stop/1
105 | %% @see start_link/1
106 | %% @see child_spec/1
107 | -spec start(Category) -> {ok, pid()} | {error, term()}
108 | when Category :: atom().
109 | start(Category) ->
110 | deigma_sup:start_child([Category]).
111 |
112 | %% @doc Stop a deigma instance named `Category'
113 | %%
114 | %%
115 | %% - `Category' must be an atom
116 | %%
117 | %%
118 | %% @see stop/1
119 | %% @see start_link/1
120 | %% @see child_spec/1
121 | -spec stop(Category) -> ok | {error, not_started}
122 | when Category :: atom().
123 | stop(Category) ->
124 | Server = deigma_util:proc_name(?MODULE, Category),
125 | try gen_server:stop(Server, shutdown, infinity) of
126 | ok -> ok
127 | catch
128 | exit:Reason when Reason =:= noproc;
129 | Reason =:= normal;
130 | Reason =:= shutdown ->
131 | {error, not_started}
132 | end.
133 |
134 | %% @doc Ask `Category' to sample an `EventType' event
135 | %%
136 | %%
137 | %% - `Category' must be an atom and correspond to an existing deigma instance
138 | %% - `EventType' can be any term
139 | %%
140 | %%
141 | %% Returns:
142 | %%
143 | %% - `{sample, SamplingPercentage}' if the event was sampled
144 | %% - `{drop, SamplingPercentage}' if the event was dropped
145 | %%
146 | %%
147 | %% `SamplingPercentage' is a floating point number between 0.0 and 1.0 representing
148 | %% the percentage of events that were sampled during the last 1000 milliseconds,
149 | %% including the event reported just now.
150 | %%
151 | %% @see ask/3
152 | %% @see ask/4
153 | -spec ask(Category, EventType) -> {Decision, SamplingPercentage}
154 | when Category :: atom(),
155 | EventType :: term(),
156 | Decision :: sample | drop,
157 | SamplingPercentage :: float().
158 | ask(Category, EventType) ->
159 | ask(Category, EventType, fun default_ask_fun/3).
160 |
161 | %% @doc Ask `Category' to sample an `EventType' event using custom function or overridden options
162 | %%
163 | %%
164 | %% - `Category' must be an atom and correspond to an existing deigma instance
165 | %% - `EventType' can be any term
166 | %% - `EventFun' must be a function which will receive the following arguments:
167 | %%
168 | %% - `Timestamp': Monotonic timestamp in native units at which the event was registered
169 | %% - `Decision': Either `sample' or `drop' depending on whether the event was sampled or not
170 | %% - `SamplingPercentage': a floating point number between 0.0 and 1.0 representing the percentage
171 | %% of events that were sampled during the last 1000 milliseconds, including the event
172 | %% reported just now.
173 | %%
174 | %%
175 | %% It will be called from within the event window for `EventType', which means
176 | %% it can be used for fullfilling serialisation constraints; at the same time,
177 | %% performance has to be taken into account (lest the event window become a bottleneck.)
178 | %%
179 | %% - `Opts' must be a list of `ask_opt()' items:
180 | %%
181 | %% - {`max_rate, MaxRate}': don't sample more than `MaxRate' `EventType' events per
182 | %% second (defaults to `100')
183 | %%
184 | %%
185 | %%
186 | %%
187 | %%
188 | %% If called with `EventFun', it will return or throw whathever `EventFun' returns or throws.
189 | %% If called with `Opts', it will return the same as `:ask/2'.
190 | %%
191 | %% @see ask/2
192 | %% @see ask/4
193 | -spec ask(Category, EventType, EventFun | Opts) -> {Decision, SamplingPercentage} | EventFunResult
194 | when Category :: atom(),
195 | EventType :: term(),
196 | EventFun :: fun ((Timestamp, Decision, SamplingPercentage) -> EventFunResult),
197 | Timestamp :: integer(),
198 | SamplingPercentage :: float(),
199 | Decision :: sample | drop,
200 | EventFunResult :: term(),
201 | Opts :: [ask_opt()].
202 | ask(Category, EventType, EventFun) when is_function(EventFun) ->
203 | ask(Category, EventType, EventFun, []);
204 | ask(Category, EventType, Opts) ->
205 | ask(Category, EventType, fun default_ask_fun/3, Opts).
206 |
207 | %% @doc Ask `Category' to sample an `EventType' event using custom function and overridden options
208 | %%
209 | %%
210 | %% - `Category' must be an atom and correspond to an existing deigma instance
211 | %% - `EventType' can be any term
212 | %% - `EventFun' must be a function which will receive the following arguments:
213 | %%
214 | %% - `Timestamp': Monotonic timestamp in native units at which the event was registered
215 | %% - `Decision': Either `sample' or `drop' depending on whether the event was sampled or not
216 | %% - `SamplingPercentage': a floating point number between 0.0 and 1.0 representing the percentage
217 | %% of events that were sampled during the last 1000 milliseconds, including the event
218 | %% reported just now.
219 | %%
220 | %%
221 | %% It will be called from within the event window for `EventType', which means
222 | %% it can be used for fullfilling serialisation constraints; at the same time,
223 | %% performance has to be taken into account (lest the event window become a bottleneck.)
224 | %%
225 | %% - `Opts' must be a list of `ask_opt()' items:
226 | %%
227 | %% - {`max_rate, MaxRate}': don't sample more than `MaxRate' `EventType' events per
228 | %% second (defaults to `100')
229 | %%
230 | %%
231 | %%
232 | %%
233 | %%
234 | %% It will return or throw whathever `EventFun' returns or throws.
235 | %%
236 | %% @see ask/2
237 | %% @see ask/3
238 | -spec ask(Category, EventType, EventFun, Opts) -> EventFunResult
239 | when Category :: atom(),
240 | EventType :: term(),
241 | EventFun :: fun ((Timestamp, Decision, SamplingPercentage) -> EventFunResult),
242 | Timestamp :: integer(),
243 | SamplingPercentage :: float(),
244 | Decision :: sample | drop,
245 | EventFunResult :: term(),
246 | Opts :: [ask_opt()].
247 | ask(Category, EventType, EventFun, Opts) ->
248 | deigma_event_window:ask(Category, EventType, EventFun, Opts).
249 |
250 | %% ------------------------------------------------------------------
251 | %% supervisor Function Definitions
252 | %% ------------------------------------------------------------------
253 |
254 | -spec init([atom(), ...])
255 | -> {ok, {supervisor:sup_flags(), [supervisor:child_spec(), ...]}}.
256 | %% @private
257 | init([Category]) ->
258 | SupFlags =
259 | #{ strategy => rest_for_one,
260 | intensity => 5,
261 | period => 1
262 | },
263 | ChildSpecs =
264 | [#{ id => proc_reg,
265 | start => {deigma_proc_reg, start_link, [Category]}
266 | },
267 | #{ id => event_windows,
268 | start => {deigma_event_window_sup, start_link, [Category]},
269 | type => supervisor
270 | }],
271 | {ok, {SupFlags, ChildSpecs}}.
272 |
273 | %% ------------------------------------------------------------------
274 | %% Internal Function Definitions
275 | %% ------------------------------------------------------------------
276 |
277 | default_ask_fun(_Timestamp, Decision, SamplingPercentage) ->
278 | {Decision, SamplingPercentage}.
279 |
--------------------------------------------------------------------------------
/src/deigma_app.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | %% @private
22 | -module(deigma_app).
23 | -behaviour(application).
24 |
25 | %% ------------------------------------------------------------------
26 | %% application Function Exports
27 | %% ------------------------------------------------------------------
28 |
29 | -export(
30 | [start/2,
31 | stop/1
32 | ]).
33 |
34 | %% ------------------------------------------------------------------
35 | %% application Function Definitions
36 | %% ------------------------------------------------------------------
37 |
38 | -spec start(term(), list()) -> {ok, pid()}.
39 | start(_StartType, _StartArgs) ->
40 | deigma_sup:start_link().
41 |
42 | -spec stop(term()) -> ok.
43 | stop(_State) ->
44 | ok.
45 |
--------------------------------------------------------------------------------
/src/deigma_event_window.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | %% @private
22 | -module(deigma_event_window).
23 |
24 | % based on https://gist.github.com/marcelog/97708058cd17f86326c82970a7f81d40#file-simpleproc-erl
25 |
26 | %%-------------------------------------------------------------------
27 | %% API Function Exports
28 | %%-------------------------------------------------------------------
29 |
30 | -export(
31 | [start_link/2,
32 | ask/4
33 | ]).
34 |
35 | -ignore_xref(
36 | [start_link/2
37 | ]).
38 |
39 | %%-------------------------------------------------------------------
40 | %% OTP Exports
41 | %%-------------------------------------------------------------------
42 |
43 | -export(
44 | [init/1,
45 | system_code_change/4,
46 | system_continue/3,
47 | system_terminate/4,
48 | write_debug/3
49 | ]).
50 |
51 | -ignore_xref(
52 | [init/1,
53 | system_code_change/4,
54 | system_continue/3,
55 | system_terminate/4,
56 | write_debug/3
57 | ]).
58 |
59 | %%-------------------------------------------------------------------
60 | %% Macro Definitions
61 | %%-------------------------------------------------------------------
62 |
63 | -define(time_span(), 1). % in seconds
64 | -define(ms_time_span(), 1000). % in milliseconds
65 | -define(native_time_span(), (erlang:convert_time_unit(?time_span(), seconds, native))). % in native units
66 |
67 | -define(DEFAULT_MAX_RATE, 100).
68 |
69 | %%-------------------------------------------------------------------
70 | %% Record and Type Definitions
71 | %%-------------------------------------------------------------------
72 |
73 | -record(state, {
74 | category :: atom(),
75 | event_type :: term(),
76 | window = queue:new() :: queue:queue(event()),
77 | window_size = 0 :: non_neg_integer(),
78 | sampled_counter = 0 :: non_neg_integer()
79 | }).
80 | -type state() :: state().
81 |
82 | -type event() :: {timestamp(), decision()}.
83 | -type timestamp() :: integer().
84 | -type decision() :: sample | drop.
85 |
86 | %%-------------------------------------------------------------------
87 | %% API Function Definitions
88 | %%-------------------------------------------------------------------
89 |
90 | -spec start_link(atom(), term()) -> {ok, pid()} | {error, {already_started, pid()}}.
91 | start_link(Category, EventType) ->
92 | proc_lib:start_link(?MODULE, init, [{self(), [Category, EventType]}]).
93 |
94 | -spec ask(atom(), term(), fun ((integer(), decision(), float())
95 | -> term()), [deigma:ask_opt()]) -> term() | no_return().
96 | ask(Category, EventType, EventFun, Opts) ->
97 | MaxRate = proplists:get_value(max_rate, Opts, ?DEFAULT_MAX_RATE),
98 | Pid = lookup_or_start(Category, EventType),
99 | Mon = monitor(process, Pid),
100 | Tag = Mon,
101 | Pid ! {ask, self(), Tag, EventFun, MaxRate},
102 | receive
103 | {Tag, Reply} ->
104 | demonitor(Mon, [flush]),
105 | case Reply of
106 | {result, Result} ->
107 | Result;
108 | {exception, Class, Reason, Stacktrace} ->
109 | erlang:raise(Class, Reason, Stacktrace)
110 | end;
111 | {'DOWN', Mon, process, _Pid, Reason} when Reason =:= noproc; Reason =:= normal ->
112 | % inactive process stopped; ask again
113 | ask(Category, EventType, EventFun, Opts);
114 | {'DOWN', Mon, process, _Pid, Reason} ->
115 | error({event_window_stopped, Category, EventType, Reason})
116 | end.
117 |
118 | %%-------------------------------------------------------------------
119 | %% OTP Function Definitions
120 | %%-------------------------------------------------------------------
121 |
122 | -spec init({pid(), [atom() | term(), ...]}) -> no_return().
123 | init({Parent, [Category, EventType]}) ->
124 | Debug = sys:debug_options([]),
125 | Server = registered_name(EventType),
126 | case deigma_proc_reg:register(Category, Server, self()) of
127 | ok ->
128 | proc_lib:init_ack(Parent, {ok, self()}),
129 | State = #state{ category = Category, event_type = EventType },
130 | loop(Parent, Debug, State);
131 | {error, {already_registered, Pid}} ->
132 | proc_lib:init_ack(Parent, {error, {already_started, Pid}}),
133 | exit(normal)
134 | end.
135 |
136 | -spec system_code_change(state(), module(), term(), term()) -> {ok, state()}.
137 | system_code_change(State, _Module, _OldVsn, _Extra) when is_record(State, state) ->
138 | % http://www.erlang.org/doc/man/sys.html#Mod:system_code_change-4
139 | {ok, State}.
140 |
141 | -spec system_continue(pid(), [sys:dbg_opt()], state()) -> no_return().
142 | system_continue(Parent, Debug, State) ->
143 | % http://www.erlang.org/doc/man/sys.html#Mod:system_continue-3
144 | loop(Parent, Debug, State).
145 |
146 | -spec system_terminate(term(), pid(), list(), state()) -> no_return().
147 | system_terminate(Reason, _Parent, _Debug, _State) ->
148 | % http://www.erlang.org/doc/man/sys.html#Mod:system_terminate-4
149 | exit(Reason).
150 |
151 | -spec write_debug(io:device(), term(), term()) -> ok.
152 | write_debug(Dev, Event, Name) ->
153 | % called by sys:handle_debug().
154 | io:format(Dev, "~p event = ~p~n", [Name, Event]).
155 |
156 | %%-------------------------------------------------------------------
157 | %% Internal Function Definitions
158 | %%-------------------------------------------------------------------
159 |
160 | registered_name(EventType) ->
161 | {?MODULE, EventType}.
162 |
163 | lookup_or_start(Category, EventType) ->
164 | Server = registered_name(EventType),
165 | case deigma_proc_reg:whereis(Category, Server) of
166 | undefined ->
167 | case start(Category, EventType) of
168 | {ok, Pid} ->
169 | Pid;
170 | {error, {already_started, ExistingPid}} ->
171 | ExistingPid
172 | end;
173 | Pid ->
174 | Pid
175 | end.
176 |
177 | start(Category, EventType) ->
178 | deigma_event_window_sup:start_child(Category, [EventType]).
179 |
180 | loop(Parent, Debug, State) ->
181 | receive
182 | Msg ->
183 | Now = erlang:monotonic_time(),
184 | UpdatedState = purge_expired(Now, State),
185 | handle_message(Now, Msg, Parent, Debug, UpdatedState)
186 | after
187 | ?ms_time_span() ->
188 | exit(normal)
189 | end.
190 |
191 | handle_message(_Now, {system, From, Request}, Parent, Debug, State) ->
192 | sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State);
193 | handle_message(Now, Msg, Parent, Debug, State) ->
194 | UpdatedDebug = sys:handle_debug(Debug, fun ?MODULE:write_debug/3, ?MODULE, {in, Msg}),
195 | UpdatedState = handle_nonsystem_msg(Now, Msg, State),
196 | loop(Parent, UpdatedDebug, UpdatedState).
197 |
198 | handle_nonsystem_msg(Now, {ask, From, Tag, EventFun, MaxRate}, State) ->
199 | Window = State#state.window,
200 | WindowSize = State#state.window_size,
201 | SampledCounter = State#state.sampled_counter,
202 |
203 | {UpdatedWindowSize, UpdatedSampledCounter, Decision} =
204 | handle_sampling(WindowSize, SampledCounter, MaxRate),
205 | UpdatedWindow = queue:in({Now, Decision}, Window),
206 |
207 | SamplingPercentage = UpdatedSampledCounter / UpdatedWindowSize,
208 | _ = call_event_fun(From, Tag, EventFun, Now, Decision, SamplingPercentage),
209 |
210 | State#state{ window = UpdatedWindow,
211 | window_size = UpdatedWindowSize,
212 | sampled_counter = UpdatedSampledCounter
213 | }.
214 |
215 | -compile({inline,{call_event_fun,6}}).
216 | call_event_fun(From, Tag, EventFun, Now, Decision, SamplingPercentage) ->
217 | try EventFun(Now, Decision, SamplingPercentage) of
218 | Result ->
219 | From ! {Tag, {result, Result}}
220 | catch
221 | Class:Reason:Stacktrace ->
222 | From ! {Tag, {exception, Class, Reason, Stacktrace}}
223 | end.
224 |
225 | purge_expired(Now, State) ->
226 | Window = State#state.window,
227 | WindowSize = State#state.window_size,
228 | SampledCounter = State#state.sampled_counter,
229 | TimeFloor = Now - ?native_time_span(),
230 | {UpdatedWindow, UpdatedWindowSize, UpdatedSampledCounter} =
231 | purge_expired(TimeFloor, Window, WindowSize, SampledCounter),
232 | State#state{
233 | window = UpdatedWindow,
234 | window_size = UpdatedWindowSize,
235 | sampled_counter = UpdatedSampledCounter
236 | }.
237 |
238 | purge_expired(TimeFloor, Window, WindowSize, SampledCounter) ->
239 | case queue:peek(Window) of
240 | {value, {EventTimestamp, EventDecision}} when EventTimestamp < TimeFloor ->
241 | UpdatedWindow = queue:drop(Window),
242 | UpdatedWindowSize = WindowSize - 1,
243 | case EventDecision of
244 | sample ->
245 | UpdatedSampledCounter = SampledCounter - 1,
246 | purge_expired(
247 | TimeFloor, UpdatedWindow, UpdatedWindowSize, UpdatedSampledCounter);
248 | drop ->
249 | purge_expired(
250 | TimeFloor, UpdatedWindow, UpdatedWindowSize, SampledCounter)
251 | end;
252 | _ ->
253 | {Window, WindowSize, SampledCounter}
254 | end.
255 |
256 | handle_sampling(WindowSize, SampledCounter, MaxRate) when SampledCounter >= MaxRate ->
257 | {WindowSize + 1, SampledCounter, drop};
258 | handle_sampling(WindowSize, SampledCounter, _MaxRate) ->
259 | {WindowSize + 1, SampledCounter + 1, sample}.
260 |
--------------------------------------------------------------------------------
/src/deigma_event_window_sup.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | %% @private
22 | -module(deigma_event_window_sup).
23 | -behaviour(supervisor).
24 |
25 | %% ------------------------------------------------------------------
26 | %% API Function Exports
27 | %% ------------------------------------------------------------------
28 |
29 | -export([start_link/1]).
30 | -export([start_child/2]).
31 |
32 | -ignore_xref([start_link/1]).
33 |
34 | %% ------------------------------------------------------------------
35 | %% supervisor Function Exports
36 | %% ------------------------------------------------------------------
37 |
38 | -export([init/1]).
39 |
40 | %% ------------------------------------------------------------------
41 | %% API Function Definitions
42 | %% ------------------------------------------------------------------
43 |
44 | -spec start_link(atom()) -> {ok, pid()} | {error, term()}.
45 | start_link(Category) ->
46 | Server = deigma_util:proc_name(?MODULE, Category),
47 | supervisor:start_link({local,Server}, ?MODULE, [Category]).
48 |
49 | -spec start_child(atom(), list()) -> {ok, pid()} | {error, term()}.
50 | start_child(Category, Args) ->
51 | Server = deigma_util:proc_name(?MODULE, Category),
52 | supervisor:start_child(Server, Args).
53 |
54 | %% ------------------------------------------------------------------
55 | %% supervisor Function Definitions
56 | %% ------------------------------------------------------------------
57 |
58 | -spec init([atom(), ...])
59 | -> {ok, {supervisor:sup_flags(), [supervisor:child_spec(), ...]}}.
60 | init([Category]) ->
61 | SupFlags = #{ strategy => simple_one_for_one },
62 | ChildSpecs =
63 | [#{ id => event_window,
64 | start => {deigma_event_window, start_link, [Category]},
65 | restart => temporary
66 | }
67 | ],
68 | {ok, {SupFlags, ChildSpecs}}.
69 |
--------------------------------------------------------------------------------
/src/deigma_proc_reg.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO WORK SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | %% @private
22 | -module(deigma_proc_reg).
23 | -behaviour(gen_server).
24 |
25 | %% ------------------------------------------------------------------
26 | %% API Function Exports
27 | %% ------------------------------------------------------------------
28 |
29 | -export(
30 | [start_link/1,
31 | register/3,
32 | whereis/2
33 | ]).
34 |
35 | -ignore_xref(
36 | [start_link/1
37 | ]).
38 |
39 | %% ------------------------------------------------------------------
40 | %% gen_server Function Exports
41 | %% ------------------------------------------------------------------
42 |
43 | -export(
44 | [init/1,
45 | handle_call/3,
46 | handle_cast/2,
47 | handle_info/2,
48 | terminate/2,
49 | code_change/3
50 | ]).
51 |
52 | %% ------------------------------------------------------------------
53 | %% Record and Type Definitions
54 | %% ------------------------------------------------------------------
55 |
56 | -record(state, {
57 | category :: atom(),
58 | table :: ets:tab(),
59 | monitors :: #{ reference() => term() }
60 | }).
61 | -type state() :: #state{}.
62 |
63 | %% ------------------------------------------------------------------
64 | %% API Function Definitions
65 | %% ------------------------------------------------------------------
66 |
67 | -spec start_link(atom()) -> {ok, pid()}.
68 | start_link(Category) ->
69 | Server = deigma_util:proc_name(?MODULE, Category),
70 | gen_server:start_link({local,Server}, ?MODULE, [Category], []).
71 |
72 | -spec register(atom(), term(), pid()) -> ok | {error, {already_registered, pid()}}.
73 | register(Category, Name, Pid) ->
74 | Server = deigma_util:proc_name(?MODULE, Category),
75 | gen_server:call(Server, {register, Name, Pid}, infinity).
76 |
77 | -spec whereis(atom(), term()) -> pid() | undefined.
78 | whereis(Category, Name) ->
79 | Table = table_name(Category),
80 | case ets:lookup(Table, Name) of
81 | [{_, Pid}] -> Pid;
82 | _ -> undefined
83 | end.
84 |
85 | %% ------------------------------------------------------------------
86 | %% gen_server Function Definitions
87 | %% ------------------------------------------------------------------
88 |
89 | -spec init([atom(), ...]) -> {ok, state()}.
90 | init([Category]) ->
91 | Table = table_name(Category),
92 | TableOpts = [named_table, protected, {read_concurrency,true}],
93 | _ = ets:new(Table, TableOpts),
94 | {ok, #state{ category = Category, table = Table, monitors = #{} }}.
95 |
96 | -spec handle_call(term(), {pid(),reference()}, state())
97 | -> {reply, Reply, state()} |
98 | {stop, unexpected_call, state()}
99 | when Reply :: ok | {error, {already_registered,pid()}}.
100 | handle_call({register, Name, Pid}, _From, State) ->
101 | Table = State#state.table,
102 | case ets:lookup(Table, Name) of
103 | [{_, ExistingPid}] ->
104 | {reply, {error, {already_registered, ExistingPid}}, State};
105 | [] ->
106 | ets:insert(Table, {Name,Pid}),
107 | NewMonitor = monitor(process, Pid),
108 | Monitors = State#state.monitors,
109 | UpdatedMonitors = Monitors#{ NewMonitor => Name },
110 | UpdatedState = State#state{ monitors = UpdatedMonitors },
111 | {reply, ok, UpdatedState}
112 | end;
113 | handle_call(_Call, _From, State) ->
114 | {stop, unexpected_call, State}.
115 |
116 | -spec handle_cast(term(), state()) -> {stop, unexpected_cast, state()}.
117 | handle_cast(_Cast, State) ->
118 | {stop, unexpected_cast, State}.
119 |
120 | -spec handle_info(term(), state())
121 | -> {noreply, state()} |
122 | {stop, unexpected_info, state()}.
123 | handle_info({'DOWN', Ref, process, _Pid, _Reason}, State) ->
124 | Monitors = State#state.monitors,
125 | {Name, UpdatedMonitors} = maps_take(Ref, Monitors),
126 | [_] = ets:take(State#state.table, Name),
127 | UpdatedState = State#state{ monitors = UpdatedMonitors },
128 | {noreply, UpdatedState};
129 | handle_info(_Info, State) ->
130 | {stop, unexpected_info, State}.
131 |
132 | -spec terminate(term(), state()) -> ok.
133 | terminate(_Reason, _State) ->
134 | ok.
135 |
136 | -spec code_change(term(), state(), term()) -> {ok, state()}.
137 | code_change(_OldVsn, #state{} = State, _Extra) ->
138 | {ok, State}.
139 |
140 | %% ------------------------------------------------------------------
141 | %% Internal Function Definitions
142 | %% ------------------------------------------------------------------
143 |
144 | table_name(Category) ->
145 | deigma_util:proc_name(?MODULE, Category).
146 |
147 | maps_take(Key, Map) ->
148 | maps:take(Key, Map).
149 |
--------------------------------------------------------------------------------
/src/deigma_sup.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | %% @private
22 | -module(deigma_sup).
23 | -behaviour(supervisor).
24 |
25 | %% ------------------------------------------------------------------
26 | %% API Function Exports
27 | %% ------------------------------------------------------------------
28 |
29 | -export(
30 | [start_link/0,
31 | start_child/1
32 | ]).
33 |
34 | -ignore_xref(
35 | [start_link/0
36 | ]).
37 |
38 | %% ------------------------------------------------------------------
39 | %% supervisor Function Exports
40 | %% ------------------------------------------------------------------
41 |
42 | -export(
43 | [init/1
44 | ]).
45 |
46 | %% ------------------------------------------------------------------
47 | %% Macro Definitions
48 | %% ------------------------------------------------------------------
49 |
50 | -define(SERVER, ?MODULE).
51 |
52 | %% ------------------------------------------------------------------
53 | %% API Function Definitions
54 | %% ------------------------------------------------------------------
55 |
56 | -spec start_link() -> {ok, pid()}.
57 | start_link() ->
58 | supervisor:start_link({local, ?SERVER}, ?MODULE, []).
59 |
60 | -spec start_child(list()) -> {ok, pid()}.
61 | start_child(Args) ->
62 | supervisor:start_child(?SERVER, Args).
63 |
64 | %% ------------------------------------------------------------------
65 | %% supervisor Function Definitions
66 | %% ------------------------------------------------------------------
67 |
68 | -spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec(), ...]}}.
69 | init([]) ->
70 | SupFlags = #{ strategy => simple_one_for_one },
71 | ChildSpecs =
72 | [#{ id => deigma,
73 | start => {deigma, start_link, []},
74 | restart => temporary,
75 | type => supervisor
76 | }],
77 | {ok, {SupFlags, ChildSpecs}}.
78 |
--------------------------------------------------------------------------------
/src/deigma_util.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | %% @private
22 | -module(deigma_util).
23 |
24 | %% ------------------------------------------------------------------
25 | %% API Function Exports
26 | %% ------------------------------------------------------------------
27 |
28 | -export([proc_name/2]).
29 |
30 | %% ------------------------------------------------------------------
31 | %% API Function Definitions
32 | %% ------------------------------------------------------------------
33 |
34 | -spec proc_name(module(), atom()) -> atom().
35 | proc_name(Module, PoolId) ->
36 | list_to_atom(
37 | atom_to_list(Module)
38 | ++ "."
39 | ++ atom_to_list(PoolId)).
40 |
--------------------------------------------------------------------------------
/test/deigma_SUITE.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2018-2022 Guilherme Andrade
2 | %%
3 | %% Permission is hereby granted, free of charge, to any person obtaining a
4 | %% copy of this software and associated documentation files (the "Software"),
5 | %% to deal in the Software without restriction, including without limitation
6 | %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | %% and/or sell copies of the Software, and to permit persons to whom the
8 | %% Software is furnished to do so, subject to the following conditions:
9 | %%
10 | %% The above copyright notice and this permission notice shall be included in
11 | %% all copies or substantial portions of the Software.
12 | %%
13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO WORK SHALL THE
16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | %% DEALINGS IN THE SOFTWARE.
20 |
21 | -module(deigma_SUITE).
22 | -compile(export_all).
23 |
24 | -include_lib("eunit/include/eunit.hrl").
25 |
26 | -ifdef(RUNNING_ON_CI).
27 | -define(ASK_TEST_DURATION, (timer:seconds(3))).
28 | -else.
29 | -define(ASK_TEST_DURATION, (timer:seconds(5))).
30 | -endif.
31 |
32 | %% ------------------------------------------------------------------
33 | %% Enumeration
34 | %% ------------------------------------------------------------------
35 |
36 | all() ->
37 | [{group, GroupName} || {GroupName, _Options, _TestCases} <- groups()].
38 |
39 | groups() ->
40 | GroupNames = [individual_tests],
41 | [{GroupName, [parallel], individual_test_cases()} || GroupName <- GroupNames].
42 |
43 | individual_test_cases() ->
44 | ModuleInfo = ?MODULE:module_info(),
45 | {exports, Exports} = lists:keyfind(exports, 1, ModuleInfo),
46 | [Name || {Name, 1} <- Exports, lists:suffix("_test", atom_to_list(Name))].
47 |
48 | %% ------------------------------------------------------------------
49 | %% Initialization
50 | %% ------------------------------------------------------------------
51 |
52 | init_per_testcase(_TestCase, Config) ->
53 | {ok, _} = application:ensure_all_started(sasl),
54 | {ok, _} = application:ensure_all_started(deigma),
55 | Config.
56 |
57 | end_per_testcase(_TestCase, Config) ->
58 | Config.
59 |
60 | %% ------------------------------------------------------------------
61 | %% Definition
62 | %% ------------------------------------------------------------------
63 |
64 | ask_10_1_test(Config) ->
65 | run_ask_test(10, 1),
66 | Config.
67 |
68 | ask_100_1_test(Config) ->
69 | run_ask_test(100, 1),
70 | Config.
71 |
72 | ask_1000_1_test(Config) ->
73 | run_ask_test(1000, 1),
74 | Config.
75 |
76 | ask_10_10_test(Config) ->
77 | run_ask_test(10, 10),
78 | Config.
79 |
80 | ask_100_10_test(Config) ->
81 | run_ask_test(100, 10),
82 | Config.
83 |
84 | ask_1000_10_test(Config) ->
85 | run_ask_test(1000, 10),
86 | Config.
87 |
88 | ask_10_100_test(Config) ->
89 | run_ask_test(10, 100),
90 | Config.
91 |
92 | ask_100_100_test(Config) ->
93 | run_ask_test(100, 100),
94 | Config.
95 |
96 | ask_1000_100_test(Config) ->
97 | run_ask_test(1000, 100),
98 | Config.
99 |
100 | ask_10_1000_test(Config) ->
101 | run_ask_test(10, 1000),
102 | Config.
103 |
104 | ask_100_1000_test(Config) ->
105 | run_ask_test(100, 1000),
106 | Config.
107 |
108 | ask_1000_1000_test(Config) ->
109 | run_ask_test(1000, 1000),
110 | Config.
111 |
112 | default_event_fun_test(_Config) ->
113 | {ok, _Pid} = deigma:start(default_event_fun_test),
114 | ?assertEqual({sample,1.0}, deigma:ask(default_event_fun_test, foobar)),
115 | ?assertEqual({sample,1.0}, deigma:ask(default_event_fun_test, foobar, [])),
116 | ?assertEqual({sample,1.0}, deigma:ask(default_event_fun_test, foobar, [{max_rate,5000}])),
117 | ok = deigma:stop(default_event_fun_test),
118 | {error, not_started} = deigma:stop(default_event_fun_test).
119 |
120 | custom_event_fun_test(_Config) ->
121 | {ok, _Pid} = deigma:start(custom_event_fun_test),
122 | Ref = make_ref(),
123 | ?assertEqual(yes, deigma:ask(custom_event_fun_test, foobar, event_fun({value, yes}))),
124 | ?assertEqual(no, deigma:ask(custom_event_fun_test, foobar, event_fun({value, no}))),
125 | ?assertEqual(self(), deigma:ask(custom_event_fun_test, foobar, event_fun({value, self()}))),
126 | ?assertEqual(Ref, deigma:ask(custom_event_fun_test, foobar, event_fun({value, Ref}))),
127 | ok = deigma:stop(custom_event_fun_test).
128 |
129 | crashing_event_fun_test(_Config) ->
130 | {ok, _Pid} = deigma:start(crashing_event_fun_test),
131 | ?assertMatch(yes,
132 | catch deigma:ask(crashing_event_fun_test, foobar, event_fun({exception, throw, yes}))),
133 | ?assertMatch(no,
134 | catch deigma:ask(crashing_event_fun_test, foobar, event_fun({exception, throw, no}))),
135 | ?assertMatch({'EXIT', {its_working, _}},
136 | catch deigma:ask(crashing_event_fun_test, foobar, event_fun({exception, error, its_working}))),
137 | ?assertMatch({'EXIT', oh_my},
138 | catch deigma:ask(crashing_event_fun_test, foobar, event_fun({exception, exit, oh_my}))),
139 | ok = deigma:stop(crashing_event_fun_test).
140 |
141 | %% ------------------------------------------------------------------
142 | %% Internal
143 | %% ------------------------------------------------------------------
144 |
145 | run_ask_test(NrOfEventTypes, MaxRate) ->
146 | Category =
147 | list_to_atom(
148 | "ask_" ++
149 | integer_to_list(NrOfEventTypes) ++
150 | "_" ++
151 | integer_to_list(MaxRate) ++
152 | "_test"),
153 | {ok, _Pid} = deigma:start(Category),
154 | _ = erlang:send_after(5000, self(), test_over),
155 | run_ask_test_recur(Category, NrOfEventTypes, MaxRate, []),
156 | ok = deigma:stop(Category).
157 |
158 | run_ask_test_recur(Category, NrOfEventTypes, MaxRate, Acc) ->
159 | Timeout = rand:uniform(2) - 1,
160 | receive
161 | test_over ->
162 | check_ask_test_results(MaxRate, Acc)
163 | after
164 | Timeout ->
165 | EventType = rand:uniform(NrOfEventTypes),
166 | {Ts, Decision, SamplingPercentage} =
167 | deigma:ask(
168 | Category, EventType,
169 | fun (Ts, Decision, SamplingPercentage) ->
170 | {Ts, Decision, SamplingPercentage}
171 | end,
172 | [{max_rate, MaxRate}]),
173 | UpdatedAcc = [{Ts, EventType, Decision, SamplingPercentage} | Acc],
174 | run_ask_test_recur(Category, NrOfEventTypes, MaxRate, UpdatedAcc)
175 | end.
176 |
177 | check_ask_test_results(MaxRate, Results) ->
178 | ResultsPerEventType =
179 | lists:foldl(
180 | fun ({Ts, EventType, Decision, SamplingPercentage}, Acc) ->
181 | maps_update_with(
182 | EventType,
183 | fun (Events) -> [{Ts, Decision, SamplingPercentage} | Events] end,
184 | [{Ts, Decision, SamplingPercentage}],
185 | Acc)
186 | end,
187 | #{}, Results),
188 |
189 | lists:foreach(
190 | fun ({_EventType, Events}) ->
191 | check_ask_test_decisions(MaxRate, Events),
192 | check_ask_test_rates(Events)
193 | end,
194 | lists:keysort(1, maps:to_list(ResultsPerEventType))).
195 |
196 | check_ask_test_decisions(MaxRate, Events) ->
197 | check_ask_test_decisions(MaxRate, Events, [], 0, 0).
198 |
199 | check_ask_test_decisions(_MaxRate, [], _Acc, RightDecisions, WrongDecisions) ->
200 | ct:pal("RightDecisions ~p, WrongDecisions ~p", [RightDecisions, WrongDecisions]),
201 | ?assert(WrongDecisions / (RightDecisions + WrongDecisions) < 0.01);
202 | check_ask_test_decisions(MaxRate, [Event | Next], Prev, RightDecisions, WrongDecisions) ->
203 | {Ts, Decision, _SamplingPercentage} = Event,
204 | RelevantPrev = relevant_history(Ts, Prev),
205 | CountPerDecision = count_history_decisions(RelevantPrev),
206 | PrevSamples = maps:get(sample, CountPerDecision),
207 | RightDecision =
208 | case PrevSamples >= MaxRate of
209 | true -> drop;
210 | false -> sample
211 | end,
212 |
213 | case RightDecision =:= Decision of
214 | false ->
215 | check_ask_test_decisions(MaxRate, Next, [Event | RelevantPrev],
216 | RightDecisions, WrongDecisions + 1);
217 | true ->
218 | check_ask_test_decisions(MaxRate, Next, [Event | RelevantPrev],
219 | RightDecisions + 1, WrongDecisions)
220 | end.
221 |
222 | relevant_history(Ts, Prev) ->
223 | TsFloor = Ts - erlang:convert_time_unit(1, seconds, native),
224 | lists:takewhile(
225 | fun ({EntryTs, _Decision, _SamplingPercentage}) ->
226 | EntryTs >= TsFloor
227 | end,
228 | Prev).
229 |
230 | count_history_decisions(Prev) ->
231 | lists:foldl(
232 | fun ({_Ts, Decision, _SamplingPercentage}, Acc) ->
233 | maps_update_with(
234 | Decision,
235 | fun (Val) -> Val + 1 end,
236 | Acc)
237 | end,
238 | #{ sample => 0,
239 | drop => 0
240 | },
241 | Prev).
242 |
243 | check_ask_test_rates(Events) ->
244 | check_ask_test_rates(Events, []).
245 |
246 | check_ask_test_rates([], _Prev) ->
247 | ok;
248 | check_ask_test_rates([Event | Next], Prev) ->
249 | {Ts, Decision, SamplingPercentage} = Event,
250 | ?assert(SamplingPercentage >= 0 andalso SamplingPercentage =< 1),
251 | RelevantPrev = relevant_history(Ts, Prev),
252 | CountPerDecision = count_history_decisions(RelevantPrev),
253 | PrevSamples = maps:get(sample, CountPerDecision),
254 | PrevDrops = maps:get(drop, CountPerDecision),
255 | ct:pal("PrevSamples ~p, PrevDrops ~p", [PrevSamples, PrevDrops]),
256 | Total = PrevSamples + PrevDrops + 1,
257 | RealSamplingPercentage =
258 | if Decision =:= sample ->
259 | (PrevSamples + 1) / Total;
260 | Decision =:= drop ->
261 | (PrevSamples / Total)
262 | end,
263 | ?assertEqual(RealSamplingPercentage, SamplingPercentage),
264 | check_ask_test_rates(Next, [Event | Prev]).
265 |
266 | event_fun({value, Value}) ->
267 | fun (_Timestamp, Decision, SamplingPercentage) ->
268 | ?assert(lists:member(Decision, [sample, drop])),
269 | ?assert(SamplingPercentage >= 0 andalso SamplingPercentage =< 1),
270 | Value
271 | end;
272 | event_fun({exception, Class, Reason}) ->
273 | fun (_Timestamp, Decision, SamplingPercentage) ->
274 | ?assert(lists:member(Decision, [sample, drop])),
275 | ?assert(SamplingPercentage >= 0 andalso SamplingPercentage =< 1),
276 | erlang:raise(Class, Reason, [])
277 | end.
278 |
279 | maps_update_with(Key, Fun, Map) ->
280 | maps:update_with(Key, Fun, Map).
281 |
282 | maps_update_with(Key, Fun, Init, Map) ->
283 | maps:update_with(Key, Fun, Init, Map).
284 |
--------------------------------------------------------------------------------