class DeferredPoolTests(SynchronousTestCase): """ Tests for :class:`DeferredPool` """ def setUp(self): """ Default DeferredPool for each case """ self.pool = DeferredPool() def test_notify_when_empty_happens_immediately(self): """ When ``notify_when_empty`` is called, if the pool is empty, the deferred returned callbacks immediately. """ d = self.pool.notify_when_empty() self.successResultOf(d) def test_notify_when_empty_does_not_callback_previous_waiting(self): """ The second time ``notify_when_empty`` is called, it only callback the deferreds that were created after the first call. """ d1 = self.pool.notify_when_empty() self.successResultOf(d1) d2 = self.pool.notify_when_empty() self.successResultOf(d2) # no AlreadyCalledError? def test_notify_does_not_notify_until_pooled_deferreds_callback(self): """ If there are one or more deferreds in the pool, ``notify_when_empty`` does not notify until they are callbacked. """ holdup = Deferred() self.pool.add(holdup) d = self.pool.notify_when_empty() self.assertNoResult(d) holdup.callback('done') self.successResultOf(d) def test_notify_does_not_notify_until_pooled_deferreds_errbacks(self): """ If there are one or more deferreds in the pool, ``notify_when_empty`` does not notify until they are fired - works with errbacks too. """ holdup = Deferred() self.pool.add(holdup) d = self.pool.notify_when_empty() self.assertNoResult(d) holdup.errback(DummyException('hey')) self.successResultOf(d) # don't leave unhandled Deferred lying around self.failureResultOf(holdup) def test_notify_when_empty_notifies_all_waiting(self): """ All waiting Deferreds resulting from previous calls to ``notify_when_empty`` will callback as soon as the pool is empty. """ holdup = Deferred() self.pool.add(holdup) previous = [self.pool.notify_when_empty() for i in range(5)] for d in previous: self.assertNoResult(d) holdup.callback('done') for d in previous: self.successResultOf(d) def test_pooled_deferred_callbacks_not_obscured(self): """ The callbacks of pooled deferreds are not obscured by removing them from the pool. """ holdup = Deferred() self.pool.add(holdup) holdup.callback('done') self.assertEqual(self.successResultOf(holdup), 'done') def test_pooled_deferred_errbbacks_not_obscured(self): """ The errbacks of pooled deferreds are not obscured by removing them from the pool. """ holdup = Deferred() self.pool.add(holdup) holdup.errback(DummyException('hey')) self.failureResultOf(holdup, DummyException) def test_len(self): """ len(pool) returns number of deferreds waiting in the pool """ self.assertEqual(len(self.pool), 0) d = Deferred() self.pool.add(d) self.assertEqual(len(self.pool), 1) d.callback(None) self.assertEqual(len(self.pool), 0)
class SupervisorService(Service, object): """ A service which manages execution of launch configurations. :ivar IAuthenticator authenticator: Authenticator to use to obtain an auth token and service catalog. :ivar callable coiterate: coiterate function that will be passed to InMemoryUndoStack. :ivar str region: The region in which this supervisor is operating. :ivar DeferredPool deferred_pool: a pool in which to store deferreds that should be waited on """ name = "supervisor" def __init__(self, authenticator, region, coiterate, service_configs): self.authenticator = authenticator self.region = region self.coiterate = coiterate self.deferred_pool = DeferredPool() self.service_configs = service_configs def _get_request_bag(self, log, scaling_group): """ Builds :obj:`RequestBag` containing a bunch of useful stuff for making HTTP requests. """ tenant_id = scaling_group.tenant_id dispatcher = get_legacy_dispatcher(reactor, self.authenticator, log, self.service_configs) lb_region = config_value('regionOverrides.cloudLoadBalancers') def authenticate(): log.msg("Authenticating for tenant") d = self.authenticator.authenticate_tenant(tenant_id, log=log) def when_authenticated((auth_token, service_catalog)): bag = RequestBag( lb_region=lb_region or self.region, region=self.region, dispatcher=dispatcher, tenant_id=tenant_id, auth_token=auth_token, service_catalog=service_catalog, re_auth=authenticate, ) return bag return d.addCallback(when_authenticated) return authenticate() def execute_config(self, log, transaction_id, scaling_group, launch_config): """ see :meth:`ISupervisor.execute_config` """ log = log.bind(worker=launch_config['type'], tenant_id=scaling_group.tenant_id) assert launch_config['type'] == 'launch_server' undo = InMemoryUndoStack(self.coiterate) d = self._get_request_bag(log, scaling_group) def got_request_bag(request_bag): log.msg("Executing launch config.") return launch_server_v1.launch_server(log, request_bag, scaling_group, launch_config['args'], undo) d.addCallback(got_request_bag) def when_launch_server_completed(result): # XXX: Something should be done with this data. Currently only # enough to pass to the controller to store in the active state is # returned server_details, lb_info = result log.msg("Done executing launch config.", server_id=server_details['server']['id']) return { 'id': server_details['server']['id'], 'links': server_details['server']['links'], 'name': server_details['server']['name'], 'lb_info': lb_info } d.addCallback(when_launch_server_completed) def when_fails(result): log.msg("Encountered an error, rewinding {worker!r} job undo stack.", exc=result.value) ud = undo.rewind() ud.addCallback(lambda _: result) return ud d.addErrback(when_fails) return d def execute_delete_server(self, log, transaction_id, scaling_group, server): """ See :meth:`ISupervisor.execute_delete_server` """ log = log.bind(server_id=server['id'], tenant_id=scaling_group.tenant_id) d = self._get_request_bag(log, scaling_group) def got_request_bag(request_bag): log.msg("Executing delete server.") instance_details = server['id'], server['lb_info'] return launch_server_v1.delete_server(log, request_bag, instance_details) return d.addCallback(got_request_bag) def scrub_otter_metadata(self, log, transaction_id, tenant_id, server_id): """ See :meth:`ISupervisor.scrub_otter_metadata`. """ log = log.bind(server_id=server_id, tenant_id=tenant_id) d = self.authenticator.authenticate_tenant(tenant_id, log=log) log.msg("Authenticating for tenant") def when_authenticated((auth_token, service_catalog)): d = launch_server_v1.scrub_otter_metadata(log, auth_token, service_catalog, self.region, server_id) return d d.addCallback(when_authenticated) return d def validate_launch_config(self, log, tenant_id, launch_config): """ Validate launch config for a tenant """ def when_authenticated((auth_token, service_catalog)): log.msg('Validating launch server config') return validate_config.validate_launch_server_config( log, self.region, service_catalog, auth_token, launch_config['args']) if launch_config['type'] != 'launch_server': raise NotImplementedError('Validating launch config for launch_server only') log = log.bind(system='otter.supervisor.validate_launch_config', tenant_id=tenant_id) d = self.authenticator.authenticate_tenant(tenant_id, log=log) log.msg('Authenticating for tenant') return d.addCallback(when_authenticated) def stopService(self): """ Returns a deferred that succeeds when the :class:`DeferredPool` is empty """ super(SupervisorService, self).stopService() return self.deferred_pool.notify_when_empty() def health_check(self): """ Check if supervisor is healthy. In this case, just return number of jobs currently running. """ return True, {'jobs': len(self.deferred_pool)}
class SupervisorService(object, Service): """ A service which manages execution of launch configurations. :ivar callable auth_function: authentication function to use to obtain an auth token and service catalog. Should accept a tenant ID. :ivar callable coiterate: coiterate function that will be passed to InMemoryUndoStack. :ivar DeferredPool deferred_pool: a pool in which to store deferreds that should be waited on """ name = "supervisor" def __init__(self, auth_function, region, coiterate): self.auth_function = auth_function self.region = region self.coiterate = coiterate self.deferred_pool = DeferredPool() def execute_config(self, log, transaction_id, scaling_group, launch_config): """ see :meth:`ISupervisor.execute_config` """ job_id = generate_job_id(scaling_group.uuid) completion_d = Deferred() log = log.bind(job_id=job_id, worker=launch_config['type'], tenant_id=scaling_group.tenant_id) assert launch_config['type'] == 'launch_server' undo = InMemoryUndoStack(self.coiterate) def when_fails(result): log.msg("Encountered an error, rewinding {worker!r} job undo stack.", exc=result.value) ud = undo.rewind() ud.addCallback(lambda _: result) return ud completion_d.addErrback(when_fails) log.msg("Authenticating for tenant") d = self.auth_function(scaling_group.tenant_id, log=log) def when_authenticated((auth_token, service_catalog)): log.msg("Executing launch config.") return launch_server_v1.launch_server( log, self.region, scaling_group, service_catalog, auth_token, launch_config['args'], undo) d.addCallback(when_authenticated) def when_launch_server_completed(result): # XXX: Something should be done with this data. Currently only enough # to pass to the controller to store in the active state is returned server_details, lb_info = result log.msg("Done executing launch config.", server_id=server_details['server']['id']) return { 'id': server_details['server']['id'], 'links': server_details['server']['links'], 'name': server_details['server']['name'], 'lb_info': lb_info } d.addCallback(when_launch_server_completed) self.deferred_pool.add(d) d.chainDeferred(completion_d) return succeed((job_id, completion_d)) def execute_delete_server(self, log, transaction_id, scaling_group, server): """ see :meth:`ISupervisor.execute_delete_server` """ log = log.bind(server_id=server['id'], tenant_id=scaling_group.tenant_id) # authenticate for tenant def when_authenticated((auth_token, service_catalog)): return launch_server_v1.delete_server( log, self.region, service_catalog, auth_token, (server['id'], server['lb_info'])) d = self.auth_function(scaling_group.tenant_id, log=log) log.msg("Authenticating for tenant") d.addCallback(when_authenticated) self.deferred_pool.add(d) return d def validate_launch_config(self, log, tenant_id, launch_config): """ Validate launch config for a tenant """ def when_authenticated((auth_token, service_catalog)): log.msg('Validating launch server config') return validate_config.validate_launch_server_config( log, self.region, service_catalog, auth_token, launch_config['args']) if launch_config['type'] != 'launch_server': raise NotImplementedError('Validating launch config for launch_server only') log = log.bind(system='otter.supervisor.validate_launch_config', tenant_id=tenant_id) d = self.auth_function(tenant_id, log=log) log.msg('Authenticating for tenant') return d.addCallback(when_authenticated) def stopService(self): """ Returns a deferred that succeeds when the :class:`DeferredPool` is empty """ super(SupervisorService, self).stopService() return self.deferred_pool.notify_when_empty() def health_check(self): """ Check if supervisor is healthy. In this case, just return number of jobs currently running. """ return True, {'jobs': len(self.deferred_pool)}
class SupervisorService(Service, object): """ A service which manages execution of launch configurations. :ivar IAuthenticator authenticator: Authenticator to use to obtain an auth token and service catalog. :ivar callable coiterate: coiterate function that will be passed to InMemoryUndoStack. :ivar str region: The region in which this supervisor is operating. :ivar DeferredPool deferred_pool: a pool in which to store deferreds that should be waited on """ name = "supervisor" def __init__(self, authenticator, region, coiterate, service_configs): self.authenticator = authenticator self.region = region self.coiterate = coiterate self.deferred_pool = DeferredPool() self.service_configs = service_configs def _get_request_bag(self, log, scaling_group): """ Builds :obj:`RequestBag` containing a bunch of useful stuff for making HTTP requests. """ tenant_id = scaling_group.tenant_id dispatcher = get_legacy_dispatcher(reactor, self.authenticator, log, self.service_configs) lb_region = config_value('regionOverrides.cloudLoadBalancers') def authenticate(): log.msg("Authenticating for tenant") d = self.authenticator.authenticate_tenant(tenant_id, log=log) def when_authenticated((auth_token, service_catalog)): bag = RequestBag( lb_region=lb_region or self.region, region=self.region, dispatcher=dispatcher, tenant_id=tenant_id, auth_token=auth_token, service_catalog=service_catalog, re_auth=authenticate, ) return bag return d.addCallback(when_authenticated) return authenticate() def execute_config(self, log, transaction_id, scaling_group, launch_config): """ see :meth:`ISupervisor.execute_config` """ log = log.bind(worker=launch_config['type'], tenant_id=scaling_group.tenant_id) assert launch_config['type'] == 'launch_server' undo = InMemoryUndoStack(self.coiterate) d = self._get_request_bag(log, scaling_group) def got_request_bag(request_bag): log.msg("Executing launch config.") return launch_server_v1.launch_server(log, request_bag, scaling_group, launch_config['args'], undo) d.addCallback(got_request_bag) def when_launch_server_completed(result): # XXX: Something should be done with this data. Currently only # enough to pass to the controller to store in the active state is # returned server_details, lb_info = result log.msg("Done executing launch config.", server_id=server_details['server']['id']) return { 'id': server_details['server']['id'], 'links': server_details['server']['links'], 'name': server_details['server']['name'], 'lb_info': lb_info } d.addCallback(when_launch_server_completed) def when_fails(result): log.msg("Encountered an error, rewinding {worker!r} job undo stack.", exc=result.value) ud = undo.rewind() ud.addCallback(lambda _: result) return ud d.addErrback(when_fails) return d def execute_delete_server(self, log, transaction_id, scaling_group, server): """ See :meth:`ISupervisor.execute_delete_server` """ log = log.bind(server_id=server['id'], tenant_id=scaling_group.tenant_id) d = self._get_request_bag(log, scaling_group) def got_request_bag(request_bag): log.msg("Executing delete server.") instance_details = server['id'], server['lb_info'] return launch_server_v1.delete_server(log, request_bag, instance_details) return d.addCallback(got_request_bag) def scrub_otter_metadata(self, log, transaction_id, tenant_id, server_id): """ See :meth:`ISupervisor.scrub_otter_metadata`. """ log = log.bind(server_id=server_id, tenant_id=tenant_id) d = self.authenticator.authenticate_tenant(tenant_id, log=log) log.msg("Authenticating for tenant") def when_authenticated((auth_token, service_catalog)): d = launch_server_v1.scrub_otter_metadata(log, auth_token, service_catalog, self.region, server_id) return d d.addCallback(when_authenticated) return d def validate_launch_config(self, log, tenant_id, launch_config): """ Validate launch config for a tenant """ def do_validate(validation_method, auth_token, service_catalog): return validation_method(log, self.region, service_catalog, auth_token, launch_config['args']) def validate_launch_server_config((auth_token, service_catalog)): log.msg('Validating launch server config') return do_validate(validate_config.validate_launch_server_config, auth_token, service_catalog) def validate_launch_stack_config((auth_token, service_catalog)): log.msg('Validating launch stack config') return do_validate(validate_config.validate_launch_stack_config, auth_token, service_catalog) if launch_config['type'] == 'launch_server': when_authenticated = validate_launch_server_config elif launch_config['type'] == 'launch_stack': when_authenticated = validate_launch_stack_config else: raise NotImplementedError('Validating launch config for ' 'launch_server or launch_stack only') log = log.bind(system='otter.supervisor.validate_launch_config', tenant_id=tenant_id) d = self.authenticator.authenticate_tenant(tenant_id, log=log) log.msg('Authenticating for tenant') return d.addCallback(when_authenticated) def stopService(self): """ Returns a deferred that succeeds when the :class:`DeferredPool` is empty """ super(SupervisorService, self).stopService() return self.deferred_pool.notify_when_empty() def health_check(self): """ Check if supervisor is healthy. In this case, just return number of jobs currently running. """ return True, {'jobs': len(self.deferred_pool)}