docs: def ready_nodes_iter(self, graph_copy):
          """Generate ready targets from the graph `graph_copy`.
  
          The input graph is mutated by this method, so it has to be a mutable
          copy of the graph (e.g. not original copy, or read-only view).
  
          Caller **must** call `done()` after processing every generated
          target, so additional ready targets can be added to the queue.
  
          The invariant: a target may be yielded from this generator only
          after all its descendant targets were notified "done".
          """
  
          def is_ready(target_name):
              """Return True if the node `target_name` is "ready" in the graph
                 `graph_copy`.
  
              "Ready" means that the graph doesn't contain any more nodes that
              `target_name` depends on (e.g. it has no successors).
              """
              try:
                  next(graph_copy.successors(target_name))
              except StopIteration:
                  return True
              return False
  
          ready_nodes = deque(sorted(
              target_name for target_name in graph_copy.nodes
              if is_ready(target_name)))
          produced_event = threading.Event()
          failed_event = threading.Event()
  
          def make_done_callback(target: Target):
              """Return a callable "done" notifier to
                 report a target as processed."""
  
              def done_notifier():
                  """Mark target as done, adding new ready nodes to queue"""
                  if graph_copy.has_node(target.name):
                      affected_nodes = list(sorted(
                          graph_copy.predecessors(target.name)))
                      graph_copy.remove_node(target.name)
                      ready_nodes.extend(
                          target_name for target_name in affected_nodes
                          if is_ready(target_name))
                      produced_event.set()
  
              return done_notifier
  
          def make_retry_callback(target: Target):
              """Return a callable "retry" notifier to
                 report a target as in need of retry.
                 Currently for tests we rebuild the target
                 when it's not necessary."""
  
              def retry_notifier():
                  """Mark target as retry, re-entering node to end of queue"""
                  if graph_copy.has_node(target.name):
                      ready_nodes.append(target.name)
                      produced_event.set()
  
              return retry_notifier
  
          def make_fail_callback(target: Target):
              """Return a callable "fail" notifier to
                 report a target as failed after all retries."""
  
              def fail_notifier(ex):
                  """Mark target as failed, taking it and ancestors
                     out of the queue"""
                  # TODO(Dana) separate "failed to build target" errors from
                  # "failed to run" errors.
                  # see: https://github.com/resonai/ybt/issues/124
                  if isinstance(ex, CalledProcessError):
                      sys.stdout.write(ex.stdout.decode('utf-8'))
                      sys.stderr.write(ex.stderr.decode('utf-8'))
                  if graph_copy.has_node(target.name):
                      self.failed_nodes[target.name] = ex
                      # removing all ancestors (nodes that depend on this one)
                      affected_nodes = get_ancestors(graph_copy, target.name)
                      graph_copy.remove_node(target.name)
                      for affected_node in affected_nodes:
                          if affected_node in self.skipped_nodes:
                              continue
                          if graph_copy.has_node(affected_node):
                              self.skipped_nodes.append(affected_node)
                              graph_copy.remove_node(affected_node)
                      if self.conf.continue_after_fail:
                          logger.info('Failed target: {} due to error: {}',
                                      target.name, ex)
                          produced_event.set()
                      else:
                          failed_event.set()
                          fatal('`{}\': {}', target.name, ex)
  
              return fail_notifier
  
          while True:
              while len(ready_nodes) == 0:
                  if graph_copy.order() == 0:
                      return
                  if failed_event.is_set():
                      return
                  produced_event.wait(0.5)
              produced_event.clear()
              next_node = ready_nodes.popleft()
              node = self.targets[next_node]
              node.done = make_done_callback(node)
              # TODO(bergden) retry assumes no need to update predecessors:
              # This means we don't support retries for targets that are
              # prerequisites of other targets (builds, installs)
              node.retry = make_retry_callback(node)
              node.fail = make_fail_callback(node)
              yield node