编辑:添加了简化的示例代码,更好地解释了正在进行的请求/终止,并添加了来自 CrazyPython 的要点。
从表面上看,你有 4 个问题需要解决。我将依次介绍它们,然后提供一些有助于澄清的示例代码:
处理 SIGTERM
这很简单。您只需要设置一个信号处理程序来注意您需要关闭。 PMOTW 有一组很好的例子来说明如何捕捉信号。您可以使用此代码的变体来捕获 SIGTERM 并设置一个表示您正在关闭的全局标志。
拒绝新请求
Django middleware 提供了一种将任何 HTTP 请求挂钩到您的应用程序的简洁方法。您可以创建一个简单的 process_request() 钩子,如果设置了全局标志(从上面),则返回错误页面。
完成现有请求
停止任何新请求后,您现在必须完成当前请求。虽然你现在可能不相信,但这意味着你什么也不做,让程序在 SIGTERM 之后照常运行。让我扩展一下……
与 heroku 的合同是您必须在 SIGTERM 的 10 秒内完成,否则无论如何它都会发送 SIGKILL。这意味着您无能为力(作为一个表现良好的应用程序)来确保所有请求始终完成。考虑两种情况:
- 您的应用程序在 10 秒内处理所有现有请求。在这种情况下,只需让您的程序运行即可完成请求。不需要特殊代码来运行请求 - 所有线程/进程都已在执行您需要的操作!
- 您的应用程序对某些请求的处理时间超过 10 秒。在这种情况下,您无能为力 - 在长请求完成之前,heroku 将用终极力量终止它。如果您认为可以忽略 SIGKILL,请考虑其他方式...这是不允许的 - 请参阅 signals documentation。
因此,在这两种情况下,解决方案都是让您的程序继续运行,以在终止之前让尽可能多的当前请求完成。
终止您的应用程序
最简单的做法可能是等待 10 秒后从 heroku 发出 SIGKILL。这并不优雅,但应该没问题,因为您拒绝了任何新请求。
如果这还不够好,您需要跟踪未完成的请求并使用它来决定何时可以关闭您的应用程序。关闭应用程序的确切方法将取决于托管它的内容,因此我无法在那里为您提供确切的指导。不过,希望示例代码能够为您提供足够的指导。
示例代码
从 PMOTW 中的信号处理程序示例开始,我加强了代码以添加多个线程处理请求和一个终止管理器来捕获信号并允许应用程序正常关闭。您应该可以在 Python2.7 中运行它,然后尝试终止该进程。
在此示例的基础上,CrazyPython 创建了 gist 以在 django 中提供具体实现。
import signal
import os
import time
import threading
import random
class TerminationManager(object):
def __init__(self):
self._running = True
self._requests = 0
self._lock = threading.Lock()
signal.signal(signal.SIGTERM, self._start_shutdown)
def _start_shutdown(self, signum, stack):
print 'Received:', signum
self._running = False
def start_request(self):
with self._lock:
self._requests += 1
def stop_request(self):
with self._lock:
self._requests -= 1
def is_running(self):
return self._running or self._requests > 0
def running_requests(self):
return self._requests
class DummyWorker(threading.Thread):
def __init__(self, app_manager):
super(DummyWorker, self).__init__()
self._manager = app_manager
def run(self):
while self._manager.is_running():
# Emulate random work and delay between requests.
if random.random() > 0.9:
self._manager.start_request()
time.sleep(random.randint(1, 3))
self._manager.stop_request()
else:
time.sleep(1)
print "Stopping worker"
manager = TerminationManager()
print 'My PID is:', os.getpid()
for _ in xrange(10):
t = DummyWorker(manager)
t.start()
while manager.is_running():
print 'Waiting with {} running requests'.format(manager.running_requests())
time.sleep(5)
print 'All done!'