fix: removed problems caused by concurrency
This commit is contained in:
@@ -6,6 +6,8 @@ config :bds,
|
|||||||
config :bds, BDS.Repo,
|
config :bds, BDS.Repo,
|
||||||
database: Path.expand("../priv/data/bds_dev.db", __DIR__),
|
database: Path.expand("../priv/data/bds_dev.db", __DIR__),
|
||||||
pool_size: 5,
|
pool_size: 5,
|
||||||
|
busy_timeout: 15_000,
|
||||||
|
log: false,
|
||||||
stacktrace: true,
|
stacktrace: true,
|
||||||
show_sensitive_data_on_connection_error: true
|
show_sensitive_data_on_connection_error: true
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
alias BDS.Tasks
|
alias BDS.Tasks
|
||||||
|
|
||||||
@site_sections [:core, :single, :category, :tag, :date]
|
@site_sections [:core, :single, :category, :tag, :date]
|
||||||
|
@rebuild_phase_timeout 600_000
|
||||||
|
|
||||||
def execute(action, params \\ %{})
|
def execute(action, params \\ %{})
|
||||||
|
|
||||||
@@ -119,44 +120,16 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
defp dispatch("rebuild_database", project, _params) do
|
defp dispatch("rebuild_database", project, _params) do
|
||||||
group_id = task_group_id("rebuild_database")
|
group_id = task_group_id("rebuild_database")
|
||||||
attrs = %{group_id: group_id, group_name: "Maintenance"}
|
attrs = %{group_id: group_id, group_name: "Maintenance"}
|
||||||
|
[first_step | remaining_steps] = rebuild_database_steps(project)
|
||||||
|
|
||||||
{:ok, posts_task} =
|
{:ok, posts_task} =
|
||||||
Tasks.submit_task("Rebuild Posts From Files", fn report ->
|
Tasks.submit_task(first_step.name, first_step.work, attrs)
|
||||||
{:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report)
|
|
||||||
report.(1.0, "Post rebuild complete")
|
|
||||||
%{project_id: project.id, counts: %{posts: length(posts)}}
|
|
||||||
end, attrs)
|
|
||||||
|
|
||||||
{:ok, _media_task} =
|
|
||||||
Tasks.submit_task("Rebuild Media From Files", fn report ->
|
|
||||||
{:ok, media} = Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report)
|
|
||||||
report.(1.0, "Media rebuild complete")
|
|
||||||
%{project_id: project.id, counts: %{media: length(media)}}
|
|
||||||
end, attrs)
|
|
||||||
|
|
||||||
{:ok, _scripts_task} =
|
|
||||||
Tasks.submit_task("Rebuild Scripts From Files", fn report ->
|
|
||||||
{:ok, scripts} = Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report)
|
|
||||||
report.(1.0, "Script rebuild complete")
|
|
||||||
%{project_id: project.id, counts: %{scripts: length(scripts)}}
|
|
||||||
end, attrs)
|
|
||||||
|
|
||||||
{:ok, _templates_task} =
|
|
||||||
Tasks.submit_task("Rebuild Templates From Files", fn report ->
|
|
||||||
{:ok, templates} = Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report)
|
|
||||||
report.(1.0, "Template rebuild complete")
|
|
||||||
%{project_id: project.id, counts: %{templates: length(templates)}}
|
|
||||||
end, attrs)
|
|
||||||
|
|
||||||
Task.start(fn ->
|
Task.start(fn ->
|
||||||
wait_for_group_phase(group_id, [
|
case wait_for_group_phase(group_id, [first_step.name], @rebuild_phase_timeout) do
|
||||||
"Rebuild Posts From Files",
|
:ok -> run_rebuild_sequence(group_id, attrs, remaining_steps)
|
||||||
"Rebuild Media From Files",
|
_other -> :ok
|
||||||
"Rebuild Scripts From Files",
|
end
|
||||||
"Rebuild Templates From Files"
|
|
||||||
])
|
|
||||||
|
|
||||||
submit_rebuild_followups(project, attrs)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
@@ -258,35 +231,80 @@ defmodule BDS.Desktop.ShellCommands do
|
|||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp submit_rebuild_followups(project, attrs) do
|
defp rebuild_database_steps(project) do
|
||||||
{:ok, _links_task} =
|
[
|
||||||
Tasks.submit_task("Rebuild Post Links", fn report ->
|
%{
|
||||||
report.(0.0, "Rebuilding link graph")
|
name: "Rebuild Posts From Files",
|
||||||
:ok = Posts.rebuild_post_links(project.id)
|
work: fn report ->
|
||||||
report.(1.0, "Post links rebuilt")
|
{:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report)
|
||||||
%{project_id: project.id}
|
report.(1.0, "Post rebuild complete")
|
||||||
end, attrs)
|
%{project_id: project.id, counts: %{posts: length(posts)}}
|
||||||
|
end
|
||||||
{:ok, _thumbs_task} =
|
},
|
||||||
Tasks.submit_task("Regenerate Missing Thumbnails", fn report ->
|
%{
|
||||||
report.(0.0, "Checking missing thumbnails")
|
name: "Rebuild Media From Files",
|
||||||
result = BDS.Media.regenerate_missing_thumbnails(project.id)
|
work: fn report ->
|
||||||
report.(1.0, "Missing thumbnails regenerated")
|
{:ok, media} = Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: report)
|
||||||
Map.put(result, :project_id, project.id)
|
report.(1.0, "Media rebuild complete")
|
||||||
end, attrs)
|
%{project_id: project.id, counts: %{media: length(media)}}
|
||||||
|
end
|
||||||
{:ok, _embeddings_task} =
|
},
|
||||||
Tasks.submit_task("Rebuild Embedding Index", fn report ->
|
%{
|
||||||
report.(0.0, "Rebuilding semantic index")
|
name: "Rebuild Scripts From Files",
|
||||||
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id)
|
work: fn report ->
|
||||||
report.(1.0, "Embedding index rebuilt")
|
{:ok, scripts} = Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: report)
|
||||||
%{project_id: project.id, rebuilt_post_ids: rebuilt_post_ids, rebuilt_count: length(rebuilt_post_ids)}
|
report.(1.0, "Script rebuild complete")
|
||||||
end, attrs)
|
%{project_id: project.id, counts: %{scripts: length(scripts)}}
|
||||||
|
end
|
||||||
:ok
|
},
|
||||||
|
%{
|
||||||
|
name: "Rebuild Templates From Files",
|
||||||
|
work: fn report ->
|
||||||
|
{:ok, templates} = Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: report)
|
||||||
|
report.(1.0, "Template rebuild complete")
|
||||||
|
%{project_id: project.id, counts: %{templates: length(templates)}}
|
||||||
|
end
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "Rebuild Post Links",
|
||||||
|
work: fn report ->
|
||||||
|
report.(0.0, "Rebuilding link graph")
|
||||||
|
:ok = Posts.rebuild_post_links(project.id)
|
||||||
|
report.(1.0, "Post links rebuilt")
|
||||||
|
%{project_id: project.id}
|
||||||
|
end
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "Regenerate Missing Thumbnails",
|
||||||
|
work: fn report ->
|
||||||
|
report.(0.0, "Checking missing thumbnails")
|
||||||
|
result = BDS.Media.regenerate_missing_thumbnails(project.id)
|
||||||
|
report.(1.0, "Missing thumbnails regenerated")
|
||||||
|
Map.put(result, :project_id, project.id)
|
||||||
|
end
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
name: "Rebuild Embedding Index",
|
||||||
|
work: fn report ->
|
||||||
|
report.(0.0, "Rebuilding semantic index")
|
||||||
|
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id)
|
||||||
|
report.(1.0, "Embedding index rebuilt")
|
||||||
|
%{project_id: project.id, rebuilt_post_ids: rebuilt_post_ids, rebuilt_count: length(rebuilt_post_ids)}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp wait_for_group_phase(group_id, names, timeout \\ 30_000)
|
defp run_rebuild_sequence(_group_id, _attrs, []), do: :ok
|
||||||
|
|
||||||
|
defp run_rebuild_sequence(group_id, attrs, [step | remaining_steps]) do
|
||||||
|
{:ok, _task} = Tasks.submit_task(step.name, step.work, attrs)
|
||||||
|
|
||||||
|
case wait_for_group_phase(group_id, [step.name], @rebuild_phase_timeout) do
|
||||||
|
:ok -> run_rebuild_sequence(group_id, attrs, remaining_steps)
|
||||||
|
_other -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp wait_for_group_phase(_group_id, _names, timeout) when timeout <= 0, do: :timeout
|
defp wait_for_group_phase(_group_id, _names, timeout) when timeout <= 0, do: :timeout
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,144 @@ defmodule BDS.Desktop.ShellCommandsTest do
|
|||||||
assert Enum.all?(tasks, &(&1.status == :completed))
|
assert Enum.all?(tasks, &(&1.status == :completed))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rebuild_database does not run multiple rebuild writers at once", %{temp_dir: temp_dir} do
|
||||||
|
original = Application.get_env(:bds, :tasks, [])
|
||||||
|
|
||||||
|
Application.put_env(
|
||||||
|
:bds,
|
||||||
|
:tasks,
|
||||||
|
original
|
||||||
|
|> Keyword.put(:max_concurrent, 4)
|
||||||
|
|> Keyword.put(:progress_throttle_ms, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
on_exit(fn -> Application.put_env(:bds, :tasks, original) end)
|
||||||
|
|
||||||
|
posts_dir = Path.join([temp_dir, "posts", "2026", "04"])
|
||||||
|
media_dir = Path.join([temp_dir, "media", "2026", "04"])
|
||||||
|
scripts_dir = Path.join(temp_dir, "scripts")
|
||||||
|
templates_dir = Path.join(temp_dir, "templates")
|
||||||
|
|
||||||
|
File.mkdir_p!(posts_dir)
|
||||||
|
File.mkdir_p!(media_dir)
|
||||||
|
File.mkdir_p!(scripts_dir)
|
||||||
|
File.mkdir_p!(templates_dir)
|
||||||
|
|
||||||
|
Enum.each(1..80, fn index ->
|
||||||
|
slug = "serial-post-#{index}"
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(posts_dir, "#{slug}.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"id: #{slug}",
|
||||||
|
"title: Serial Post #{index}",
|
||||||
|
"slug: #{slug}",
|
||||||
|
"status: published",
|
||||||
|
"createdAt: 1711843200",
|
||||||
|
"updatedAt: 1711929600",
|
||||||
|
"publishedAt: 1712016000",
|
||||||
|
"tags:",
|
||||||
|
"categories:",
|
||||||
|
"---",
|
||||||
|
"Body #{index}",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|> Enum.join("\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(Path.join(media_dir, "asset-#{index}.txt"), "asset #{index}")
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(media_dir, "asset-#{index}.txt.meta"),
|
||||||
|
[
|
||||||
|
"id: serial-media-#{index}",
|
||||||
|
"originalName: asset-#{index}.txt",
|
||||||
|
"mimeType: text/plain",
|
||||||
|
"size: 7",
|
||||||
|
"createdAt: 1711843200",
|
||||||
|
"updatedAt: 1711929600",
|
||||||
|
"tags:",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|> Enum.join("\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(scripts_dir, "serial-script-#{index}.lua"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"id: serial-script-#{index}",
|
||||||
|
"slug: serial-script-#{index}",
|
||||||
|
"title: Serial Script #{index}",
|
||||||
|
"kind: utility",
|
||||||
|
"entrypoint: main",
|
||||||
|
"enabled: true",
|
||||||
|
"version: 1",
|
||||||
|
"createdAt: 301",
|
||||||
|
"updatedAt: 404",
|
||||||
|
"---",
|
||||||
|
"function main() return true end",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|> Enum.join("\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(templates_dir, "serial-template-#{index}.liquid"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"id: serial-template-#{index}",
|
||||||
|
"slug: serial-template-#{index}",
|
||||||
|
"title: Serial Template #{index}",
|
||||||
|
"kind: list",
|
||||||
|
"enabled: true",
|
||||||
|
"version: 1",
|
||||||
|
"createdAt: 101",
|
||||||
|
"updatedAt: 202",
|
||||||
|
"---",
|
||||||
|
"<section>Template #{index}</section>",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|> Enum.join("\n")
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, _result} = ShellCommands.execute("rebuild_database")
|
||||||
|
|
||||||
|
_posts_task =
|
||||||
|
wait_for_named_task(
|
||||||
|
"Rebuild Posts From Files",
|
||||||
|
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.0 and &1.progress < 1.0),
|
||||||
|
10_000
|
||||||
|
)
|
||||||
|
|
||||||
|
phase_one_tasks =
|
||||||
|
BDS.Tasks.list_tasks()
|
||||||
|
|> Enum.filter(&(&1.name in [
|
||||||
|
"Rebuild Posts From Files",
|
||||||
|
"Rebuild Media From Files",
|
||||||
|
"Rebuild Scripts From Files",
|
||||||
|
"Rebuild Templates From Files"
|
||||||
|
]))
|
||||||
|
|
||||||
|
assert Enum.count(phase_one_tasks, &(&1.status == :running)) == 1
|
||||||
|
|
||||||
|
assert Enum.find(phase_one_tasks, &(&1.status == :running)).name == "Rebuild Posts From Files"
|
||||||
|
|
||||||
|
tasks = wait_for_tasks_by_name([
|
||||||
|
"Rebuild Posts From Files",
|
||||||
|
"Rebuild Media From Files",
|
||||||
|
"Rebuild Scripts From Files",
|
||||||
|
"Rebuild Templates From Files",
|
||||||
|
"Rebuild Post Links",
|
||||||
|
"Regenerate Missing Thumbnails",
|
||||||
|
"Rebuild Embedding Index"
|
||||||
|
], &(&1.status == :completed), 20_000)
|
||||||
|
|
||||||
|
assert Enum.all?(tasks, &(&1.status == :completed))
|
||||||
|
end
|
||||||
|
|
||||||
test "rebuild_database exposes live in-task progress for rebuild work", %{temp_dir: temp_dir} do
|
test "rebuild_database exposes live in-task progress for rebuild work", %{temp_dir: temp_dir} do
|
||||||
original = Application.get_env(:bds, :tasks, [])
|
original = Application.get_env(:bds, :tasks, [])
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,30 @@ defmodule BDS.Repo.BootstrapTest do
|
|||||||
assert %Project{id: "default", name: "My Blog", is_active: true} = BDS.Projects.get_active_project()
|
assert %Project{id: "default", name: "My Blog", is_active: true} = BDS.Projects.get_active_project()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "dev repo config disables query logging by default" do
|
||||||
|
config_path = Path.expand("../../../config/config.exs", __DIR__)
|
||||||
|
config = Config.Reader.read!(config_path, env: :dev)
|
||||||
|
|
||||||
|
repo_config =
|
||||||
|
config
|
||||||
|
|> Keyword.fetch!(:bds)
|
||||||
|
|> Keyword.fetch!(BDS.Repo)
|
||||||
|
|
||||||
|
assert repo_config[:log] == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dev repo config sets a rebuild-safe sqlite busy timeout" do
|
||||||
|
config_path = Path.expand("../../../config/config.exs", __DIR__)
|
||||||
|
config = Config.Reader.read!(config_path, env: :dev)
|
||||||
|
|
||||||
|
repo_config =
|
||||||
|
config
|
||||||
|
|> Keyword.fetch!(:bds)
|
||||||
|
|> Keyword.fetch!(BDS.Repo)
|
||||||
|
|
||||||
|
assert repo_config[:busy_timeout] == 15_000
|
||||||
|
end
|
||||||
|
|
||||||
defmodule RepoConfigBackup do
|
defmodule RepoConfigBackup do
|
||||||
def put_env do
|
def put_env do
|
||||||
Process.put({__MODULE__, :temp_repo_config}, Application.get_env(:bds, TempRepo))
|
Process.put({__MODULE__, :temp_repo_config}, Application.get_env(:bds, TempRepo))
|
||||||
|
|||||||
Reference in New Issue
Block a user