summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activities/views/follows.py4
-rw-r--r--activities/views/posts.py2
-rw-r--r--activities/views/timelines.py14
-rw-r--r--core/decorators.py41
-rw-r--r--core/middleware.py12
-rw-r--r--core/models/config.py5
-rw-r--r--core/views.py3
-rw-r--r--docs/tuning.rst22
-rw-r--r--requirements.txt1
-rw-r--r--takahe/settings.py11
-rw-r--r--takahe/urls.py5
-rw-r--r--templates/settings/_menu.html3
-rw-r--r--users/views/activitypub.py4
-rw-r--r--users/views/admin/__init__.py2
-rw-r--r--users/views/admin/settings.py49
-rw-r--r--users/views/identity.py5
-rw-r--r--users/views/settings/interface.py6
17 files changed, 186 insertions, 3 deletions
diff --git a/activities/views/follows.py b/activities/views/follows.py
index 44d8adc..841c8cc 100644
--- a/activities/views/follows.py
+++ b/activities/views/follows.py
@@ -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.
diff --git a/activities/views/posts.py b/activities/views/posts.py
index ef16713..60873a7 100644
--- a/activities/views/posts.py
+++ b/activities/views/posts.py
@@ -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"
diff --git a/activities/views/timelines.py b/activities/views/timelines.py
index 753add6..156a20d 100644
--- a/activities/views/timelines.py
+++ b/activities/views/timelines.py
@@ -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"
diff --git a/core/decorators.py b/core/decorators.py
new file mode 100644
index 0000000..4934b60
--- /dev/null
+++ b/core/decorators.py
@@ -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)
diff --git a/core/middleware.py b/core/middleware.py
index 08c28fa..bd89d1c 100644
--- a/core/middleware.py
+++ b/core/middleware.py
@@ -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
diff --git a/core/models/config.py b/core/models/config.py
index dd7da9f..b18471e 100644
--- a/core/models/config.py
+++ b/core/models/config.py
@@ -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):
diff --git a/core/views.py b/core/views.py
index 889fa27..ea8a1ca 100644
--- a/core/views.py
+++ b/core/views.py
@@ -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"
diff --git a/docs/tuning.rst b/docs/tuning.rst
index 2b5e5d8..6f7badc 100644
--- a/docs/tuning.rst
+++ b/docs/tuning.rst
@@ -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``
diff --git a/requirements.txt b/requirements.txt
index 89c1bd3..9f3e3ab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/takahe/settings.py b/takahe/settings.py
index 7952024..9769e98 100644
--- a/takahe/settings.py
+++ b/takahe/settings.py
@@ -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]
diff --git a/takahe/urls.py b/takahe/urls.py
index 59f14ba..a6e8a6e 100644
--- a/takahe/urls.py
+++ b/takahe/urls.py
@@ -56,6 +56,11 @@ urlpatterns = [
name="admin_basic",
),
path(
+ "admin/tuning/",
+ admin.TuningSettings.as_view(),
+ name="admin_tuning",
+ ),
+ path(
"admin/domains/",
admin.Domains.as_view(),
name="admin_domains",
diff --git a/templates/settings/_menu.html b/templates/settings/_menu.html
index 55c0100..fa2e74e 100644
--- a/templates/settings/_menu.html
+++ b/templates/settings/_menu.html
@@ -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>
diff --git a/users/views/activitypub.py b/users/views/activitypub.py
index cca57fb..c3395a4 100644
--- a/users/views/activitypub.py
+++ b/users/views/activitypub.py
@@ -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
diff --git a/users/views/admin/__init__.py b/users/views/admin/__init__.py
index 101ca30..04e1195 100644
--- a/users/views/admin/__init__.py
+++ b/users/views/admin/__init__.py
@@ -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")
diff --git a/users/views/admin/settings.py b/users/views/admin/settings.py
index a9ec78b..dc56693 100644
--- a/users/views/admin/settings.py
+++ b/users/views/admin/settings.py
@@ -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",
+ ],
+ }
diff --git a/users/views/identity.py b/users/views/identity.py
index 6cfcff9..d98ce9d 100644
--- a/users/views/identity.py
+++ b/users/views/identity.py
@@ -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
diff --git a/users/views/settings/interface.py b/users/views/settings/interface.py
index 5c4f229..e8c73a6 100644
--- a/users/views/settings/interface.py
+++ b/users/views/settings/interface.py
@@ -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_}")