question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Error] select_for_update happening when using replica(read-only) and default(write-only) DB.

See original GitHub issue

I’ve been getting a select_for_update cannot be used outside of a transaction error when using a replica set in my applications.

Here are my settings

DATABASES = {
    "default": {
        "ENGINE": os.getenv("DB_ENGINE"),
        "NAME": os.getenv("DB_NAME"),
        "USER": os.environ.get("DB_USER"),
        "HOST": os.environ.get("DB_HOST"),
        "PORT": os.environ.get("DB_PORT"),
        "PASSWORD": os.environ.get("DB_PASSWORD"),
    },
    "replica": {
        "ENGINE": os.getenv("DB_ENGINE"),
        "NAME": os.getenv("DB_NAME_REPLICA"),
        "USER": os.environ.get("DB_USER_REPLICA"),
        "HOST": os.environ.get("DB_HOST_REPLICA"),
        "PORT": os.environ.get("DB_PORT_REPLICA"),
        "PASSWORD": os.environ.get("DB_PASSWORD_REPLICA"),
    }
}

Q_CLUSTER = {
    "name": "myscheduler",
    "orm": "default",  # Use Django's ORM + database for broker
    ....
}

My database router currently uses the replica only to read and the default just to write.

class DatabaseRouter:

    def db_for_read(self, model, **hints):
        """Always read from REPLICA database"""
        return "replica"

    def db_for_write(self, model, **hints):
        """Always write to DEFAULT database"""
        return "default"
        
    def allow_relation(self, obj1, obj2, **hints):
        """Objects from REPLICA and DEFAULT are de same, then True always"""
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """Only DEFAULT database"""
        return db == "default"

I’ve been digging through the code (awesome documenation btw!) and found that during the task creation in the scheduler function it forces the database used in the transaction block.

# Here it seems to force the usage, in this case it will be the replica database.
with db.transaction.atomic(using=Schedule.objects.db):  
            for s in (
                Schedule.objects.select_for_update()
                .exclude(repeats=0)
                .filter(next_run__lt=timezone.now())
                .filter(db.models.Q(cluster__isnull=True) | db.models.Q(cluster=Conf.PREFIX))
            ):

Is there a reason for this behaviour? I couldn’t really understand why, since when removing the using from the transaction block made it work like a charm, reading only from replica and writing only on default.

Dependencies

  • python = 3.9.5
  • Django = 3.1.7
  • psycopg2-binary = "2.8.6
  • django-q = 1.3.6

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:5
  • Comments:9 (7 by maintainers)

github_iconTop GitHub Comments

4reactions
Lizardscommented, May 19, 2021

We are also experiencing this issue. As a temporary workaround, we modified our database router to always use the write db for Django-Q’s Schedule model:

class PrimaryReplicaRouter(object):
    def db_for_read(self, model, **hints):
        if model._meta.app_label == "django_q" and model.__name__ == "Schedule":
            return "default"
        return "replica"
2reactions
abxsantoscommented, May 20, 2021

Although modifying the router works, I would suggest adding a has_replica boolean attribute in the Django Q settings.

In the conf.py, add a HAS_REPLICA attribute.

# conf.py

class Conf:
    """
    Configuration class
    """
    # other settings...
    # Support for read/write replicas
    HAS_REPLICA = conf.get("has_replica", False)

And the scheduler function in the cluster.py will now look like this:

# cluster.py
def scheduler(broker: Broker = None):
    """
    Creates a task from a schedule at the scheduled time and schedules next run
    """
    if not broker:
        broker = get_broker()
    close_old_django_connections()
    try:
        # addition of database_to_use
        database_to_use = {"using": Schedule.objects.db} if not Conf.HAS_REPLICA else {}
        with db.transaction.atomic(**database_to_use):
            for s in (
                Schedule.objects.select_for_update()
                .exclude(repeats=0)
                .filter(next_run__lt=timezone.now())
            ):

When we use read/write replicas we don’t specify a database to use in the transaction. The transaction will be made without any problems, since the router will correctly use the write database when a write operation is made (in this case the select_for_update will always be made using the write database, which must be configured in the orm setting). All the other read operations will be made using the read database, just as intended.

The scenarios where some apps needs to write into one database and other apps into another, like it was suggested in a previous modification, would also works correctly when applying the with db.transaction.atomic(using=Schedule.objects.db)

That way the current usage would not break and it would allow the usage of read only replicas via a setting in the configuration.

What do you all think? Does it make sense to add the has_replica in the Django Q settings?

If it does can I open a PR suggesting these additions and tests covering some scenarios around the multiple databases?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Database cannot be upgraded because it is read-only or has ...
When I try to attach it, it throws an error message with "attach database failed". Closing SSMS and reopening it as an Administrator...
Read more >
Strange MySQL "read-only" error - database - Stack Overflow
If you're in AWS Aurora, you might be accessing the replica instance which is read-only so you need to use the DB Cluster...
Read more >
How to change Postgresql database from Read-only to Writable
Since SELECT pg_is_in_recovery() is true you're connected to a read-only replica server in hot_standby mode. The replica configuration is in ...
Read more >
MySQL Replication
more MySQL database servers (known as replicas). Replication is asynchronous by default; replicas do not need to be connected permanently to receive updates ......
Read more >
SQL SERVER - Error: Fix for Error Msg 3906 - Failed to update ...
Let us mark the just created database as ReadOnly. USE MASTER GO ALTER DATABASE [ReaOnlyDB] SET READ_ONLY GO.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found