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

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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 backend_entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cmd_init() {

wait-for-it "${CVAT_REDIS_INMEM_HOST}:${CVAT_REDIS_INMEM_PORT:-6379}" -t 0
~/manage.py syncperiodicjobs
~/manage.py migrateredis
}

cmd_run() {
Expand Down
50 changes: 50 additions & 0 deletions cvat/apps/engine/management/commands/migrateredis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright (C) 2025 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

import importlib.util as importlib_util
import sys
from pathlib import Path
from typing import cast

from django.conf import settings
from django.core.management.base import BaseCommand

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


def get_migration_class(module_name: str, file_path: Path) -> BaseMigration:
spec = importlib_util.spec_from_file_location(module_name, file_path)
module = importlib_util.module_from_spec(spec)
spec.loader.exec_module(module)
MigrationClass = getattr(module, "Migration", None)
if not MigrationClass or not issubclass(MigrationClass, BaseMigration):
raise Exception(f"Invalid migration: {file_path}")

return MigrationClass


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

def handle(self, *args, **options) -> None:
migrations_dir = Path(settings.REDIS_MIGRATIONS_ROOT)
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems a bit inflexible, because it only allows for one migration root. What will we do if we need to add migrations to the Enterprise repository, for example?

The database migration functionality in Django loads migrations from each app; perhaps we could do the same here?

applied_migrations = RedisMigration.objects.all().values_list("name")

for migration_file in sorted(migrations_dir.glob("[0-9]*.py")):
migration_name = migration_file.stem
if migration_name in applied_migrations:
continue

migration = get_migration_class(module_name=migration_name, file_path=migration_file)
try:
migration.run()
Copy link
Contributor

Choose a reason for hiding this comment

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

For reviewing purposes, it would be useful to see an example of a migration class.

RedisMigration.objects.create(name=migration_name)
self.stdout.write(
self.style.SUCCESS(f"Successfully applied migration: {migration_name}")
)
except Exception as e:
self.stderr.write(self.style.ERROR(f"Failed to apply migration: {migration_name}"))
self.stderr.write(str(e))
Copy link
Contributor

Choose a reason for hiding this comment

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

A full traceback would be useful here, and not just the message.

break
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't an exception cause the command to exit with a failure status? Right now it seems to be exiting successfully.

26 changes: 26 additions & 0 deletions cvat/apps/engine/migrations/0087_redismigration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.16 on 2025-01-03 17:08

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("engine", "0086_profile_has_analytics_access"),
]

operations = [
migrations.CreateModel(
name="RedisMigration",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.CharField(max_length=256)),
("applied_date", models.DateTimeField(auto_now_add=True)),
],
),
]
6 changes: 6 additions & 0 deletions cvat/apps/engine/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1345,3 +1345,9 @@ class RequestSubresource(TextChoices):
ANNOTATIONS = "annotations"
DATASET = "dataset"
BACKUP = "backup"


class RedisMigration(models.Model):
# todo: redis_inmem/redis_ondisk
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean by this?

name = models.CharField(max_length=256)
applied_date = models.DateTimeField(auto_now_add=True)
8 changes: 8 additions & 0 deletions cvat/apps/engine/redis_migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from abc import ABCMeta, abstractmethod


class BaseMigration(metaclass=ABCMeta):

@staticmethod
@abstractmethod
def run() -> None: ...
2 changes: 2 additions & 0 deletions cvat/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,8 @@ class CVAT_QUEUES(Enum):
# Make sure to update other config files when updating these directories
DATA_ROOT = os.path.join(BASE_DIR, 'data')

REDIS_MIGRATIONS_ROOT = os.path.join(BASE_DIR, 'cvat', 'apps', 'engine', 'redis_migrations')

MEDIA_DATA_ROOT = os.path.join(DATA_ROOT, 'data')
os.makedirs(MEDIA_DATA_ROOT, exist_ok=True)

Expand Down
2 changes: 2 additions & 0 deletions dev/format_python_code.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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/migrateredis.py" \
"cvat/apps/engine/redis_migrations/*.py" \
; do
${BLACK} -- ${paths}
${ISORT} -- ${paths}
Expand Down
Loading