class NonConcurrentlyTests(SynchronousTestCase): """Tests for :func:`non_concurrently`.""" def setUp(self): self.locks = Reference(pset()) def _get_locks(self): """Get the locks set.""" return sync_perform(_get_dispatcher(), self.locks.read()) def _add_lock(self, value): """Add an item to the locks set.""" return sync_perform(_get_dispatcher(), self.locks.modify(lambda cc: cc.add(value))) def test_success(self): """ :func:`non_concurrently` returns the result of the passed effect, and adds the ``key`` to the ``locks`` while executing. """ dispatcher = _get_dispatcher() def execute_stuff(): self.assertEqual(self._get_locks(), pset(['the-key'])) return 'foo' eff = Effect(Func(execute_stuff)) non_c_eff = non_concurrently(self.locks, 'the-key', eff) self.assertEqual(sync_perform(dispatcher, non_c_eff), 'foo') # after the effect completes, its lock is released self.assertEqual(self._get_locks(), pset([])) def test_refuses_concurrency(self): """ :func:`non_concurrently` raises :obj:`ConcurrentError` when the key is already locked. """ self._add_lock('the-key') eff = Effect(Error(RuntimeError('foo'))) non_c_eff = non_concurrently(self.locks, 'the-key', eff) self.assertRaises( ConcurrentError, sync_perform, _get_dispatcher(), non_c_eff) self.assertEqual(self._get_locks(), pset(['the-key'])) def test_cleans_up_on_exception(self): """ When the effect results in error, the key is still removed from the locked set. """ dispatcher = _get_dispatcher() eff = Effect(Error(RuntimeError('foo!'))) non_c_eff = non_concurrently(self.locks, 'the-key', eff) e = self.assertRaises(RuntimeError, sync_perform, dispatcher, non_c_eff) self.assertEqual(str(e), 'foo!') self.assertEqual(self._get_locks(), pset([]))
def __init__(self, log, dispatcher, num_buckets, partitioner_factory, build_timeout, interval, limited_retry_iterations, step_limits, converge_all_groups=converge_all_groups): """ :param log: a bound log :param dispatcher: The dispatcher to use to perform effects. :param int buckets: the number of logical `buckets` which are be shared between all Otter nodes running this service. The buckets will be partitioned up between nodes to detirmine which nodes should work on which groups. :param partitioner_factory: Callable of (all_buckets, log, callback) which should create an :obj:`Partitioner` to distribute the buckets. :param number build_timeout: number of seconds to wait for servers to be in building before it's is timed out and deleted :param interval: Interval between convergence steps, per group. :param callable converge_all_groups: like :func:`converge_all_groups`, to be used for test injection only :param int limited_retry_iterations: number of iterations to wait for LIMITED_RETRY steps :param dict step_limits: Mapping of step name to number of executions allowed in a convergence cycle """ MultiService.__init__(self) self.log = log.bind(otter_service='converger') self._dispatcher = dispatcher self._buckets = range(num_buckets) self.partitioner = partitioner_factory( buckets=self._buckets, log=self.log, got_buckets=self.buckets_acquired) self.partitioner.setServiceParent(self) self.build_timeout = build_timeout self._converge_all_groups = converge_all_groups self.interval = interval self.limited_retry_iterations = limited_retry_iterations self.step_limits = get_step_limits_from_conf(step_limits) # ephemeral mutable state self.currently_converging = Reference(pset()) self.recently_converged = Reference(pmap()) # Groups we're waiting on temporarily, and may give up on. self.waiting = Reference(pmap()) # {group_id: num_iterations_waited}
def simple_intents(): return [ Authenticate(None, None, None), InvalidateToken(None, None), Request(method='GET', url='http://example.com/'), Retry(effect=Effect(Constant(None)), should_retry=lambda e: False), Delay(0), Constant(None), ReadReference(ref=Reference(None)), ]
def setUp(self): self.locks = Reference(pset())