Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*
# Explicitly include the bits we want
# TODO: find a good way to keep this in sync
!gunicorn.conf.py
!handler/*.py
!handler/chat/*.py
!handler/management/*.py
Expand Down
27 changes: 27 additions & 0 deletions gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# We intentionally do NOT run with --preload. `simone.urls` starts a
# singleton Cron background thread (and opens a DB connection) as an import
# side effect (see wsgi.py's `cron.start()`), and --preload would run that
# in the gunicorn master before workers are forked. fork() then duplicates
# whatever the Cron thread has in flight (its DB connection, mid-transaction
# or not) into the worker, which can leave things like table locks stuck
# open with nothing left to ever commit/release them -- every request that
# then needs the same rows hangs forever with nothing logged, until the
# client (Slack, ~3s) gives up. Without --preload, each worker loads the app
# (and starts its own Cron thread) fully independently, post-fork, so there
# is nothing shared to corrupt.
#
# This relies on running a single worker: script/run doesn't pass --workers,
# so gunicorn defaults to one. If that's ever increased, Cron would start
# once per worker and tick (and message Slack) that many times over.


def worker_abort(worker):
'''
Called when the arbiter SIGABRTs a worker for failing to heartbeat
within --timeout. Dump every thread's stack so a hang shows up in the
logs instead of just a silent restart.
'''
import faulthandler
import sys

faulthandler.dump_traceback(file=sys.stderr)
67 changes: 67 additions & 0 deletions handler_about/migrations/0004_replace_index_together.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.db import migrations, models

# Django 5.1 removed the `index_together` Meta option, so the model now
# declares this same composite index via `Meta.indexes` instead. The name
# below is the one Django's schema editor deterministically assigned to the
# old index_together-based index (a hash of the table + column names, so
# it's the same on every backend) when it was created back in
# 0002_add_workspace_fk. On SQLite it no longer actually exists — the
# 0003_make_workspace_nonnull AlterField rebuilt the table, and SQLite's
# table-rebuild only restores indexes tracked via Meta.indexes, silently
# dropping the index_together one — but on MySQL it's still present, so we
# look for it by name and drop it if we find it.
LEGACY_INDEX_NAME = (
'handler_about_fact_workspace_id_user_id_created_at_cff0afa0_idx'
)
NEW_INDEX_NAME = 'handler_abo_workspa_4d097e_idx'


def drop_legacy_index(apps, schema_editor):
Fact = apps.get_model('handler_about', 'Fact')
table = Fact._meta.db_table
with schema_editor.connection.cursor() as cursor:
constraints = schema_editor.connection.introspection.get_constraints(
cursor, table
)
if LEGACY_INDEX_NAME in constraints:
schema_editor.remove_index(
Fact,
models.Index(
fields=['workspace', 'user_id', 'created_at'],
name=LEGACY_INDEX_NAME,
),
)


class Migration(migrations.Migration):

dependencies = [('handler_about', '0003_make_workspace_nonnull')]

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterIndexTogether(
name='fact', index_together=set()
),
migrations.AddIndex(
model_name='fact',
index=models.Index(
fields=['workspace', 'user_id', 'created_at'],
name=NEW_INDEX_NAME,
),
),
],
database_operations=[
migrations.RunPython(
drop_legacy_index, migrations.RunPython.noop
),
migrations.AddIndex(
model_name='fact',
index=models.Index(
fields=['workspace', 'user_id', 'created_at'],
name=NEW_INDEX_NAME,
),
),
],
)
]
2 changes: 1 addition & 1 deletion handler_about/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ def __str__(self):
return f'{self.id} - {self.value}'

class Meta:
index_together = (('workspace', 'user_id', 'created_at'),)
indexes = [models.Index(fields=['workspace', 'user_id', 'created_at'])]
unique_together = (('workspace', 'user_id', 'value'),)
ordering = ('user_id', 'created_at')
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ packages = []
name = "simone"
version = "0.0.1"
dependencies = [
"Django>=4.0.8,<5.0",
"Django>=4.0.8,<6.0",
"cron-validator>=1.0.3",
"cryptography>=43.0.0",
"gunicorn>=20.1.0",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ charset-normalizer==3.4.7
click==8.4.2
cron-validator==1.0.8
cryptography==49.0.0
django==4.2.30
django==5.2.16
django-debug-toolbar==6.3.0
exceptiongroup==1.3.1; python_version=='3.10'
gunicorn==26.0.0
Expand Down
2 changes: 1 addition & 1 deletion script/run
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ set -e

./manage.py collectstatic --no-input
./manage.py migrate --no-input
gunicorn simone.wsgi --bind 0.0.0.0:6444 --graceful-timeout 5 --preload --threads 4
gunicorn simone.wsgi --config gunicorn.conf.py --bind 0.0.0.0:6444 --graceful-timeout 5 --threads 4
Loading