【问题标题】:How to let long tasks be "cancellable" via HTTP on Tornado HTTP servers?如何通过 Tornado HTTP 服务器上的 HTTP 让长任务“可取消”?
【发布时间】:2015-12-26 15:09:30
【问题描述】:

我已经实现了某种繁重任务的 HTTP 包装器,我选择 Tornado 作为前端服务器框架(这是因为繁重的任务是用 Python 编写的,而我只是习惯了 Tornado)。

目前,我只是直接从 Tornado 的进程中调用繁重的任务。我用 jQuery 准备了某种基于 Web 的界面,让它处理 AJAX 请求,并在表单中设置参数。

正如您想象的那样,我从网络浏览器中抛出的任务是不可取消的。我可以取消的唯一方法是向 Python 进程发送 9 或 15 信号,这不是用户通常可以做的。

我想通过 HTTP 请求某种“取消”请求来取消当前工作任务。如何做呢?大多数处理繁重任务(例如 YouTube 中的视频编码)的 Web 服务都在做什么?

【问题讨论】:

    标签: python http asynchronous tornado request-cancelling


    【解决方案1】:

    其实Tornado的Futures不支持取消(docs)。此外,即使使用with_timeout,超时的作业仍在运行,只是没有等待它的结果。

    正如How can I cancel a hanging asyncronous task in tornado, with a timeout? 中所述,唯一的方法是以可以取消的方式实现逻辑(使用一些标志或其他)。

    例子:

    • 作业是一个简单的异步睡眠
    • / 列出职位
    • /add/TIME 添加新作业 - TIME(以秒为单位) - 指定睡眠时间
    • /cancel/ID取消作业

    代码可能如下所示:

    from tornado.ioloop import IOLoop
    from tornado import gen, web
    from time import time
    
    class Job():
    
        def __init__(self, run_sec):
            self.run_sec = int(run_sec)
            self.start_time = None
            self.end_time = None
            self._cancelled = False
    
        @gen.coroutine
        def run(self):
            """ Some job
    
            The job is simple: sleep for a given number of seconds.
            It could be implemented as:
                 yield gen.sleep(self.run_sec)
            but this way makes it not cancellable, so
            it is divided: run 1s sleep, run_sec times 
            """
            self.start_time = time()
            deadline = self.start_time + self.run_sec
            while not self._cancelled:
                yield gen.sleep(1)
                if time() >= deadline:
                    break
            self.end_time = time()
    
        def cancel(self):
        """ Cancels job
    
        Returns None on success,
        raises Exception on error:
          if job is already cancelled or done
        """
            if self._cancelled:
                raise Exception('Job is already cancelled')
            if self.end_time is not None:
                raise Exception('Job is already done')
            self._cancelled = True
    
        def get_state(self):
            if self._cancelled:
                if self.end_time is None:
                    # job might be running still
                    # and will be stopped on the next while check
                    return 'CANCELING...'
                else:
                    return 'CANCELLED'
            elif self.end_time is None:
                return 'RUNNING...'
            elif self.start_time is None:
                # actually this never will shown
                # as after creation, job is immediately started
                return 'NOT STARTED'
            else:
                return 'DONE'
    
    
    class MainHandler(web.RequestHandler):
    
        def get(self, op=None, param=None):
            if op == 'add':
                # add new job
                new_job = Job(run_sec=param)
                self.application.jobs.append(new_job)
                new_job.run()
                self.write('Job added')
            elif op == 'cancel':
                # cancel job - stop running
                self.application.jobs[int(param)].cancel()
                self.write('Job cancelled')
            else:
                # list jobs
                self.write('<pre>') # this is so ugly... ;P
                self.write('ID\tRUNSEC\tSTART_TIME\tSTATE\tEND_TIME\n')
                for idx, job in enumerate(self.application.jobs):
                    self.write('%s\t%s\t%s\t%s\t%s\n' % (
                        idx, job.run_sec, job.start_time,
                        job.get_state(), job.end_time
                    ))
    
    
    class MyApplication(web.Application):
    
        def __init__(self):
    
            # to store tasks
            self.jobs = []
    
            super(MyApplication, self).__init__([
                (r"/", MainHandler),
                (r"/(add)/(\d*)", MainHandler),
                (r"/(cancel)/(\d*)", MainHandler),
            ])
    
    if __name__ == "__main__":
        MyApplication().listen(8888)
        IOLoop.current().start()
    

    添加几个工作:

    for a in `seq 12 120`; do curl http://127.0.0.1:8888/add/$a; done
    

    然后取消一些...注意 - 它只需要 Tornado。

    这个例子很简单,gen.sleep 是你的繁重的任务。当然,并不是所有的作业都像可取消的那样简单。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-04
      • 2012-03-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多