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 13 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
5 changes: 5 additions & 0 deletions cvat/apps/redis_handler/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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
44 changes: 44 additions & 0 deletions cvat/apps/redis_handler/management/commands/migrateredis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

import sys
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")
sys.exit(0)
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved

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
96 changes: 96 additions & 0 deletions cvat/apps/redis_handler/migration_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

import importlib
from pathlib import Path
from typing import Any, Generator, Mapping

from django.apps import AppConfig, apps
from django.utils.module_loading import module_has_submodule
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved

from cvat.apps.redis_handler.models import RedisMigration as DBRedisMigration
from cvat.apps.redis_handler.redis_migrations import BaseMigration as BaseRedisMigration
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved

_MigrationsPerApp = dict[int, list[str]]
SpecLad marked this conversation as resolved.
Show resolved Hide resolved


class LoaderError(Exception):
pass


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

class AppConfigs:
def __init__(self):
self._app_configs = self._find_app_configs()
self._app_configs_mapping: Mapping[str, AppConfig] = self._init_app_configs_mapping()

def _find_app_configs(self):
return [
app_config
for app_config in apps.get_app_configs()
if app_config.name.startswith("cvat")
and module_has_submodule(
app_config.module, MigrationLoader.REDIS_MIGRATIONS_DIR_NAME
)
]

def _init_app_configs_mapping(self):
return {app_config.label: app_config for app_config in self._app_configs}

def __getitem__(self, label: str) -> AppConfig:
return self._app_configs_mapping[label]

def __iter__(self) -> Generator[AppConfig, Any, Any]:
yield from self._app_configs

def __init__(self) -> None:
self._app_configs = self.AppConfigs()
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved
self._disk_migrations_per_app: _MigrationsPerApp = {}
self._unapplied_migrations: list[BaseRedisMigration] = []

self._load_from_disk()
self._init_unapplied_migrations()

@property
def app_configs(self) -> "AppConfigs":
return self._app_configs

def _load_from_disk(self):
for app_config in self._app_configs:
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_config.label, [])).append(
migration_name
)

def _init_unapplied_migrations(self):
applied_migrations = DBRedisMigration.objects.all().values_list("name", "app_label")
SpecLad marked this conversation as resolved.
Show resolved Hide resolved

for app_label, migration_names in self._disk_migrations_per_app.items():
app_config = self.app_configs[app_label]
app_applied_migrations = {m[0] for m in applied_migrations if m[1] == 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) -> BaseRedisMigration:
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, BaseRedisMigration):
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
1 change: 1 addition & 0 deletions dev/format_python_code.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ for paths in \
"cvat/apps/events/signals.py" \
"cvat/apps/engine/management/commands/syncperiodicjobs.py" \
"cvat/apps/dataset_manager/management/commands/cleanuplegacyexportcache.py" \
"cvat/apps/redis_handler/**/*.py" \
Marishka17 marked this conversation as resolved.
Show resolved Hide resolved
; do
${BLACK} -- ${paths}
${ISORT} -- ${paths}
Expand Down
Loading