Skip to content

Commit a0eaeed

Browse files
committed
Merge pull request #263 from Codeacious/jrm_node_reconfigure_fix
Made service reconfiguration slightly better
2 parents 1573fdf + a991b80 commit a0eaeed

File tree

5 files changed

+178
-20
lines changed

5 files changed

+178
-20
lines changed

tests/core/service_test.py

+71-10
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,28 @@ def test_restore_state(self):
149149
self.instances.restore_state.return_value)
150150
self.service.enable.assert_called_with()
151151

152+
def test_update_node_pool_enabled(self):
153+
autospec_method(self.service.repair)
154+
self.service.enabled = True
155+
node_pool = mock.Mock()
156+
157+
self.service.update_node_pool(node_pool)
158+
159+
self.service.instances.update_node_pool.assert_called_once_with(node_pool)
160+
self.service.instances.clear_extra.assert_called_once_with()
161+
self.service.repair.assert_called_once_with()
162+
163+
def test_update_node_pool_disabled(self):
164+
autospec_method(self.service.repair)
165+
self.service.enabled = False
166+
node_pool = mock.Mock()
167+
168+
self.service.update_node_pool(node_pool)
169+
170+
self.service.instances.update_node_pool.assert_called_once_with(node_pool)
171+
self.service.instances.clear_extra.assert_called_once_with()
172+
assert not self.service.repair.called
173+
152174

153175
class ServiceCollectionTestCase(TestCase):
154176

@@ -162,25 +184,64 @@ def _add_service(self):
162184
self.collection.services.update(
163185
(serv.name, serv) for serv in self.service_list)
164186

187+
def test_build_with_new_config(self):
188+
new_config = mock.Mock(
189+
name='i_come_from_the_land_of_ice_and_snow',
190+
count=42)
191+
new_service = mock.Mock(config=new_config)
192+
old_service = mock.Mock()
193+
with mock.patch.object(self.collection, 'get_by_name', return_value=old_service) \
194+
as get_patch:
195+
assert not self.collection._build(new_service)
196+
assert not old_service.update_node_pool.called
197+
get_patch.assert_called_once_with(new_config.name)
198+
199+
def test_build_with_same_config(self):
200+
config = mock.Mock(
201+
name='hamsteak',
202+
count=413)
203+
old_service = mock.Mock(config=config)
204+
new_service = mock.Mock(config=config)
205+
with mock.patch.object(self.collection, 'get_by_name', return_value=old_service) \
206+
as get_patch:
207+
assert self.collection._build(new_service)
208+
get_patch.assert_called_once_with(config.name)
209+
old_service.update_node_pool.assert_called_once_with(new_service.instances.node_pool)
210+
assert_equal(old_service.instances.context, new_service.instances.context)
211+
212+
def test_build_with_diff_count(self):
213+
name = 'ni'
214+
old_config = mock.Mock(
215+
count=77)
216+
new_config = mock.Mock(
217+
count=1111111111111)
218+
new_eq = lambda s, o: (s.name == o.name and s.count == o.count)
219+
old_config.__eq__ = new_eq
220+
new_config.__eq__ = new_eq
221+
# We have to do this, since name is an actual kwarg for mock.Mock().
222+
old_config.name = name
223+
new_config.name = name
224+
old_service = mock.Mock(config=old_config)
225+
new_service = mock.Mock(config=new_config)
226+
with mock.patch.object(self.collection, 'get_by_name', return_value=old_service) \
227+
as get_patch:
228+
assert self.collection._build(new_service)
229+
get_patch.assert_called_once_with(new_service.config.name)
230+
old_service.update_node_pool.assert_called_once_with(new_service.instances.node_pool)
231+
assert_equal(old_service.instances.context, new_service.instances.context)
232+
165233
@mock.patch('tron.core.service.Service', autospec=True)
166234
def test_load_from_config(self, mock_service):
167235
autospec_method(self.collection.get_names)
168-
autospec_method(self.collection.add)
236+
autospec_method(self.collection.services.add)
169237
service_configs = {'a': mock.Mock(), 'b': mock.Mock()}
170238
context = mock.create_autospec(command_context.CommandContext)
171239
result = list(self.collection.load_from_config(service_configs, context))
172240
expected = [mock.call(config, context)
173241
for config in service_configs.itervalues()]
174242
assert_mock_calls(expected, mock_service.from_config.mock_calls)
175-
expected = [mock.call(s) for s in result]
176-
assert_mock_calls(expected, self.collection.add.mock_calls)
177-
178-
def test_add(self):
179-
self.collection.services = mock.MagicMock()
180-
service = mock.Mock()
181-
result = self.collection.add(service)
182-
self.collection.services.replace.assert_called_with(service)
183-
assert_equal(result, self.collection.services.replace.return_value)
243+
expected = [mock.call(s, self.collection._build) for s in result]
244+
assert_mock_calls(expected, self.collection.services.add.mock_calls)
184245

185246
def test_restore_state(self):
186247
state_count = 2

tests/core/serviceinstance_test.py

+42
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,48 @@ def test_get_by_number(self):
502502
instance = self.collection.get_by_number(3)
503503
assert_equal(instance, instances[3])
504504

505+
def test_update_node_pool_same_pool(self):
506+
self.collection.update_node_pool(self.collection.node_pool)
507+
assert not self.collection.node_pool.get_by_name.called
508+
509+
def test_update_node_pool_diff_pool_same_nodes(self):
510+
new_instances = [mock.Mock(), mock.Mock()]
511+
self.collection.instances = new_instances
512+
nodes = [instance.node for instance in new_instances]
513+
node_pool = mock.Mock(get_by_name=mock.Mock(side_effect=iter(nodes)))
514+
515+
self.collection.update_node_pool(node_pool)
516+
517+
assert_equal(self.collection.node_pool, node_pool)
518+
calls = [mock.call(instance.node.name) for instance in new_instances]
519+
node_pool.get_by_name.assert_calls(calls)
520+
assert not any([instance.stop.called for instance in new_instances])
521+
assert_equal(self.collection.instances, new_instances)
522+
523+
def test_update_node_pool_diff_everything(self):
524+
new_instances = [mock.Mock(), mock.Mock()]
525+
self.collection.instances = [mock.Mock(), mock.Mock()]
526+
nodes = [instance.node for instance in new_instances]
527+
node_pool = mock.Mock(get_by_name=mock.Mock(side_effect=iter(nodes)))
528+
529+
self.collection.update_node_pool(node_pool)
530+
531+
assert_equal(self.collection.node_pool, node_pool)
532+
calls = [mock.call(instance.node.name) for instance in self.collection.instances]
533+
node_pool.get_by_name.assert_calls(calls)
534+
assert all([instance.stop.called for instance in self.collection.instances])
535+
assert_equal(self.collection.instances, [])
536+
537+
def test_clear_extra(self):
538+
instance_a = mock.Mock()
539+
instance_b = mock.Mock()
540+
instance_c = mock.Mock()
541+
self.collection.instances = [instance_a, instance_b, instance_c]
542+
self.collection.config.count = 2
543+
self.collection.clear_extra()
544+
assert_equal(self.collection.instances, [instance_a, instance_b])
545+
instance_c.stop.assert_called_once_with()
546+
505547

506548
if __name__ == "__main__":
507549
run()

tests/trond_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def test_node_reconfig(self):
153153
self.sandbox.tronfig(second_config)
154154

155155
sandbox.wait_on_state(self.client.service, service_url,
156-
service.ServiceState.DISABLED)
156+
service.ServiceState.FAILED)
157157

158158
job_url = self.client.get_url('MASTER.a_job')
159159
def wait_on_next_run():

tron/core/service.py

+40-9
Original file line numberDiff line numberDiff line change
@@ -156,28 +156,59 @@ def restore_state(self, state_data):
156156
(self.enable if state_data.get('enabled') else self.disable)()
157157
self.event_recorder.info("restored")
158158

159+
def update_node_pool(self, node_pool):
160+
self.instances.update_node_pool(node_pool)
161+
self.instances.clear_extra()
162+
if self.enabled:
163+
self.repair()
164+
159165

160166
class ServiceCollection(object):
161167
"""A collection of services."""
162168

163169
def __init__(self):
164170
self.services = collections.MappingCollection('services')
165171

172+
def _build(self, new_service):
173+
"""A method to be used as an update function for MappingCollection.add.
174+
This function attempts to load an old Service object, and if one
175+
exists, see if we don't actually have to use an entirely new
176+
Service object on reconfiguration.
177+
178+
To do this, we first check if the number of instances (config.count) is
179+
different, as we have a method to fix this when updating the service's
180+
node pool. Then, if the configs are now equal, we can simply update
181+
the node pool of the old Service object and be done- no need for the
182+
new Service object. Otherwise, we use the new object as normal.
183+
"""
184+
old_service = self.get_by_name(new_service.config.name)
185+
186+
if not old_service:
187+
log.debug("Building new service %s", new_service.config.name)
188+
return False
189+
190+
if old_service.config.count != new_service.config.count:
191+
old_service.config.count = new_service.config.count
192+
193+
if old_service.config == new_service.config:
194+
log.debug("Updating service %s\'s node pool" % new_service.config.name)
195+
old_service.instances.context = new_service.instances.context
196+
old_service.update_node_pool(new_service.instances.node_pool)
197+
return True
198+
else:
199+
log.debug("Building new service %s", new_service.config.name)
200+
old_service.disable()
201+
return False
202+
166203
def load_from_config(self, service_configs, context):
167204
"""Apply a configuration to this collection and return a generator of
168205
services which were added.
169206
"""
170207
self.services.filter_by_name(service_configs.keys())
171208

172-
def build(config):
173-
log.debug("Building new service %s", config.name)
174-
return Service.from_config(config, context)
175-
176-
seq = (build(config) for config in service_configs.itervalues())
177-
return itertools.ifilter(self.add, seq)
178-
179-
def add(self, service):
180-
return self.services.replace(service)
209+
seq = (Service.from_config(config, context)
210+
for config in service_configs.itervalues())
211+
return itertools.ifilter(lambda e: self.services.add(e, self._build), seq)
181212

182213
def restore_state(self, service_state_data):
183214
self.services.restore_state(service_state_data)

tron/core/serviceinstance.py

+24
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,30 @@ def get_by_number(self, instance_number):
461461
if instance.instance_number == instance_number:
462462
return instance
463463

464+
def update_node_pool(self, node_pool):
465+
"""Attempt to load a new node pool from the NodePoolRepository, and
466+
remove instances that no longer have their node in the NodePool."""
467+
if node_pool == self.node_pool:
468+
return
469+
470+
self.node_pool = node_pool
471+
472+
def _trim_old_nodes():
473+
for instance in self.instances:
474+
new_node = self.node_pool.get_by_name(instance.node.name)
475+
if new_node != instance.node:
476+
instance.stop()
477+
else:
478+
yield instance
479+
480+
self.instances = list(_trim_old_nodes())
481+
482+
def clear_extra(self):
483+
"""Clear out instances if too many exist."""
484+
for i in range(0, self.missing, -1):
485+
instance = self.instances.pop()
486+
instance.stop()
487+
464488
@property
465489
def missing(self):
466490
return self.config.count - len(self.instances)

0 commit comments

Comments
 (0)