docs: 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)