diff --git a/nucypher/characters.py b/nucypher/characters.py index 7f8faf67d..461144e2f 100644 --- a/nucypher/characters.py +++ b/nucypher/characters.py @@ -314,24 +314,58 @@ class Character: self._node_ids_to_learn_about_immediately.update(canonical_addresses) # hmmmm self.learn_about_nodes_now() - def block_until_nodes_are_known(self, canonical_addresses: Set, timeout=10, allow_missing=0, - learn_on_this_thread=False): + # TODO: Dehydrate these next two methods. + + def block_until_number_of_known_nodes_is(self, number_of_nodes_to_know: int, + timeout=10, + learn_on_this_thread=False): start = maya.now() starting_round = self._learning_round while True: + rounds_undertaken = self._learning_round - starting_round + if len(self._known_nodes) >= number_of_nodes_to_know: + if rounds_undertaken: + self.log.info("Learned about enough nodes after {} rounds.".format(rounds_undertaken)) + return True + if not self._learning_task.running: self.log.warning("Blocking to learn about nodes, but learning loop isn't running.") if learn_on_this_thread: self.learn_from_teacher_node(eager=True) - rounds_undertaken = self._learning_round - starting_round - if (maya.now() - start).seconds < timeout: - if canonical_addresses.issubset(self._known_nodes): - self.log.info("Learned about all nodes after {} rounds.".format(rounds_undertaken)) - return True + + if (maya.now() - start).seconds > timeout: + if not self._learning_task.running: + raise self.NotEnoughUrsulas( + "We didn't discover any nodes because the learning loop isn't running. Start it with start_learning().") else: - time.sleep(.1) + raise self.NotEnoughUrsulas("After {} seconds and {} rounds, didn't find {} nodes".format( + timeout, rounds_undertaken, number_of_nodes_to_know)) else: + time.sleep(.1) + + def block_until_specific_nodes_are_known(self, + canonical_addresses: Set, + timeout=10, + allow_missing=0, + learn_on_this_thread=False): + start = maya.now() + starting_round = self._learning_round + + while True: + rounds_undertaken = self._learning_round - starting_round + if canonical_addresses.issubset(self._known_nodes): + if rounds_undertaken: + self.log.info("Learned about all nodes after {} rounds.".format(rounds_undertaken)) + return True + + if not self._learning_task.running: + self.log.warning("Blocking to learn about nodes, but learning loop isn't running.") + if learn_on_this_thread: + self.learn_from_teacher_node(eager=True) + + if (maya.now() - start).seconds > timeout: + still_unknown = canonical_addresses.difference(self._known_nodes) if len(still_unknown) <= allow_missing: @@ -343,6 +377,9 @@ class Character: raise self.NotEnoughUrsulas("After {} seconds and {} rounds, didn't find these {} nodes: {}".format( timeout, rounds_undertaken, len(still_unknown), still_unknown)) + else: + time.sleep(.1) + def learn_from_teacher_node(self, eager=True): """ Sends a request to node_url to find out about known nodes. @@ -696,10 +733,26 @@ class Alice(Character, PolicyAuthor): # Users may decide to inject some market strategies here. # # TODO: 289 + + # If we're federated only, we need to block to make sure we have enough nodes. + if self.federated_only and len(self._known_nodes) < n: + good_to_go = self.block_until_number_of_known_nodes_is(n, learn_on_this_thread=True) + if not good_to_go: + raise ValueError( + "To make a Policy in federated mode, you need to know about\ + all the Ursulas you need (in this case, {}); there's no other way to\ + know which nodes to use. Either pass them here or when you make\ + the Policy, or run the learning loop on a network with enough Ursulas.".format(self.n)) + + if len(handpicked_ursulas) < n: + number_of_ursulas_needed = n - len(handpicked_ursulas) + new_ursulas = random.sample(list(self._known_nodes.values()), number_of_ursulas_needed) + handpicked_ursulas.update(new_ursulas) + policy.make_arrangements(network_middleware=self.network_middleware, deposit=deposit, expiration=expiration, - ursulas=ursulas, + handpicked_ursulas=handpicked_ursulas, ) # REST call happens here, as does population of TreasureMap. @@ -784,14 +837,14 @@ class Bob(Character): if block: if new_thread: - return threads.deferToThread(self.block_until_nodes_are_known, unknown_ursulas, + return threads.deferToThread(self.block_until_specific_nodes_are_known, unknown_ursulas, timeout=timeout, allow_missing=allow_missing) else: - self.block_until_nodes_are_known(unknown_ursulas, - timeout=timeout, - allow_missing=allow_missing, - learn_on_this_thread=True) + self.block_until_specific_nodes_are_known(unknown_ursulas, + timeout=timeout, + allow_missing=allow_missing, + learn_on_this_thread=True) return unknown_ursulas, known_ursulas @@ -913,7 +966,7 @@ class Bob(Character): verifying=alice_verifying_key) hrac, map_id = self.construct_hrac_and_map_id(alice_verifying_key, data_source.label) - self.follow_treasure_map(map_id=map_id) + self.follow_treasure_map(map_id=map_id, block=True) work_orders = self.generate_work_orders(map_id, message_kit.capsule) diff --git a/tests/characters/test_bob_handles_frags.py b/tests/characters/test_bob_handles_frags.py index 70f9c735c..339cda869 100644 --- a/tests/characters/test_bob_handles_frags.py +++ b/tests/characters/test_bob_handles_frags.py @@ -87,7 +87,7 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f bob.start_learning_loop() # ...and block until the unknown_nodes have all been found. - yield threads.deferToThread(bob.block_until_nodes_are_known, unknown_nodes) + yield threads.deferToThread(bob.block_until_specific_nodes_are_known, unknown_nodes) # ...and he now has no more unknown_nodes. print(len(bob._known_nodes)) diff --git a/tests/fixtures.py b/tests/fixtures.py index fb6218bfa..95c9c970f 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -81,7 +81,7 @@ def enacted_federated_policy(idle_federated_policy, ursulas): idle_federated_policy.make_arrangements(network_middleware, deposit=deposit, expiration=contract_end_datetime, - ursulas=ursulas) + handpicked_ursulas=ursulas) idle_federated_policy.enact(network_middleware) # REST call happens here, as does population of TreasureMap. return idle_federated_policy