-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use ZeroMQ message bus in lieu of build queue using new BuildManager #29
Changes from 24 commits
79c217e
e122da8
f406d33
9352907
4002ba1
a062a93
1dcc17d
2c88eb5
9c9811b
68105dc
a955b28
01f5da1
f05fded
c928a96
ae086d0
ee52c6d
259d105
d2d803e
962f796
99bc49b
68f46ec
fb10fab
90c3960
c38889d
932dd1e
c08b4d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/usr/bin/env python | ||
|
||
# Schedule some or all papers for build | ||
|
||
from procbuild.pr_list import get_papers | ||
from procbuild.submitter import BuildRequestSubmitter | ||
import sys | ||
|
||
if len(sys.argv) > 1: | ||
to_build = sys.argv[1:] | ||
else: | ||
to_build = [nr for nr, info in get_papers()] | ||
|
||
submitter = BuildRequestSubmitter(verbose=True) | ||
for p in to_build: | ||
print(f"Submitting paper {p} to build queue.") | ||
submitter.submit(p) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,6 @@ | ||
from __future__ import print_function, absolute_import | ||
|
||
import os | ||
|
||
package_dir = os.path.abspath(os.path.dirname(__file__)) | ||
package_path = os.path.abspath(os.path.dirname(__file__)) | ||
|
||
MASTER_BRANCH = os.environ.get('MASTER_BRANCH', '2017') | ||
ALLOW_MANUAL_BUILD_TRIGGER = bool(int( | ||
os.environ.get('ALLOW_MANUAL_BUILD_TRIGGER', 1)) | ||
) | ||
|
||
#from .server import (app, log, get_papers, monitor_queue, | ||
# MASTER_BRANCH, ALLOW_MANUAL_BUILD_TRIGGER) | ||
|
||
# --- Customize these variables --- | ||
|
||
__all__ = ['app', 'log', 'MASTER_BRANCH', 'paper_queue'] | ||
ALLOW_MANUAL_BUILD_TRIGGER = bool(int(os.environ.get('ALLOW_MANUAL_BUILD_TRIGGER', 1))) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
import json | ||
import io | ||
import codecs | ||
import time | ||
import asyncio | ||
|
||
from multiprocessing import Process | ||
from concurrent.futures import ThreadPoolExecutor | ||
|
||
import zmq | ||
from zmq.asyncio import Context | ||
|
||
from . import MASTER_BRANCH | ||
from .message_proxy import OUT | ||
from .utils import file_age, log | ||
from .pr_list import get_pr_info, status_file, cache | ||
from .builder import BuildManager | ||
|
||
|
||
class Listener: | ||
""" Listener class for defining zmq sockets and maintaining a build queue. | ||
|
||
Attributes: | ||
------------ | ||
ctx: zmq.asyncio.Context, the main context for the listener class | ||
|
||
prefix: str, the prefix listened for by zmq sockets | ||
|
||
socket: zmq.socket, the socket for listening to | ||
|
||
queue: asyncio.Queue, the queue for holding the builds | ||
|
||
dont_build: set, unique collection of PRs currently in self.queue | ||
Note: Only modify self.dont_build within synchronous blocks. | ||
|
||
""" | ||
|
||
def __init__(self, prefix='build_queue'): | ||
""" | ||
Parameters: | ||
------------ | ||
build_queue: str, the prefix that will be checked for by the zmq socket | ||
""" | ||
self.ctx = Context.instance() | ||
self.prefix = prefix | ||
|
||
self.socket = self.ctx.socket(zmq.SUB) | ||
self.socket.connect(OUT) | ||
self.socket.setsockopt(zmq.SUBSCRIBE, self.prefix.encode('utf-8')) | ||
|
||
self.queue = asyncio.Queue() | ||
self.dont_build = set() | ||
|
||
async def listen(self): | ||
"""Listener method, containing while loop for checking socket | ||
""" | ||
while True: | ||
msg = await self.socket.recv_multipart() | ||
target, raw_payload = msg | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use consistent naming for |
||
payload = json.loads(raw_payload.decode('utf-8')) | ||
print('received', payload) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Debug statement? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I'll remove it, I found it useful even when it was running to know that it was receiving things appropriately and it would show up in the heroku logs (I think). But I'm ok abandoning this. |
||
paper_to_build = payload.get('build_paper', None) | ||
|
||
if self.check_age_and_queue(paper_to_build): | ||
continue | ||
self.dont_build.add(paper_to_build) | ||
await self.queue.put(paper_to_build) | ||
|
||
def check_age(self, nr): | ||
"""Check the age of a PR's status_file based on its number. | ||
|
||
Parameters: | ||
------------ | ||
nr: int, the number of the PR in order of receipt | ||
""" | ||
age = file_age(status_file(nr)) | ||
min_wait = 0.5 | ||
too_young = False | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
if age is not None and age <= min_wait: | ||
log(f"Did not build paper {nr}--recently built.") | ||
too_young = True | ||
return too_young | ||
|
||
def check_queue(self, nr): | ||
"""Check whether the queue currently contains a build request for a PR. | ||
|
||
Parameters: | ||
------------ | ||
nr: int, the number of the PR to check | ||
""" | ||
in_queue = False | ||
if nr in self.dont_build: | ||
log(f"Did not queue paper {nr}--already in queue.") | ||
in_queue = True | ||
return in_queue | ||
|
||
def check_age_and_queue(self, nr): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd recommend removing this method and use these two functions inline above. It doesn't add enough to warrant its own function. |
||
"""Check whether the PR is old enough or whether it is already in queue. | ||
|
||
Parameters: | ||
------------ | ||
nr: int, the number of the PR to check | ||
""" | ||
return self.check_age(nr) or self.check_queue(nr) | ||
|
||
def report_status(self, nr): | ||
"""prints status notification from status_file for paper `nr` | ||
|
||
Parameters: | ||
------------ | ||
nr: int, the number of the PR to check | ||
""" | ||
with io.open(status_file(nr), 'r') as f: | ||
status = json.load(f)['status'] | ||
|
||
if status == 'success': | ||
print(f"Completed build for paper {nr}.") | ||
else: | ||
print(f"Paper for {nr} did not build successfully.") | ||
|
||
|
||
async def queue_builder(self, loop=None): | ||
"""Manage queue and trigger builds, report results. | ||
|
||
loop: asyncio.loop, the loop on which to be running these tasks | ||
""" | ||
while True: | ||
# await an item from the queue | ||
nr = await self.queue.get() | ||
# launch subprocess to build item | ||
with ThreadPoolExecutor(max_workers=1) as e: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How many CPUs do we have on Heroku? We could ramp this up, if multiple are available. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When i just ran |
||
await loop.run_in_executor(e, self.build_and_log, nr) | ||
self.dont_build.remove(nr) | ||
self.report_status(nr) | ||
|
||
def paper_log(self, nr, record): | ||
"""Writes status to PR's log file | ||
|
||
Parameters: | ||
------------ | ||
nr: int, the number of the PR to check | ||
record: dict, the dictionary content to be written to the log | ||
""" | ||
status_log = status_file(nr) | ||
with io.open(status_log, 'wb') as f: | ||
json.dump(record, codecs.getwriter('utf-8')(f), ensure_ascii=False) | ||
|
||
def build_and_log(self, nr): | ||
"""Builds paper for PR number and logs the resulting status | ||
|
||
Parameters: | ||
------------ | ||
nr: int, the number of the PR to check | ||
""" | ||
pr_info = get_pr_info() | ||
pr = pr_info[int(nr)] | ||
|
||
build_record = {'status': 'fail', | ||
'data': {'build_status': 'Building...', | ||
'build_output': 'Initializing build...', | ||
'build_timestamp': ''}} | ||
self.paper_log(nr, build_record) | ||
|
||
build_manager = BuildManager(user=pr['user'], | ||
branch=pr['branch'], | ||
cache=cache(), | ||
master_branch=MASTER_BRANCH, | ||
target=nr, | ||
log=log) | ||
|
||
status = build_manager.build_paper() | ||
self.paper_log(nr, status) | ||
|
||
if __name__ == "__main__": | ||
print('Listening for incoming messages...') | ||
|
||
listener = Listener() | ||
|
||
loop = asyncio.get_event_loop() | ||
tasks = asyncio.gather(listener.listen(), | ||
listener.queue_builder(loop)) | ||
try: | ||
loop.run_until_complete(tasks) | ||
finally: | ||
loop.run_until_complete(loop.shutdown_asyncgens()) | ||
loop.close() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# http://zguide.zeromq.org/page:all#The-Dynamic-Discovery-Problem | ||
|
||
import zmq | ||
from . import package_path | ||
|
||
|
||
IN = f'ipc://{package_path}/queue.in' | ||
OUT = f'ipc://{package_path}/queue.out' | ||
|
||
|
||
if __name__ == "__main__": | ||
context = zmq.Context() | ||
|
||
feed_in = context.socket(zmq.PULL) | ||
feed_in.bind(IN) | ||
|
||
feed_out = context.socket(zmq.PUB) | ||
feed_out.bind(OUT) | ||
|
||
print('[message_proxy] Forwarding messages between {} and {}'.format(IN, OUT)) | ||
zmq.proxy(feed_in, feed_out) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use the NumPy documentation format, also below.