【问题标题】:Django: Signal/Method called after "AppConfig.ready()"Django:在“AppConfig.ready()”之后调用的信号/方法
【发布时间】:2018-08-17 09:37:45
【问题描述】:

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

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

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

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

例子:

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

“app_b”处理完AppConfig.ready()后,“app_a”如何接收信号(或方法调用)?

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

【问题讨论】:

  • Django 处理应用程序in the order of INSTALLED_APPS,因此只要您正确订购应用程序,您就应该能够将代码放入ready() 方法中。
  • @Alasdair 谢谢你的提示。我更新了问题。

标签: django django-signals django-apps


【解决方案1】:

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

这里有一些想法:

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

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

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

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

【讨论】:

    【解决方案2】:

    所以有一种非常骇人听闻的方式来完成你可能想要的......

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

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

    Apps 类的精简源代码是单例 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 的上下文中将允许您以某种理想的顺序链接配置。您可以使用以下 sn-p 查看如何完成此操作:

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

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

    【讨论】:

    • (Ab)使用锁很聪明!我想知道在 Bar 中生成一个等待 app._lock 被释放的线程是否还不够。另外,我对问题的理解是无法修改正在等待的应用程序。如果这些应用程序可以一起工作,那么可能有一个更简单的解决方案。也许@guettli 可以澄清一下。
    • @DanielHepper 哈哈“滥用”绝对是正确的词!如果你说的是真的,被“等待”的应用程序无法修改,这肯定不是直接的解决方案。我认为,正如你所说,等待锁定可能就足够了。
    • 是的,你是对的。这是非常骇人听闻的。不过,谢谢您的回答。
    • 它们不必修改。第三方 AppConfigs 可以被覆盖,尽管这可能意味着一些重复的代码(或一些 hacky 方法复制:你可以将超级调用设置为线程的目标吗?)。
    【解决方案3】:

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

    将此应用放在INSTALLED_APPS的末尾。

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

    【讨论】:

      【解决方案4】:

      另一种解决方案:

      继承 AppConfig 并在ready 的末尾发送一个信号。在您的所有应用程序中使用此子类。如果您依赖于正在加载的一个,请连接到该信号/发送器对。

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

      这种方法有一些微妙之处:

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

      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

      【讨论】:

      • 我更新了问题并添加了一个示例。你认为你的问题仍然有效吗?
      • @guettli 我添加了更多细节。它比我最初想的要复杂一些,但我怀疑它应该基于 Django 文档工作。诀窍是把所有东西都放在正确的地方;)
      猜你喜欢
      • 2020-08-02
      • 2016-11-20
      • 1970-01-01
      • 2016-02-22
      • 1970-01-01
      • 1970-01-01
      • 2021-12-17
      • 1970-01-01
      • 2020-04-01
      相关资源
      最近更新 更多