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

Migration 007 relation "axes_accessattempt" does not exist #949

Closed
js-truework opened this issue Nov 21, 2022 · 8 comments
Closed

Migration 007 relation "axes_accessattempt" does not exist #949

js-truework opened this issue Nov 21, 2022 · 8 comments

Comments

@js-truework
Copy link

Python version: 3.10.8
Django Version: 4.0.8
Django-axes version: 5.40.0

Scenario: When upgrading from django-axes 5.39.0 to django-axes 5.40.0, with the django version of 4.0.8 remaining constant, we're encountering this exception below when trying to run all migrations in order on a fresh database. Reverting to django-axes 5.39.0 causes this error to disappear.

I've previously searched for errors similar to this and found that this one might be tangentially related, but it also seems to predate the 5.40.0 version release, and may be specific to django 4.1, which is not the case here.

Snippet of exception below:

File "/.venv/lib/python3.10/site-packages/axes/migrations/0007_alter_accessattempt_unique_together.py", line 18, in deduplicate_attempts
    for attempt in duplicated_attempts:
  File "/.venv/lib/python3.10/site-packages/django/db/models/query.py", line 320, in __iter__
    self._fetch_all()
  File "/.venv/lib/python3.10/site-packages/django/db/models/query.py", line 1507, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/.venv/lib/python3.10/site-packages/django/db/models/query.py", line 130, in __iter__
    for row in compiler.results_iter(
  File "/.venv/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1312, in results_iter
    results = self.execute_sql(
  File "/.venv/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1361, in execute_sql
    cursor.execute(sql, params)
  File "/.venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
  File "/.venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/.venv/lib/python3.10/site-packages/django_read_only/__init__.py", line 77, in blocker
    return execute(sql, params, many, context)
  File "/.venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "/.venv/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/.venv/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
django.db.utils.ProgrammingError: relation "axes_accessattempt" does not exist
LINE 1: ...NT("axes_accessattempt"."id") AS "id__count" FROM "axes_acce...
@aleksihakli
Copy link
Member

Could #932 be the cause for this?

@js-truework
Copy link
Author

That did seem like the most likely culprit, with my minimal knowledge. I also didn't specify here but we do have several different databases configured in our application, which might make a difference here?

@aleksihakli
Copy link
Member

Could you post your database configuration for reference?

@js-truework
Copy link
Author

I've scrubbed out the way we populate the values for the params, and changed the names that we use to reference the DB. Otherwise this captures it. A Database enum that we use to ensure that all database references are one of the configured ones, and mostly just a series of databases / replicas.

I haven't sought to do a minimal repro yet, but if I had to guess I'd guess either (1) using the enum values as the DATABASES keys, or (2) simply having more than one database configured is what is causing the issue. I'd be kinda surprised if it was the latter, simply because I feel like more people would probably also encounter the issue. But who knows :)

CONN_MAX_AGE = 60
CONNECT_TIMEOUT = 3

class Database(Enum):
    default = "default"
    default_replica = "default_replica"
    two = "two"
    three = "three"
    three_replica = "three_replica"
    four = "four"

DATABASES: dict[str, dict[str, Any]] = {
    Database.default.value: {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "",
        "USER": "",
        "PASSWORD": "",
        "HOST": "",
        "PORT": "",
        "TEST": {},
        "OPTIONS": {
            "sslmode": "",
            "connect_timeout": CONNECT_TIMEOUT,
        },
        "CONN_MAX_AGE": CONN_MAX_AGE,
    },
    Database.default_replica.value: {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "",
        "USER": "",
        "PASSWORD": "",
        "HOST": "",
        "PORT": "",
        "TEST": {
            "MIRROR": Database.default.value,
        },
        "OPTIONS": {
            "sslmode": "",
            "connect_timeout": CONNECT_TIMEOUT,
        },
        "CONN_MAX_AGE": CONN_MAX_AGE,
    },
    Database.two.value: {
        "ENGINE": "django.db.backends.postgresql",
        "NAME":  "",
        "USER":  "",
        "PASSWORD": "" ,
        "HOST": "",
        "PORT": "",
        "TEST": {
            "DEPENDENCIES": ["default"],
        },
        "OPTIONS": {
            "sslmode": "",
            "connect_timeout": CONNECT_TIMEOUT,
        },
        "CONN_MAX_AGE": CONN_MAX_AGE,
    },
    Database.three.value: {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "",
        "USER": "",
        "PASSWORD": "",
        "HOST": "",
        "PORT": "",
        "TEST": {
            "DEPENDENCIES": ["default"],
        },
        "OPTIONS": {
            "sslmode": "",
            "connect_timeout": CONNECT_TIMEOUT,
        },
        "CONN_MAX_AGE": CONN_MAX_AGE,
    },
    Database.three_replica.value: {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "",
        "USER": "",
        "PASSWORD": "",
        "HOST": "",
        "PORT": "",
        "TEST": {
            "MIRROR": Database.three.value,
        },
        "OPTIONS": {
            "sslmode": "",
            "connect_timeout": CONNECT_TIMEOUT,
        },
    },
    Database.four.value: {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "",
        "USER": "",
        "PASSWORD": "",
        "HOST": "",
        "PORT": "",
        "TEST": {
            "DEPENDENCIES": ["default"],
        },
        "OPTIONS": {
            "sslmode": "",
            "connect_timeout": CONNECT_TIMEOUT,
        },
        "CONN_MAX_AGE": CONN_MAX_AGE,
    },
}

@liampauling
Copy link
Contributor

Certainly looks like #932 is the cause, how are you applying migrations to these databases?

@js-truework
Copy link
Author

We have a Django command that loops over the databases and runs the migrate command on each (./manage.py migrate_all). As far as I can understand it, we're applying the migrations the same way that ./manage.py migrate --database default would apply it.

from django.core.management.base import BaseCommand
from django.core.management.commands import migrate

def generate_default_args(
    subcommand: str, command_instance: BaseCommand
) -> tuple[list[Any], dict[str, Any]]:
    parser = command_instance.create_parser(sys.argv[0], subcommand)
    options = parser.parse_args([])
    kwargs = vars(options)
    args = kwargs.pop("args", ())
    return args, kwargs

class Command(BaseCommand):
    def handle(self, *args, **options):  # type: ignore[no-untyped-def]
        target_dbs = [
                Database.default,
                Database.two,
                Database.three,
                Database.four,
        ]
    
        args, base_kwargs = generate_default_args("migrate", migrate.Command())  # type: ignore[assignment]
        base_kwargs["interactive"] = False
    
        for database in target_dbs:
            self.stdout.write(f"Migrating {database.value}")
            kwargs = copy.deepcopy(base_kwargs)
            kwargs["database"] = database.value
            migrate.Command().execute(*args, **kwargs)

@aleksihakli
Copy link
Member

aleksihakli commented Dec 3, 2022

I'd suspect the code causes a condition that tries to apply the Axes migration on a mismatching database that doesn't have the Axes data in it. What happens if you apply migrations on only the default database (or the database axes models are defined in)?

I'm wondering if this is caused by a bug in the axes implementation or in the custom custom migration command.

We can not verify the case since we can not reproduce the issue with the current information; if you could produce a setup or scenario that reproduces it like it happens in your environment then we could help with the debugging.

@js-truework
Copy link
Author

Sorry I've gone silent on this for so long, work comes up and this got deprioritized. Upon revisiting it, I am wondering if perhaps the problem is that the way the alias is defined on the migration effectively says "run this migration on any database connection" and we're saying "Let's try to run all migrations against all databases" and our database router has a special meta field we use to indicate which database to a model is stored on.

My first inclination was that the migrations should apply to the default alias, but then if that were the case then the previous issue would just come back up, since it wouldn't be possible to apply it to another database if axes supports applying to a specific database (and I can't see why it wouldn't).

So maybe, then, our database router is actually at fault and none of the other django apps we've used have had an issue because they don't have any migrations, or their migrations aren't written to be multi-database aware (and as a result always just apply to the default, maybe?). An example would be https://github.com/django-otp/django-otp-twilio/tree/master/src/otp_twilio/migrations, which doesn't declare the alias on migrations and so we haven't noticed the problem before.

I guess what I'm unclear about, though, is how we would appropriately make sure that the django axes migrations run on the default alias, but not on the others, since the migration would try to apply on every alias passed to it. Apologies, as I'm not very well versed in how django does the database routing, migrations, etc. The django documentation on writing migrations with multiple databases seems to suggest things that would work for our migrations but would not work for third party migrations unless those third party migrations followed the same pattern 🤔

Another thing I'm a little unclear on is that none of the other django-axes migrations seem to make assertions about the connection alias, only 0007. So I guess, maybe a dumb question here, but how could you end up in a situation where you have all of these migrations that presumably apply to the default alias and then have this new one that you might apply to a different alias? Or am I misunderstanding how the migration itself works and you can actually install django-axes to any database, not just the default?

I guess a hacky way I can probably solve this in the short term is to add a check in our allow_migrate to look for django-axes and make sure we only run that on the default alias, as thats where we've run all the previous migrations before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants