️(backend) change db state instead of using long running row lock

When analysing a file, the previous commit introduced a row level lock
to make sure we would analyse and promote a single file.
This commit changes the locking mechanism so that it happens with
the upload state which avoids long running db locks and potential
perf issues.
This commit is contained in:
Florent Chehab
2026-06-02 15:12:25 +02:00
parent a82d7f885a
commit d13e3a8a5d
3 changed files with 40 additions and 14 deletions
+21 -13
View File
@@ -1197,22 +1197,25 @@ class FileViewSet(
Check the actual uploaded file and mark it as ready.
"""
# Ensures we go through authorization checks
self.get_object()
file = self.get_object()
# Try to update the file with the new state. If the file is already in this state
# we are in a concurrent request, and we should reject that request
updated_rows = models.File.objects.filter(
upload_state=models.FileUploadStateChoices.PENDING,
pk=kwargs["pk"],
).update(upload_state=models.FileUploadStateChoices.ANALYZING)
if updated_rows != 1:
raise drf_exceptions.ValidationError(
{"file": "This action is only available for files in PENDING state."},
code="file_upload_state_not_pending",
)
file.refresh_from_db()
s3_client = default_storage.connection.meta.client
validation_error = None
with transaction.atomic():
file = self.get_queryset().select_for_update().get(pk=kwargs["pk"])
if not file.is_pending_upload:
raise drf_exceptions.ValidationError(
{
"file": "This action is only available for files in PENDING state."
},
code="file_upload_state_not_pending",
)
try:
# We copy the file to its final destination, we will run the checks on that
# final file and ignore any updates to the temporary file. (We cannot revoke the policy,
# so the temporary file might still be updated after that.)
@@ -1302,6 +1305,11 @@ class FileViewSet(
Metadata=head_response["Metadata"],
MetadataDirective="REPLACE",
)
except Exception as e:
logger.exception("Failed to analyze file, reverting to pending state")
file.upload_state = models.FileUploadStateChoices.PENDING
file.save()
raise e
if validation_error:
raise validation_error
@@ -1318,7 +1326,7 @@ class FileViewSet(
"""Delete a file completely."""
file.soft_delete()
file.hard_delete()
transaction.on_commit(lambda: process_file_deletion.delay(file.id))
process_file_deletion.delay(file.id)
def _authorize_subrequest(self, request, pattern):
"""
@@ -0,0 +1,18 @@
# Generated by Django 5.2.14 on 2026-06-02 12:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0018_rename_active_application_is_active'),
]
operations = [
migrations.AlterField(
model_name='file',
name='upload_state',
field=models.CharField(choices=[('pending', 'Pending'), ('analyzing', 'Analyzing'), ('ready', 'Ready')], max_length=25),
),
]
+1 -1
View File
@@ -846,8 +846,8 @@ class FileUploadStateChoices(models.TextChoices):
"""Possible states of a file."""
PENDING = "pending", _("Pending")
ANALYZING = "analyzing", _("Analyzing")
# Commented out for now, as we may need this when we implement the malware detection logic.
# ANALYZING = "analyzing", _("Analyzing")
# SUSPICIOUS = "suspicious", _("Suspicious")
# FILE_TOO_LARGE_TO_ANALYZE = (
# "file_too_large_to_analyze",