├── .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://img.shields.io/hexpm/v/deigma.svg?style=flat)](https://hex.pm/packages/deigma) 7 | [![](https://github.com/g-andrade/deigma/workflows/build/badge.svg)](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 | 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 | 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 | 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 | %% 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 | %% 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 | %% 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 | %% 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 | %% 140 | %% 141 | %% Returns: 142 | %% 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 | %% 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 | %% 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 | --------------------------------------------------------------------------------