Skip to content

Commit 9e82548

Browse files
authored
Merge pull request #543 from remind101/blocking
Improved signal handling
2 parents 937f222 + b17620e commit 9e82548

11 files changed

Lines changed: 55 additions & 90 deletions

File tree

stacker/actions/base.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
import logging
34
import threading
45

@@ -7,6 +8,7 @@
78

89
import botocore.exceptions
910
from stacker.session_cache import get_session
11+
from stacker.exceptions import PlanFailed
1012

1113
from stacker.util import (
1214
ensure_s3_bucket,
@@ -188,9 +190,13 @@ def s3_stack_push(self, blueprint, force=False):
188190
return template_url
189191

190192
def execute(self, *args, **kwargs):
191-
self.pre_run(*args, **kwargs)
192-
self.run(*args, **kwargs)
193-
self.post_run(*args, **kwargs)
193+
try:
194+
self.pre_run(*args, **kwargs)
195+
self.run(*args, **kwargs)
196+
self.post_run(*args, **kwargs)
197+
except PlanFailed as e:
198+
logger.error(e.message)
199+
sys.exit(1)
194200

195201
def pre_run(self, *args, **kwargs):
196202
pass

stacker/actions/build.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import sys
32

43
from .base import BaseAction, plan, build_walker
54
from .base import STACK_POLL_TIME
@@ -354,8 +353,7 @@ def run(self, concurrency=0, outline=False,
354353
plan.outline(logging.DEBUG)
355354
logger.debug("Launching stacks: %s", ", ".join(plan.keys()))
356355
walker = build_walker(concurrency)
357-
if not plan.execute(walker):
358-
sys.exit(1)
356+
plan.execute(walker)
359357
else:
360358
if outline:
361359
plan.outline()

stacker/actions/destroy.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import sys
32

43
from .base import BaseAction, plan, build_walker
54
from .base import STACK_POLL_TIME
@@ -91,8 +90,7 @@ def run(self, force, concurrency=0, tail=False, *args, **kwargs):
9190
# steps to COMPLETE in order to log them
9291
plan.outline(logging.DEBUG)
9392
walker = build_walker(concurrency)
94-
if not plan.execute(walker):
95-
sys.exit(1)
93+
plan.execute(walker)
9694
else:
9795
plan.outline(message="To execute this plan, run with \"--force\" "
9896
"flag.")

stacker/actions/diff.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import difflib
22
import json
33
import logging
4-
import sys
54
from operator import attrgetter
65

76
from .base import plan, build_walker
@@ -270,8 +269,7 @@ def run(self, concurrency=0, *args, **kwargs):
270269
plan.outline(logging.DEBUG)
271270
logger.info("Diffing stacks: %s", ", ".join(plan.keys()))
272271
walker = build_walker(concurrency)
273-
if not plan.execute(walker):
274-
sys.exit(1)
272+
plan.execute(walker)
275273

276274
"""Don't ever do anything for pre_run or post_run"""
277275
def pre_run(self, *args, **kwargs):

stacker/commands/stacker/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
from ...environment import parse_environment
88

9+
logger = logging.getLogger(__name__)
10+
11+
SIGNAL_NAMES = {
12+
signal.SIGINT: "SIGINT",
13+
signal.SIGTERM: "SIGTERM",
14+
}
15+
916

1017
def cancel():
1118
"""Returns a threading.Event() that will get set when SIGTERM, or
@@ -14,6 +21,9 @@ def cancel():
1421
cancel = threading.Event()
1522

1623
def cancel_execution(signum, frame):
24+
signame = SIGNAL_NAMES.get(signum, signum)
25+
logger.info("Signal %s received, quitting "
26+
"(this can take some time)...", signame)
1727
cancel.set()
1828

1929
signal.signal(signal.SIGINT, cancel_execution)

stacker/dag/__init__.py

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import collections
22
import logging
3-
import threading
3+
from threading import Thread
44
from copy import copy, deepcopy
55
from collections import deque
66

@@ -153,21 +153,13 @@ def walk(self, walk_func):
153153
Args:
154154
walk_func (:class:`types.FunctionType`): The function to be called
155155
on each node of the graph.
156-
157-
Returns:
158-
bool: True if the function succeeded on every node, otherwise
159-
False.
160156
"""
161157
nodes = self.topological_sort()
162158
# Reverse so we start with nodes that have no dependencies.
163159
nodes.reverse()
164160

165-
failed = False
166161
for n in nodes:
167-
if not walk_func(n):
168-
failed = True
169-
170-
return not failed
162+
walk_func(n)
171163

172164
def rename_edges(self, old_node_name, new_node_name):
173165
""" Change references to a node in existing edges.
@@ -383,25 +375,6 @@ def release(self):
383375
pass
384376

385377

386-
class Thread(threading.Thread):
387-
"""Used when executing walk_func's in parallel, to provide access to the
388-
return value.
389-
"""
390-
def __init__(self, *args, **kwargs):
391-
super(Thread, self).__init__(*args, **kwargs)
392-
self._return = None
393-
394-
def run(self):
395-
if self._Thread__target is not None:
396-
self._return = self._Thread__target(
397-
*self._Thread__args,
398-
**self._Thread__kwargs)
399-
400-
def join(self, *args, **kwargs):
401-
super(Thread, self).join(*args, **kwargs)
402-
return self._return
403-
404-
405378
class ThreadedWalker(object):
406379
"""A DAG walker that walks the graph as quickly as the graph topology
407380
allows, using threads.
@@ -437,14 +410,14 @@ def walk(self, dag, walk_func):
437410
# Blocks until all of the given nodes have completed execution (whether
438411
# successfully, or errored). Returns True if all nodes returned True.
439412
def wait_for(nodes):
440-
return all(threads[node].join() for node in nodes)
413+
for node in nodes:
414+
thread = threads[node]
415+
while thread.is_alive():
416+
threads[node].join(0.5)
441417

442418
# For each node in the graph, we're going to allocate a thread to
443419
# execute. The thread will block executing walk_func, until all of the
444-
# nodes dependencies have executed successfully.
445-
#
446-
# If any node fails for some reason (e.g. raising an exception), any
447-
# downstream nodes will be cancelled.
420+
# nodes dependencies have executed.
448421
for node in nodes:
449422
def fn(n, deps):
450423
if deps:
@@ -460,17 +433,10 @@ def fn(n, deps):
460433

461434
self.semaphore.acquire()
462435
try:
463-
ret = walk_func(n)
436+
return walk_func(n)
464437
finally:
465438
self.semaphore.release()
466439

467-
if ret:
468-
logger.debug("%s completed", n)
469-
else:
470-
logger.debug("%s failed", n)
471-
472-
return ret
473-
474440
deps = dag.all_downstreams(node)
475441
threads[node] = Thread(target=fn, args=(node, deps), name=node)
476442

@@ -479,4 +445,4 @@ def fn(n, deps):
479445
threads[node].start()
480446

481447
# Wait for all threads to complete executing.
482-
return wait_for(nodes)
448+
wait_for(nodes)

stacker/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def __init__(self, failed_stacks, *args, **kwargs):
214214
self.failed_stacks = failed_stacks
215215

216216
stack_names = ', '.join(stack.name for stack in failed_stacks)
217-
message = "The following stacks failed: %s\n" % (stack_names,)
217+
message = "The following stacks failed: %s" % (stack_names,)
218218

219219
super(PlanFailed, self).__init__(message, *args, **kwargs)
220220

stacker/plan.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .exceptions import (
99
CancelExecution,
1010
GraphError,
11+
PlanFailed,
1112
)
1213
from .ui import ui
1314
from .dag import DAG, DAGValidationError, walk
@@ -345,7 +346,17 @@ def walk_func(step):
345346
return self.graph.walk(walk, walk_func)
346347

347348
def execute(self, *args, **kwargs):
348-
return self.walk(*args, **kwargs)
349+
"""Walks each step in the underlying graph, and raises an exception if
350+
any of the steps fail.
351+
352+
Raises:
353+
PlanFailed: Raised if any of the steps fail.
354+
"""
355+
self.walk(*args, **kwargs)
356+
357+
failed_steps = [step for step in self.steps if step.status == FAILED]
358+
if failed_steps:
359+
raise PlanFailed(failed_steps)
349360

350361
def walk(self, walker):
351362
"""Walks each step in the underlying graph, in topological order.

stacker/tests/test_dag.py

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -83,32 +83,7 @@ def walk_func(n):
8383
nodes.append(n)
8484
return True
8585

86-
ok = dag.walk(walk_func)
87-
assert ok == True # noqa: E712
88-
assert nodes == ['d', 'c', 'b', 'a'] or nodes == ['d', 'b', 'c', 'a']
89-
90-
91-
@with_setup(blank_setup)
92-
def test_walk_failed():
93-
dag = DAG()
94-
95-
# b and c should be executed at the same time.
96-
dag.from_dict({'a': ['b', 'c'],
97-
'b': ['d'],
98-
'c': ['d'],
99-
'd': []})
100-
101-
nodes = []
102-
103-
def walk_func(n):
104-
nodes.append(n)
105-
return False
106-
107-
ok = dag.walk(walk_func)
108-
109-
# Only 2 should have been hit. The rest are canceled because they depend on
110-
# the success of d.
111-
assert ok == False # noqa: E712
86+
dag.walk(walk_func)
11287
assert nodes == ['d', 'c', 'b', 'a'] or nodes == ['d', 'b', 'c', 'a']
11388

11489

@@ -209,6 +184,5 @@ def walk_func(n):
209184
lock.release()
210185
return True
211186

212-
ok = walker.walk(dag, walk_func)
213-
assert ok == True # noqa: E712
187+
walker.walk(dag, walk_func)
214188
assert nodes == ['d', 'c', 'b', 'a'] or nodes == ['d', 'b', 'c', 'a']

stacker/tests/test_plan.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from stacker.exceptions import (
2020
CancelExecution,
2121
GraphError,
22+
PlanFailed,
2223
)
2324
from stacker.status import (
2425
COMPLETE,
@@ -116,7 +117,7 @@ def fn(stack, status=None):
116117
description="Test",
117118
steps=[Step(vpc, fn), Step(db, fn), Step(app, fn)],
118119
targets=['db.1'])
119-
self.assertTrue(plan.execute(walk))
120+
plan.execute(walk)
120121

121122
self.assertEquals(calls, [
122123
'namespace-vpc.1', 'namespace-db.1'])
@@ -141,7 +142,8 @@ def fn(stack, status=None):
141142
bastion_step = Step(bastion, fn)
142143
plan = build_plan(description="Test", steps=[vpc_step, bastion_step])
143144

144-
plan.execute(walk)
145+
with self.assertRaises(PlanFailed):
146+
plan.execute(walk)
145147

146148
self.assertEquals(calls, ['namespace-vpc.1'])
147149
self.assertEquals(vpc_step.status, FAILED)
@@ -166,7 +168,7 @@ def fn(stack, status=None):
166168
bastion_step = Step(bastion, fn)
167169

168170
plan = build_plan(description="Test", steps=[vpc_step, bastion_step])
169-
self.assertTrue(plan.execute(walk))
171+
plan.execute(walk)
170172

171173
self.assertEquals(calls, ['namespace-vpc.1', 'namespace-bastion.1'])
172174

@@ -195,7 +197,8 @@ def fn(stack, status=None):
195197

196198
plan = build_plan(description="Test", steps=[
197199
vpc_step, bastion_step, db_step])
198-
self.assertFalse(plan.execute(walk))
200+
with self.assertRaises(PlanFailed):
201+
plan.execute(walk)
199202

200203
calls.sort()
201204

@@ -222,7 +225,7 @@ def fn(stack, status=None):
222225

223226
plan = build_plan(description="Test", steps=[
224227
vpc_step, bastion_step])
225-
self.assertTrue(plan.execute(walk))
228+
plan.execute(walk)
226229

227230
self.assertEquals(calls, ['namespace-vpc.1', 'namespace-bastion.1'])
228231

0 commit comments

Comments
 (0)