GAE - очень полезный инструмент для создания масштабируемых веб-приложений. Немногие из ограничений, на которые указывают многие, - это отсутствие поддержки фоновых задач, отсутствие периодических задач и строгое ограничение времени, которое занимает каждый HTTP-запрос, если запрос превышает этот лимит времени, операция завершается, что делает невозможным выполнение задач, требующих много времени. .
Как запустить фоновое задание?
В GAE код выполняется только при наличии HTTP-запроса. Существует строгий лимит времени (я думаю, 10 сек), сколько времени может занять код. Так что если нет запросов, то код не выполняется. Одним из предложенных способов решения этой проблемы было использование внешнего окна для непрерывной отправки запросов, что создавало фоновую задачу. Но для этого нам нужен внешний бокс, и теперь мы зависим от еще одного элемента. Другой альтернативой была отправка 302 ответа о перенаправлении, чтобы клиент повторно отправлял запрос, что также делает нас зависимыми от внешнего элемента, который является клиентом. Что если эта внешняя коробка - сама GAE? Каждый, кто использовал функциональный язык, который не поддерживает циклическую конструкцию в языке, знает об альтернативе, то есть рекурсия является заменой цикла. Так что, если мы завершим часть вычисления и сделаем HTTP GET по тому же URL с очень коротким временем ожидания, скажем, 1 секунда? Это создает цикл (рекурсию) для PHP-кода, работающего на Apache.
<?php
$i = 0;
if(isset($_REQUEST["i"])){
$i= $_REQUEST["i"];
sleep(1);
}
$ch = curl_init("http://localhost".$_SERVER["PHP_SELF"]."?i=".($i+1));
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
curl_exec($ch);
print "hello world\n";
?>
Некоторые, как это не работает на GAE. Так что, если мы сделаем HTTP GET для другого URL, скажем, url2, который выполняет HTTP GET для первого URL? Это похоже на работу в GAE. Код для этого выглядит следующим образом.
class FirstUrl(webapp.RequestHandler):
def get(self):
self.response.out.write("ok")
time.sleep(2)
urlfetch.fetch("http://"+self.request.headers["HOST"]+'/url2')
class SecondUrl(webapp.RequestHandler):
def get(self):
self.response.out.write("ok")
time.sleep(2)
urlfetch.fetch("http://"+self.request.headers["HOST"]+'/url1')
application = webapp.WSGIApplication([('/url1', FirstUrl), ('/url2', SecondUrl)])
def main():
run_wsgi_app(application)
if __name__ == "__main__":
main()
Поскольку мы нашли способ запуска фоновой задачи, давайте создадим абстракции для периодической задачи (таймера) и циклическую конструкцию, охватывающую множество HTTP-запросов (foreach).
Таймер
Теперь таймер строительства прямо вперед. Основная идея состоит в том, чтобы иметь список таймеров и интервал, с которым каждый из них должен быть вызван. Как только мы достигнем этого интервала, вызовем функцию обратного вызова. Мы будем использовать memcache для поддержки списка таймеров. Чтобы узнать, когда вызывать обратный вызов, мы сохраним ключ в memcache с интервалом как время истечения. Мы периодически (скажем, 5 секунд) проверяем, присутствует ли этот ключ, если он отсутствует, затем вызываем обратный вызов и снова устанавливаем этот ключ с интервалом.
def timer(func, interval):
timerlist = memcache.get('timer')
if(None == timerlist):
timerlist = []
timerlist.append({'func':func, 'interval':interval})
memcache.set('timer-'+func, '1', interval)
memcache.set('timer', timerlist)
def checktimers():
timerlist = memcache.get('timer')
if(None == timerlist):
return False
for current in timerlist:
if(None == memcache.get('timer-'+current['func'])):
#reset interval
memcache.set('timer-'+current['func'], '1', current['interval'])
#invoke callback function
try:
eval(current['func']+'()')
except:
pass
return True
return False
Foreach
Это необходимо, когда мы хотим долго вычислять, скажем, выполнить какую-то операцию с 1000 строками базы данных или получить 1000 URL-адресов и т. Д. Основная идея состоит в том, чтобы поддерживать список обратных вызовов и аргументов в memcache и каждый раз вызывать обратный вызов с аргументом.
def foreach(func, args):
looplist = memcache.get('foreach')
if(None == looplist):
looplist = []
looplist.append({'func':func, 'args':args})
memcache.set('foreach', looplist)
def checkloops():
looplist = memcache.get('foreach')
if(None == looplist):
return False
if((len(looplist) > 0) and (len(looplist[0]['args']) > 0)):
arg = looplist[0]['args'].pop(0)
func = looplist[0]['func']
if(len(looplist[0]['args']) == 0):
looplist.pop(0)
if((len(looplist) > 0) and (len(looplist[0]['args']) > 0)):
memcache.set('foreach', looplist)
else:
memcache.delete('foreach')
try:
eval(func+'('+repr(arg)+')')
except:
pass
return True
else:
return False
# instead of
# foreach index in range(0, 1000):
# someoperaton(index)
# we will say
# foreach('someoperaton', range(0, 1000))
Теперь просто построить программу, которая выбирает список URL-адресов каждый час. Вот код.
def getone(url):
try:
result = urlfetch.fetch(url)
if(result.status_code == 200):
memcache.set(url, '1', 60*60)
#process result.content
except :
pass
def getallurl():
#list of urls to be fetched
urllist = ['http://www.google.com/', 'http://www.cnn.com/', 'http://www.yahoo.com', 'http://news.google.com']
fetchlist = []
for url in urllist:
if (memcache.get(url) is None):
fetchlist.append(url)
#this is equivalent to
#for url in fetchlist: getone(url)
if(len(fetchlist) > 0):
foreach('getone', fetchlist)
#register the timer callback
timer('getallurl', 3*60)
полный код здесь http://groups.google.com/group/httpmr-discuss/t/1648611a54c01aa
Я запускаю этот код в appengine в течение нескольких дней без особых проблем.
Предупреждение: мы интенсивно используем urlfetch. Ограничение на количество urlfetch в день составляет 160000. Поэтому будьте осторожны, чтобы не достичь этого предела.