Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: Signal/Method called after "AppConfig.ready()"

I have an AppConfig.ready() implementation which depends on the readiness of an other application.

Is there a signal or method (which I could implement) which gets called after all application ready() methods have been called?

I know that django processes the signals in the order of INSTALLED_APPS.

But I don't want to enforce a particular ordering of INSTALLED_APPS.

Example:

INSTALLED_APPS=[
   'app_a',
   'app_b',
   ...
]

How can "app_a" receive a signal (or method call) after "app_b" processed AppConfig.ready()?

(reordering INSTALLED_APPS is not a solution)

like image 212
guettli Avatar asked Aug 17 '18 09:08

guettli


People also ask

How do you use signals in Django?

There are 3 types of signal. pre_save/post_save: This signal works before/after the method save(). pre_delete/post_delete: This signal works before after delete a model's instance (method delete()) this signal is thrown.

What is pre save signal in Django?

Django Signals - pre_save()To execute some code dealing with another part of your application before the object gets saved to the database, you have to use a pre_save signal. That way, the signal is sent at the beginning of a model's save() method.

What is the use of the post delete signal in Django?

Whenever you delete any object/row from django user model, Django post delete method will automatically start after delete method is called.

Are Django signals asynchronous?

For the uninitiated, signals are synchronously (this will be important later) firing events that trigger handlers. There are a few common built in ones, we'll be discussing post_save and pre_save signals that are triggered whenever a Django model is saved.


3 Answers

I'm afraid the answer is No. Populating the application registry happens in django.setup(). If you look at the source code, you will see that neither apps.registry.Apps.populate() nor django.setup() dispatch any signals upon completion.

Here are some ideas:

  • You could dispatch a custom signal yourself, but that would require that you do that in all entry points of your Django project, e.g. manage.py, wsgi.py and any scripts that use django.setup().

  • You could connect to request_started and disconnect when your handler is called.

  • If you are initializing some kind of property, you could defer that initialization until the first access.

If any of these approaches work for you obviously depends on what exactly you are trying to achieve.

like image 170
Daniel Hepper Avatar answered Oct 23 '22 16:10

Daniel Hepper


So there is a VERY hackish way to accomplish what you might want...

Inside the django.apps.registry is the singleton apps which is used by Django to populate the applications. See setup in django.__init__.py.

The way that apps.populate works is it uses a non-reentrant (thread-based) locking mechanism to only allow apps.populate to happen in an idempotent, thread-safe manner.

The stripped down source for the Apps class which is what the singleton apps is instantiated from:

class Apps(object):

    def __init__(self, installed_apps=()):
        # Lock for thread-safe population.
        self._lock = threading.Lock()

    def populate(self, installed_apps=None):
        if self.ready:
            return

        with self._lock:
            if self.ready:
                return

            for app_config in self.get_app_configs():
                app_config.ready()

            self.ready = True

With this knowledge, you could create some threading.Thread's that await on some condition. These consumer threads will utilize threading.Condition to send cross-thread signals (which will enforce your ordering problem). Here is a mocked out example of how that would work:

import threading

from django.apps import apps, AppConfig

# here we are using the "apps._lock" to synchronize our threads, which
# is the dirty little trick that makes this work
foo_ready = threading.Condition(apps._lock)

class FooAppConfig(AppConfig):
    name = "foo"

    def ready(self):
        t = threading.Thread(name='Foo.ready', target=self._ready_foo, args=(foo_ready,))
        t.daemon = True
        t.start()

    def _ready_foo(self, foo_ready):
        with foo_ready:
            # setup foo
            foo_ready.notifyAll() # let everyone else waiting continue

class BarAppConfig(AppConfig):
    name = "bar"

    def ready(self):
        t = threading.Thread(name='Bar.ready', target=self._ready_bar, args=(foo_ready,))
        t.daemon = True
        t.start()

    def _ready_bar(self, foo_ready):
        with foo_ready:
            foo_ready.wait() # wait until foo is ready
            # setup bar

Again, this ONLY allows you to control the flow of the ready calls from the individual AppConfig's. This doesn't control the order models get loaded, etc.

But if your first assertion was true, you have an app.ready implementation that depends on another app being ready first, this should do the trick.

Reasoning:

Why Conditions? The reason this uses threading.Condition over threading.Event is two-fold. Firstly, conditions are wrapped in a locking layer. This means that you will continue to operate under controlled circumstances if the need arises (accessing shared resources, etc). Secondly, because of this tight level of control, staying inside the threading.Condition's context will allow you to chain the configurations in some desirable ordering. You can see how that might be done with the following snippet:

lock = threading.Lock()
foo_ready = threading.Condition(lock)
bar_ready = threading.Condition(lock)
baz_ready = threading.Condition(lock)

Why Deamonic Threads? The reason for this is, if your Django application were to die sometime between acquiring and releasing the lock in apps.populate, the background threads would continue to spin waiting for the lock to release. Setting them to daemon-mode will allow the process to exit cleanly without needing to .join those threads.

like image 2
Julian Avatar answered Oct 23 '22 17:10

Julian


You can add a dummy app which only purpose is to fire a custom all_apps_are_ready signal (or method call on AppConfig).

Put this app at the end of INSTALLED_APPS.

If this app receives the AppConfig.ready() method call, you know that all other apps are ready.

like image 1
guettli Avatar answered Oct 23 '22 17:10

guettli