Adds a task to be processed later.

    Args:
      target: A callable which should be processed by one of the workers.
      args: A tuple of arguments to target.
      name: The name of this task. Used to identify tasks in the log.
      blocking: If True we block until the task is finished, otherwise we raise
        queue.Full
      inline: If set, process the task inline when the queue is full. This
        implies no blocking. Specifying inline helps if the worker tasks are
        blocked because it still ensures some progress is made. However, this
        can generally block the calling thread even after the threadpool is
        available again and therefore decrease efficiency.

    Raises:
      ThreadPoolNotStartedError: if the pool was not started yet.
      queue.Full: if the pool is full and can not accept new jobs.

def AddTask(self,
              target,
              args=(),
              name="Unnamed task",
              blocking=True,
              inline=True):
    """Adds a task to be processed later.

    Args:
      target: A callable which should be processed by one of the workers.
      args: A tuple of arguments to target.
      name: The name of this task. Used to identify tasks in the log.
      blocking: If True we block until the task is finished, otherwise we raise
        queue.Full
      inline: If set, process the task inline when the queue is full. This
        implies no blocking. Specifying inline helps if the worker tasks are
        blocked because it still ensures some progress is made. However, this
        can generally block the calling thread even after the threadpool is
        available again and therefore decrease efficiency.

    Raises:
      ThreadPoolNotStartedError: if the pool was not started yet.
      queue.Full: if the pool is full and can not accept new jobs.
    """
    if not self.started:
      raise ThreadPoolNotStartedError(self.name)

    # This pool should have no worker threads - just run the task inline.
    if self.max_threads == 0:
      target(*args)
      return

    if inline:
      blocking = False

    with self.lock:
      while True:
        # This check makes sure that the threadpool will add new workers
        # even if the queue is not full. This is needed for a scenario when
        # a fresh threadpool is created (say, with min_threads=1 and
        # max_threads=10) and 2 long-running tasks are added. The code below
        # will spawn a new worker for a second long-running task.
        if len(self) < self.max_threads:
          try:
            self._AddWorker()
          except (RuntimeError, threading.ThreadError) as e:
            logging.error(
                "Threadpool exception: "
                "Could not spawn worker threads: %s", e)

        try:
          # Push the task on the queue but raise if unsuccessful.
          self._queue.put((target, args, name, time.time()), block=False)
          return
        except queue.Full:
          # We increase the number of active threads if we do not exceed the
          # maximum _and_ our process CPU utilization is not too high. This
          # ensures that if the workers are waiting on IO we add more workers,
          # but we do not waste workers when tasks are CPU bound.
          if len(self) < self.max_threads:
            try:
              self._AddWorker()
              continue

            # If we fail to add a worker we should keep going anyway.
            except (RuntimeError, threading.ThreadError) as e:
              logging.error(
                  "Threadpool exception: "
                  "Could not spawn worker threads: %s", e)

          # If we need to process the task inline just break out of the loop,
          # therefore releasing the lock and run the task inline.
          if inline:
            break

          # We should block and try again soon.
          elif blocking:
            try:
              self._queue.put((target, args, name, time.time()),
                              block=True,
                              timeout=1)
              return
            except queue.Full:
              continue

          else:
            raise Full()

    # We don't want to hold the lock while running the task inline
    if inline:
      target(*args)