diff --git a/bookwyrm/templates/settings/layout.html b/bookwyrm/templates/settings/layout.html index 70c7ef0f4..e61900e33 100644 --- a/bookwyrm/templates/settings/layout.html +++ b/bookwyrm/templates/settings/layout.html @@ -85,6 +85,10 @@ {% url 'settings-celery' as url %} {% trans "Celery status" %} +
  • + {% url 'settings-redis' as url %} + {% trans "Redis status" %} +
  • {% url 'settings-schedules' as url %} {% trans "Scheduled tasks" %} diff --git a/bookwyrm/templates/settings/redis.html b/bookwyrm/templates/settings/redis.html new file mode 100644 index 000000000..8344f5942 --- /dev/null +++ b/bookwyrm/templates/settings/redis.html @@ -0,0 +1,68 @@ +{% extends 'settings/layout.html' %} +{% load humanize %} +{% load i18n %} + +{% block title %}{% trans "Redis Status" %}{% endblock %} + +{% block header %}{% trans "Redis Status" %}{% endblock %} + +{% block panel %} + +{% if info %} +
    +

    {% trans "Info" %}

    +
    +
    +
    +

    {% trans "Used memory" %}

    +

    {{ info.used_memory_human }}

    +
    +
    +
    +
    +

    {% trans "Total system memory" %}

    +

    {{ info.total_system_memory_human }}

    +
    +
    +
    +
    +

    {% trans "Keys" %}

    +

    {{ info.db0.keys | intcomma }}

    +
    +
    +
    +
    + +
    + {{ dead_key_count }} +
    + {% csrf_token %} + +
    +
    + {% csrf_token %} + + +
    +
    +{% else %} +
    + + + {% trans "Could not connect to Redis Activity" %} + +
    + +{% endif %} + +{% if errors %} +
    +

    {% trans "Errors" %}

    + {% for error in errors %} +
    {{ error }}
    +{% endfor %} + +
    +{% endif %} + +{% endblock %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index bfa1aacce..5a26efbc8 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -369,6 +369,7 @@ urlpatterns = [ re_path( r"^settings/celery/ping/?$", views.celery_ping, name="settings-celery-ping" ), + re_path(r"^settings/redis/?$", views.RedisStatus.as_view(), name="settings-redis"), re_path( r"^settings/schedules/(?P\d+)?$", views.ScheduledTasks.as_view(), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index ebc851847..24b00ffff 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -5,6 +5,7 @@ from .admin.announcements import EditAnnouncement, delete_announcement from .admin.automod import AutoMod, automod_delete, run_automod from .admin.automod import schedule_automod_task, unschedule_automod_task from .admin.celery_status import CeleryStatus, celery_ping +from .admin.redis import RedisStatus from .admin.schedule import ScheduledTasks from .admin.dashboard import Dashboard from .admin.federation import Federation, FederatedServer diff --git a/bookwyrm/views/admin/redis.py b/bookwyrm/views/admin/redis.py new file mode 100644 index 000000000..38d390e0f --- /dev/null +++ b/bookwyrm/views/admin/redis.py @@ -0,0 +1,63 @@ +""" redis cache status """ +from django.contrib.auth.decorators import login_required, permission_required +from django.http import HttpResponse +from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator +from django.views import View +import redis + +from bookwyrm import models, settings + +r = redis.from_url(settings.REDIS_ACTIVITY_URL) + +# pylint: disable= no-self-use +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.edit_instance_settings", raise_exception=True), + name="dispatch", +) +class RedisStatus(View): + """Are your tasks running? Well you'd better go catch them""" + + def get(self, request): + """See workers and active tasks""" + data = {"errors": []} + try: + data["info"] = r.info + # pylint: disable=broad-except + except Exception as err: + data["errors"].append(err) + + return TemplateResponse(request, "settings/redis.html", data) + + # pylint: disable=unused-argument + def post(self, request): + """Erase invalid keys""" + dry_run = request.POST.get("dry_run") + patterns = [":*:*"] # this pattern is a django cache with no prefix + for user_id in models.User.objects.filter( + is_deleted=True, local=True + ).values_list("id", flat=True): + patterns.append(f"{user_id}-*") + + deleted_count = 0 + for pattern in patterns: + deleted_count += erase_keys(pattern, dry_run=dry_run) + + if dry_run: + return HttpResponse(f"{deleted_count} keys identified for deletion") + return HttpResponse(f"{deleted_count} keys deleted") + + +def erase_keys(pattern, count=1000, dry_run=False): + """Delete all redis activity keys according to a provided regex pattern""" + pipeline = r.pipeline() + key_count = 0 + for keys in r.scan_iter(match=pattern, count=count): + key_count += len(keys) + if not dry_run: + for key in keys: + pipeline.delete(key) + if not dry_run: + pipeline.execute() + return key_count