Skip to content
Snippets Groups Projects
load_test_data.py 11 KiB
Newer Older
Eliot Berriot's avatar
Eliot Berriot committed
import math
import random

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


from funkwhale_api.federation import keys
from funkwhale_api.federation import models as federation_models
from funkwhale_api.music import models as music_models
from funkwhale_api.tags import models as tags_models
from funkwhale_api.users import models as users_models


BATCH_SIZE = 500


def create_local_accounts(factories, count, dependencies):
    password = factories["users.User"].build().password
    users = factories["users.User"].build_batch(size=count)
    for user in users:
        # we set the hashed password by hand, because computing one for each user
        # is CPU intensive
        user.password = password
    users = users_models.User.objects.bulk_create(users, batch_size=BATCH_SIZE)
    actors = []
    domain = federation_models.Domain.objects.get_or_create(
        name=settings.FEDERATION_HOSTNAME
    )[0]
    users = [u for u in users if u.pk]
    private, public = keys.get_key_pair()
    for user in users:
        if not user.pk:
            continue
        actor = federation_models.Actor(
            private_key=private.decode("utf-8"),
            public_key=public.decode("utf-8"),
            **users_models.get_actor_data(user.username, domain=domain)
        )
        actors.append(actor)
    actors = federation_models.Actor.objects.bulk_create(actors, batch_size=BATCH_SIZE)
    for user, actor in zip(users, actors):
        user.actor = actor
    users_models.User.objects.bulk_update(users, ["actor"])
    return actors


def create_taggable_items(dependency):
    def inner(factories, count, dependencies):
        objs = []
        tagged_objects = dependencies.get(
            dependency, list(CONFIG_BY_ID[dependency]["model"].objects.all().only("pk"))
        )
        tags = dependencies.get("tags", list(tags_models.Tag.objects.all().only("pk")))
        for i in range(count):
            tag = random.choice(tags)
            tagged_object = random.choice(tagged_objects)
            objs.append(
                factories["tags.TaggedItem"].build(
                    content_object=tagged_object, tag=tag
                )
            )

        return tags_models.TaggedItem.objects.bulk_create(
            objs, batch_size=BATCH_SIZE, ignore_conflicts=True
        )
Eliot Berriot's avatar
Eliot Berriot committed


CONFIG = [
    {
        "id": "tracks",
        "model": music_models.Track,
        "factory": "music.Track",
        "factory_kwargs": {"artist": None, "album": None},
        "depends_on": [
            {"field": "album", "id": "albums", "default_factor": 0.1},
            {"field": "artist", "id": "artists", "default_factor": 0.05},
        ],
    },
    {
        "id": "albums",
        "model": music_models.Album,
        "factory": "music.Album",
        "factory_kwargs": {"artist": None},
        "depends_on": [{"field": "artist", "id": "artists", "default_factor": 0.3}],
    },
    {"id": "artists", "model": music_models.Artist, "factory": "music.Artist"},
    {
        "id": "local_accounts",
        "model": federation_models.Actor,
        "handler": create_local_accounts,
    },
    {
        "id": "local_libraries",
        "model": music_models.Library,
        "factory": "music.Library",
        "factory_kwargs": {"actor": None},
        "depends_on": [{"field": "actor", "id": "local_accounts", "default_factor": 1}],
    },
    {
        "id": "local_uploads",
        "model": music_models.Upload,
        "factory": "music.Upload",
        "factory_kwargs": {"import_status": "finished", "library": None, "track": None},
        "depends_on": [
            {
                "field": "library",
                "id": "local_libraries",
                "default_factor": 0.05,
                "queryset": music_models.Library.objects.all().select_related(
                    "actor__user"
                ),
            },
            {"field": "track", "id": "tracks", "default_factor": 1},
        ],
    },
    {"id": "tags", "model": tags_models.Tag, "factory": "tags.Tag"},
    {
        "id": "track_tags",
        "model": tags_models.TaggedItem,
        "queryset": tags_models.TaggedItem.objects.filter(
            content_type__app_label="music", content_type__model="track"
        ),
        "handler": create_taggable_items("tracks"),
Eliot Berriot's avatar
Eliot Berriot committed
        "depends_on": [
            {
                "field": "tag",
                "id": "tags",
                "default_factor": 0.1,
                "queryset": tags_models.Tag.objects.all(),
                "set": False,
            },
            {
                "field": "content_object",
                "id": "tracks",
                "default_factor": 1,
                "set": False,
            },
        ],
    },
    {
        "id": "album_tags",
        "model": tags_models.TaggedItem,
        "queryset": tags_models.TaggedItem.objects.filter(
            content_type__app_label="music", content_type__model="album"
        ),
        "handler": create_taggable_items("albums"),
        "depends_on": [
            {
                "field": "tag",
                "id": "tags",
                "default_factor": 0.1,
                "queryset": tags_models.Tag.objects.all(),
                "set": False,
            },
            {
                "field": "content_object",
                "id": "albums",
                "default_factor": 1,
                "set": False,
            },
        ],
    },
    {
        "id": "artist_tags",
        "model": tags_models.TaggedItem,
        "queryset": tags_models.TaggedItem.objects.filter(
            content_type__app_label="music", content_type__model="artist"
        ),
        "handler": create_taggable_items("artists"),
        "depends_on": [
            {
                "field": "tag",
                "id": "tags",
                "default_factor": 0.1,
                "queryset": tags_models.Tag.objects.all(),
                "set": False,
            },
            {
                "field": "content_object",
                "id": "artists",
                "default_factor": 1,
                "set": False,
            },
        ],
    },
Eliot Berriot's avatar
Eliot Berriot committed
]

CONFIG_BY_ID = {c["id"]: c for c in CONFIG}


class Rollback(Exception):
    pass


def create_objects(row, factories, count, **factory_kwargs):
    return factories[row["factory"]].build_batch(size=count, **factory_kwargs)


class Command(BaseCommand):
    help = """
    Inject demo data into your database. Useful for load testing, or setting up a demo instance.

    Use with caution and only if you know what you are doing.
    """

    def add_arguments(self, parser):
        parser.add_argument(
            "--no-dry-run",
            action="store_false",
            dest="dry_run",
            help="Commit the changes to the database",
        )
        parser.add_argument(
            "--create-dependencies", action="store_true", dest="create_dependencies"
        )
        for row in CONFIG:
            parser.add_argument(
                "--{}".format(row["id"].replace("_", "-")),
                dest=row["id"],
                type=int,
                help="Number of {} objects to create".format(row["id"]),
            )
            dependencies = row.get("depends_on", [])
            for dependency in dependencies:
                parser.add_argument(
                    "--{}-{}-factor".format(row["id"], dependency["field"]),
                    dest="{}_{}_factor".format(row["id"], dependency["field"]),
                    type=float,
                    help="Number of {} objects to create per {} object".format(
                        dependency["id"], row["id"]
                    ),
                )

    def handle(self, *args, **options):
        from django.apps import apps
        from funkwhale_api import factories

        app_names = [app.name for app in apps.app_configs.values()]
        factories.registry.autodiscover(app_names)
        try:
            return self.inner_handle(*args, **options)
        except Rollback:
            pass

    @transaction.atomic
    def inner_handle(self, *args, **options):
        results = {}
        for row in CONFIG:
            self.create_batch(row, results, options, count=options.get(row["id"]))

        self.stdout.write("\nFinal state of database:\n\n")
        for row in CONFIG:
            qs = row.get("queryset", row["model"].objects.all())
            total = qs.count()
Eliot Berriot's avatar
Eliot Berriot committed
            self.stdout.write("- {} {} objects".format(total, row["id"]))

        self.stdout.write("")
        if options["dry_run"]:

            self.stdout.write(
                "Run this command with --no-dry-run to commit the changes to the database"
            )
            raise Rollback()

        self.stdout.write(self.style.SUCCESS("Done!"))

    def create_batch(self, row, results, options, count):
        from funkwhale_api import factories

        if row["id"] in results:
            # already generated
            return results[row["id"]]
        if not count:
            return []
        dependencies = row.get("depends_on", [])
        create_dependencies = options.get("create_dependencies")
        for dependency in dependencies:
            dep_count = options.get(dependency["id"])
            if not create_dependencies and dep_count is None:
                continue
Eliot Berriot's avatar
Eliot Berriot committed
            if dep_count is None:
                factor = options[
                    "{}_{}_factor".format(row["id"], dependency["field"])
                ] or dependency.get("default_factor")
                dep_count = math.ceil(factor * count)

            results[dependency["id"]] = self.create_batch(
Eliot Berriot's avatar
Eliot Berriot committed
                CONFIG_BY_ID[dependency["id"]], results, options, count=dep_count
            )
        self.stdout.write("Creating {} {}…".format(count, row["id"]))
        handler = row.get("handler")
        if handler:
            objects = handler(factories.registry, count, dependencies=results)
Eliot Berriot's avatar
Eliot Berriot committed
        else:
            objects = create_objects(
                row, factories.registry, count, **row.get("factory_kwargs", {})
            )
        for dependency in dependencies:
            if not dependency.get("set", True):
                continue
            if create_dependencies:
                candidates = results[dependency["id"]]
Eliot Berriot's avatar
Eliot Berriot committed
            else:
                # we use existing objects in the database
                queryset = dependency.get(
                    "queryset", CONFIG_BY_ID[dependency["id"]]["model"].objects.all()
                )
                candidates = list(queryset.values_list("pk", flat=True))
                picked_pks = [random.choice(candidates) for _ in objects]
                picked_objects = {o.pk: o for o in queryset.filter(pk__in=picked_pks)}
            for i, obj in enumerate(objects):
                if create_dependencies:
                    value = random.choice(candidates)
                else:
                    value = picked_objects[picked_pks[i]]
                setattr(obj, dependency["field"], value)
        if not handler:
            objects = row["model"].objects.bulk_create(objects, batch_size=BATCH_SIZE)
        results[row["id"]] = objects
        return objects