-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathbackground.py
More file actions
125 lines (106 loc) · 4.38 KB
/
Copy pathbackground.py
File metadata and controls
125 lines (106 loc) · 4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
"""In-process replacements for App Engine's task queue and cron.
Started once from ``wsgi_local`` with the Flask app. Runs:
* one worker thread draining the task FIFO strictly serially (mirrors the
GAE ``default`` queue's ``max_concurrent_requests: 1`` that the upload math
relies on),
* one scheduler thread firing the ``cron.yaml`` jobs on their intervals.
Both dispatch by building a Flask request context and calling the handler
directly (no real HTTP). ``drain()`` and ``tick()`` let the test suite advance
the system deterministically instead of sleeping.
"""
import logging
import threading
import time
import gae_shim
import BatchRankings
import FetchParseFlags
import HandleQueuedResults
import RumbleSelect
import RumbleStats
_log = logging.getLogger("gunicorn.error") # INFO-level, wired to journald
_app = None
_started = False
# task url -> handler (raw functions; the decorators' header checks are bypassed
# because we invoke in-process and set X-Appengine-TaskName ourselves)
_TASK_HANDLERS = {
"/HandleQueuedResults": HandleQueuedResults.handle_queued_results,
"/BatchRankings": BatchRankings.batch_rankings,
}
# scheduled jobs: (interval_seconds, path, handler). Path carries query args the
# handler reads via request.query_string. A single scheduler thread runs these
# serially, so no cross-job staggering is needed.
_SCHEDULE = [
(60, "/", RumbleSelect.rumble_select),
(60, "/?theme=dark", RumbleSelect.rumble_select),
(600, "/RumbleStats?regen=1", RumbleStats.rumble_stats),
(600, "/RumbleStats?regen=1&theme=dark", RumbleStats.rumble_stats),
(7200, "/FetchParseFlags", FetchParseFlags.fetch_parse_flags),
(3600, "/QueueHourlyBatchRankings", BatchRankings.queue_hourly_batch_rankings),
]
_JOBS_BY_PATH = {path: fn for _, path, fn in _SCHEDULE}
def _run(path, fn, method="GET", data=None, headers=None):
with _app.test_request_context(path=path, method=method, data=data,
headers=headers or {}):
return fn()
def _worker():
q = gae_shim._task_queue
while True:
url, payload, headers = q.get()
start = time.monotonic()
status = "ok"
try:
fn = _TASK_HANDLERS.get(url)
if fn is None:
_log.error("no local handler for task url " + url)
status = "no-handler"
else:
headers = {**headers, "X-Appengine-TaskName": "local"}
status = "error"
for attempt in range(3):
try:
_run(url, fn, method="POST", data=payload, headers=headers)
status = "ok"
break
except Exception:
_log.exception("task " + url + " failed (attempt " + str(attempt + 1) + ")")
finally:
gae_shim._record_execution()
q.task_done()
_log.info("task %s %s %.0fms qlen=%d", url, status,
(time.monotonic() - start) * 1000, q.qsize())
def _scheduler():
next_run = {path: time.monotonic() for _, path, _ in _SCHEDULE} # first run at startup
while True:
now = time.monotonic()
for interval, path, fn in _SCHEDULE:
if now >= next_run[path]:
next_run[path] = now + interval
try:
_run(path, fn)
except Exception:
logging.exception("scheduled job " + path + " failed")
time.sleep(1)
def start(app):
"""Start the worker + scheduler once. Idempotent."""
global _app, _started
_app = app
if _started:
return
_started = True
threading.Thread(target=_worker, name="taskqueue-worker", daemon=True).start()
threading.Thread(target=_scheduler, name="cron-scheduler", daemon=True).start()
# deterministic hooks for tests
def drain(timeout=30):
"""Block until the task queue is empty and the worker is idle."""
q = gae_shim._task_queue
deadline = time.time() + timeout
while q.unfinished_tasks and time.time() < deadline:
time.sleep(0.01)
if q.unfinished_tasks:
raise TimeoutError("task queue did not drain within " + str(timeout) + "s")
def tick(path):
"""Run a single scheduled job synchronously, by its path."""
fn = _JOBS_BY_PATH.get(path)
if fn is None:
raise KeyError("no scheduled job for " + path)
return _run(path, fn)