Skip to content

Commit 26c4da0

Browse files
authored
Batched deletion of rendered content (boostorg#1959) (boostorg#1969)
1 parent 65fdf7c commit 26c4da0

File tree

5 files changed

+112
-0
lines changed

5 files changed

+112
-0
lines changed

core/admin.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,50 @@
11
from django.contrib import admin
2+
from django.urls import path
3+
from django.shortcuts import redirect, render
4+
from django.contrib import messages
25
from .models import RenderedContent, SiteSettings
6+
from .tasks import delete_all_rendered_content
37

48

59
@admin.register(RenderedContent)
610
class RenderedContentAdmin(admin.ModelAdmin):
711
list_display = ("cache_key", "content_type", "modified")
812
search_fields = ("cache_key",)
913

14+
def get_urls(self):
15+
urls = super().get_urls()
16+
custom_urls = [
17+
path(
18+
"delete-all/",
19+
self.admin_site.admin_view(self.delete_all_view),
20+
name="core_renderedcontent_delete_all",
21+
),
22+
]
23+
return custom_urls + urls
24+
25+
def delete_all_view(self, request):
26+
if request.method == "POST":
27+
delete_all_rendered_content.delay()
28+
messages.success(
29+
request,
30+
"Mass deletion task has been queued. All rendered content "
31+
"records will be deleted in batches. This may take some time.",
32+
)
33+
return redirect("..")
34+
35+
context = {
36+
**self.admin_site.each_context(request),
37+
"title": "Delete All Rendered Content",
38+
}
39+
return render(
40+
request, "admin/core/renderedcontent/delete_all_confirmation.html", context
41+
)
42+
43+
def changelist_view(self, request, extra_context=None):
44+
extra_context = extra_context or {}
45+
extra_context["has_delete_all"] = True
46+
return super().changelist_view(request, extra_context=extra_context)
47+
1048

1149
@admin.register(SiteSettings)
1250
class SiteSettingsAdmin(admin.ModelAdmin):

core/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,4 @@ class SourceDocType(Enum):
6969
"master/libs/redis",
7070
"doc/antora/url",
7171
]
72+
RENDERED_CONTENT_BATCH_DELETE_SIZE = 10000

core/tasks.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from core.asciidoc import convert_adoc_to_html
99
from .boostrenderer import get_content_from_s3
10+
from .constants import RENDERED_CONTENT_BATCH_DELETE_SIZE
1011
from .models import RenderedContent
1112

1213
logger = structlog.get_logger()
@@ -84,3 +85,33 @@ def save_rendered_content(cache_key, content_type, content_html, last_updated_at
8485
obj_id=obj.id,
8586
obj_created=created,
8687
)
88+
89+
90+
@shared_task
91+
def delete_all_rendered_content():
92+
"""
93+
Deletes all RenderedContent objects, in batches to avoid locking the entire table.
94+
"""
95+
from django.db import connection
96+
97+
deleted_count = 0
98+
99+
while True:
100+
pks = RenderedContent.objects.values_list("pk", flat=True)[
101+
:RENDERED_CONTENT_BATCH_DELETE_SIZE
102+
]
103+
if not pks:
104+
break
105+
batch_size, _ = RenderedContent.objects.filter(pk__in=pks).delete()
106+
107+
deleted_count += batch_size
108+
logger.info(f"batch deleted {batch_size=} {deleted_count=}")
109+
110+
# Reset auto-increment sequence to 1
111+
with connection.cursor() as cursor:
112+
cursor.execute(
113+
f"ALTER SEQUENCE {RenderedContent._meta.db_table}_id_seq RESTART WITH 1"
114+
)
115+
116+
logger.info("all_rendered_content_deleted", total_count=deleted_count)
117+
return deleted_count
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends "admin/change_list.html" %}
2+
{% load i18n admin_urls %}
3+
4+
{% block object-tools-items %}
5+
{{ block.super }}
6+
{% if has_delete_all %}
7+
<li>
8+
<a href="{% url 'admin:core_renderedcontent_delete_all' %}" class="deletelink">
9+
{% trans "Delete All Records" %}
10+
</a>
11+
</li>
12+
{% endif %}
13+
{% endblock %}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{% extends "admin/base_site.html" %}
2+
{% load i18n static %}
3+
4+
{% block extrahead %}
5+
{{ block.super }}
6+
<script src="{% static 'admin/js/cancel.js' %}" async></script>
7+
{% endblock %}
8+
9+
{% block bodyclass %}{{ block.super }} app-core model-renderedcontent delete-confirmation{% endblock %}
10+
11+
{% block breadcrumbs %}
12+
<div class="breadcrumbs">
13+
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
14+
&rsaquo; <a href="{% url 'admin:core_renderedcontent_changelist' %}">{% translate 'Rendered Contents' %}</a>
15+
&rsaquo; {% translate 'Delete All' %}
16+
</div>
17+
{% endblock %}
18+
19+
{% block content %}
20+
<p>{% translate "Are you sure you want to delete ALL rendered content records?" %}</p>
21+
<p><strong>{% translate "Warning:" %}</strong> {% translate "This action will queue a background task to delete all records in batches. This cannot be undone." %}</p>
22+
<form method="post">{% csrf_token %}
23+
<div>
24+
<input type="hidden" name="post" value="yes">
25+
<input type="submit" value="Yes, I'm sure">
26+
<a href="#" class="button cancel-link">No, take me back</a>
27+
</div>
28+
</form>
29+
{% endblock %}

0 commit comments

Comments
 (0)