Added caching and initial settings

This commit is contained in:
Michael Manfre 2022-12-05 12:55:30 -05:00 committed by GitHub
parent a9bb4a7122
commit d6eb16a398
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 186 additions and 3 deletions

View File

@ -1,11 +1,15 @@
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import TemplateView from django.views.generic import TemplateView
from core.decorators import per_identity_cache_page
from users.decorators import identity_required from users.decorators import identity_required
from users.models import FollowStates from users.models import FollowStates
@method_decorator(identity_required, name="dispatch") @method_decorator(identity_required, name="dispatch")
@method_decorator(
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
)
class FollowsPage(TemplateView): class FollowsPage(TemplateView):
""" """
Shows followers/follows. Shows followers/follows.

View File

@ -6,11 +6,13 @@ from django.utils.decorators import method_decorator
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from activities.models import Post, PostInteraction, PostInteractionStates, PostStates from activities.models import Post, PostInteraction, PostInteractionStates, PostStates
from core.decorators import per_identity_cache_page
from core.ld import canonicalise from core.ld import canonicalise
from users.decorators import identity_required from users.decorators import identity_required
from users.shortcuts import by_handle_or_404 from users.shortcuts import by_handle_or_404
@method_decorator(per_identity_cache_page("cache_timeout_page_post"), name="dispatch")
class Individual(TemplateView): class Individual(TemplateView):
template_name = "activities/post.html" template_name = "activities/post.html"

View File

@ -5,11 +5,13 @@ from django.utils.decorators import method_decorator
from django.views.generic import FormView, ListView from django.views.generic import FormView, ListView
from activities.models import Hashtag, Post, PostInteraction, TimelineEvent from activities.models import Hashtag, Post, PostInteraction, TimelineEvent
from core.decorators import per_identity_cache_page
from core.models import Config from core.models import Config
from users.decorators import identity_required from users.decorators import identity_required
@method_decorator(identity_required, name="dispatch") @method_decorator(identity_required, name="dispatch")
@method_decorator(per_identity_cache_page(), name="dispatch")
class Home(FormView): class Home(FormView):
template_name = "activities/home.html" template_name = "activities/home.html"
@ -61,6 +63,9 @@ class Home(FormView):
return redirect(".") return redirect(".")
@method_decorator(
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
)
class Tag(ListView): class Tag(ListView):
template_name = "activities/tag.html" template_name = "activities/tag.html"
@ -96,6 +101,9 @@ class Tag(ListView):
return context return context
@method_decorator(
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
)
class Local(ListView): class Local(ListView):
template_name = "activities/local.html" template_name = "activities/local.html"
@ -122,6 +130,9 @@ class Local(ListView):
@method_decorator(identity_required, name="dispatch") @method_decorator(identity_required, name="dispatch")
@method_decorator(
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
)
class Federated(ListView): class Federated(ListView):
template_name = "activities/federated.html" template_name = "activities/federated.html"
@ -150,6 +161,9 @@ class Federated(ListView):
@method_decorator(identity_required, name="dispatch") @method_decorator(identity_required, name="dispatch")
@method_decorator(
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
)
class Notifications(ListView): class Notifications(ListView):
template_name = "activities/notifications.html" template_name = "activities/notifications.html"

41
core/decorators.py Normal file
View File

@ -0,0 +1,41 @@
from functools import partial, wraps
from django.views.decorators.cache import cache_page as dj_cache_page
from core.models import Config
def cache_page(
timeout: int | str = "cache_timeout_page_default",
*,
per_identity: bool = False,
key_prefix: str = "",
):
"""
Decorator for views that caches the page result.
timeout can either be the number of seconds or the name of a SystemOptions
value.
"""
if isinstance(timeout, str):
timeout = Config.lazy_system_value(timeout)
def decorator(function):
@wraps(function)
def inner(request, *args, **kwargs):
prefix = key_prefix
if per_identity:
identity_id = request.identity.pk if request.identity else "0"
prefix = f"{key_prefix or ''}:ident{identity_id}"
_timeout = timeout
if callable(_timeout):
_timeout = _timeout()
return dj_cache_page(timeout=_timeout, key_prefix=prefix)(function)(
request, *args, **kwargs
)
return inner
return decorator
per_identity_cache_page = partial(cache_page, per_identity=True)

View File

@ -1,3 +1,5 @@
from time import time
from django.core.exceptions import MiddlewareNotUsed from django.core.exceptions import MiddlewareNotUsed
from core import sentry from core import sentry
@ -9,11 +11,19 @@ class ConfigLoadingMiddleware:
Caches the system config every request Caches the system config every request
""" """
refresh_interval: float = 30.0
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
self.config_ts: float = 0.0
def __call__(self, request): def __call__(self, request):
if (
not getattr(Config, "system", None)
or (time() - self.config_ts) >= self.refresh_interval
):
Config.system = Config.load_system() Config.system = Config.load_system()
self.config_ts = time()
response = self.get_response(request) response = self.get_response(request)
return response return response

View File

@ -218,6 +218,11 @@ class Config(models.Model):
hashtag_unreviewed_are_public: bool = True hashtag_unreviewed_are_public: bool = True
hashtag_stats_max_age: int = 60 * 60 hashtag_stats_max_age: int = 60 * 60
cache_timeout_page_default: int = 60
cache_timeout_page_timeline: int = 60 * 3
cache_timeout_page_post: int = 60 * 2
cache_timeout_identity_feed: int = 60 * 5
restricted_usernames: str = "admin\nadmins\nadministrator\nadministrators\nsystem\nroot\nannounce\nannouncement\nannouncements" restricted_usernames: str = "admin\nadmins\nadministrator\nadministrators\nsystem\nroot\nannounce\nannouncement\nannouncements"
class UserOptions(pydantic.BaseModel): class UserOptions(pydantic.BaseModel):

View File

@ -1,8 +1,10 @@
from django.http import JsonResponse from django.http import JsonResponse
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from activities.views.timelines import Home from activities.views.timelines import Home
from core.decorators import cache_page
from users.models import Identity from users.models import Identity
@ -13,6 +15,7 @@ def homepage(request):
return LoggedOutHomepage.as_view()(request) return LoggedOutHomepage.as_view()(request)
@method_decorator(cache_page(), name="dispatch")
class LoggedOutHomepage(TemplateView): class LoggedOutHomepage(TemplateView):
template_name = "index.html" template_name = "index.html"

View File

@ -14,3 +14,25 @@ Environment Variable:
making remote requests to other Fediverse instances. This may also be a making remote requests to other Fediverse instances. This may also be a
tuple of four floats to set the timeouts for connect, read, write, and tuple of four floats to set the timeouts for connect, read, write, and
pool. Example ``TAKAHE_REMOTE_TIMEOUT='[0.5, 1.0, 1.0, 0.5]'`` pool. Example ``TAKAHE_REMOTE_TIMEOUT='[0.5, 1.0, 1.0, 0.5]'``
Caching
--------
By default Takakē has caching disabled. The caching needs of a server can
varying drastically based upon the number of users and how interconnected
they are with other servers.
Caching is configured by specifying a cache DSN in the environment variable
``TAKAHE_CACHES_DEFAULT``. The DSN format can be any supported by
`django-cache-url <https://github.com/epicserve/django-cache-url>`_, but
some cache backends will require additional Python pacakages not required
by Takahē.
**Examples**
* LocMem cache for a small server: ``locmem://default``
* Memcache cache for a service named ``memcache`` in a docker compose file:
``memcached://memcache:11211?key_prefix=takahe``
* Multiple memcache cache servers:
``memcached://server1:11211,server2:11211``

View File

@ -2,6 +2,7 @@ bleach~=5.0.1
blurhash-python~=1.1.3 blurhash-python~=1.1.3
cryptography~=38.0 cryptography~=38.0
dj_database_url~=1.0.0 dj_database_url~=1.0.0
django-cache-url~=3.4.2
django-htmx~=1.13.0 django-htmx~=1.13.0
django-storages[google,boto3]~=1.13.1 django-storages[google,boto3]~=1.13.1
django~=4.1 django~=4.1

View File

@ -6,6 +6,7 @@ from pathlib import Path
from typing import Literal from typing import Literal
import dj_database_url import dj_database_url
import django_cache_url
import sentry_sdk import sentry_sdk
from pydantic import AnyUrl, BaseSettings, EmailStr, Field, validator from pydantic import AnyUrl, BaseSettings, EmailStr, Field, validator
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
@ -15,6 +16,11 @@ from takahe import __version__
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
class CacheBackendUrl(AnyUrl):
host_required = False
allowed_schemes = django_cache_url.BACKENDS.keys()
class ImplicitHostname(AnyUrl): class ImplicitHostname(AnyUrl):
host_required = False host_required = False
@ -107,6 +113,9 @@ class Settings(BaseSettings):
#: (placeholder setting, no effect) #: (placeholder setting, no effect)
SEARCH: bool = True SEARCH: bool = True
#: Default cache backend
CACHES_DEFAULT: CacheBackendUrl | None = None
PGHOST: str | None = None PGHOST: str | None = None
PGPORT: int | None = 5432 PGPORT: int | None = 5432
PGNAME: str = "takahe" PGNAME: str = "takahe"
@ -339,5 +348,7 @@ if SETUP.MEDIA_BACKEND:
else: else:
raise ValueError(f"Unsupported media backend {parsed.scheme}") raise ValueError(f"Unsupported media backend {parsed.scheme}")
CACHES = {"default": django_cache_url.parse(SETUP.CACHES_DEFAULT or "dummy://")}
if SETUP.ERROR_EMAILS: if SETUP.ERROR_EMAILS:
ADMINS = [("Admin", e) for e in SETUP.ERROR_EMAILS] ADMINS = [("Admin", e) for e in SETUP.ERROR_EMAILS]

View File

@ -55,6 +55,11 @@ urlpatterns = [
admin.BasicSettings.as_view(), admin.BasicSettings.as_view(),
name="admin_basic", name="admin_basic",
), ),
path(
"admin/tuning/",
admin.TuningSettings.as_view(),
name="admin_tuning",
),
path( path(
"admin/domains/", "admin/domains/",
admin.Domains.as_view(), admin.Domains.as_view(),

View File

@ -36,6 +36,9 @@
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags"> <a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
<i class="fa-solid fa-hashtag"></i> Hashtags <i class="fa-solid fa-hashtag"></i> Hashtags
</a> </a>
<a href="{% url "admin_tuning" %}" {% if section == "tuning" %}class="selected"{% endif %} title="Tuning">
<i class="fa-solid fa-wrench"></i> Tuning
</a>
<a href="/djadmin" title=""> <a href="/djadmin" title="">
<i class="fa-solid fa-gear"></i> Django Admin <i class="fa-solid fa-gear"></i> Django Admin
</a> </a>

View File

@ -9,6 +9,7 @@ from django.views.generic import View
from activities.models import Post from activities.models import Post
from core import exceptions from core import exceptions
from core.decorators import cache_page
from core.ld import canonicalise from core.ld import canonicalise
from core.models import Config from core.models import Config
from core.signatures import ( from core.signatures import (
@ -61,6 +62,7 @@ class NodeInfo(View):
) )
@method_decorator(cache_page(), name="dispatch")
class NodeInfo2(View): class NodeInfo2(View):
""" """
Returns the nodeinfo 2.0 response Returns the nodeinfo 2.0 response
@ -87,6 +89,7 @@ class NodeInfo2(View):
) )
@method_decorator(cache_page(), name="dispatch")
class Webfinger(View): class Webfinger(View):
""" """
Services webfinger requests Services webfinger requests
@ -189,6 +192,7 @@ class Inbox(View):
return HttpResponse(status=202) return HttpResponse(status=202)
@method_decorator(cache_page(), name="dispatch")
class SystemActorView(View): class SystemActorView(View):
""" """
Special endpoint for the overall system actor Special endpoint for the overall system actor

View File

@ -17,7 +17,7 @@ from users.views.admin.hashtags import ( # noqa
HashtagEdit, HashtagEdit,
Hashtags, Hashtags,
) )
from users.views.admin.settings import BasicSettings # noqa from users.views.admin.settings import BasicSettings, TuningSettings # noqa
@method_decorator(admin_required, name="dispatch") @method_decorator(admin_required, name="dispatch")

View File

@ -1,4 +1,5 @@
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.safestring import mark_safe
from core.models import Config from core.models import Config
from users.decorators import admin_required from users.decorators import admin_required
@ -106,3 +107,51 @@ class BasicSettings(AdminSettingsPage):
"restricted_usernames", "restricted_usernames",
], ],
} }
cache_field_defaults = {
"min_value": 0,
"max_value": 900,
"step_size": 15,
}
class TuningSettings(AdminSettingsPage):
section = "tuning"
options = {
"cache_timeout_page_default": {
**cache_field_defaults,
"title": "Default Timeout",
"help_text": "The number of seconds to cache a rendered page",
},
"cache_timeout_page_timeline": {
**cache_field_defaults,
"title": "Timeline Timeout",
"help_text": "The number of seconds to cache a rendered timeline page",
},
"cache_timeout_page_post": {
**cache_field_defaults,
"title": "Individual Post Timeout",
"help_text": mark_safe(
"The number of seconds to cache a rendered individual Post page<br>Note: This includes the JSON responses to other servers"
),
},
"cache_timeout_identity_feed": {
**cache_field_defaults,
"title": "Identity Feed Timeout",
"help_text": "The number of seconds to cache a rendered Identity RSS feed",
},
}
layout = {
"Rendered Page Cache": [
"cache_timeout_page_default",
"cache_timeout_page_timeline",
"cache_timeout_page_post",
],
"RSS Feeds": [
"cache_timeout_identity_feed",
],
}

View File

@ -10,6 +10,7 @@ from django.utils.decorators import method_decorator
from django.views.generic import FormView, ListView, TemplateView, View from django.views.generic import FormView, ListView, TemplateView, View
from activities.models import Post, PostInteraction from activities.models import Post, PostInteraction
from core.decorators import per_identity_cache_page
from core.ld import canonicalise from core.ld import canonicalise
from core.models import Config from core.models import Config
from users.decorators import identity_required from users.decorators import identity_required
@ -17,6 +18,7 @@ from users.models import Domain, Follow, FollowStates, Identity, IdentityStates
from users.shortcuts import by_handle_or_404 from users.shortcuts import by_handle_or_404
@method_decorator(per_identity_cache_page(), name="dispatch")
class ViewIdentity(ListView): class ViewIdentity(ListView):
""" """
Shows identity profile pages, and also acts as the Actor endpoint when Shows identity profile pages, and also acts as the Actor endpoint when
@ -90,6 +92,9 @@ class ViewIdentity(ListView):
return context return context
@method_decorator(
per_identity_cache_page("cache_timeout_identity_feed"), name="__call__"
)
class IdentityFeed(Feed): class IdentityFeed(Feed):
""" """
Serves a local user's Public posts as an RSS feed Serves a local user's Public posts as an RSS feed

View File

@ -21,7 +21,7 @@ class SettingsPage(FormView):
options_class = Config.IdentityOptions options_class = Config.IdentityOptions
template_name = "settings/settings.html" template_name = "settings/settings.html"
section: ClassVar[str] section: ClassVar[str]
options: dict[str, dict[str, str]] options: dict[str, dict[str, str | int]]
layout: dict[str, list[str]] layout: dict[str, list[str]]
def get_form_class(self): def get_form_class(self):
@ -51,6 +51,10 @@ class SettingsPage(FormView):
choices = details.get("choices") choices = details.get("choices")
if choices: if choices:
field_kwargs["widget"] = forms.Select(choices=choices) field_kwargs["widget"] = forms.Select(choices=choices)
for int_kwarg in {"min_value", "max_value", "step_size"}:
val = details.get(int_kwarg)
if val:
field_kwargs[int_kwarg] = val
form_field = forms.IntegerField form_field = forms.IntegerField
else: else:
raise ValueError(f"Cannot render settings type {config_field.type_}") raise ValueError(f"Cannot render settings type {config_field.type_}")