Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support redis migrations #8898

Merged
merged 33 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7a32e8b
Draft: add custom 'migrateredis' management command
Marishka17 Jan 3, 2025
78338c7
Merge branch 'develop' into mk/redis-migrations
Marishka17 Jan 10, 2025
10e3a1d
[draft] Add MigrationLoader
Marishka17 Jan 10, 2025
fae0974
Move logic to a separate app
Marishka17 Jan 15, 2025
88ef5a0
Merge branch 'develop' into mk/redis-migrations
Marishka17 Jan 15, 2025
dc1f3bb
Fix linter issues
Marishka17 Jan 15, 2025
233d0a9
Add test migration
Marishka17 Jan 16, 2025
a9ae8fd
Merge branch 'develop' into mk/redis-migrations
Marishka17 Jan 16, 2025
fac9f58
Run migrateredis before syncperiodicjobs
Marishka17 Jan 16, 2025
603fe90
linters
Marishka17 Jan 16, 2025
0b49bbe
Notify if there are no migrations
Marishka17 Jan 16, 2025
170e0f3
Support Redis migrations from enterprise apps
Marishka17 Jan 16, 2025
2bb68c9
Run CI
Marishka17 Jan 16, 2025
bd756e1
Fix minor stupid comments
Marishka17 Jan 16, 2025
580f9a8
Simplify the code
Marishka17 Jan 17, 2025
bbb12b6
Move periodic job commands to redis_handler
Marishka17 Jan 17, 2025
0bea986
Update development guide
Marishka17 Jan 17, 2025
0f75340
Store applied migartions in Redis
Marishka17 Jan 17, 2025
c46a7b4
Add check option
Marishka17 Jan 17, 2025
1131db9
Merge branch 'develop' into mk/redis-migrations
Marishka17 Jan 17, 2025
96f7a7a
allow using shared connection
Marishka17 Jan 17, 2025
1834099
Merge remote-tracking branch 'origin/mk/redis-migrations' into mk/red…
Marishka17 Jan 17, 2025
634f460
changelog
Marishka17 Jan 17, 2025
5582bd9
Update cvat/apps/redis_handler/migration_loader.py
Marishka17 Jan 17, 2025
36ae237
Resolve conflicts
Marishka17 Jan 27, 2025
ba1ef79
add empty line
Marishka17 Jan 27, 2025
942a78c
fix black issue
Marishka17 Jan 27, 2025
32243a7
[tests] Do not reset cvat migrations from redis
Marishka17 Jan 27, 2025
7478890
Apply comments
Marishka17 Jan 30, 2025
f08bd9b
Merge branch 'develop' into mk/redis-migrations
Marishka17 Jan 30, 2025
4d5f70c
Merge branch 'develop' into mk/redis-migrations
Marishka17 Jan 30, 2025
98d0037
Merge branch 'develop' into mk/redis-migrations
Marishka17 Jan 31, 2025
af4b01a
Pass connection to migration class && do not use key prefix in the set
Marishka17 Feb 4, 2025
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 backend_entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ cmd_init() {
~/manage.py migrate

wait-for-it "${CVAT_REDIS_INMEM_HOST}:${CVAT_REDIS_INMEM_PORT:-6379}" -t 0
~/manage.py migrateredis
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved
~/manage.py syncperiodicjobs
}

Expand Down
20 changes: 20 additions & 0 deletions cvat/apps/engine/redis_migrations/001_cleanup_scheduled_jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

import django_rq
from django.conf import settings
from rq_scheduler import Scheduler

from cvat.apps.redis_handler.redis_migrations import BaseMigration


class Migration(BaseMigration):
@staticmethod
def run():
scheduler: Scheduler = django_rq.get_scheduler(settings.CVAT_QUEUES.EXPORT_DATA.value)
SpecLad marked this conversation as resolved.
Show resolved Hide resolved

for job in scheduler.get_jobs():
if job.func_name == "cvat.apps.dataset_manager.views.clear_export_cache":
scheduler.cancel(job)
job.delete()
4 changes: 4 additions & 0 deletions cvat/apps/engine/redis_migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright (C) 2025 CVAT.ai Corporation
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved
#
# SPDX-License-Identifier: MIT

3 changes: 3 additions & 0 deletions cvat/apps/redis_handler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
10 changes: 10 additions & 0 deletions cvat/apps/redis_handler/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT


from django.apps import AppConfig
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved


class RedisHandlerConfig(AppConfig):
name = "cvat.apps.redis_handler"
3 changes: 3 additions & 0 deletions cvat/apps/redis_handler/management/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
3 changes: 3 additions & 0 deletions cvat/apps/redis_handler/management/commands/__init__.py
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
43 changes: 43 additions & 0 deletions cvat/apps/redis_handler/management/commands/migrateredis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

import traceback

from django.core.management.base import BaseCommand, CommandError
from django.db import transaction

from cvat.apps.redis_handler.migration_loader import MigrationLoader
from cvat.apps.redis_handler.models import RedisMigration


class Command(BaseCommand):
help = "Applies Redis migrations and records them in the database"

def handle(self, *args, **options) -> None:
loader = MigrationLoader()

if not loader:
self.stdout.write("No migrations to apply")
return

for migration in loader:
try:
with transaction.atomic():
RedisMigration.objects.create(
name=migration.name, app_label=migration.app_label
)
migration.run()
self.stdout.write(
self.style.SUCCESS(
f"[{migration.app_label}] Successfully applied migration: {migration.name}"
)
)
except Exception as ex:
self.stderr.write(
self.style.ERROR(
f"[{migration.app_label}] Failed to apply migration: {migration.name}"
)
)
self.stderr.write(self.style.ERROR(f"\n{traceback.format_exc()}"))
raise CommandError(str(ex))
SpecLad marked this conversation as resolved.
Show resolved Hide resolved
74 changes: 74 additions & 0 deletions cvat/apps/redis_handler/migration_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

import importlib
from pathlib import Path

from django.apps import AppConfig, apps

from cvat.apps.redis_handler.models import RedisMigration
from cvat.apps.redis_handler.redis_migrations import BaseMigration


class LoaderError(Exception):
pass


class MigrationLoader:
REDIS_MIGRATIONS_DIR_NAME = "redis_migrations"
REDIS_MIGRATION_CLASS_NAME = "Migration"

def __init__(self) -> None:
self._app_config_mapping = {
app_config.label: app_config for app_config in self._find_app_configs()
}
self._disk_migrations_per_app: dict[str, list[str]] = {}
self._unapplied_migrations: list[BaseMigration] = []

self._load_from_disk()
self._init_unapplied_migrations()

def _find_app_configs(self) -> list[AppConfig]:
return [
app_config
for app_config in apps.get_app_configs()
if app_config.name.startswith("cvat")
and (Path(app_config.path) / self.REDIS_MIGRATIONS_DIR_NAME).exists()
]

def _load_from_disk(self):
for app_label, app_config in self._app_config_mapping.items():
migrations_dir = Path(app_config.path) / self.REDIS_MIGRATIONS_DIR_NAME
for migration_file in sorted(migrations_dir.glob("[0-9]*.py")):
migration_name = migration_file.stem
(self._disk_migrations_per_app.setdefault(app_label, [])).append(migration_name)

def _init_unapplied_migrations(self):
applied_migrations = RedisMigration.objects.all()

for app_label, migration_names in self._disk_migrations_per_app.items():
app_config = self._app_config_mapping[app_label]
app_applied_migrations = {
m.name for m in applied_migrations if m.app_label == app_config.label
}
app_unapplied_migrations = sorted(set(migration_names) - app_applied_migrations)
for migration_name in app_unapplied_migrations:
MigrationClass = self.get_migration_class(app_config.name, migration_name)
self._unapplied_migrations.append(MigrationClass(migration_name, app_config.label))

def get_migration_class(self, app_name: str, migration_name: str) -> BaseMigration:
migration_module_path = ".".join([app_name, self.REDIS_MIGRATIONS_DIR_NAME, migration_name])
module = importlib.import_module(migration_module_path)
MigrationClass = getattr(module, self.REDIS_MIGRATION_CLASS_NAME, None)

if not MigrationClass or not issubclass(MigrationClass, BaseMigration):
raise LoaderError(f"Invalid migration: {migration_module_path}")

return MigrationClass

def __iter__(self):
yield from self._unapplied_migrations

def __len__(self):
return len(self._unapplied_migrations)
33 changes: 33 additions & 0 deletions cvat/apps/redis_handler/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.17 on 2025-01-15 12:36

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="RedisMigration",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("app_label", models.CharField(max_length=128)),
("name", models.CharField(max_length=128)),
("applied_date", models.DateTimeField(auto_now_add=True)),
],
),
migrations.AddConstraint(
model_name="redismigration",
constraint=models.UniqueConstraint(
fields=("app_label", "name"), name="migration_name_unique"
),
),
]
3 changes: 3 additions & 0 deletions cvat/apps/redis_handler/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
15 changes: 15 additions & 0 deletions cvat/apps/redis_handler/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.db import models


class RedisMigration(models.Model):
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved
app_label = models.CharField(max_length=128)
name = models.CharField(max_length=128)
applied_date = models.DateTimeField(auto_now_add=True)

class Meta:
constraints = [
models.UniqueConstraint(
name="migration_name_unique",
fields=("app_label", "name"),
),
]
17 changes: 17 additions & 0 deletions cvat/apps/redis_handler/redis_migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

from abc import ABCMeta, abstractmethod

from attrs import define, field, validators


@define
class BaseMigration(metaclass=ABCMeta):
name: str = field(validator=[validators.instance_of(str)])
app_label: str = field(validator=[validators.instance_of(str)])

@staticmethod
@abstractmethod
def run() -> None: ...
1 change: 1 addition & 0 deletions cvat/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def generate_secret_key():
'cvat.apps.events',
'cvat.apps.quality_control',
'cvat.apps.analytics_report',
'cvat.apps.redis_handler',
]

SITE_ID = 1
Expand Down
2 changes: 1 addition & 1 deletion dev/format_python_code.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ for paths in \
"cvat/apps/dataset_manager/tests/test_annotation.py" \
"cvat/apps/dataset_manager/tests/utils.py" \
"cvat/apps/events/signals.py" \
"cvat/apps/engine/management/commands/syncperiodicjobs.py" \
"cvat/apps/dataset_manager/management/commands/cleanuplegacyexportcache.py" \
"cvat/apps/redis_handler" \
; do
${BLACK} -- ${paths}
${ISORT} -- ${paths}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ description: 'Installing a development environment for different operating syste

```bash
python manage.py migrate
python manage.py migrateredis
python manage.py collectstatic
python manage.py syncperiodicjobs
python manage.py createsuperuser
Expand Down
Loading