Skip to content

Commit f9ea109

Browse files
committed
Added random devlog speed up with same logic as random projects logic. Cache prewarming
1 parent 3343afd commit f9ea109

6 files changed

Lines changed: 197 additions & 107 deletions

File tree

config/config.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import Config
99

1010
config :ftpdb,
11-
generators: [timestamp_type: :utc_datetime]
11+
generators: [timestamp_type: :utc_datetime],
12+
warm_random_cache_on_start: true
1213

1314
# Configure the endpoint
1415
config :ftpdb, FtpdbWeb.Endpoint,

config/test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ config :ftpdb, FtpdbWeb.Endpoint,
77
secret_key_base: "ElCR6TV0Hrxke9LrQYUKjKUwY7D6jV6jGgtfyG1HaaZ0OWh7teUxRdDjI/NkePhf",
88
server: false
99

10+
config :ftpdb,
11+
warm_random_cache_on_start: false
12+
1013
# In test we don't send emails
1114
config :ftpdb, Ftpdb.Mailer, adapter: Swoosh.Adapters.Test
1215

lib/ftpdb/DB.ex

Lines changed: 161 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,38 @@ defmodule Ftpdb.DB do
22
require Logger
33

44
@random_project_batch_size 100
5+
@random_devlog_batch_size 100
56
@random_project_cache_ttl :timer.minutes(30)
67
@default_project_banner_url "https://flavortown.hackclub.com/assets/default-banner-3d4e1b67.png"
78

9+
def warm_random_caches do
10+
[
11+
{:random_project_cache, random_project_batch_key(0),
12+
fn -> fetch_recent_project_batch(0) end},
13+
{:random_devlog_cache, random_devlog_batch_key(0), fn -> fetch_recent_devlog_batch(0) end}
14+
]
15+
|> Task.async_stream(
16+
fn {cache_name, key, fetcher} ->
17+
Cachex.fetch!(
18+
cache_name,
19+
key,
20+
fn _key -> fetcher.() end,
21+
expiration: @random_project_cache_ttl
22+
)
23+
end,
24+
max_concurrency: 2,
25+
ordered: false,
26+
timeout: :infinity
27+
)
28+
|> Stream.run()
29+
30+
:ok
31+
rescue
32+
exception ->
33+
Logger.warning("Failed to warm random caches on startup: #{Exception.message(exception)}")
34+
:ok
35+
end
36+
837
defp project_duration_fields(duration_seconds) do
938
duration_seconds = duration_seconds || 0
1039

@@ -627,23 +656,61 @@ defmodule Ftpdb.DB do
627656
end)
628657
end
629658

630-
defp get_max_devlog_id do
631-
{:ok, response} =
632-
Supabase.PostgREST.from(client(), "devlogs")
633-
|> Supabase.PostgREST.select(["id"])
634-
|> Supabase.PostgREST.order("id", desc: true)
635-
|> Supabase.PostgREST.limit(1)
636-
|> Map.put(:method, :get)
637-
|> Supabase.PostgREST.execute()
659+
def random_devlogs(limit \\ 25, excluded_devlog_ids \\ [])
638660

639-
case response.body do
640-
[%{"id" => max_id}] -> max_id
641-
_ -> 0
661+
def random_devlogs(limit, excluded_devlog_ids)
662+
when is_integer(limit) and limit > 0 and is_list(excluded_devlog_ids) do
663+
excluded_devlog_ids =
664+
excluded_devlog_ids
665+
|> Enum.map(&to_string/1)
666+
|> MapSet.new()
667+
668+
limit
669+
|> build_random_devlog_pool(0, [], excluded_devlog_ids)
670+
|> Enum.take_random(limit)
671+
end
672+
673+
def random_devlogs(_limit, _excluded_devlog_ids), do: []
674+
675+
defp build_random_devlog_pool(limit, _batch_index, acc, _excluded_devlog_ids)
676+
when length(acc) >= limit,
677+
do: acc
678+
679+
defp build_random_devlog_pool(limit, batch_index, acc, excluded_devlog_ids) do
680+
batch =
681+
Cachex.fetch!(
682+
:random_devlog_cache,
683+
random_devlog_batch_key(batch_index),
684+
fn _key -> fetch_recent_devlog_batch(batch_index) end,
685+
expiration: @random_project_cache_ttl
686+
)
687+
688+
case batch do
689+
[] ->
690+
acc
691+
692+
devlogs ->
693+
filtered_devlogs =
694+
Enum.reject(devlogs, fn devlog ->
695+
MapSet.member?(excluded_devlog_ids, to_string(devlog.id))
696+
end)
697+
698+
build_random_devlog_pool(
699+
limit,
700+
batch_index + 1,
701+
acc ++ filtered_devlogs,
702+
excluded_devlog_ids
703+
)
642704
end
643705
end
644706

645-
defp fetch_and_format_devlog(id) do
646-
result =
707+
defp random_devlog_batch_key(batch_index), do: "recent_devlogs_batch:#{batch_index}"
708+
709+
defp fetch_recent_devlog_batch(batch_index) do
710+
start_index = batch_index * @random_devlog_batch_size
711+
end_index = start_index + @random_devlog_batch_size - 1
712+
713+
{:ok, response} =
647714
Supabase.PostgREST.from(client(), "devlogs")
648715
|> Supabase.PostgREST.select([
649716
"id",
@@ -654,103 +721,101 @@ defmodule Ftpdb.DB do
654721
"media_urls",
655722
"project_id"
656723
])
657-
|> Supabase.PostgREST.eq("id", id)
658724
|> then(fn req ->
659725
Supabase.Fetcher.Request.with_query(req, %{"project_id" => "not.is.null"})
660726
end)
727+
|> Supabase.PostgREST.order("created_at", desc: true)
728+
|> Supabase.PostgREST.range(start_index, end_index)
661729
|> Map.put(:method, :get)
662730
|> Supabase.PostgREST.execute()
663731

664-
case result do
665-
{:ok, %{body: [item]}} when not is_nil(item) ->
666-
duration = item["duration_seconds"] || 0
667-
media_urls = item["media_urls"] || []
668-
project_id = item["project_id"]
669-
670-
# Fetch project
671-
{:ok, project_resp} =
672-
Supabase.PostgREST.from(client(), "projects")
673-
|> Supabase.PostgREST.select(["title", "banner_url"])
674-
|> Supabase.PostgREST.eq("id", project_id)
675-
|> Map.put(:method, :get)
676-
|> Supabase.PostgREST.execute()
677-
678-
{project_title, project_banner} =
679-
case project_resp.body do
680-
[p] -> {p["title"] || "Unknown", p["banner_url"]}
681-
_ -> {"Unknown", nil}
682-
end
732+
response.body
733+
|> List.wrap()
734+
|> Task.async_stream(&format_recent_devlog_for_random/1,
735+
max_concurrency: System.schedulers_online() * 2,
736+
ordered: false,
737+
timeout: :infinity
738+
)
739+
|> Enum.reduce([], fn
740+
{:ok, nil}, acc ->
741+
acc
683742

684-
# Fetch user
685-
user_id = get_user_id(project_id)
686-
687-
{user_avatar, user_display_name} =
688-
if user_id do
689-
case get_user_info(user_id) do
690-
[u] -> {u.avatar_url, u.display_name || "Unknown User"}
691-
_ -> {nil, "Unknown User"}
692-
end
693-
else
694-
{nil, "Unknown User"}
743+
{:ok, devlog}, acc ->
744+
[devlog | acc]
745+
746+
{:exit, reason}, acc ->
747+
Logger.warning("Failed to build random devlog batch #{batch_index}: #{inspect(reason)}")
748+
acc
749+
end)
750+
|> Enum.reverse()
751+
end
752+
753+
defp format_recent_devlog_for_random(item) do
754+
duration = item["duration_seconds"] || 0
755+
media_urls = item["media_urls"] || []
756+
project_id = item["project_id"]
757+
758+
with project_id when not is_nil(project_id) <- project_id,
759+
%{title: project_title, banner_url: project_banner} <-
760+
cached_random_devlog_project(project_id) do
761+
user_id = cached_user_id(project_id)
762+
763+
{user_avatar, user_display_name} =
764+
if user_id do
765+
case cached_user_info(user_id) do
766+
[user_info] -> {user_info.avatar_url, user_info.display_name || "Unknown User"}
767+
_ -> {nil, "Unknown User"}
695768
end
769+
else
770+
{nil, "Unknown User"}
771+
end
696772

773+
%{
774+
id: item["id"],
775+
body: item["body"],
776+
total_hours: div(duration, 3600),
777+
comments_count: item["comments_count"] || 0,
778+
created_at: item["created_at"],
779+
media_urls: media_urls,
780+
project_id: project_id,
781+
project_title: project_title,
782+
project_banner: project_banner,
783+
project_avatar: user_avatar,
784+
user_id: user_id,
785+
user_avatar: user_avatar,
786+
user_display_name: user_display_name
787+
}
788+
else
789+
_ -> nil
790+
end
791+
end
792+
793+
defp cached_random_devlog_project(project_id) do
794+
Cachex.fetch!(
795+
:random_devlog_cache,
796+
"devlog_project_summary:#{project_id}",
797+
fn _key -> fetch_random_devlog_project(project_id) end,
798+
expiration: @random_project_cache_ttl
799+
)
800+
end
801+
802+
defp fetch_random_devlog_project(project_id) do
803+
{:ok, response} =
804+
Supabase.PostgREST.from(client(), "projects")
805+
|> Supabase.PostgREST.select(["title", "banner_url"])
806+
|> Supabase.PostgREST.eq("id", project_id)
807+
|> Map.put(:method, :get)
808+
|> Supabase.PostgREST.execute()
809+
810+
case response.body do
811+
[project] ->
697812
%{
698-
id: item["id"],
699-
body: item["body"],
700-
total_hours: div(duration, 3600),
701-
comments_count: item["comments_count"] || 0,
702-
created_at: item["created_at"],
703-
media_urls: media_urls,
704-
project_id: project_id,
705-
project_title: project_title,
706-
project_banner: project_banner,
707-
project_avatar: user_avatar,
708-
user_id: user_id,
709-
user_avatar: user_avatar,
710-
user_display_name: user_display_name
813+
title: project["title"] || "Unknown",
814+
banner_url: project["banner_url"]
711815
}
712816

713817
_ ->
714818
nil
715819
end
716820
end
717-
718-
def random_devlogs do
719-
max_id = get_max_devlog_id()
720-
721-
if max_id == 0 do
722-
[]
723-
else
724-
# We need 25 devlogs, but some IDs might be missing or without project_id
725-
# Generate more IDs to ensure we get 25 valid ones.
726-
random_ids =
727-
1..200
728-
|> Enum.map(fn _ -> :rand.uniform(max_id) end)
729-
|> Enum.uniq()
730-
731-
random_ids
732-
|> Enum.reduce_while([], fn id, acc ->
733-
if length(acc) >= 25 do
734-
{:halt, acc}
735-
else
736-
devlog =
737-
Cachex.fetch!(
738-
:random_devlog_cache,
739-
to_string(id),
740-
fn _key ->
741-
fetch_and_format_devlog(id)
742-
end,
743-
expiration: :timer.minutes(30)
744-
)
745-
746-
if devlog do
747-
{:cont, [devlog | acc]}
748-
else
749-
{:cont, acc}
750-
end
751-
end
752-
end)
753-
|> Enum.sort_by(fn devlog -> devlog.created_at end, :desc)
754-
end
755-
end
756821
end

lib/ftpdb/application.ex

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,21 @@ defmodule Ftpdb.Application do
3939
# See https://hexdocs.pm/elixir/Supervisor.html
4040
# for other strategies and supported options
4141
opts = [strategy: :one_for_one, name: Ftpdb.Supervisor]
42-
Supervisor.start_link(children, opts)
42+
43+
case Supervisor.start_link(children, opts) do
44+
{:ok, _pid} = result ->
45+
maybe_warm_random_caches()
46+
result
47+
48+
other ->
49+
other
50+
end
51+
end
52+
53+
defp maybe_warm_random_caches do
54+
if Application.get_env(:ftpdb, :warm_random_cache_on_start, true) do
55+
Task.start(fn -> Ftpdb.DB.warm_random_caches() end)
56+
end
4357
end
4458

4559
# Tell Phoenix to update the endpoint configuration

lib/ftpdb_web/controllers/api_controller.ex

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ defmodule FtpdbWeb.ApiController do
9797

9898
def random_projects(conn, %{"filter" => filter} = params) do
9999
limit = String.to_integer(Map.get(params, "limit", "10"))
100-
excluded_project_ids = parse_excluded_project_ids(params)
100+
excluded_project_ids = parse_excluded_ids(params)
101101

102102
case filter do
103103
"hottest" ->
@@ -116,18 +116,20 @@ defmodule FtpdbWeb.ApiController do
116116

117117
def random_projects(conn, params) do
118118
limit = String.to_integer(Map.get(params, "limit", "10"))
119-
excluded_project_ids = parse_excluded_project_ids(params)
119+
excluded_project_ids = parse_excluded_ids(params)
120120
json(conn, Ftpdb.DB.random_projects(limit, excluded_project_ids))
121121
end
122122

123-
defp parse_excluded_project_ids(params) do
123+
defp parse_excluded_ids(params) do
124124
params
125125
|> Map.get("exclude_ids", "")
126126
|> String.split(",", trim: true)
127127
end
128128

129-
def random_devlogs(conn, _params) do
130-
devlogs = Ftpdb.DB.random_devlogs()
129+
def random_devlogs(conn, params) do
130+
limit = String.to_integer(Map.get(params, "limit", "25"))
131+
excluded_devlog_ids = parse_excluded_ids(params)
132+
devlogs = Ftpdb.DB.random_devlogs(limit, excluded_devlog_ids)
131133
json(conn, devlogs)
132134
end
133135

0 commit comments

Comments
 (0)