Add revision deleter in worker to use infinite repo transaction timeout

This commit is contained in:
Simon Prévost 2023-08-03 11:43:20 -04:00
parent cd256fa7f5
commit 21d7e42766
19 changed files with 190 additions and 48 deletions

View File

@ -37,7 +37,7 @@ config :accent, hook_github_file_server: Accent.Hook.Inbounds.GitHub.FileServer.
config :accent, Oban,
plugins: [Oban.Plugins.Pruner],
queues: [hook: 10],
queues: [hook: 10, operations: 10],
repo: Accent.Repo
config :absinthe, :schema, Accent.GraphQL.Schema

View File

@ -0,0 +1,13 @@
defmodule Accent.Revisions.DeleteWorker do
use Oban.Worker, queue: :operations
alias Accent.Repo
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
revision = Repo.get!(Accent.Revision, args["revision_id"])
Repo.transaction(fn -> Repo.delete(revision) end, timeout: :infinity)
:ok
end
end

View File

@ -1,6 +1,5 @@
defmodule Accent.RevisionManager do
alias Accent.Repo
alias Ecto.Multi
import Ecto.Changeset
import Ecto.Query
@ -21,14 +20,12 @@ defmodule Accent.RevisionManager do
end
def delete(revision) do
translations = Ecto.assoc(revision, :translations)
operations = Ecto.assoc(revision, :operations)
Multi.new()
|> Multi.delete_all(:operations, operations)
|> Multi.delete_all(:translations, translations)
|> Multi.delete(:revision, revision)
|> Repo.transaction()
with {:ok, revision} <- Repo.update(change(revision, marked_as_deleted: true)) do
Oban.insert(Accent.Revisions.DeleteWorker.new(%{revision_id: revision.id}))
{:ok, %{revision: revision}}
else
_ -> {:error, nil}
end
end
def promote(revision = %{master: true}) do

View File

@ -22,6 +22,8 @@ defmodule Accent.Revision do
has_many(:translations, Accent.Translation)
has_many(:operations, Accent.Operation)
field(:marked_as_deleted, :boolean)
field(:translations_count, :any, virtual: true, default: :not_loaded)
field(:reviewed_count, :any, virtual: true, default: :not_loaded)
field(:conflicts_count, :any, virtual: true, default: :not_loaded)

View File

@ -16,6 +16,7 @@ defmodule Accent.GraphQL.Types.Revision do
field(:name, :string)
field(:slug, :string)
field(:rtl, :boolean)
field(:marked_as_deleted, non_null(:boolean))
field(:language, non_null(:language), resolve: dataloader(Accent.Language))

View File

@ -52,7 +52,9 @@ defmodule Movement.Persisters.Base do
context
|> persist_operations()
|> migrate_up_operations()
|> tap(fn _ -> Oban.insert(ProjectStateChangeWorker.new(project_state_change_context)) end)
|> tap(fn _ ->
project_state_change_context.previous_project_state && Oban.insert(ProjectStateChangeWorker.new(project_state_change_context))
end)
end
@spec rollback(Movement.Context.t()) :: {Movement.Context.t(), [Operation.t()]}

View File

@ -0,0 +1,36 @@
defmodule Accent.Repo.Migrations.ModifyRevisionForeignKeyToAllowDelete do
use Ecto.Migration
def change do
execute("ALTER TABLE operations DROP CONSTRAINT operations_revision_id_fkey")
alter table(:operations) do
modify(:revision_id, references(:revisions, type: :uuid, on_delete: :delete_all), null: true)
end
execute("ALTER TABLE translations DROP CONSTRAINT translations_revision_id_fkey")
execute("ALTER TABLE translations DROP CONSTRAINT translations_source_translation_id_fkey")
alter table(:translations) do
modify(:revision_id, references(:revisions, type: :uuid, on_delete: :delete_all), null: false)
modify(
:source_translation_id,
references(:translations, type: :uuid, on_delete: :delete_all),
null: true
)
end
execute("ALTER TABLE comments DROP CONSTRAINT comments_translation_id_fkey")
alter table(:comments) do
modify(:translation_id, references(:translations, type: :uuid, on_delete: :delete_all), null: false)
end
execute("ALTER TABLE translation_comments_subscriptions DROP CONSTRAINT translation_comments_subscriptions_translation_id_fkey")
alter table(:translation_comments_subscriptions) do
modify(:translation_id, references(:translations, type: :uuid, on_delete: :delete_all), null: false)
end
end
end

View File

@ -0,0 +1,15 @@
defmodule Accent.Repo.Migrations.AddMarkedAsDeletedOnRevisions do
use Ecto.Migration
def change do
alter table(:revisions) do
add(:marked_as_deleted, :boolean)
end
execute("UPDATE revisions SET marked_as_deleted = FALSE")
alter table(:revisions) do
modify(:marked_as_deleted, :boolean, null: false, default: false)
end
end
end

View File

@ -5,6 +5,7 @@ defmodule AccentTest.Migrator.Down do
Operation,
PreviousTranslation,
Repo,
Revision,
Translation
}
@ -28,6 +29,7 @@ defmodule AccentTest.Migrator.Down do
translation =
Repo.insert!(%Translation{
key: "to_be_in_conflict",
revision: Repo.insert!(%Revision{}),
corrected_text: nil,
proposed_text: "new proposed text",
conflicted_text: "corrected_text",
@ -65,6 +67,7 @@ defmodule AccentTest.Migrator.Down do
translation =
Repo.insert!(%Translation{
key: "to_be_in_proposed",
revision: Repo.insert!(%Revision{}),
corrected_text: nil,
proposed_text: "new proposed text",
conflicted_text: "proposed_text",
@ -92,6 +95,7 @@ defmodule AccentTest.Migrator.Down do
translation =
Repo.insert!(%Translation{
key: "to_be_added_down",
revision: Repo.insert!(%Revision{}),
corrected_text: nil,
proposed_text: "new text",
conflicted_text: nil,
@ -115,6 +119,7 @@ defmodule AccentTest.Migrator.Down do
translation =
Repo.insert!(%Translation{
key: "to_be_added_down",
revision: Repo.insert!(%Revision{}),
corrected_text: nil,
proposed_text: "new text",
conflicted_text: nil,
@ -138,6 +143,7 @@ defmodule AccentTest.Migrator.Down do
translation =
Repo.insert!(%Translation{
value_type: "",
revision: Repo.insert!(%Revision{}),
key: "to_be_added_down",
corrected_text: nil,
proposed_text: "new text",

View File

@ -106,12 +106,15 @@ defmodule AccentTest.Movement.Persisters.Base do
end
test "new operation with removed translation" do
revision = %Revision{} |> Repo.insert!()
translation =
%Translation{
key: "a",
proposed_text: "A",
conflicted: true,
removed: true
removed: true,
revision_id: revision.id
}
|> Repo.insert!()
@ -120,6 +123,7 @@ defmodule AccentTest.Movement.Persisters.Base do
action: "new",
key: "a",
text: "B",
revision_id: revision.id,
previous_translation: %PreviousTranslation{
removed: true
}
@ -138,9 +142,12 @@ defmodule AccentTest.Movement.Persisters.Base do
end
test "version operation with source translation" do
revision = %Revision{} |> Repo.insert!()
translation =
%Translation{
key: "a",
revision_id: revision.id,
proposed_text: "A",
conflicted: true,
removed: true
@ -152,6 +159,7 @@ defmodule AccentTest.Movement.Persisters.Base do
action: "version_new",
key: "a",
text: "B",
revision_id: revision.id,
translation_id: translation.id
}
]
@ -168,9 +176,12 @@ defmodule AccentTest.Movement.Persisters.Base do
end
test "version operation add operation on source translation" do
revision = %Revision{} |> Repo.insert!()
translation =
%Translation{
key: "a",
revision_id: revision.id,
proposed_text: "A",
conflicted: true,
removed: true
@ -182,6 +193,7 @@ defmodule AccentTest.Movement.Persisters.Base do
action: "version_new",
key: "a",
text: "B",
revision_id: revision.id,
translation_id: translation.id
}
]
@ -211,6 +223,7 @@ defmodule AccentTest.Movement.Persisters.Base do
translation =
%Translation{
key: "a",
revision_id: revision.id,
proposed_text: "A",
conflicted: true,
removed: true,
@ -223,6 +236,7 @@ defmodule AccentTest.Movement.Persisters.Base do
action: "update",
key: "a",
text: "B",
revision_id: revision.id,
value_type: "string",
translation_id: translation.id,
version_id: version.id

View File

@ -22,14 +22,25 @@ defmodule AccentTest.Movement.Persisters.NewVersion do
setup do
user = Repo.insert!(@user)
language = Repo.insert!(%Language{name: "English", slug: Ecto.UUID.generate()})
{:ok, project} = ProjectCreator.create(params: %{main_color: "#f00", name: "My project", language_id: language.id}, user: user)
{:ok, project} =
ProjectCreator.create(
params: %{main_color: "#f00", name: "My project", language_id: language.id},
user: user
)
revision = project |> Repo.preload(:revisions) |> Map.get(:revisions) |> hd()
document = Repo.insert!(%Document{project_id: project.id, path: "test", format: "json"})
{:ok, [revision: revision, document: document, project: project, user: user]}
end
test "builder fetch translations and process operations", %{revision: revision, user: user, project: project, document: document} do
test "builder fetch translations and process operations", %{
revision: revision,
user: user,
project: project,
document: document
} do
translation =
%Translation{
key: "a",
@ -51,6 +62,7 @@ defmodule AccentTest.Movement.Persisters.NewVersion do
text: "B",
translation_id: translation.id,
value_type: "string",
revision_id: revision.id,
placeholders: []
}
]

View File

@ -23,6 +23,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_corrected",
revision: Repo.insert!(%Revision{}),
file_comment: "",
corrected_text: nil,
proposed_text: "proposed_text",
@ -58,6 +59,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_merged",
revision: Repo.insert!(%Revision{}),
file_comment: "",
corrected_text: "corrected_text",
proposed_text: "proposed_text",
@ -92,6 +94,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_merged",
revision: Repo.insert!(%Revision{}),
corrected_text: "corrected_text",
proposed_text: "proposed_text",
conflicted_text: nil,
@ -119,6 +122,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_uncorrected",
revision: Repo.insert!(%Revision{}),
file_comment: "",
file_index: 1,
corrected_text: "new proposed text",
@ -146,6 +150,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_uncorrected",
revision: Repo.insert!(%Revision{}),
file_comment: "",
file_index: 1,
corrected_text: "proposed_text",
@ -173,6 +178,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_in_conflict",
revision: Repo.insert!(%Revision{}),
file_comment: "",
file_index: 1,
corrected_text: "corrected_text",
@ -201,6 +207,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_in_conflict",
revision: Repo.insert!(%Revision{}),
file_comment: "",
file_index: 1,
corrected_text: "corrected_text",
@ -228,6 +235,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_removed",
revision: Repo.insert!(%Revision{}),
corrected_text: "corrected_text",
proposed_text: "proposed_text",
conflicted: false,
@ -249,6 +257,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_renewed",
revision: Repo.insert!(%Revision{}),
corrected_text: "corrected_text",
proposed_text: "proposed_text",
conflicted: false,
@ -273,6 +282,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "to_be_rollbacked",
revision: Repo.insert!(%Revision{}),
corrected_text: "corrected_text",
proposed_text: "proposed_text",
conflicted: false,
@ -298,6 +308,7 @@ defmodule AccentTest.Movement.Migrator.Up do
Repo.insert!(%Translation{
value_type: "",
key: "to_be_conflict_on_proposed",
revision: Repo.insert!(%Revision{}),
file_comment: "",
file_index: 1,
corrected_text: "corrected_text",
@ -331,6 +342,7 @@ defmodule AccentTest.Movement.Migrator.Up do
translation =
Repo.insert!(%Translation{
key: "updated_proposed",
revision: Repo.insert!(%Revision{}),
corrected_text: "corrected_text",
proposed_text: "proposed_text",
conflicted_text: "conflict",

View File

@ -8,8 +8,18 @@ defmodule AccentTest.RevisionDeleter do
french_language = %Language{name: "french"} |> Repo.insert!()
english_language = %Language{name: "english"} |> Repo.insert!()
master_revision = %Revision{language_id: french_language.id, project_id: project.id, master: true} |> Repo.insert!()
slave_revision = %Revision{language_id: english_language.id, project_id: project.id, master: false, master_revision_id: master_revision.id} |> Repo.insert!()
master_revision =
%Revision{language_id: french_language.id, project_id: project.id, master: true}
|> Repo.insert!()
slave_revision =
%Revision{
language_id: english_language.id,
project_id: project.id,
master: false,
master_revision_id: master_revision.id
}
|> Repo.insert!()
{:ok, [master_revision: master_revision, slave_revision: slave_revision]}
end
@ -17,7 +27,7 @@ defmodule AccentTest.RevisionDeleter do
test "delete slave", %{slave_revision: revision} do
{:ok, _revision} = RevisionManager.delete(revision)
assert Repo.get(Revision, revision.id) == nil
assert Repo.get(Revision, revision.id).marked_as_deleted
end
test "delete master", %{master_revision: revision} do
@ -29,7 +39,7 @@ defmodule AccentTest.RevisionDeleter do
test "delete operations", %{slave_revision: revision} do
operation = %Operation{action: "new", key: "a", revision_id: revision.id} |> Repo.insert!()
{:ok, _revision} = RevisionManager.delete(revision)
Accent.Revisions.DeleteWorker.perform(%Oban.Job{args: %{"revision_id" => revision.id}})
assert Repo.get(Operation, operation.id) == nil
end
@ -37,7 +47,7 @@ defmodule AccentTest.RevisionDeleter do
test "delete translations", %{slave_revision: revision} do
translation = %Translation{key: "a", revision_id: revision.id} |> Repo.insert!()
{:ok, _revision} = RevisionManager.delete(revision)
Accent.Revisions.DeleteWorker.perform(%Oban.Job{args: %{"revision_id" => revision.id}})
assert Repo.get(Translation, translation.id) == nil
end

View File

@ -741,6 +741,7 @@
"project_manage_languages_overview": {
"list_languages": "Languages:",
"revision_inserted_at_label": "Created",
"revision_deleted_label": "The language is currently being removed from your project. It will automatically disappear once all operations are propagated in the system.",
"master_badge": "master",
"delete_revision_confirm": "Are you sure you want to remove this language from your project? This action cannot be rollbacked.",
"delete_revision_button": "Remove this language",

View File

@ -741,6 +741,7 @@
"project_manage_languages_overview": {
"list_languages": "Langues :",
"revision_inserted_at_label": "Créé",
"revision_deleted_label": "La langue est en cours de suppression de votre projet. Elle disparaîtra automatiquement une fois toutes les opérations propagées dans le système.",
"master_badge": "principale",
"delete_revision_confirm": "Voulez-vous vraiment supprimer cette langue de votre projet ? Cette action ne peut pas être annulée.",
"delete_revision_button": "Supprimer cette langue",

View File

@ -24,7 +24,7 @@ export default class OverviewItem extends Component<Args> {
isDeleting = false;
@tracked
isDeleted = false;
isDeleted = this.args.revision.markedAsDeleted;
get name() {
return this.args.revision.name || this.args.revision.language.name;

View File

@ -5,7 +5,14 @@
font-size: 14px;
&.list-item--deleted {
display: none;
background: var(--background-light);
border: 1px dashed var(--background-light-highlight);
padding: 6px 10px;
pointer-events: none;
.list-item-infos-date {
font-size: 11px;
}
}
&.list-item--deleting {

View File

@ -1,20 +1,26 @@
<div
local-class='list-item {{if @master "list-item--master"}} {{if this.isPromoting "list-item--promoting"}} {{if this.isDeleting "list-item--deleting"}} {{if
this.deleted
this.isDeleted
"list-item--deleted"
}}'
>
<div local-class='list-item-header'>
<LinkTo @route='logged-in.project.manage-languages.edit' @models={{array @project.id @revision.id}} local-class='list-item-header-edit'>
{{inline-svg 'assets/pencil.svg' local-class='item-edit-icon'}}
</LinkTo>
{{#unless this.isDeleted}}
<LinkTo @route='logged-in.project.manage-languages.edit' @models={{array @project.id @revision.id}} local-class='list-item-header-edit'>
{{inline-svg 'assets/pencil.svg' local-class='item-edit-icon'}}
</LinkTo>
{{/unless}}
<LinkTo @route='logged-in.project.revision.translations' @models={{array @project.id @revision.id}} local-class='list-link'>
{{this.name}}
<small local-class='list-link-small'>
{{this.slug}}
</small>
</LinkTo>
{{#if this.isDeleted}}
<span local-class='list-link'>{{this.name}}</span>
{{else}}
<LinkTo @route='logged-in.project.revision.translations' @models={{array @project.id @revision.id}} local-class='list-link'>
{{this.name}}
<small local-class='list-link-small'>
{{this.slug}}
</small>
</LinkTo>
{{/if}}
{{#if @revision.isMaster}}
<AccBadge local-class='masterBadge'>
@ -26,25 +32,31 @@
<div local-class='list-item-infos'>
{{#unless @revision.isMaster}}
<div local-class='list-item-infos-date'>
{{t 'components.project_manage_languages_overview.revision_inserted_at_label'}}
<TimeAgoInWordsTag @date={{@revision.insertedAt}} />
</div>
<div local-class='list-item-actions'>
{{#if (get @permissions 'promote_slave')}}
<AsyncButton @loading={{this.isPromoting}} local-class='promoteSlaveButton' class='button--grey button--small' @onClick={{fn this.promoteRevision}}>
{{inline-svg '/assets/chevron-top.svg' class='button-icon'}}
{{t 'components.project_manage_languages_overview.promote_revision_master_button'}}
</AsyncButton>
{{/if}}
{{#if (get @permissions 'delete_slave')}}
<AsyncButton @loading={{this.isDeleting}} class='button--red button--filled button--small' local-class='deleteSlaveButton' @onClick={{fn this.deleteRevision}}>
{{inline-svg '/assets/x.svg' class='button-icon'}}
{{t 'components.project_manage_languages_overview.delete_revision_button'}}
</AsyncButton>
{{#if this.isDeleted}}
{{t 'components.project_manage_languages_overview.revision_deleted_label'}}
{{else}}
{{t 'components.project_manage_languages_overview.revision_inserted_at_label'}}
<TimeAgoInWordsTag @date={{@revision.insertedAt}} />
{{/if}}
</div>
{{#unless this.isDeleted}}
<div local-class='list-item-actions'>
{{#if (get @permissions 'promote_slave')}}
<AsyncButton @loading={{this.isPromoting}} local-class='promoteSlaveButton' class='button--grey button--small' @onClick={{fn this.promoteRevision}}>
{{inline-svg '/assets/chevron-top.svg' class='button-icon'}}
{{t 'components.project_manage_languages_overview.promote_revision_master_button'}}
</AsyncButton>
{{/if}}
{{#if (get @permissions 'delete_slave')}}
<AsyncButton @loading={{this.isDeleting}} class='button--red button--filled button--small' local-class='deleteSlaveButton' @onClick={{fn this.deleteRevision}}>
{{inline-svg '/assets/x.svg' class='button-icon'}}
{{t 'components.project_manage_languages_overview.delete_revision_button'}}
</AsyncButton>
{{/if}}
</div>
{{/unless}}
{{/unless}}
</div>
</div>

View File

@ -18,6 +18,7 @@ export default gql`
name
slug
isMaster
markedAsDeleted
insertedAt
language {