A few more tweaks for an initial deploy

This commit is contained in:
Andrew Godwin 2022-11-19 10:20:13 -07:00
parent 8019311490
commit 2142677b01
8 changed files with 92 additions and 74 deletions

View file

@ -16,4 +16,7 @@ RUN DJANGO_SETTINGS_MODULE=takahe.settings.development python3 manage.py collect
EXPOSE 8000 EXPOSE 8000
# Set some sensible defaults
ENV GUNICORN_CMD_ARGS="--workers 8"
CMD ["sh", "/takahe/docker/start.sh"] CMD ["sh", "/takahe/docker/start.sh"]

View file

@ -2,4 +2,4 @@
python3 manage.py migrate 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

View file

@ -608,6 +608,10 @@ form .button:hover {
/* Logged out homepage */ /* Logged out homepage */
.about p {
margin: 0 0 15px 0;
}
.about img.banner { .about img.banner {
width: calc(100% + 30px); width: calc(100% + 30px);
height: auto; height: auto;

View file

@ -10,7 +10,7 @@ from stator.runner import StatorRunner
class Command(BaseCommand): class Command(BaseCommand):
help = "Runs a Stator runner for a short period" help = "Runs a Stator runner"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
@ -20,9 +20,30 @@ class Command(BaseCommand):
default=30, default=30,
help="How many tasks to run at once", 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) 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 # Cache system config
Config.system = Config.load_system() Config.system = Config.load_system()
# Resolve the models list into names # Resolve the models list into names
@ -34,5 +55,10 @@ class Command(BaseCommand):
models = StatorModel.subclasses models = StatorModel.subclasses
print("Running for models: " + " ".join(m._meta.label_lower for m in models)) print("Running for models: " + " ".join(m._meta.label_lower for m in models))
# Run a runner # 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)() async_to_sync(runner.run)()

View file

@ -3,7 +3,7 @@ import datetime
import time import time
import traceback import traceback
import uuid import uuid
from typing import List, Type from typing import List, Optional, Type
from django.utils import timezone from django.utils import timezone
@ -13,7 +13,7 @@ from stator.models import StatorModel
class StatorRunner: class StatorRunner:
""" """
Runs tasks on models that are looking for state changes. 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__( def __init__(
@ -21,57 +21,63 @@ class StatorRunner:
models: List[Type[StatorModel]], models: List[Type[StatorModel]],
concurrency: int = 50, concurrency: int = 50,
concurrency_per_model: int = 10, concurrency_per_model: int = 10,
run_period: int = 60, liveness_file: Optional[str] = None,
wait_period: int = 30, schedule_interval: int = 30,
lock_expiry: int = 300,
): ):
self.models = models self.models = models
self.runner_id = uuid.uuid4().hex self.runner_id = uuid.uuid4().hex
self.concurrency = concurrency self.concurrency = concurrency
self.concurrency_per_model = concurrency_per_model self.concurrency_per_model = concurrency_per_model
self.run_period = run_period self.liveness_file = liveness_file
self.total_period = run_period + wait_period self.schedule_interval = schedule_interval
self.lock_expiry = lock_expiry
async def run(self): async def run(self):
start_time = time.monotonic()
self.handled = 0 self.handled = 0
self.last_clean = time.monotonic() - self.schedule_interval
self.tasks = [] 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 # For the first time period, launch tasks
print("Running main task loop") print("Running main task loop")
while (time.monotonic() - start_time) < self.run_period: try:
self.remove_completed_tasks() while True:
space_remaining = self.concurrency - len(self.tasks) # Do we need to do cleaning?
# Fetch new tasks if (time.monotonic() - self.last_clean) >= self.schedule_interval:
for model in self.models: print(f"{self.handled} tasks processed so far")
if space_remaining > 0: print("Running cleaning and scheduling")
for instance in await model.atransition_get_with_lock( self.remove_completed_tasks()
number=min(space_remaining, self.concurrency_per_model), for model in self.models:
lock_expiry=( asyncio.create_task(model.atransition_clean_locks())
timezone.now() asyncio.create_task(model.atransition_schedule_due())
+ datetime.timedelta(seconds=(self.total_period * 2) + 60) self.last_clean = time.monotonic()
), # Calculate space left for tasks
): space_remaining = self.concurrency - len(self.tasks)
self.tasks.append( # Fetch new tasks
asyncio.create_task(self.run_transition(instance)) for model in self.models:
) if space_remaining > 0:
self.handled += 1 for instance in await model.atransition_get_with_lock(
space_remaining -= 1 number=min(space_remaining, self.concurrency_per_model),
# Prevent busylooping lock_expiry=(
await asyncio.sleep(0.1) timezone.now()
# Then wait for tasks to finish + datetime.timedelta(seconds=self.lock_expiry)
print("Waiting for tasks to complete") ),
while (time.monotonic() - start_time) < self.total_period: ):
self.remove_completed_tasks() self.tasks.append(
if not self.tasks: asyncio.create_task(self.run_transition(instance))
break )
# Prevent busylooping self.handled += 1
await asyncio.sleep(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") print("Complete")
return self.handled return self.handled

View file

@ -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}")

View file

@ -5,7 +5,6 @@ from django.views.static import serve
from activities.views import posts, search, timelines from activities.views import posts, search, timelines
from core import views as core from core import views as core
from stator import views as stator
from users.views import activitypub, admin, auth, follows, identity, settings from users.views import activitypub, admin, auth, follows, identity, settings
urlpatterns = [ urlpatterns = [
@ -110,8 +109,6 @@ urlpatterns = [
path(".well-known/host-meta", activitypub.HostMeta.as_view()), path(".well-known/host-meta", activitypub.HostMeta.as_view()),
path(".well-known/nodeinfo", activitypub.NodeInfo.as_view()), path(".well-known/nodeinfo", activitypub.NodeInfo.as_view()),
path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()), path("nodeinfo/2.0/", activitypub.NodeInfo2.as_view()),
# Task runner
path(".stator/runner/", stator.RequestRunner.as_view()),
# Django admin # Django admin
path("djadmin/", djadmin.site.urls), path("djadmin/", djadmin.site.urls),
# Media files # Media files

View file

@ -39,7 +39,7 @@ class BasicSettings(AdminSettingsPage):
}, },
"site_about": { "site_about": {
"title": "About This Site", "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", "display": "textarea",
}, },
"site_icon": { "site_icon": {
@ -67,6 +67,11 @@ class BasicSettings(AdminSettingsPage):
"help_text": "Shown above the signup form", "help_text": "Shown above the signup form",
"display": "textarea", "display": "textarea",
}, },
"restricted_usernames": {
"title": "Restricted Usernames",
"help_text": "Usernames that only admins can register for identities. One per line.",
"display": "textarea",
},
} }
layout = { layout = {
@ -79,5 +84,5 @@ class BasicSettings(AdminSettingsPage):
], ],
"Signups": ["signup_allowed", "signup_invite_only", "signup_text"], "Signups": ["signup_allowed", "signup_invite_only", "signup_text"],
"Posts": ["post_length"], "Posts": ["post_length"],
"Identities": ["identity_max_per_user"], "Identities": ["identity_max_per_user", "restricted_usernames"],
} }