├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── mix.exs ├── rebar.config └── src ├── ratx.app.src └── ratx.erl /.gitignore: -------------------------------------------------------------------------------- 1 | /ebin/ 2 | .rebar 3 | /_build 4 | /cover 5 | /deps 6 | erl_crash.dump 7 | *.ez 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | First release! 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Dmitry Russ(Aleksandrov), Travelping 2 | 3 | The software may only be used for the great good and the 4 | true happiness of all sentient beings. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS 7 | ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 8 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO 9 | EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER 11 | RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 13 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 14 | OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ratx 2 | 3 | The Ratx Erlang application provides overload protection for 4 | Erlang systems. It provides queueing facilities for tasks to be 5 | executed so their concurrency can be limited on a running 6 | system. 7 | 8 | # Inspiration 9 | 10 | Ratx owes its inspiration to Ulf Wigers `jobs` framework and 11 | Jesper Louis Andersen `safetyvalve`, but it is a different 12 | implementation and a different approach as well. 13 | 14 | The main difference and goal is to try to eliminate the one gen_server 15 | process to handle all requests. And, the same as `safetyvalve` to 16 | have one process per queue, which can be supervised(and restarted, 17 | if something go wrong). 18 | 19 | `ets:update_counter/3` seems the best options and natural to try, and 20 | heavily used in production for such use cases with good effort. 21 | 22 | Due to this change, the queue process should only handle all `done`-s, 23 | and dropping of entries in a queue after timeout. If queue is full, no 24 | messages or calls will be sent to a queue process, that is a protection 25 | for a process on big birsts. 26 | 27 | # Using 28 | 29 | Example of using 30 | 31 | ```erlang 32 | ratx:start_link(test_queue, [{limit, 10}, {queue, 100}, {queue_timeout, 1000}]), 33 | Action = fun(I, Time) -> io:format("~p..", [I]), timer:sleep(Time) end, 34 | [spawn(fun() -> ratx:run(test_queue, fun() -> Action(I, 40) end) end) || I <- lists:seq(1, 30)], ok. 35 | ``` 36 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ratx.Mixfile do 2 | use Mix.Project 3 | @vsn "0.1.0" 4 | @github "https://github.com/liveforeverx/ratx" 5 | 6 | def project do 7 | [app: :ratx, 8 | version: @vsn, 9 | description: description, 10 | package: package] 11 | end 12 | 13 | defp description do 14 | """ 15 | Rate limiter and overload protection for erlang and elixir applications. 16 | """ 17 | end 18 | 19 | defp package do 20 | [files: ["src", "priv", "mix.exs", "rebar.config", "README*", "LICENSE*"], 21 | licenses: ["MIT"], 22 | links: %{"GitHub" => @github}] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | -------------------------------------------------------------------------------- /src/ratx.app.src: -------------------------------------------------------------------------------- 1 | {application, ratx, [ 2 | {description, "Rate limiter and overload protection erlang app"}, 3 | {vsn, "1.0"}, 4 | {applications, [kernel, stdlib]}, 5 | {registered, []}, 6 | {env, []} 7 | ]}. -------------------------------------------------------------------------------- /src/ratx.erl: -------------------------------------------------------------------------------- 1 | -module(ratx). 2 | -export([start_link/2, start/2]). 3 | -export([init/1, handle_call/3, handle_cast/2, 4 | handle_info/2, terminate/2, code_change/3]). 5 | -export([run/2, run_unsave/2, ask/1, done/1]). 6 | -export([info/1]). 7 | 8 | -define(DEFAULT_LIMIT, 100). 9 | -define(DEFAULT_QUEUE, 1000). 10 | -define(QUEUE_TIMEOUT, 1000). 11 | -define(counter, <<>>). 12 | -compile([export_all]). 13 | 14 | run_unsave(Name, Fun) -> 15 | case ask(Name) of 16 | drop -> 17 | drop; 18 | Ref when is_reference(Ref) -> 19 | Res = Fun(), 20 | done(Name), 21 | Res 22 | end. 23 | 24 | run(Name, Fun) -> 25 | case ask(Name) of 26 | drop -> 27 | drop; 28 | Ref when is_reference(Ref) -> 29 | Res = (catch Fun()), 30 | done(Name), 31 | Res 32 | end. 33 | 34 | ask(Name) -> 35 | Ref = make_ref(), 36 | [{opts, Queue, Limit, QueueSize}] = ets:lookup(Name, opts), 37 | case ets:update_counter(Name, ?counter, [{2, 0}, {2, 1, Limit, Limit}]) of 38 | [Counter, _] when Counter < Limit -> 39 | Ref; 40 | [Limit, Limit] -> 41 | try_queue(Ref, Name, Queue, QueueSize) 42 | end. 43 | 44 | try_queue(Ref, Name, Queue, QueueSize) -> 45 | case ets:update_counter(Queue, ?counter, [{2, 0}, {2, 1, QueueSize, QueueSize}]) of 46 | [ActualQueue, _] when ActualQueue < QueueSize -> 47 | Mref = monitor(process, whereis(Name)), 48 | ets:insert(Queue, {{os:timestamp(), Ref, self()}}), 49 | receive 50 | {Ref, Reply} -> 51 | erlang:demonitor(Mref, [flush]), 52 | Reply; 53 | {'DOWN', _Mref, _, _, _} -> 54 | broken 55 | end; 56 | [QueueSize, QueueSize] -> 57 | drop 58 | end. 59 | 60 | info(Name) -> 61 | Queue = list_to_atom(atom_to_list(Name) ++ "_queue"), 62 | {ets:tab2list(Name), ets:tab2list(Queue)}. 63 | 64 | done(Name) -> 65 | gen_server:cast(Name, dequeue). 66 | 67 | start_link(Name, Opts) -> 68 | gen_server:start_link({local, Name}, ?MODULE, {Name, Opts}, []). 69 | 70 | start(Name, Opts) -> 71 | gen_server:start({local, Name}, ?MODULE, {Name, Opts}, []). 72 | 73 | -record(state, {name, limit, queue, queue_size, queue_timeout}). 74 | 75 | init({Name, Opts}) -> 76 | Limit = proplists:get_value(limit, Opts, ?DEFAULT_LIMIT), 77 | QueueSize = proplists:get_value(queue, Opts, ?DEFAULT_QUEUE), 78 | Timeout = proplists:get_value(queue_timeout, Opts, ?QUEUE_TIMEOUT), 79 | Queue = list_to_atom(atom_to_list(Name) ++ "_queue"), 80 | 81 | ets:new(Name, [named_table, public, set, {keypos, 1}]), 82 | ets:insert(Name, [{<<>>, 0}, {opts, Queue, Limit, QueueSize}]), 83 | 84 | ets:new(Queue, [named_table, public, ordered_set, {keypos, 1}]), 85 | ets:insert(Queue, {<<>>, 0}), 86 | 87 | send_after(Timeout, self(), queue_check), 88 | 89 | {ok, #state{name = Name, 90 | queue = Queue, 91 | limit = Limit, 92 | queue_size = QueueSize, 93 | queue_timeout = Timeout}}. 94 | 95 | handle_call(_Req, _From, S) -> 96 | {reply, badarg, S}. 97 | handle_cast(dequeue, #state{name = Name, queue = Queue, queue_timeout = Timeout} = S) -> 98 | DropTimeout = subsctruct_ms_from_now(os:timestamp(), Timeout), 99 | dequeue(Name, Queue, DropTimeout), 100 | {noreply, S}; 101 | handle_cast(_Req, S) -> 102 | {noreply, S}. 103 | handle_info(queue_check, #state{queue = Queue, queue_timeout = Timeout} = S) -> 104 | DropTimeout = subsctruct_ms_from_now(os:timestamp(), Timeout), 105 | NextTimeout = drop(Queue, DropTimeout, Timeout), 106 | erlang:send_after(NextTimeout, self(), queue_check), 107 | {noreply, S}; 108 | handle_info(_Info, S) -> 109 | {noreply, S}. 110 | terminate(_,_) -> 111 | ok. 112 | code_change(_FromVsn, State, _Extra) -> 113 | {ok, State}. 114 | 115 | dequeue(Name, Queue, DropTimeout) -> 116 | case find_next(Queue, DropTimeout) of 117 | false -> 118 | ets:update_counter(Name, ?counter, [{2, -1, 0, 0}]); 119 | {_TS, Ref, Pid} = Key -> 120 | dequeue_key(Queue, Key), 121 | Pid ! {Ref, Ref} 122 | end. 123 | 124 | dequeue_key(Queue, Key) -> 125 | ets:delete(Queue, Key), 126 | ets:update_counter(Queue, ?counter, [{2, -1, 0, 0}]). 127 | 128 | drop(Queue, DropTimeout, Timeout) -> 129 | case find_next(Queue, DropTimeout) of 130 | false -> 131 | Timeout; 132 | {TS, _Ref, _Pid} -> 133 | (timer:now_diff(TS, DropTimeout) div 1000) 134 | end. 135 | 136 | find_next(Queue, DropTimeout) -> 137 | case ets:first(Queue) of 138 | ?counter -> 139 | false; 140 | {TS, Ref, Pid} = Key -> 141 | case TS < DropTimeout of 142 | true -> 143 | dequeue_key(Queue, Key), 144 | Pid ! {Ref, drop}, 145 | find_next(Queue, DropTimeout); 146 | false -> 147 | Key 148 | end 149 | end. 150 | 151 | subsctruct_ms_from_now({MegaSecs, Secs, MicroSecs}, Ms) -> 152 | MinusMegaSecs = Ms div 1000000000, 153 | MinusSecs = (Ms - MinusMegaSecs * 1000000000) div 1000, 154 | MinusMicroSecs = (Ms rem 1000) * 1000, 155 | {MegaSecs - MinusMegaSecs, Secs - MinusSecs, MicroSecs - MinusMicroSecs}. 156 | 157 | send_after(infinity, _, _Action) -> undefined; 158 | send_after(Timeout, _, Action) -> erlang:send_after(Timeout, self(), Action). 159 | --------------------------------------------------------------------------------