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