Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provides basic burst protection by default #35

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 52 additions & 8 deletions lib/tower.ex
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,12 @@ defmodule Tower do
passed along to the reporter.
"""

require Logger

alias Tower.Event

@default_burst_limit_period 1
@default_burst_limit_hits 10
@default_reporters [Tower.EphemeralReporter]

@doc """
Expand All @@ -226,8 +230,14 @@ defmodule Tower do
Note that `Tower.attach/0` is not a precondition for `Tower` `handle_*` functions to work
properly and inform reporters. They are independent.
"""
@spec attach() :: :ok
def attach do
@spec attach(Keyword.t()) :: :ok
def attach(options \\ []) do
rate_limiter_init(%{
burst_limit_period: Keyword.get(options, :burst_limit_period, @default_burst_limit_period),
burst_limit_hits: Keyword.get(options, :burst_limit_hits, @default_burst_limit_hits)
})

:ok = Tower.Config.update(options)
:ok = Tower.LoggerHandler.attach()
:ok = Tower.BanditExceptionHandler.attach()
:ok = Tower.ObanExceptionHandler.attach()
Expand All @@ -242,6 +252,8 @@ defmodule Tower do
"""
@spec detach() :: :ok
def detach do
rate_limiter_delete()

:ok = Tower.LoggerHandler.detach()
:ok = Tower.BanditExceptionHandler.detach()
:ok = Tower.ObanExceptionHandler.detach()
Expand Down Expand Up @@ -394,12 +406,24 @@ defmodule Tower do
end

defp report_event(%Event{} = event) do
reporters()
|> Enum.each(fn reporter ->
async(fn ->
reporter.report_event(event)
end)
end)
hit()
|> case do
:ok ->
reporters()
|> Enum.each(fn reporter ->
async(fn ->
reporter.report_event(event)
end)
end)

{:error, expected_wait_time_in_ms} ->
Logger.log(
:warning,
"Tower.LoggerHandler burst limited, ignoring log event. Expected to resume in #{expected_wait_time_in_ms}ms."
)

:ignore
end
end

defp reporters do
Expand All @@ -410,4 +434,24 @@ defmodule Tower do
Tower.TaskSupervisor
|> Task.Supervisor.start_child(fun)
end

defp hit do
rate_limiter()
|> RateLimiter.hit()
end

defp rate_limiter_init(%{
burst_limit_period: burst_limit_period,
burst_limit_hits: burst_limit_hits
}) do
RateLimiter.new(__MODULE__, burst_limit_period, burst_limit_hits)
end

defp rate_limiter_delete do
RateLimiter.delete(__MODULE__)
end

defp rate_limiter do
RateLimiter.get!(__MODULE__)
end
end
1 change: 1 addition & 0 deletions lib/tower/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Tower.Application do
def start(_type, _args) do
Supervisor.start_link(
[
Tower.Config,
{Task.Supervisor, name: Tower.TaskSupervisor}
],
strategy: :one_for_one,
Expand Down
15 changes: 15 additions & 0 deletions lib/tower/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Tower.Config do
use Agent

def start_link(_opts) do
Agent.start_link(fn -> [] end, name: __MODULE__)
end

def update(options) do
Agent.update(__MODULE__, fn current -> Keyword.merge(current, options) end)
end

def get do
Agent.get(__MODULE__, & &1)
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ defmodule Tower.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:rate_limiter, "~> 0.4.0"},
{:uniq, "~> 0.6.0"},
{:telemetry, "~> 1.1"},

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"rate_limiter": {:hex, :rate_limiter, "0.4.0", "9664f9b3b6c57aa4f2b76a59d3cefa3b9fca5d39ff26ce2020e217e652e52f56", [:mix], [], "hexpm", "04b8dcc7e2b0e2bd62196df2931e60f0da16c8efd6a737bc07ce75ec16b2e502"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
"uniq": {:hex, :uniq, "0.6.1", "369660ecbc19051be526df3aa85dc393af5f61f45209bce2fa6d7adb051ae03c", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "6426c34d677054b3056947125b22e0daafd10367b85f349e24ac60f44effb916"},
Expand Down
16 changes: 11 additions & 5 deletions test/tower_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup do
start_reporter()
Tower.attach()
Tower.attach(burst_limit_period: 10, burst_limit_hits: 1)

on_exit(fn ->
Tower.detach()
Expand All @@ -20,11 +20,16 @@
end

test "reports arithmetic error" do
capture_log(fn ->
in_unlinked_process(fn ->
1 / 0
captured_log =
capture_log(fn ->
in_unlinked_process(fn ->
1 / 0
end)

in_unlinked_process(fn ->
1 / 0
end)
end)
end)

assert_eventually(
[
Expand All @@ -42,6 +47,7 @@
assert String.length(id) == 36
assert recent_datetime?(datetime)
assert is_list(stacktrace)
assert captured_log =~ "[warning] Tower.LoggerHandler burst limited, ignoring log event"
end

test "reports a raise" do
Expand Down Expand Up @@ -311,7 +317,7 @@
test "reports Exception manually" do
in_unlinked_process(fn ->
try do
1 / 0

Check warning on line 320 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.12)

the call to //2 will fail with ArithmeticError

Check warning on line 320 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 24.3.4.17)

the call to //2 will fail with ArithmeticError

Check warning on line 320 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.12)

the call to //2 will fail with ArithmeticError

Check warning on line 320 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 24.3.4.17)

the call to //2 will fail with ArithmeticError
catch
kind, reason ->
Tower.handle_caught(kind, reason, __STACKTRACE__)
Expand All @@ -337,7 +343,7 @@
test "reports Exception manually (shorthand)" do
in_unlinked_process(fn ->
try do
1 / 0

Check warning on line 346 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.12)

the call to //2 will fail with ArithmeticError

Check warning on line 346 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 24.3.4.17)

the call to //2 will fail with ArithmeticError

Check warning on line 346 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 25.3.2.12)

the call to //2 will fail with ArithmeticError

Check warning on line 346 in test/tower_test.exs

View workflow job for this annotation

GitHub Actions / main (1.15, 24.3.4.17)

the call to //2 will fail with ArithmeticError
rescue
e ->
Tower.handle_exception(e, __STACKTRACE__)
Expand Down
Loading