Merge pull request #300 from philomena-dev/ad-extraction

Ads logic cleanup
This commit is contained in:
liamwhite 2024-06-24 16:25:46 -04:00 committed by GitHub
commit 1a7fbb29eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 233 additions and 85 deletions

View file

@ -7,53 +7,88 @@ defmodule Philomena.Adverts do
alias Philomena.Repo alias Philomena.Repo
alias Philomena.Adverts.Advert alias Philomena.Adverts.Advert
alias Philomena.Adverts.Restrictions
alias Philomena.Adverts.Server
alias Philomena.Adverts.Uploader alias Philomena.Adverts.Uploader
@doc """
Gets an advert that is currently live.
Returns the advert, or nil if nothing was live.
iex> random_live()
nil
iex> random_live()
%Advert{}
"""
def random_live do def random_live do
random_live_for_tags([])
end
@doc """
Gets an advert that is currently live, matching any tagging restrictions
for the given image.
Returns the advert, or nil if nothing was live.
## Examples
iex> random_live(%Image{})
nil
iex> random_live(%Image{})
%Advert{}
"""
def random_live(image) do
image
|> Repo.preload(:tags)
|> Map.get(:tags)
|> Enum.map(& &1.name)
|> random_live_for_tags()
end
defp random_live_for_tags(tags) do
now = DateTime.utc_now() now = DateTime.utc_now()
restrictions = Restrictions.tags(tags)
Advert query =
|> where(live: true, restrictions: "none") from a in Advert,
|> where([a], a.start_date < ^now and a.finish_date > ^now) where: a.live == true,
|> order_by(asc: fragment("random()")) where: a.restrictions in ^restrictions,
|> limit(1) where: a.start_date < ^now and a.finish_date > ^now,
|> Repo.one() order_by: [asc: fragment("random()")],
limit: 1
Repo.one(query)
end end
def random_live_for(image) do @doc """
image = Repo.preload(image, :tags) Asynchronously records a new impression.
now = DateTime.utc_now()
Advert ## Example
|> where(live: true)
|> where([a], a.restrictions in ^restrictions(image)) iex> record_impression(%Advert{})
|> where([a], a.start_date < ^now and a.finish_date > ^now) :ok
|> order_by(asc: fragment("random()"))
|> limit(1) """
|> Repo.one() def record_impression(%Advert{id: id}) do
Server.record_impression(id)
end end
defp sfw?(image) do @doc """
image_tags = MapSet.new(image.tags |> Enum.map(& &1.name)) Asynchronously records a new click.
sfw_tags = MapSet.new(["safe", "suggestive"])
intersect = MapSet.intersection(image_tags, sfw_tags)
MapSet.size(intersect) > 0 ## Example
end
defp nsfw?(image) do iex> record_click(%Advert{})
image_tags = MapSet.new(image.tags |> Enum.map(& &1.name)) :ok
nsfw_tags = MapSet.new(["questionable", "explicit"])
intersect = MapSet.intersection(image_tags, nsfw_tags)
MapSet.size(intersect) > 0 """
end def record_click(%Advert{id: id}) do
Server.record_click(id)
defp restrictions(image) do
restrictions = ["none"]
restrictions = if nsfw?(image), do: ["nsfw" | restrictions], else: restrictions
restrictions = if sfw?(image), do: ["sfw" | restrictions], else: restrictions
restrictions
end end
@doc """ @doc """
@ -102,7 +137,7 @@ defmodule Philomena.Adverts do
end end
@doc """ @doc """
Updates an advert. Updates an Advert without updating its image.
## Examples ## Examples
@ -119,6 +154,18 @@ defmodule Philomena.Adverts do
|> Repo.update() |> Repo.update()
end end
@doc """
Updates the image for an Advert.
## Examples
iex> update_advert_image(advert, %{image: new_value})
{:ok, %Advert{}}
iex> update_advert(advert, %{image: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_advert_image(%Advert{} = advert, attrs) do def update_advert_image(%Advert{} = advert, attrs) do
advert advert
|> Advert.changeset(attrs) |> Advert.changeset(attrs)

View file

@ -1,33 +1,9 @@
defmodule PhilomenaWeb.AdvertUpdater do defmodule Philomena.Adverts.Recorder do
alias Philomena.Adverts.Advert alias Philomena.Adverts.Advert
alias Philomena.Repo alias Philomena.Repo
import Ecto.Query import Ecto.Query
def child_spec([]) do def run(%{impressions: impressions, clicks: clicks}) 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 = DateTime.utc_now() |> DateTime.truncate(:second) now = DateTime.utc_now() |> DateTime.truncate(:second)
# Create insert statements for Ecto # Create insert statements for Ecto
@ -41,24 +17,7 @@ defmodule PhilomenaWeb.AdvertUpdater do
Repo.insert_all(Advert, impressions, on_conflict: impressions_update, conflict_target: [:id]) Repo.insert_all(Advert, impressions, on_conflict: impressions_update, conflict_target: [:id])
Repo.insert_all(Advert, clicks, on_conflict: clicks_update, conflict_target: [:id]) Repo.insert_all(Advert, clicks, on_conflict: clicks_update, conflict_target: [:id])
:timer.sleep(:timer.seconds(10)) :ok
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 end
defp impressions_insert_all({advert_id, impressions}, now) do defp impressions_insert_all({advert_id, impressions}, now) do

View file

@ -0,0 +1,47 @@
defmodule Philomena.Adverts.Restrictions do
@moduledoc """
Advert restriction application.
"""
@type restriction :: String.t()
@type restriction_list :: [restriction()]
@type tag_list :: [String.t()]
@nsfw_tags MapSet.new(["questionable", "explicit"])
@sfw_tags MapSet.new(["safe", "suggestive"])
@doc """
Calculates the restrictions available to a given tag list.
Returns a list containing `"none"`, and neither or one of `"sfw"`, `"nsfw"`.
## Examples
iex> tags([])
["none"]
iex> tags(["safe"])
["sfw", "none"]
iex> tags(["explicit"])
["nsfw", "none"]
"""
@spec tags(tag_list()) :: restriction_list()
def tags(tags) do
tags = MapSet.new(tags)
["none"]
|> apply_if(tags, @nsfw_tags, "nsfw")
|> apply_if(tags, @sfw_tags, "sfw")
end
@spec apply_if(restriction_list(), MapSet.t(), MapSet.t(), restriction()) :: restriction_list()
defp apply_if(restrictions, tags, test, new_restriction) do
if MapSet.disjoint?(tags, test) do
restrictions
else
[new_restriction | restrictions]
end
end
end

View file

@ -0,0 +1,94 @@
defmodule Philomena.Adverts.Server do
@moduledoc """
Advert impression and click aggregator.
Updating the impression count for adverts and clicks on every pageload is unnecessary
and slows down requests. This module collects the adverts and clicks and submits a batch
of updates to the database after every 10 seconds asynchronously, reducing the amount of
work to be done.
"""
use GenServer
alias Philomena.Adverts.Recorder
@type advert_id :: integer()
@doc """
Starts the GenServer.
See `GenServer.start_link/2` for more information.
"""
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc """
Asynchronously records a new impression.
## Example
iex> record_impression(advert.id)
:ok
"""
@spec record_impression(advert_id()) :: :ok
def record_impression(advert_id) do
GenServer.cast(__MODULE__, {:impressions, advert_id})
end
@doc """
Asynchronously records a new click.
## Example
iex> record_click(advert.id)
:ok
"""
@spec record_click(advert_id()) :: :ok
def record_click(advert_id) do
GenServer.cast(__MODULE__, {:clicks, advert_id})
end
# Used to force the GenServer to immediately sleep when no
# messages are available.
@timeout 0
@sleep :timer.seconds(10)
@impl true
@doc false
def init(_) do
{:ok, initial_state(), @timeout}
end
@impl true
@doc false
def handle_cast({type, advert_id}, state) do
# Update the counter described by the message
state = update_in(state[type], &increment_counter(&1, advert_id))
# Return to GenServer event loop
{:noreply, state, @timeout}
end
@impl true
@doc false
def handle_info(:timeout, state) do
# Process all updates from state now
Recorder.run(state)
# Sleep for the specified delay
:timer.sleep(@sleep)
# Return to GenServer event loop
{:noreply, initial_state(), @timeout}
end
defp increment_counter(map, advert_id) do
Map.update(map, advert_id, 1, &(&1 + 1))
end
defp initial_state do
%{impressions: %{}, clicks: %{}}
end
end

View file

@ -28,8 +28,10 @@ defmodule Philomena.Application do
node_name: valid_node_name(node()) node_name: valid_node_name(node())
]}, ]},
# Advert update batching
Philomena.Adverts.Server,
# Start the endpoint when the application starts # Start the endpoint when the application starts
PhilomenaWeb.AdvertUpdater,
PhilomenaWeb.UserFingerprintUpdater, PhilomenaWeb.UserFingerprintUpdater,
PhilomenaWeb.UserIpUpdater, PhilomenaWeb.UserIpUpdater,
PhilomenaWeb.Endpoint PhilomenaWeb.Endpoint

View file

@ -1,15 +1,15 @@
defmodule PhilomenaWeb.AdvertController do defmodule PhilomenaWeb.AdvertController do
use PhilomenaWeb, :controller use PhilomenaWeb, :controller
alias PhilomenaWeb.AdvertUpdater
alias Philomena.Adverts.Advert alias Philomena.Adverts.Advert
alias Philomena.Adverts
plug :load_resource, model: Advert plug :load_resource, model: Advert
def show(conn, _params) do def show(conn, _params) do
advert = conn.assigns.advert advert = conn.assigns.advert
AdvertUpdater.cast(:click, advert.id) Adverts.record_click(advert)
redirect(conn, external: advert.link) redirect(conn, external: advert.link)
end end

View file

@ -1,5 +1,4 @@
defmodule PhilomenaWeb.AdvertPlug do defmodule PhilomenaWeb.AdvertPlug do
alias PhilomenaWeb.AdvertUpdater
alias Philomena.Adverts alias Philomena.Adverts
alias Plug.Conn alias Plug.Conn
@ -19,7 +18,7 @@ defmodule PhilomenaWeb.AdvertPlug do
do: Conn.assign(conn, :advert, record_impression(Adverts.random_live())) do: Conn.assign(conn, :advert, record_impression(Adverts.random_live()))
defp maybe_assign_ad(conn, image, true), defp maybe_assign_ad(conn, image, true),
do: Conn.assign(conn, :advert, record_impression(Adverts.random_live_for(image))) do: Conn.assign(conn, :advert, record_impression(Adverts.random_live(image)))
defp maybe_assign_ad(conn, _image, _false), defp maybe_assign_ad(conn, _image, _false),
do: Conn.assign(conn, :advert, nil) do: Conn.assign(conn, :advert, nil)
@ -33,7 +32,7 @@ defmodule PhilomenaWeb.AdvertPlug do
defp record_impression(nil), do: nil defp record_impression(nil), do: nil
defp record_impression(advert) do defp record_impression(advert) do
AdvertUpdater.cast(:impression, advert.id) Adverts.record_impression(advert)
advert advert
end end