rate limiter: Fix a race condition
authorrinpatch <rinpatch@sdf.org>
Fri, 28 Feb 2020 14:35:01 +0000 (17:35 +0300)
committerrinpatch <rinpatch@sdf.org>
Sat, 29 Feb 2020 22:13:07 +0000 (01:13 +0300)
When multiple requests are processed by rate limiter plug at the same
time and the bucket is not yet initialized, both would try to initialize
the bucket resulting in an internal server error.

lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
lib/pleroma/plugs/rate_limiter/rate_limiter.ex
test/plugs/rate_limiter_test.exs

index 187582edef4c9849f794766d1c14a94d7a3fd287..884268d96249572784d3b0458fa6f7fc43feb28e 100644 (file)
@@ -7,8 +7,8 @@ defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
     DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
   end
 
-  def add_limiter(limiter_name, expiration) do
-    {:ok, _pid} =
+  def add_or_return_limiter(limiter_name, expiration) do
+    result =
       DynamicSupervisor.start_child(
         __MODULE__,
         %{
@@ -28,6 +28,12 @@ defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
              ]}
         }
       )
+
+    case result do
+      {:ok, _pid} = result -> result
+      {:error, {:already_started, pid}} -> {:ok, pid}
+      _ -> result
+    end
   end
 
   @impl true
index 9c362a392c3663b0d542310e04f3b1d5df7931f3..b9cbe9716d5be00ee33dec3ff8b377be644424dd 100644 (file)
@@ -171,7 +171,7 @@ defmodule Pleroma.Plugs.RateLimiter do
         {:error, value}
 
       {:error, :no_cache} ->
-        initialize_buckets(action_settings)
+        initialize_buckets!(action_settings)
         check_rate(action_settings)
     end
   end
@@ -250,11 +250,16 @@ defmodule Pleroma.Plugs.RateLimiter do
     |> String.replace_leading(":", "")
   end
 
-  defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
+  defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok
 
-  defp initialize_buckets(%{name: name, limits: limits}) do
-    LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
-    LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
+  defp initialize_buckets!(%{name: name, limits: limits}) do
+    {:ok, _pid} =
+      LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits))
+
+    {:ok, _pid} =
+      LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits))
+
+    :ok
   end
 
   defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
index 104d67611bff9028c8ce191fc027b90694c6d819..8cdc8d1a2de0bdaca4848b4bc86b0809436fdede 100644 (file)
@@ -242,4 +242,35 @@ defmodule Pleroma.Plugs.RateLimiterTest do
       refute conn_2.halted
     end
   end
+
+  test "doesn't crash due to a race condition when multiple requests are made at the same time and the bucket is not yet initialized" do
+    limiter_name = :test_race_condition
+    Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
+    Pleroma.Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
+
+    opts = RateLimiter.init(name: limiter_name)
+
+    conn = conn(:get, "/")
+    conn_2 = conn(:get, "/")
+
+    %Task{pid: pid1} =
+      task1 =
+      Task.async(fn ->
+        receive do
+          :process2_up ->
+            RateLimiter.call(conn, opts)
+        end
+      end)
+
+    task2 =
+      Task.async(fn ->
+        send(pid1, :process2_up)
+        RateLimiter.call(conn_2, opts)
+      end)
+
+    Task.await(task1)
+    Task.await(task2)
+
+    refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts)
+  end
 end