Skip to content

Commit

Permalink
feat: cache HN calls
Browse files Browse the repository at this point in the history
Use Cachex to cache results from HN calls. This should theoretically reduce the
amount of calls this bot makes to the HN API.

Once again, this task is a purely academic pursuit.
  • Loading branch information
chaychoong committed Aug 9, 2024
1 parent e2117d1 commit b72b913
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 3 deletions.
14 changes: 13 additions & 1 deletion lib/hn/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ defmodule Hn.Client do
"""
@link_regex ~r{^https://news.ycombinator.com/item\?id=(?<id>\d+)$}
@escape_regex ~r/([_\[\]()~`>#+=|{}\.!-])/
@cache_ttl_sec 3_600

@spec process_link(String.t()) :: {:ok, String.t()} | {:error, String.t()}
def process_link(link) do
with {:ok, id} <- get_id_from_link(link),
{:ok, hn_resp} <- Hn.API.fetch_item(id),
{:ok, hn_resp} <- fetch_item(id),
{:ok, reply} <- parse_item(hn_resp, link) do
{:ok, reply |> String.trim_trailing() |> escape_tg_reserved_chars()}
else
Expand All @@ -18,6 +19,17 @@ defmodule Hn.Client do
end
end

defp fetch_item(id) do
Cachex.fetch(:hn_cache, id, fn id ->
{:commit, Hn.API.fetch_item(id), ttl: :timer.seconds(@cache_ttl_sec)}
end)
|> case do
{:ok, {:ok, hn_resp}} -> {:ok, hn_resp}
{:commit, {:ok, hn_resp}, _} -> {:ok, hn_resp}
_ -> :api_error
end
end

defp get_id_from_link(link) do
case Regex.named_captures(@link_regex, link) do
%{"id" => id} -> {:ok, id}
Expand Down
3 changes: 2 additions & 1 deletion lib/hntg/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ defmodule Hntg.Application do
# {Hntg.Worker, arg}
{DNSCluster, query: Application.get_env(:hntg, :dns_cluster_query) || :ignore},
Hntg.Server,
{Task.Supervisor, name: Hntg.TaskSupervisor}
{Task.Supervisor, name: Hntg.TaskSupervisor},
{Cachex, name: :hn_cache}
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ defmodule Hntg.MixProject do
[
{:req, "~> 0.5.0"},
{:dns_cluster, "~> 0.1.3"},
{:cachex, "~> 3.6"},
{:mox, "~> 1.1", only: :test}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
Expand Down
5 changes: 5 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
%{
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"},
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
"stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
}
19 changes: 18 additions & 1 deletion test/hn/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Hn.ClientTest do
alias Hn.Client
alias Hn.API.Fixtures

setup :set_mox_from_context
setup :verify_on_exit!

describe "process_link/1" do
Expand All @@ -27,11 +28,27 @@ defmodule Hn.ClientTest do
|> expect(:fetch_item, fn _id -> Fixtures.sample_valid_text_response() end)

# ACT
assert {:ok, reply} = Client.process_link("https://news.ycombinator.com/item?id=00000000")
assert {:ok, reply} = Client.process_link("https://news.ycombinator.com/item?id=00000001")

# ASSERT
assert reply =~ "Link"
refute reply =~ "Comments"
end

test "multiple process_link/1 calls with the same link only fetches once" do
# ARRANGE
Hn.MockAPI
|> expect(:fetch_item, 1, fn _id -> Fixtures.sample_valid_link_response() end)

# ACT
Client.process_link("https://news.ycombinator.com/item?id=00000002")

# ASSERT
Hn.MockAPI
|> expect(:fetch_item, 0, fn _id -> Fixtures.sample_valid_link_response() end)

Client.process_link("https://news.ycombinator.com/item?id=00000002")
Client.process_link("https://news.ycombinator.com/item?id=00000002")
end
end
end

0 comments on commit b72b913

Please sign in to comment.