Added caching and initial settings
This commit is contained in:
parent
a9bb4a7122
commit
d6eb16a398
@ -1,11 +1,15 @@
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from core.decorators import per_identity_cache_page
|
||||
from users.decorators import identity_required
|
||||
from users.models import FollowStates
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(
|
||||
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
|
||||
)
|
||||
class FollowsPage(TemplateView):
|
||||
"""
|
||||
Shows followers/follows.
|
||||
|
@ -6,11 +6,13 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
from activities.models import Post, PostInteraction, PostInteractionStates, PostStates
|
||||
from core.decorators import per_identity_cache_page
|
||||
from core.ld import canonicalise
|
||||
from users.decorators import identity_required
|
||||
from users.shortcuts import by_handle_or_404
|
||||
|
||||
|
||||
@method_decorator(per_identity_cache_page("cache_timeout_page_post"), name="dispatch")
|
||||
class Individual(TemplateView):
|
||||
|
||||
template_name = "activities/post.html"
|
||||
|
@ -5,11 +5,13 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.generic import FormView, ListView
|
||||
|
||||
from activities.models import Hashtag, Post, PostInteraction, TimelineEvent
|
||||
from core.decorators import per_identity_cache_page
|
||||
from core.models import Config
|
||||
from users.decorators import identity_required
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(per_identity_cache_page(), name="dispatch")
|
||||
class Home(FormView):
|
||||
|
||||
template_name = "activities/home.html"
|
||||
@ -61,6 +63,9 @@ class Home(FormView):
|
||||
return redirect(".")
|
||||
|
||||
|
||||
@method_decorator(
|
||||
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
|
||||
)
|
||||
class Tag(ListView):
|
||||
|
||||
template_name = "activities/tag.html"
|
||||
@ -96,6 +101,9 @@ class Tag(ListView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(
|
||||
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
|
||||
)
|
||||
class Local(ListView):
|
||||
|
||||
template_name = "activities/local.html"
|
||||
@ -122,6 +130,9 @@ class Local(ListView):
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(
|
||||
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
|
||||
)
|
||||
class Federated(ListView):
|
||||
|
||||
template_name = "activities/federated.html"
|
||||
@ -150,6 +161,9 @@ class Federated(ListView):
|
||||
|
||||
|
||||
@method_decorator(identity_required, name="dispatch")
|
||||
@method_decorator(
|
||||
per_identity_cache_page("cache_timeout_page_timeline"), name="dispatch"
|
||||
)
|
||||
class Notifications(ListView):
|
||||
|
||||
template_name = "activities/notifications.html"
|
||||
|
41
core/decorators.py
Normal file
41
core/decorators.py
Normal 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)
|
@ -1,3 +1,5 @@
|
||||
from time import time
|
||||
|
||||
from django.core.exceptions import MiddlewareNotUsed
|
||||
|
||||
from core import sentry
|
||||
@ -9,11 +11,19 @@ class ConfigLoadingMiddleware:
|
||||
Caches the system config every request
|
||||
"""
|
||||
|
||||
refresh_interval: float = 30.0
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
self.config_ts: float = 0.0
|
||||
|
||||
def __call__(self, request):
|
||||
Config.system = Config.load_system()
|
||||
if (
|
||||
not getattr(Config, "system", None)
|
||||
or (time() - self.config_ts) >= self.refresh_interval
|
||||
):
|
||||
Config.system = Config.load_system()
|
||||
self.config_ts = time()
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
|
@ -218,6 +218,11 @@ class Config(models.Model):
|
||||
hashtag_unreviewed_are_public: bool = True
|
||||
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"
|
||||
|
||||
class UserOptions(pydantic.BaseModel):
|
||||
|
@ -1,8 +1,10 @@
|
||||
from django.http import JsonResponse
|
||||
from django.templatetags.static import static
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import TemplateView, View
|
||||
|
||||
from activities.views.timelines import Home
|
||||
from core.decorators import cache_page
|
||||
from users.models import Identity
|
||||
|
||||
|
||||
@ -13,6 +15,7 @@ def homepage(request):
|
||||
return LoggedOutHomepage.as_view()(request)
|
||||
|
||||
|
||||
@method_decorator(cache_page(), name="dispatch")
|
||||
class LoggedOutHomepage(TemplateView):
|
||||
|
||||
template_name = "index.html"
|
||||
|
@ -14,3 +14,25 @@ Environment Variable:
|
||||
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
|
||||
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``
|
||||
|
@ -2,6 +2,7 @@ bleach~=5.0.1
|
||||
blurhash-python~=1.1.3
|
||||
cryptography~=38.0
|
||||
dj_database_url~=1.0.0
|
||||
django-cache-url~=3.4.2
|
||||
django-htmx~=1.13.0
|
||||
django-storages[google,boto3]~=1.13.1
|
||||
django~=4.1
|
||||
|
@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import dj_database_url
|
||||
import django_cache_url
|
||||
import sentry_sdk
|
||||
from pydantic import AnyUrl, BaseSettings, EmailStr, Field, validator
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
@ -15,6 +16,11 @@ from takahe import __version__
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class CacheBackendUrl(AnyUrl):
|
||||
host_required = False
|
||||
allowed_schemes = django_cache_url.BACKENDS.keys()
|
||||
|
||||
|
||||
class ImplicitHostname(AnyUrl):
|
||||
host_required = False
|
||||
|
||||
@ -107,6 +113,9 @@ class Settings(BaseSettings):
|
||||
#: (placeholder setting, no effect)
|
||||
SEARCH: bool = True
|
||||
|
||||
#: Default cache backend
|
||||
CACHES_DEFAULT: CacheBackendUrl | None = None
|
||||
|
||||
PGHOST: str | None = None
|
||||
PGPORT: int | None = 5432
|
||||
PGNAME: str = "takahe"
|
||||
@ -339,5 +348,7 @@ if SETUP.MEDIA_BACKEND:
|
||||
else:
|
||||
raise ValueError(f"Unsupported media backend {parsed.scheme}")
|
||||
|
||||
CACHES = {"default": django_cache_url.parse(SETUP.CACHES_DEFAULT or "dummy://")}
|
||||
|
||||
if SETUP.ERROR_EMAILS:
|
||||
ADMINS = [("Admin", e) for e in SETUP.ERROR_EMAILS]
|
||||
|
@ -55,6 +55,11 @@ urlpatterns = [
|
||||
admin.BasicSettings.as_view(),
|
||||
name="admin_basic",
|
||||
),
|
||||
path(
|
||||
"admin/tuning/",
|
||||
admin.TuningSettings.as_view(),
|
||||
name="admin_tuning",
|
||||
),
|
||||
path(
|
||||
"admin/domains/",
|
||||
admin.Domains.as_view(),
|
||||
|
@ -36,6 +36,9 @@
|
||||
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
|
||||
<i class="fa-solid fa-hashtag"></i> Hashtags
|
||||
</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="">
|
||||
<i class="fa-solid fa-gear"></i> Django Admin
|
||||
</a>
|
||||
|
@ -9,6 +9,7 @@ from django.views.generic import View
|
||||
|
||||
from activities.models import Post
|
||||
from core import exceptions
|
||||
from core.decorators import cache_page
|
||||
from core.ld import canonicalise
|
||||
from core.models import Config
|
||||
from core.signatures import (
|
||||
@ -61,6 +62,7 @@ class NodeInfo(View):
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(cache_page(), name="dispatch")
|
||||
class NodeInfo2(View):
|
||||
"""
|
||||
Returns the nodeinfo 2.0 response
|
||||
@ -87,6 +89,7 @@ class NodeInfo2(View):
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(cache_page(), name="dispatch")
|
||||
class Webfinger(View):
|
||||
"""
|
||||
Services webfinger requests
|
||||
@ -189,6 +192,7 @@ class Inbox(View):
|
||||
return HttpResponse(status=202)
|
||||
|
||||
|
||||
@method_decorator(cache_page(), name="dispatch")
|
||||
class SystemActorView(View):
|
||||
"""
|
||||
Special endpoint for the overall system actor
|
||||
|
@ -17,7 +17,7 @@ from users.views.admin.hashtags import ( # noqa
|
||||
HashtagEdit,
|
||||
Hashtags,
|
||||
)
|
||||
from users.views.admin.settings import BasicSettings # noqa
|
||||
from users.views.admin.settings import BasicSettings, TuningSettings # noqa
|
||||
|
||||
|
||||
@method_decorator(admin_required, name="dispatch")
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from core.models import Config
|
||||
from users.decorators import admin_required
|
||||
@ -106,3 +107,51 @@ class BasicSettings(AdminSettingsPage):
|
||||
"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",
|
||||
],
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.generic import FormView, ListView, TemplateView, View
|
||||
|
||||
from activities.models import Post, PostInteraction
|
||||
from core.decorators import per_identity_cache_page
|
||||
from core.ld import canonicalise
|
||||
from core.models import Config
|
||||
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
|
||||
|
||||
|
||||
@method_decorator(per_identity_cache_page(), name="dispatch")
|
||||
class ViewIdentity(ListView):
|
||||
"""
|
||||
Shows identity profile pages, and also acts as the Actor endpoint when
|
||||
@ -90,6 +92,9 @@ class ViewIdentity(ListView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(
|
||||
per_identity_cache_page("cache_timeout_identity_feed"), name="__call__"
|
||||
)
|
||||
class IdentityFeed(Feed):
|
||||
"""
|
||||
Serves a local user's Public posts as an RSS feed
|
||||
|
@ -21,7 +21,7 @@ class SettingsPage(FormView):
|
||||
options_class = Config.IdentityOptions
|
||||
template_name = "settings/settings.html"
|
||||
section: ClassVar[str]
|
||||
options: dict[str, dict[str, str]]
|
||||
options: dict[str, dict[str, str | int]]
|
||||
layout: dict[str, list[str]]
|
||||
|
||||
def get_form_class(self):
|
||||
@ -51,6 +51,10 @@ class SettingsPage(FormView):
|
||||
choices = details.get("choices")
|
||||
if 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
|
||||
else:
|
||||
raise ValueError(f"Cannot render settings type {config_field.type_}")
|
||||
|
Reference in New Issue
Block a user