【问题标题】:How to avoid AppConfig.ready() method running twice in Django如何避免 AppConfig.ready() 方法在 Django 中运行两次
【发布时间】:2016-02-22 05:29:04
【问题描述】:

我想在 Django 服务器启动时执行一些代码,但我希望它只运行一次。目前,当我启动服务器时,它执行了两次。 Documentation 表示这可能会发生,并且:

您应该在 AppConfig 类上放置一个标志以防止重新运行 应该只执行一次的代码。

知道如何实现这一目标吗?下面的打印语句仍然执行了两次。

from django.apps import AppConfig
import app.mqtt
from apscheduler.schedulers.background import BackgroundScheduler

class MyAppConfig(AppConfig):
    name = 'app'
    verbose_name = "HomeIoT"
    run_already = False

    def ready(self):
        if MyAppConfig.run_already: return
        MyAppConfig.run_already = True
        print("Hello")

【问题讨论】:

  • 缩进正确吗? ready 应该在 MyAppConfig 类中。
  • 你是对的。这只是复制/粘贴错误。我修好了。
  • 如果使用--noreload运行,是否会发生双重打印?
  • 不错。谢谢!用--noreload 启动服务器实际上解决了这个问题。但是,即使没有这个,是否可以避免多次运行 ready() ?我的理解是,运行任何 manage.py 命令时,ready() 仍然会被执行。
  • 如果你不使用runserver - 就像在生产中...... - 它不起作用。 --noreload 做了什么来实现这一点?

标签: python django


【解决方案1】:

当你使用python manage.py runserver Django 启动两个进程,一个用于实际的开发服务器,另一个用于在代码更改时重新加载你的应用程序。

你也可以不带reload选项启动服务器,你会看到只有一个进程在运行,只会执行一次:

python manage.py runserver --noreload

You can see this link, it resolves the ready() method running twice in Django

【讨论】:

    【解决方案2】:

    如果您不想使用--noreload,您可以:

    替换您应用的__init__.py 中用于指定配置的行:

    default_app_config = 'mydjangoapp.apps.MydjangoappConfig'
    

    通过这个:

    import os
    
    if os.environ.get('RUN_MAIN', None) != 'true':
        default_app_config = 'mydjangoapp.apps.MydjangoappConfig'
    

    【讨论】:

    • 您也可以只检查ready() 方法本身中的RUN_MAIN env var。
    • 检查ready() 中的RUN_MAIN 环境变量所以比处理文件锁或线程锁要简单得多。谢谢!
    • 我确实像@alexdlaird 所说的那样:在 ready 方法中检查 RUN_MAIN,它就像一个魅力。我认为这个答案应该是一个很好的答案,因为 --noreload 适用于 runserver,但不是例如当您执行 python manage.py collectstatic 时:没有 RUN_MAIN 检查保护,您的代码将被执行,您可能不想要。
    【解决方案3】:

    您需要实现锁定。这不是一个简单的问题,当您处理进程和线程时,解决方案会感觉不自然。请注意,锁定问题有很多答案,一些更简单的方法:

    文件锁:Ensure a single instance of an application in Linux(请注意,线程默认共享文件锁,因此需要扩展此答案以考虑线程)。

    还有这个答案,它使用了一个名为tendo 的 Python 包,它封装了文件锁实现:https://stackoverflow.com/a/1265445/181907

    Django 本身在django.core.files.locks 中提供了一个抽象的可移植文件锁定实用程序。

    【讨论】:

      【解决方案4】:

      正如 Roberto 提到的,如果您想使用默认的 auto_reload 功能,则需要在通过 runserver 命令运行服务器时实现锁定。

      Django 通过线程实现它的 auto_reload,因此在两个单独的线程中导入 AppConfig,主“命令/监视”线程和运行服务器的“重新加载”线程。将打印语句添加到模块中,您将看到它的实际效果。 “主”线程加载 AppConfig 文件作为其 BaseCommand 执行的一部分,然后“重新加载”线程在服务器启动期间再次加载它们。

      如果您的代码不能在这两个线程中运行,那么您的选择就会受到一定限制。您可以实现线程锁,以便“重新加载”线程不会运行 ready();您可以转移到生产环境来运行您的服务器(例如,Gunicorn 的设置速度非常快,即使用于测试也是如此);或者您可以以其他方式调用您的方法,而不是使用 ready()。

      我建议转移到一个合适的环境,但最好的选择实际上取决于你调用的方法到底应该做什么。

      【讨论】:

      • 您在这里将线程与进程混淆了。如最佳答案中所述,Django 启动两个进程,而不是用于自动重新加载的线程。
      • 感谢指正。我会指出,虽然你对 Django 没有启动两个线程是不正确的。至少在 2016 年,Djano 还在子服务器进程中生成了一个 reload_thread,该进程在检测到更改时处理终止进程。当子进程以退出代码 3 死亡时,主进程只是简单地重新生成了子进程,表示它应该被重新加载。因此我当时很困惑。
      【解决方案5】:

      我发现这对我有用没有python manage.py runserver 中使用--noreload 标志。

      检查ready() 方法中的环境变量。 env 变量在应用程序结束后不会持续存在,但如果服务器检测到代码更改并且在它自动重新加载自身后会持续存在。

      # File located in mysite/apps.py
      
      from django.apps import AppConfig
      import os
      
      class CommandLineRunner(AppConfig):
          name = 'mysite'
      
          def ready(self):
              run_once = os.environ.get('CMDLINERUNNER_RUN_ONCE') 
              if run_once is not None:
                  return
              os.environ['CMDLINERUNNER_RUN_ONCE'] = 'True' 
      
              # The code you want to run ONCE here
        
      

      【讨论】:

      • 如果函数被不同的线程调用两次,是否存在竞争条件的风险?是否有可能在环境中定义变量,以便代码可能永远不会运行?您可能可以通过收缩环境来避免这种情况,但我仍然会担心比赛条件。
      • @joanis 好点!我已经修改了代码,以便在 env var 检查之后立即设置它(而不是在命令运行后设置它)。希望这将减少或消除任何潜在的竞争条件!
      【解决方案6】:

      发现AppConfig被触发了两次,导致调度器用这个配置启动了两次。相反,在 urls.py 中实例化调度程序,如下所示:

      urlpatterns = [
          path('api/v1/', include(router.urls)),
          path('api/v1/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
          path('api/v1/login/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
          path('api/v1/', include('rest_registration.api.urls'))
      ]
      
      scheduler = BackgroundScheduler()
      scheduler.add_job(task.run, trigger='cron', hour=settings.TASK_RUNNER_HOURS, minute=settings.TASK_RUNNER_MINUTES, max_instances=1)
      scheduler.start()
      

      【讨论】:

        猜你喜欢
        • 2017-09-20
        • 2022-11-23
        • 1970-01-01
        • 1970-01-01
        • 2016-11-20
        • 2020-11-27
        • 2013-06-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多