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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,5 @@ jobs:
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_header_syntax_validation_task --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_syntax_validation_task --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_schema_validation_task --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_status_combine --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_status_combine --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_management_commands --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
1 change: 1 addition & 0 deletions .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ jobs:
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_header_syntax_validation_task --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_syntax_validation_task --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_schema_validation_task --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3
MEDIA_ROOT=./apps/ifc_validation/fixtures python3 manage.py test apps.ifc_validation.tests.tests_management_commands --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3

deploy:

Expand Down
5 changes: 4 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ stop-worker:
-$(PYTHON) -m celery -A core control shutdown \
--destination=worker@$(shell hostname) || true

test: test-models test-header-validation-task test-syntax-task test-syntax-header-validation-task test-schema-task test-magic-and-av-task test-utils test-file-retention-task test-status-combine
test: test-models test-header-validation-task test-syntax-task test-syntax-header-validation-task test-schema-task test-magic-and-av-task test-utils test-file-retention-task test-status-combine test-management-commands

test-models:
MEDIA_ROOT=./apps/ifc_validation/fixtures $(PYTHON) manage.py test apps/ifc_validation_models --settings apps.ifc_validation_models.test_settings --debug-mode --verbosity 3
Expand All @@ -92,6 +92,9 @@ test-file-retention-task:
test-status-combine:
MEDIA_ROOT=./apps/ifc_validation/fixtures $(PYTHON) manage.py test apps.ifc_validation.tests.tests_status_combine --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3

test-management-commands:
MEDIA_ROOT=./apps/ifc_validation/fixtures $(PYTHON) manage.py test apps.ifc_validation.tests.tests_management_commands --settings apps.ifc_validation.test_settings --debug-mode --verbosity 3

test-utils:
$(PYTHON) manage.py test core.tests.test_utils --debug-mode --verbosity 3

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from redis import Redis
from redis.lock import Lock
import logging

from django.core.management.base import BaseCommand

from core.settings import CELERY_BROKER_URL

logger = logging.getLogger(__name__)
redis_client: Redis = Redis.from_url(CELERY_BROKER_URL, decode_responses=True)

class Command(BaseCommand):

help = (
'Scans and displays all current user locks (user ID, task name, TTL)'
)

def handle(self, *args, **options):

# Scan for keys matching the lock pattern
lock_pattern = "lock:celery:user:*:task:*"

@aothms aothms Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried for a second or so to make this a bit more declarative like but I guess it's not worth it until we have a lot more of these patterns:

@dataclass
class placeholder:
  name: str
  def to_glob(self): return '*'
  def to_re(self): return r'(?P<{self.name}>[^:]+)'

@dataclass
class query:
  items : list
  def to_glob(self): return ':'.join(i.to_glob() for i in self.items if not isinstance(i, str) else i)
  def to_re(self): return re.compile(':'.join(i.to_re() for i in self.items if not isinstance(i, str) else i))

q = query(['lock', 'celery', 'user', placeholder('user_id'), 'task', palceholder('task_name')])

redis_client.keys(q.to_glob())

...

user_id, task_name = q.to_re().match(key).group('user_id', 'task_name')

...

lock_keys = redis_client.keys(lock_pattern)

if not lock_keys:
logger.info("No active user locks found.")
return

logger.info(f"Found {len(lock_keys)} active user lock(s):")
for key in lock_keys:
# Extract user_id and task_name from the key
parts = key.split(":")
if len(parts) >= 6:
user_id = parts[3]
task_name = parts[5]
ttl = redis_client.ttl(key)
logger.info(f"- User ID: {user_id}, Task: {task_name}, TTL: {ttl:,} seconds")
34 changes: 34 additions & 0 deletions backend/apps/ifc_validation/tests/tests_management_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.core.management import call_command
from django.contrib.auth.models import User
from django.test import TransactionTestCase

from core.redis_lock import acquire_user_lock


class DisplayUserLocksManagementCommandTestCase(TransactionTestCase):

def test_display_user_locks_no_active_locks(self):

# arrange
test_user = User.objects.create_user(username='testuser', password='testpass')

# act
with self.assertLogs(level='INFO') as cm:
call_command('display_user_locks')

# assert
self.assertTrue(any("No active user locks found." in message for message in cm.output))

def test_display_user_locks_active_user_lock(self):

# arrange
test_user = User.objects.create_user(username='testuser', password='testpass')
with acquire_user_lock(user_id=test_user.id, task_name='test_task') as lock:

# act
with self.assertLogs(level='INFO') as cm:
call_command('display_user_locks')

# assert
print(cm.output) # for debugging if test fails
self.assertTrue(any(f"User ID: {test_user.id}, Task: test_task" in message for message in cm.output))
Loading