summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docker/Dockerfile3
-rw-r--r--docker/start.sh2
-rw-r--r--static/css/style.css4
-rw-r--r--stator/management/commands/runstator.py32
-rw-r--r--stator/runner.py90
-rw-r--r--stator/views.py23
-rw-r--r--takahe/urls.py3
-rw-r--r--users/views/admin/settings.py9
8 files changed, 92 insertions, 74 deletions
diff --git a/docker/Dockerfile b/docker/Dockerfile
index a35f1ff..b4d9d57 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -16,4 +16,7 @@ RUN DJANGO_SETTINGS_MODULE=takahe.settings.development python3 manage.py collect
EXPOSE 8000
+# Set some sensible defaults
+ENV GUNICORN_CMD_ARGS="--workers 8"
+
CMD ["sh", "/takahe/docker/start.sh"]
diff --git a/docker/start.sh b/docker/start.sh
index 1c01b6e..d7dd3fd 100644
--- a/docker/start.sh
+++ b/docker/start.sh
@@ -2,4 +2,4 @@
python3 manage.py migrate
-exec gunicorn takahe.wsgi:application -w 8 -b 0.0.0.0:8000
+exec gunicorn takahe.wsgi:application -b 0.0.0.0:8000
diff --git a/static/css/style.css b/static/css/style.css
index 571e812..426308d 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -608,6 +608,10 @@ form .button:hover {
/* Logged out homepage */
+.about p {
+ margin: 0 0 15px 0;
+}
+
.about img.banner {
width: calc(100% + 30px);
height: auto;
diff --git a/stator/management/commands/runstator.py b/stator/management/commands/runstator.py
index eaa2585..3030960 100644
--- a/stator/management/commands/runstator.py
+++ b/stator/management/commands/runstator.py
@@ -10,7 +10,7 @@ from stator.runner import StatorRunner
class Command(BaseCommand):
- help = "Runs a Stator runner for a short period"
+ help = "Runs a Stator runner"
def add_arguments(self, parser):
parser.add_argument(
@@ -20,9 +20,30 @@ class Command(BaseCommand):
default=30,
help="How many tasks to run at once",
)
+ parser.add_argument(
+ "--liveness-file",
+ type=str,
+ default=None,
+ help="A file to touch at least every 30 seconds to say the runner is alive",
+ )
+ parser.add_argument(
+ "--schedule-interval",
+ "-s",
+ type=int,
+ default=30,
+ help="How often to run cleaning and scheduling",
+ )
parser.add_argument("model_labels", nargs="*", type=str)
- def handle(self, model_labels: List[str], concurrency: int, *args, **options):
+ def handle(
+ self,
+ model_labels: List[str],
+ concurrency: int,
+ liveness_file: str,
+ schedule_interval: int,
+ *args,
+ **options
+ ):
# Cache system config
Config.system = Config.load_system()
# Resolve the models list into names
@@ -34,5 +55,10 @@ class Command(BaseCommand):
models = StatorModel.subclasses
print("Running for models: " + " ".join(m._meta.label_lower for m in models))
# Run a runner
- runner = StatorRunner(models, concurrency=concurrency)
+ runner = StatorRunner(
+ models,
+ concurrency=concurrency,
+ liveness_file=liveness_file,
+ schedule_interval=schedule_interval,
+ )
async_to_sync(runner.run)()
diff --git a/stator/runner.py b/stator/runner.py
index bb1b009..d286bc1 100644
--- a/stator/runner.py
+++ b/stator/runner.py
@@ -3,7 +3,7 @@ import datetime
import time
import traceback
import uuid
-from typing import List, Type
+from typing import List, Optional, Type
from django.utils import timezone
@@ -13,7 +13,7 @@ from stator.models import StatorModel
class StatorRunner:
"""
Runs tasks on models that are looking for state changes.
- Designed to run in a one-shot mode, living inside a request.
+ Designed to run for a determinate amount of time, and then exit.
"""
def __init__(
@@ -21,57 +21,63 @@ class StatorRunner:
models: List[Type[StatorModel]],
concurrency: int = 50,
concurrency_per_model: int = 10,
- run_period: int = 60,
- wait_period: int = 30,
+ liveness_file: Optional[str] = None,
+ schedule_interval: int = 30,
+ lock_expiry: int = 300,
):
self.models = models
self.runner_id = uuid.uuid4().hex
self.concurrency = concurrency
self.concurrency_per_model = concurrency_per_model
- self.run_period = run_period
- self.total_period = run_period + wait_period
+ self.liveness_file = liveness_file
+ self.schedule_interval = schedule_interval
+ self.lock_expiry = lock_expiry
async def run(self):
- start_time = time.monotonic()
self.handled = 0
+ self.last_clean = time.monotonic() - self.schedule_interval
self.tasks = []
- # Clean up old locks
- print("Running initial cleaning and scheduling")
- initial_tasks = []
- for model in self.models:
- initial_tasks.append(model.atransition_clean_locks())
- initial_tasks.append(model.atransition_schedule_due())
- await asyncio.gather(*initial_tasks)
# For the first time period, launch tasks
print("Running main task loop")
- while (time.monotonic() - start_time) < self.run_period:
- self.remove_completed_tasks()
- space_remaining = self.concurrency - len(self.tasks)
- # Fetch new tasks
- for model in self.models:
- if space_remaining > 0:
- for instance in await model.atransition_get_with_lock(
- number=min(space_remaining, self.concurrency_per_model),
- lock_expiry=(
- timezone.now()
- + datetime.timedelta(seconds=(self.total_period * 2) + 60)
- ),
- ):
- self.tasks.append(
- asyncio.create_task(self.run_transition(instance))
- )
- self.handled += 1
- space_remaining -= 1
- # Prevent busylooping
- await asyncio.sleep(0.1)
- # Then wait for tasks to finish
- print("Waiting for tasks to complete")
- while (time.monotonic() - start_time) < self.total_period:
- self.remove_completed_tasks()
- if not self.tasks:
- break
- # Prevent busylooping
- await asyncio.sleep(1)
+ try:
+ while True:
+ # Do we need to do cleaning?
+ if (time.monotonic() - self.last_clean) >= self.schedule_interval:
+ print(f"{self.handled} tasks processed so far")
+ print("Running cleaning and scheduling")
+ self.remove_completed_tasks()
+ for model in self.models:
+ asyncio.create_task(model.atransition_clean_locks())
+ asyncio.create_task(model.atransition_schedule_due())
+ self.last_clean = time.monotonic()
+ # Calculate space left for tasks
+ space_remaining = self.concurrency - len(self.tasks)
+ # Fetch new tasks
+ for model in self.models:
+ if space_remaining > 0:
+ for instance in await model.atransition_get_with_lock(
+ number=min(space_remaining, self.concurrency_per_model),
+ lock_expiry=(
+ timezone.now()
+ + datetime.timedelta(seconds=self.lock_expiry)
+ ),
+ ):
+ self.tasks.append(
+ asyncio.create_task(self.run_transition(instance))
+ )
+ self.handled += 1
+ space_remaining -= 1
+ # Prevent busylooping
+ await asyncio.sleep(0.1)
+ except KeyboardInterrupt:
+ # Wait for tasks to finish
+ print("Waiting for tasks to complete")
+ while True:
+ self.remove_completed_tasks()
+ if not self.tasks:
+ break
+ # Prevent busylooping
+ await asyncio.sleep(1)
print("Complete")
return self.handled
diff --git a/stator/views.py b/stator/views.py
deleted file mode 100644
index 9d2e154..0000000
--- a/stator/views.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from django.conf import settings
-from django.http import HttpResponse, HttpResponseForbidden
-from django.views import View
-
-from stator.models import StatorModel
-from stator.runner import StatorRunner
-
-
-class RequestRunner(View):
- """
- Runs a Stator runner within a HTTP request. For when you're on something
- serverless.
- """
-
- async def get(self, request):
- # Check the token, if supplied
- if settings.STATOR_TOKEN:
- if request.GET.get("token") != settings.STATOR_TOKEN:
- return HttpResponseForbidden()
- # Run on all models
- runner = StatorRunner(StatorModel.subclasses)
- handled = await runner.run()
- return HttpResponse(f"Handled {handled}")
diff --git a/takahe/urls.py b/takahe/urls.py
index 1f1b203..abb8b2c 100644
--- a/takahe/urls.py
+++ b/takahe/urls.py
@@ -5,7 +5,6 @@ from django.views.static import serve
from activities.views import posts, search, timelines
from core import views as core
-from stator import views as stator
from users.views import activitypub, admin, auth, follows, identity, settings
urlpatterns = [
@@ -110,8 +109,6 @@ urlpatterns = [
path(".well-known/host-meta", activitypub.HostMeta.as_view()),
path(".well-known/nodeinfo", activitypub.NodeInfo.as_view()),
path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()),
- # Task runner
- path(".stator/runner/", stator.RequestRunner.as_view()),
# Django admin
path("djadmin/", djadmin.site.urls),
# Media files
diff --git a/users/views/admin/settings.py b/users/views/admin/settings.py
index a528f93..e11aba5 100644
--- a/users/views/admin/settings.py
+++ b/users/views/admin/settings.py
@@ -39,7 +39,7 @@ class BasicSettings(AdminSettingsPage):
},
"site_about": {
"title": "About This Site",
- "help_text": "Displayed on the homepage and the about page",
+ "help_text": "Displayed on the homepage and the about page.\nNewlines are preserved; HTML also allowed.",
"display": "textarea",
},
"site_icon": {
@@ -67,6 +67,11 @@ class BasicSettings(AdminSettingsPage):
"help_text": "Shown above the signup form",
"display": "textarea",
},
+ "restricted_usernames": {
+ "title": "Restricted Usernames",
+ "help_text": "Usernames that only admins can register for identities. One per line.",
+ "display": "textarea",
+ },
}
layout = {
@@ -79,5 +84,5 @@ class BasicSettings(AdminSettingsPage):
],
"Signups": ["signup_allowed", "signup_invite_only", "signup_text"],
"Posts": ["post_length"],
- "Identities": ["identity_max_per_user"],
+ "Identities": ["identity_max_per_user", "restricted_usernames"],
}