Helper method that takes expired tasks (where we didn't get a
        heartbeat until we reached a timeout) and puts them back into the
        QUEUED queue for re-execution if they're idempotent, i.e. retriable
        on JobTimeoutException. Otherwise, tasks are moved to the ERROR queue
        and an exception is logged.

def _worker_queue_expired_tasks(self):
        """
        Helper method that takes expired tasks (where we didn't get a
        heartbeat until we reached a timeout) and puts them back into the
        QUEUED queue for re-execution if they're idempotent, i.e. retriable
        on JobTimeoutException. Otherwise, tasks are moved to the ERROR queue
        and an exception is logged.
        """

        # Note that we use the lock both to unnecessarily prevent multiple
        # workers from requeueing expired tasks, as well as to space out
        # this task (i.e. we don't release the lock unless we've exhausted our
        # batch size, which will hopefully never happen)
        lock = Lock(self.connection, self._key('lock', 'queue_expired_tasks'),
                    timeout=self.config['REQUEUE_EXPIRED_TASKS_INTERVAL'])

        acquired = lock.acquire(blocking=False)
        if not acquired:
            return

        now = time.time()

        # Get a batch of expired tasks.
        task_data = self.scripts.get_expired_tasks(
            self.config['REDIS_PREFIX'],
            now - self.config['ACTIVE_TASK_UPDATE_TIMEOUT'],
            self.config['REQUEUE_EXPIRED_TASKS_BATCH_SIZE']
        )

        for (queue, task_id) in task_data:
            self.log.debug('expiring task', queue=queue, task_id=task_id)
            self._did_work = True
            try:
                task = Task.from_id(self.tiger, queue, ACTIVE, task_id)
                if task.should_retry_on(JobTimeoutException, logger=self.log):
                    self.log.info('queueing expired task',
                                  queue=queue, task_id=task_id)

                    # Task is idempotent and can be requeued. If the task
                    # already exists in the QUEUED queue, don't change its
                    # time.
                    task._move(from_state=ACTIVE,
                               to_state=QUEUED,
                               when=now,
                               mode='nx')
                else:
                    self.log.error('failing expired task',
                                   queue=queue, task_id=task_id)

                    # Assume the task can't be retried and move it to the error
                    # queue.
                    task._move(from_state=ACTIVE, to_state=ERROR, when=now)
            except TaskNotFound:
                # Either the task was requeued by another worker, or we
                # have a task without a task object.

                # XXX: Ideally, the following block should be atomic.
                if not self.connection.get(self._key('task', task_id)):
                    self.log.error('not found', queue=queue, task_id=task_id)
                    task = Task(self.tiger, queue=queue,
                                _data={'id': task_id}, _state=ACTIVE)
                    task._move()

        # Release the lock immediately if we processed a full batch. This way,
        # another process will be able to pick up another batch immediately
        # without waiting for the lock to time out.
        if len(task_data) == self.config['REQUEUE_EXPIRED_TASKS_BATCH_SIZE']:
            lock.release()