Django:Signal/Method 在 "AppConfig.ready()" 之后调用

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

我有一个 AppConfig.ready() 实现,它取决于其他应用程序的准备情况。

是否有信号或方法(我可以实现)在调用所有应用程序 ready() 方法后被调用?

我知道django是按照INSTALLED_APPS的顺序处理信号的。

但我不想强制执行 INSTALLED_APPS 的特定顺序。

示例:

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

"app_a" 如何在 "app_b" 处理 AppConfig.ready() 后接收信号(或方法调用)?

(重新排序 INSTALLED_APPS 不是解决方案)

恐怕答案是否定的。填充应用程序注册表发生在 django.setup()。如果您查看源代码,您会发现 apps.registry.Apps.populate() nor django.setup() 都不会在完成时发送任何信号。

这里有一些想法:

  • 您可以自己发送自定义信号,但这需要您在 Django 项目的所有入口点执行此操作,例如manage.pywsgi.py 和任何使用 django.setup().

  • 的脚本
  • 您可以连接到 request_started 并在调用处理程序时断开连接。

  • 如果您正在初始化某种 属性,您可以将初始化推迟到第一次访问时。

这些方法中的任何一种是否适合您显然取决于您想要实现的目标。

所以有一个非常 hackish 的方式来完成你可能想要的...

django.apps.registry 内部是单例 apps,Django 使用它来填充应用程序。请参阅 django.__init__.py 中的 setup

apps.populate 的工作方式是使用不可重入(基于线程)的锁定机制,只允许 apps.populate 以幂等、线程安全的方式发生。

Apps class 的精简源,这是单例 apps 的实例化来源:

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

有了这些知识,您可以创建一些 threading.Thread 在某些条件下等待的内容。这些消费者线程将利用 threading.Condition 发送跨线程信号(这将强制执行您的排序问题)。这是一个如何工作的模拟示例:

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

同样,这只允许您控制来自个人 AppConfigready 呼叫的流程。这不控制模型加载的顺序等。

但是,如果您的第一个断言是正确的,那么您的 app.ready 实现依赖于另一个应用程序首先准备就绪,这应该可以解决问题。

推理:

为什么使用条件? 使用 threading.Condition 而不是 threading.Event 的原因有两个。首先,条件被包裹在一个锁定层中。这意味着如果需要(访问共享资源等),您将继续在受控环境下运营。其次,由于这种严格的控制级别,留在 threading.Condition 的上下文中将允许您以某种理想的顺序链接配置。您可以通过以下代码片段了解如何完成此操作:

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

为什么使用 Deamonic 线程? 这样做的原因是,如果您的 Django 应用程序在 apps.populate 中获取和释放锁之间的某个时间死机,后台线程将继续自旋等待锁释放。将它们设置为守护进程模式将允许进程干净地退出而无需 .join 这些线程。

另一种解决方案:

子类 AppConfig 并在 ready 结束时发送信号。在您所有的应用程序中使用这个子类。如果您依赖于正在加载的一个,请连接到 signal/sender 对。

如果您需要更多详细信息,请不要犹豫!

此方法有一些微妙之处:

1) 将信号定义放在哪里(我怀疑 manage.py 会起作用,或者你甚至可以猴子修补 django.setup 以确保它在任何地方都被调用)。您可以放入一个 core 应用程序,该应用程序始终是 installed_apps 中的第一个应用程序或 django 始终在加载任何 AppConfig 之前加载它的地方。

2) 在何处注册信号接收器(您应该可以在 AppConfig.__init__ 中或可能只是在该文件中全局执行此操作)。

https://docs.djangoproject.com/en/dev/ref/applications/#how-applications-are-loaded

因此,设置如下:

  • 当django第一次启动时,注册信号。
  • 在每个 app_config.ready 结束时发送信号(以 AppConfig 实例作为发送者)
  • 在需要响应信号的 AppConfigs 中,在 __init__ 中向适当的发送者注册一个接收者。

让我知道进展如何!

如果您需要它为第三方应用程序工作,请记住您可以覆盖这些应用程序的 AppConfigs(惯例是将它们放在名为 apps 的目录中)。或者,你可以猴子补丁 AppConfig

您可以添加一个虚拟应用程序,其唯一目的是触发自定义 all_apps_are_ready 信号(或 AppConfig 上的方法调用)。

将此应用放在 INSTALLED_APPS 的末尾。

如果此应用收到 AppConfig.ready() 方法调用,您就知道所有其他应用都已准备就绪。