diff --git a/lib/philomena/adverts.ex b/lib/philomena/adverts.ex index 4e2a9c9d..2752c112 100644 --- a/lib/philomena/adverts.ex +++ b/lib/philomena/adverts.ex @@ -19,7 +19,6 @@ defmodule Philomena.Adverts do |> order_by(asc: fragment("random()")) |> limit(1) |> Repo.one() - |> record_impression() end def random_live_for(image) do @@ -33,25 +32,6 @@ defmodule Philomena.Adverts do |> order_by(asc: fragment("random()")) |> limit(1) |> Repo.one() - |> record_impression() - end - - def click(%Advert{} = advert) do - spawn(fn -> - query = where(Advert, id: ^advert.id) - Repo.update_all(query, inc: [clicks: 1]) - end) - end - - defp record_impression(nil), do: nil - - defp record_impression(advert) do - spawn(fn -> - query = where(Advert, id: ^advert.id) - Repo.update_all(query, inc: [impressions: 1]) - end) - - advert end defp sfw?(image) do diff --git a/lib/philomena/application.ex b/lib/philomena/application.ex index 87193f81..97a04cd0 100644 --- a/lib/philomena/application.ex +++ b/lib/philomena/application.ex @@ -17,6 +17,9 @@ defmodule Philomena.Application do # Start the Ecto repository Philomena.Repo, + # Background queueing system + Philomena.ExqSupervisor, + # Starts a worker by calling: Philomena.Worker.start_link(arg) # {Philomena.Worker, arg}, Philomena.Servers.ImageProcessor, @@ -28,6 +31,7 @@ defmodule Philomena.Application do {Phoenix.PubSub, [name: Philomena.PubSub, adapter: Phoenix.PubSub.PG2]}, # Start the endpoint when the application starts + PhilomenaWeb.AdvertUpdater, PhilomenaWeb.StatsUpdater, PhilomenaWeb.UserFingerprintUpdater, PhilomenaWeb.UserIpUpdater, diff --git a/lib/philomena_web/advert_updater.ex b/lib/philomena_web/advert_updater.ex new file mode 100644 index 00000000..b3941148 --- /dev/null +++ b/lib/philomena_web/advert_updater.ex @@ -0,0 +1,81 @@ +defmodule PhilomenaWeb.AdvertUpdater do + alias Philomena.Adverts.Advert + alias Philomena.Repo + import Ecto.Query + + def child_spec([]) do + %{ + id: PhilomenaWeb.AdvertUpdater, + start: {PhilomenaWeb.AdvertUpdater, :start_link, [[]]} + } + end + + def start_link([]) do + {:ok, spawn_link(&init/0)} + end + + def cast(type, advert_id) when type in [:impression, :click] do + pid = Process.whereis(:advert_updater) + if pid, do: send(pid, {type, advert_id}) + end + + defp init do + Process.register(self(), :advert_updater) + run() + end + + defp run do + # Read impression counts from mailbox + {impressions, clicks} = receive_all() + + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + + # Create insert statements for Ecto + impressions = Enum.map(impressions, &impressions_insert_all(&1, now)) + clicks = Enum.map(clicks, &clicks_insert_all(&1, now)) + + # Merge into table + impressions_update = update(Advert, inc: [impressions: fragment("EXCLUDED.impressions")]) + clicks_update = update(Advert, inc: [clicks: fragment("EXCLUDED.clicks")]) + + Repo.insert_all(Advert, impressions, on_conflict: impressions_update, conflict_target: [:id]) + Repo.insert_all(Advert, clicks, on_conflict: clicks_update, conflict_target: [:id]) + + :timer.sleep(:timer.seconds(10)) + + run() + end + + defp receive_all(impressions \\ %{}, clicks \\ %{}) do + receive do + {:impression, advert_id} -> + impressions = Map.update(impressions, advert_id, 1, &(&1 + 1)) + receive_all(impressions, clicks) + + {:click, advert_id} -> + clicks = Map.update(clicks, advert_id, 1, &(&1 + 1)) + receive_all(impressions, clicks) + after + 0 -> + {impressions, clicks} + end + end + + defp impressions_insert_all({advert_id, impressions}, now) do + %{ + id: advert_id, + impressions: impressions, + created_at: now, + updated_at: now + } + end + + defp clicks_insert_all({advert_id, clicks}, now) do + %{ + id: advert_id, + clicks: clicks, + created_at: now, + updated_at: now + } + end +end diff --git a/lib/philomena_web/controllers/advert_controller.ex b/lib/philomena_web/controllers/advert_controller.ex index f0845322..9387fbd3 100644 --- a/lib/philomena_web/controllers/advert_controller.ex +++ b/lib/philomena_web/controllers/advert_controller.ex @@ -1,7 +1,7 @@ defmodule PhilomenaWeb.AdvertController do use PhilomenaWeb, :controller - alias Philomena.Adverts + alias PhilomenaWeb.AdvertUpdater alias Philomena.Adverts.Advert plug :load_resource, model: Advert @@ -9,9 +9,8 @@ defmodule PhilomenaWeb.AdvertController do def show(conn, _params) do advert = conn.assigns.advert - Adverts.click(advert) + AdvertUpdater.cast(:click, advert.id) - conn - |> redirect(external: advert.link) + redirect(conn, external: advert.link) end end diff --git a/lib/philomena_web/plugs/advert_plug.ex b/lib/philomena_web/plugs/advert_plug.ex index c45ae20c..5dfeb0a3 100644 --- a/lib/philomena_web/plugs/advert_plug.ex +++ b/lib/philomena_web/plugs/advert_plug.ex @@ -1,4 +1,5 @@ defmodule PhilomenaWeb.AdvertPlug do + alias PhilomenaWeb.AdvertUpdater alias Philomena.Adverts alias Plug.Conn @@ -9,15 +10,14 @@ defmodule PhilomenaWeb.AdvertPlug do image = conn.assigns[:image] show_ads? = show_ads?(user) - conn - |> maybe_assign_ad(image, show_ads?) + maybe_assign_ad(conn, image, show_ads?) end defp maybe_assign_ad(conn, nil, true), - do: Conn.assign(conn, :advert, Adverts.random_live()) + do: Conn.assign(conn, :advert, record_impression(Adverts.random_live())) defp maybe_assign_ad(conn, image, true), - do: Conn.assign(conn, :advert, Adverts.random_live_for(image)) + do: Conn.assign(conn, :advert, record_impression(Adverts.random_live_for(image))) defp maybe_assign_ad(conn, _image, _false), do: conn @@ -27,4 +27,12 @@ defmodule PhilomenaWeb.AdvertPlug do defp show_ads?(_user), do: true + + defp record_impression(nil), do: nil + + defp record_impression(advert) do + AdvertUpdater.cast(:impression, advert.id) + + advert + end end