简体   繁体   English

Django - 每 x 秒运行一次 function

[英]Django - run a function every x seconds

I'm working on a Django app.我正在开发 Django 应用程序。 I have an API endpoint, which if requested, must carry out a function that must be repeated a few times (until a certain condition is true).我有一个 API 端点,如果请求,它必须执行 function 必须重复几次(直到某个条件为真)。 How I'm dealing with it right now is -我现在的处理方式是——

def shut_down(request):
  # Do some stuff
  while True:
    result = some_fn()
    if result:
      break
    time.sleep(2)

  return True

While I know that this is a terrible approach and that I shouldn't be blocking for 2 seconds, I can't figure out how to get around it.虽然我知道这是一种糟糕的方法并且我不应该阻塞 2 秒,但我不知道如何绕过它。
This works, after say a wait of 4 seconds.这有效,等待 4 秒后。 But I'd like something that keeps the loop running in the background, and stop once some_fn returns True.但我想要一些让循环在后台运行的东西,并在 some_fn 返回 True 时停止。 (Also, it is certain that some_fn will return True) (还有,肯定some_fn会返回True)

EDIT -编辑 -
Reading Oz123's response gave me an idea which seems to work.阅读 Oz123 的回复给了我一个似乎可行的想法。 Here's what I did -这是我所做的 -

def shut_down(params):
    # Do some stuff
    # Offload the blocking job to a new thread

    t = threading.Thread(target=some_fn, args=(id, ), kwargs={})
    t.setDaemon(True)
    t.start()

    return True

def some_fn(id):
    while True:
        # Do the job, get result in res
        # If the job is done, return. Or sleep the thread for 2 seconds before trying again.

        if res:
            return
        else:
            time.sleep(2)

This does the job for me.这对我有用。 It's simple but I don't know how efficient multithreading is in conjunction with Django.很简单但是不知道多线程配合Django效率如何。
If anyone can point out pitfalls of this, criticism is appreciated.如果有人能指出其中的缺陷,不胜感激。

For many small projects celery is overkill.对于许多小项目,芹菜是矫枉过正的。 For those projects you can use schedule , it's very easy to use.对于那些可以使用schedule 的项目,它非常易于使用。

With this library you can make any function execute a task periodically:使用此库,您可以使任何函数定期执行任务:

import schedule
import time

def job():
    print("I'm working...")

schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)
schedule.every().monday.do(job)
schedule.every().wednesday.at("13:15").do(job)

while True:
    schedule.run_pending()
    time.sleep(1) 

The example runs in a blocking manner, but if you look in the FAQ, you will find that you can also run tasks in a parallel thread, such that you are not blocking, and remove the task once not needed anymore:该示例以阻塞方式运行,但是如果您查看常见问题解答,您会发现您也可以在并行线程中运行任务,这样您就不会阻塞,并删除不再需要的任务:

from schedule import Scheduler

def run_continuously(self, interval=1):
    """Continuously run, while executing pending jobs at each elapsed
    time interval.
    @return cease_continuous_run: threading.Event which can be set to
    cease continuous run.
    Please note that it is *intended behavior that run_continuously()
    does not run missed jobs*. For example, if you've registered a job
    that should run every minute and you set a continuous run interval
    of one hour then your job won't be run 60 times at each interval but
    only once.
    """

    cease_continuous_run = threading.Event()

    class ScheduleThread(threading.Thread):

        @classmethod
        def run(cls):
            while not cease_continuous_run.is_set():
                self.run_pending()
                time.sleep(interval)

    continuous_thread = ScheduleThread()
    continuous_thread.setDaemon(True)
    continuous_thread.start()
    return cease_continuous_run


Scheduler.run_continuously = run_continuously

Here is an example for usage in a class method:这是在类方法中使用的示例:

    def foo(self):
        ...
        if some_condition():
           return schedule.CancelJob  # a job can dequeue it

    # can be put in __enter__ or __init__
    self._job_stop = self.scheduler.run_continuously()

    logger.debug("doing foo"...)
    self.foo() # call foo
    self.scheduler.every(5).seconds.do(
        self.foo) # schedule foo for running every 5 seconds

    ...
    # later on foo is not needed any more:
    self._job_stop.set()

    ...

    def __exit__(self, exec_type, exc_value, traceback):
        # if the jobs are not stop, you can stop them
        self._job_stop.set()

This answer expands on Oz123's answer a little bit.这个答案稍微扩展了Oz123 的答案

In order to get things working, I created a file called mainapp/jobs.py to contain my scheduled jobs.为了让工作正常进行,我创建了一个名为mainapp/jobs.py的文件来包含我的预定作业。 Then, in my apps.py module, I put from . import jobs然后,在我的apps.py模块中,我from . import jobs from . import jobs in the ready method.ready方法中from . import jobs Here's my entire apps.py file:这是我的整个apps.py文件:

from django.apps import AppConfig
import os

class MainappConfig(AppConfig):
    name = 'mainapp'

    def ready(self):
        from . import jobs

        if os.environ.get('RUN_MAIN', None) != 'true':
            jobs.start_scheduler()

(The RUN_MAIN check is because python manage.py runserver runs the ready method twice —once in each of two processes—but we only want to run it once.) RUN_MAIN检查是因为python manage.py runserver运行ready方法两次——在两个进程中的每一个中运行一次——但我们只想运行一次。)

Now, here's what I put in my jobs.py file.现在,这是我放在我的jobs.py文件中的内容。 First, the imports.第一,进口。 You'll need to import Scheduler , threading and time as below.您需要导入Schedulerthreadingtime ,如下所示。 The F and UserHolding imports are just for what my job does; FUserHolding导入仅用于我的工作; you won't import these.你不会导入这些。

from django.db.models import F
from schedule import Scheduler
import threading
import time

from .models import UserHolding

Next, write the function you want to schedule.接下来,编写要调度的函数。 The following is purely an example;以下纯粹是一个例子; your function won't look anything like this.你的函数不会像这样。

def give_admin_gold():
    admin_gold_holding = (UserHolding.objects
        .filter(inventory__user__username='admin', commodity__name='gold'))

    admin_gold_holding.update(amount=F('amount') + 1)

Next, monkey-patch the schedule module by adding a run_continuously method to its Scheduler class.接下来,通过向Scheduler类添加run_continuously方法来修补schedule模块。 Do this by using the below code, which is copied verbatim from Oz123's answer.使用以下代码执行此操作,该代码是从 Oz123 的答案中逐字复制的。

def run_continuously(self, interval=1):
    """Continuously run, while executing pending jobs at each elapsed
    time interval.
    @return cease_continuous_run: threading.Event which can be set to
    cease continuous run.
    Please note that it is *intended behavior that run_continuously()
    does not run missed jobs*. For example, if you've registered a job
    that should run every minute and you set a continuous run interval
    of one hour then your job won't be run 60 times at each interval but
    only once.
    """

    cease_continuous_run = threading.Event()

    class ScheduleThread(threading.Thread):

        @classmethod
        def run(cls):
            while not cease_continuous_run.is_set():
                self.run_pending()
                time.sleep(interval)

    continuous_thread = ScheduleThread()
    continuous_thread.setDaemon(True)
    continuous_thread.start()
    return cease_continuous_run

Scheduler.run_continuously = run_continuously

Finally, define a function to create a Scheduler object, wire up your job, and call the scheduler's run_continuously method.最后,定义一个函数来创建Scheduler对象,连接您的作业,并调用调度程序的run_continuously方法。

def start_scheduler():
    scheduler = Scheduler()
    scheduler.every().second.do(give_admin_gold)
    scheduler.run_continuously()

I recommend you use Celery's task management.我推荐你使用 Celery 的任务管理。 You can refer this to set up this app ( package if you're from javaScript background ).你可以参考这个来设置这个应用程序(如果你来自 javaScript 背景,则包)。

Once set, you can alter the code to:设置后,您可以将代码更改为:

@app.task
def check_shut_down():
    if not some_fun():
        # add task that'll run again after 2 secs
        check_shut_down.delay((), countdown=3)
    else:
        # task completed; do something to notify yourself
        return True

I can't comment on oz123's ( https://stackoverflow.com/a/44897678/1108505 ) and Tanner Swett's ( https://stackoverflow.com/a/60244694/5378866 ) excellent post, but as a final note I wanted to add that if you use Gunicorn and you have X number of workers, the section:我无法评论 oz123( https://stackoverflow.com/a/44897678/1108505 )和 Tanner Swett( https://stackoverflow.com/a/60244694/5378866 )的优秀帖子,但作为最后一点我想要补充一点,如果您使用 Gunicorn 并且您有 X 个工人,则该部分:

from django.apps import AppConfig
import os

class MainappConfig(AppConfig):
    name = 'mainapp'

    def ready(self):
        from . import jobs

        if os.environ.get('RUN_MAIN', None) != 'true':
            jobs.start_scheduler()

will be executed that same number of times, launching X schedulers at the same time.将执行相同的次数,同时启动 X 个调度程序。

If we only want it to run only one instance (for example if you're going to create objects in the database), we would have to add in our gunicorn.conf.py file something like this:如果我们只希望它只运行一个实例(例如,如果您要在数据库中创建对象),我们必须在我们的 gunicorn.conf.py 文件中添加如下内容:

def on_starting(server):
    from app_project import jobs
    jobs.start_scheduler()

And finally in the gunicorn call add the argument --preload最后在 gunicorn 调用中添加参数 --preload

Here is my solution, with sources noted.这是我的解决方案,并注明了来源。 This function will allow you to create a scheduler that you can start with your app, then add and subtract jobs at will.这个 function 将允许您创建一个调度程序,您可以从您的应用程序开始,然后随意添加和删除作业。 The check_interval variable allows you to trade-off between system resources and job execution timing. check_interval 变量允许您在系统资源和作业执行时间之间进行权衡。

from schedule import Scheduler
import threading
import warnings
import time


class RepeatTimer(threading.Timer):
    """Add repeated run of target to timer functionality. Source: https://stackoverflow.com/a/48741004/16466191"""
    running: bool = False

    def __init__(self, *args, **kwargs):
        threading.Timer.__init__(self, *args, **kwargs)

    def start(self) -> None:
        """Protect from running start method multiple times"""
        if not self.running:
            super(RepeatTimer, self).start()
            self.running = True
        else:
            warnings.warn('Timer is already running, cannot be started again.')

    def cancel(self) -> None:
        """Protect from running stop method multiple times"""
        if self.running:
            super(RepeatTimer, self).cancel()
            self.running = False
        else:
            warnings.warn('Timer is already canceled, cannot be canceled again.')

    def run(self):
        """Replace run method of timer to run continuously"""
        while not self.finished.wait(self.interval):
            self.function(*self.args, **self.kwargs)


class ThreadedScheduler(Scheduler, RepeatTimer):
    """Non-blocking scheduler. Advice taken from: https://stackoverflow.com/a/50465583/16466191"""
    def __init__(
            self,
            run_pending_interval: float,
    ):
        """Initialize parent classes"""
        Scheduler.__init__(self)
        super(RepeatTimer, self).__init__(
            interval=run_pending_interval,
            function=self.run_pending,
        )


def print_work(what_to_say: str):
    print(what_to_say)


if __name__ == '__main__':
    my_schedule = ThreadedScheduler(run_pending_interval=1)
    job1 = my_schedule.every(1).seconds.do(print_work, what_to_say='Did_job1')
    job2 = my_schedule.every(2).seconds.do(print_work, what_to_say='Did_job2')
    my_schedule.cancel()
    my_schedule.start()
    time.sleep(7)
    my_schedule.cancel_job(job1)
    my_schedule.start()
    time.sleep(7)
    my_schedule.cancel()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM