Skip to content

Commit c7d053c

Browse files
authored
Merge pull request #33 from crowdresearch/develop
Travis setup + Websocket graceful closure
2 parents 79d5e34 + f8bcc12 commit c7d053c

9 files changed

Lines changed: 124 additions & 85 deletions

File tree

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ before_script:
1818
flake8 .
1919

2020
# command to run tests
21-
script: pytest
21+
script: python -m pytest
2222

2323
deploy:
2424
provider: pypi

daemo/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ def get_task_results_by_taskworker_id(self, taskworker_id):
4848
return None
4949

5050
def update_approval_status(self, task):
51-
log.debug(msg="updating status for task %d" % task["task_id"])
51+
log.debug(msg="updating status for task worker %d" % task["taskworker_id"])
5252

5353
STATUS_ACCEPTED = 3
5454
STATUS_REJECTED = 4
5555

5656
data = {
5757
"status": STATUS_ACCEPTED if task["accept"] else STATUS_REJECTED,
58-
"workers": [task["id"]]
58+
"workers": [task["taskworker_id"]]
5959
}
6060

6161
response = self.client.post(self.route.update_task_status, data=json.dumps(data))

daemo/channel.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def __init__(self, queue, api_client, url):
2828
self.url = url
2929
self.api_client = api_client
3030
self.lock = threading.Lock()
31+
# defer.setDebugging(True)
32+
3133
self.clientDisconnected = defer.Deferred()
3234

3335
access_token = self.api_client.get_auth_token()
@@ -38,9 +40,7 @@ def __init__(self, queue, api_client, url):
3840

3941
self.factory = ClientFactory(self.url, headers=headers)
4042

41-
self.factory.onConnectionLost = self.clientDisconnected
42-
self.clientDisconnected.addCallback(self.stop_reactor)
43-
43+
self.factory.force_close = False
4444
self.factory.protocol = ClientProtocol
4545
self.factory.queue = self.queue
4646

@@ -50,33 +50,30 @@ def return_name(self):
5050
def run(self):
5151
log.debug(msg="opening channel...")
5252

53-
self.connector = connectWS(self.factory)
54-
5553
if self.state == 0:
5654
self.state = 1
55+
56+
self.factory.onConnectionLost = self.clientDisconnected
57+
self.clientDisconnected.addCallback(self.on_client_disconnected)
58+
59+
self.connector = connectWS(self.factory)
5760
self.connector.reactor.run()
5861

59-
def stop(self):
60-
log.info(msg="closing channel...")
61-
# print self.clientDisconnected
62+
def stop(self, forced_closure):
63+
if forced_closure:
64+
self.factory.force_close = forced_closure
65+
self.factory.stopTrying()
66+
self.connector.disconnect()
6267

63-
self.factory.stopTrying()
64-
self.connector.disconnect()
6568
return defer.gatherResults([self.clientDisconnected])
6669

67-
def stop_reactor(self, protocol):
68-
if not self.factory.continueTrying > 0:
69-
70+
def on_client_disconnected(self, protocol):
71+
if not (self.factory.continueTrying > 0):
7072
time.sleep(2)
7173

72-
# print "channel: stop_reactor"
73-
7474
with self.lock:
7575
if self.state > 0:
7676
self.state = 0
7777
self.connector.reactor.stop()
78-
else:
79-
print "setup factory callback"
80-
81-
# self.factory.onConnectionLost = self.clientDisconnected
82-
# self.clientDisconnected.addCallback(self.stop_reactor)
78+
# else:
79+
# log.warning("continue trying")

daemo/client.py

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ def __init__(self, credentials_path='credentials.json', rerun_key=None, multi_th
7373
if host is not None:
7474
self.host = host
7575

76+
self.pid = os.getpid()
77+
7678
self.api_client = ApiClient(self.credentials_path, self.host, self.http_proto)
7779

7880
self.store = Store()
@@ -566,9 +568,11 @@ def _process_task(self, payload):
566568
self.store.batches[batch_index]["submissions"][task_group_id] += 1
567569

568570
if stream:
569-
self._stream_response(batch_index, task_id, task_group_id, task_data, approve, completed)
571+
self._stream_response(batch_index, task_id, task_group_id, taskworker_id, task_data, approve,
572+
completed)
570573
else:
571-
self._aggregate_responses(batch_index, task_id, task_group_id, task_data, approve, completed)
574+
self._aggregate_responses(batch_index, task_id, task_group_id, taskworker_id, task_data, approve,
575+
completed)
572576

573577
self.check_for_pending_tasks_reviews()
574578
else:
@@ -598,17 +602,17 @@ def check_for_pending_tasks_reviews(self):
598602
def _review_completed(self, project_key, ratings, ignore_history=True):
599603
self.rate(project_key, ratings, ignore_history=ignore_history)
600604

601-
def _stream_response(self, batch_index, task_id, task_group_id, task_data, approve, completed):
605+
def _stream_response(self, batch_index, task_id, task_group_id, taskworker_id, task_data, approve, completed):
602606
log.info(msg="streaming responses...")
603607

604608
log.info(msg="calling approve callback...")
605609

606610
if approve([task_data]):
607611
task_data["accept"] = True
608-
log.info(msg="task %d approved" % task_id)
612+
log.info(msg="task worker %d approved" % taskworker_id)
609613
else:
610614
task_data["accept"] = False
611-
log.info(msg="task %d rejected" % task_id)
615+
log.info(msg="task worker %d rejected" % taskworker_id)
612616

613617
# reverse increment as rejection will create another task
614618
self.store.batches[batch_index]["submissions"][task_group_id] -= 1
@@ -624,11 +628,11 @@ def _stream_response(self, batch_index, task_id, task_group_id, task_data, appro
624628
if is_done:
625629
self.store.mark_task_completed(batch_index, task_id, task_group_id)
626630

627-
def _aggregate_responses(self, batch_index, task_id, task_group_id, task_data, approve, completed):
631+
def _aggregate_responses(self, batch_index, task_id, task_group_id, taskworker_id, task_data, approve, completed):
628632
log.info(msg="aggregating responses...")
629633

630634
# store it for aggregation (stream = False)
631-
self.store.aggregate(batch_index, task_id, task_group_id, task_data)
635+
self.store.aggregate(batch_index, task_id, task_group_id, taskworker_id, task_data)
632636

633637
is_done = self.store.is_task_complete(batch_index, task_id, task_group_id)
634638

@@ -638,44 +642,52 @@ def _aggregate_responses(self, batch_index, task_id, task_group_id, task_data, a
638642
is_done = self.store.is_batch_complete(batch_index)
639643

640644
if is_done:
641-
self.store.mark_batch_completed(batch_index)
645+
self._on_batch_complete(batch_index, approve, completed)
642646

643-
tasks_data = self.store.get_aggregated(batch_index)
647+
def _on_batch_complete(self, batch_index, approve, completed):
648+
self.store.mark_batch_completed(batch_index)
644649

645-
log.info(msg="calling approve callback...")
646-
approvals = approve(tasks_data)
650+
tasks_data = self.store.get_aggregated(batch_index)
647651

648-
tasks_approvals = zip(tasks_data, approvals)
652+
log.info(msg="calling approve callback...")
653+
approvals = approve(tasks_data)
649654

650-
for task_approval in tasks_approvals:
651-
task_data = task_approval[0]
652-
approval = task_approval[1]
655+
tasks_approvals = zip(tasks_data, approvals)
653656

654-
task_data["accept"] = approval
657+
for task_approval in tasks_approvals:
658+
task_data = task_approval[0]
659+
approval = task_approval[1]
655660

656-
if approval:
657-
log.info(msg="task %d approved" % task_data.get("task_id"))
658-
else:
659-
log.info(msg="task %d rejected" % task_data.get("task_id"))
660-
self.store.batches[batch_index]["submissions"][task_group_id] -= 1
661-
self.store.mark_task_incomplete(batch_index, task_group_id)
662-
self.store.mark_batch_incomplete(batch_index)
661+
task_data["accept"] = approval
663662

664-
self.api_client.update_approval_status(task_data)
663+
if approval:
664+
log.info(msg="task worker %d approved" % task_data.get("taskworker_id"))
665+
else:
666+
log.info(msg="task worker %d rejected" % task_data.get("taskworker_id"))
667+
self.store.mark_task_incomplete(
668+
batch_index,
669+
task_data.get("task_id"),
670+
task_data.get("task_group_id")
671+
)
672+
self.store.mark_batch_incomplete(batch_index)
665673

666-
is_done = self.store.is_batch_complete(batch_index)
674+
self.api_client.update_approval_status(task_data)
667675

668-
if is_done:
669-
approved_tasks = [x[0] for x in zip(tasks_data, approvals) if x[1]]
676+
is_done = self.store.is_batch_complete(batch_index)
670677

671-
log.info(msg="calling completed callback...")
672-
completed(approved_tasks)
678+
if is_done:
679+
approved_tasks = [x[0] for x in zip(tasks_data, approvals) if x[1]]
680+
681+
log.info(msg="calling completed callback...")
682+
completed(approved_tasks)
673683

674684
def _fetch_task(self, task_id):
675685
data = self.api_client.fetch_task(task_id)
676686
return transform_task(data)
677687

678688
def _open_channel(self):
689+
signal.signal(signal.SIGINT, self._handler)
690+
679691
# shared queue between main process and channel for message passing
680692
self.queue = multiprocessing.Queue()
681693

@@ -687,23 +699,29 @@ def _open_channel(self):
687699
thread = callback_thread(name='signal monitor', target=signal.pause)
688700
thread.start()
689701

690-
signal.signal(signal.SIGINT, self._handler)
691-
692702
subscribe_url = self.websock_proto + self.host + self.api_client.route.subscribe
693703

694-
print "starting channel..."
695704
self.channel = Channel(self.queue, self.api_client, subscribe_url)
696705
self.channel.start()
697706

698707
def _handler(self, signum, frame):
708+
forced_closure = signum in [signal.SIGINT, signal.SIGTERM]
709+
699710
# call this handler to stop the processes definitively
700-
if signum in [signal.SIGINT, signal.SIGTERM, signal.SIGABRT] and os.getpid() == self.channel.pid:
701-
self.channel.stop()
711+
if signum in [signal.SIGINT, signal.SIGTERM, signal.SIGABRT]:
712+
713+
if self.channel is not None and os.getpid() == self.channel.pid:
714+
# log.warn(msg="closing channel thread")
715+
716+
self.channel.stop(forced_closure)
717+
718+
if self.queue is not None:
719+
self.queue.put(None)
720+
self.queue = None
702721

703-
if self.queue is not None:
704-
self.queue.put(None)
705-
self.queue = None
722+
# if self.pid is not None and os.getpid() == self.pid:
723+
# log.warning(msg="client:closing main thread")
706724

707725
def _stop(self):
708-
log.info(msg="disconnecting...")
726+
log.warn(msg="disconnecting...")
709727
os.kill(int(self.channel.pid), signal.SIGINT)

daemo/client_factory.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,33 @@
88

99
class ClientFactory(WebSocketClientFactory, ReconnectingClientFactory):
1010
maxRetries = 5
11+
maxDelay = 300 # 5 min
1112

1213
def clientConnectionFailed(self, connector, reason):
1314
log.warning("websocket connection failed.")
14-
log.warning(reason.value)
15+
# log.warning(reason.value)
1516

16-
if self.continueTrying > 0:
17-
log.info("connecting again %d..." % self.retries)
18-
self.retry(connector)
17+
self.retryConnection(connector, reason, True)
1918

2019
def clientConnectionLost(self, connector, reason):
21-
# super(ClientFactory, self).clientConnectionLost(connector, reason)
2220
log.warning("websocket connection lost.")
2321

24-
if self.continueTrying > 0:
25-
log.info("connecting again %d..." % self.retries)
26-
self.retry(connector)
22+
self.retryConnection(connector, reason, False)
23+
24+
def retryConnection(self, connector, reason, fail=False):
25+
if self.continueTrying and self.retries < self.maxRetries:
26+
self.connector = connector
27+
log.info("connecting again (%d/%d)..." % (self.retries + 1, self.maxRetries))
28+
self.retry(self.connector)
29+
else:
30+
self.stop(connector, fail)
31+
32+
def stop(self, connector, fail):
33+
connector.disconnect()
34+
35+
if self.queue is not None:
36+
self.queue.put(None)
37+
self.queue = None
38+
39+
if fail:
40+
connector.reactor.stop()

daemo/protocol.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
class ClientProtocol(WebSocketClientProtocol):
1111
lost = False
1212

13-
def onConnect(self, response):
13+
def connectionMade(self):
1414
log.info("channel connected")
1515
self.factory.resetDelay()
1616

17+
# def onConnect(self, response):
18+
# log.info("channel connected")
19+
1720
def onOpen(self):
1821
log.info("channel opened")
1922

@@ -31,19 +34,16 @@ def onSend(self, data):
3134
log.debug("<<<{}>>>".format(data))
3235

3336
def onClose(self, wasClean, code, reason):
34-
log.debug("channel closed")
37+
log.warning("channel closed")
3538

3639
if not wasClean:
3740
log.error(reason.value)
3841

3942
def connectionLost(self, reason):
40-
log.debug("connection lost")
41-
# log.error(reason.value)
42-
43-
self.factory.onConnectionLost.callback(self)
43+
if self.factory.force_close:
44+
onConnectionLost = self.factory.onConnectionLost
4445

45-
# if isinstance(reason.value, ConnectionDone):
46-
# try:
47-
# self.factory.onConnectionLost.callback(self)
48-
# except Exception as e:
49-
# log.error(e)
46+
# do not let callback fire again
47+
if onConnectionLost is not None:
48+
self.factory.onConnectionLost = None
49+
onConnectionLost.callback(self)

daemo/storage.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,19 @@ def is_task_complete(self, batch_index, task_id, task_group_id):
5757
def mark_task_completed(self, batch_index, task_id, task_group_id):
5858
if task_group_id in self.batches[batch_index]["status"]:
5959
self.batches[batch_index]["status"][task_group_id] = True
60-
log.debug(msg="task %d is complete" % task_id)
60+
log.debug(msg="task %d is complete" % task_group_id)
6161

6262
def mark_task_incomplete(self, batch_index, task_id, task_group_id):
6363
if task_group_id in self.batches[batch_index]["status"]:
64+
self.batches[batch_index]["submissions"][task_group_id] = self.batches[batch_index]["submissions"][
65+
task_group_id] - 1
6466
self.batches[batch_index]["status"][task_group_id] = False
65-
log.debug(msg="task %d is NOT complete" % task_id)
67+
log.debug(msg="task %d is NOT complete" % task_group_id)
6668

6769
def is_batch_complete(self, batch_index):
68-
return all(self.batches[batch_index]["status"].values())
70+
is_complete = all(self.batches[batch_index]["status"].values())
71+
log.debug(msg="batch %d is %s complete" % (batch_index, ''if is_complete else 'NOT'))
72+
return is_complete
6973

7074
def mark_batch_completed(self, batch_index):
7175
log.debug(msg="batch %d is complete" % batch_index)
@@ -88,7 +92,8 @@ def all_batches_complete(self):
8892
def all_reviews_complete(self):
8993
return all([self.cache[match_group_id]["is_complete"] for match_group_id in self.cache.keys()])
9094

91-
def aggregate(self, batch_index, task_id, task_group_id, task_data):
95+
def aggregate(self, batch_index, task_id, task_group_id, taskworker_id, task_data):
96+
task_data["taskworker_id"] = taskworker_id
9297
self.batches[batch_index]["aggregated_data"].append({
9398
"task_id": task_id,
9499
"task_group_id": task_group_id,

daemo/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ def transform_task_results(data):
118118

119119
data["task_id"] = data["task"]
120120
data["worker_id"] = data["worker"]
121+
data["taskworker_id"] = data["id"]
122+
123+
del data['task']
124+
del data['worker']
125+
del data['id']
121126

122127
return data
123128

0 commit comments

Comments
 (0)