|  | @@ -0,0 +1,140 @@
 | 
	
		
			
				|  |  | +"""
 | 
	
		
			
				|  |  | +=========================
 | 
	
		
			
				|  |  | +Cancelable tasks overview
 | 
	
		
			
				|  |  | +=========================
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +For long-running :class:`Task`'s, it can be desirable to support
 | 
	
		
			
				|  |  | +aborting during execution. Of course, these tasks should be built to
 | 
	
		
			
				|  |  | +support abortion specifically.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +The :class:`CancelableTask` serves as a base class for all :class:`Task`
 | 
	
		
			
				|  |  | +objects that should support cancellation by producers.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +* Producers may invoke the :meth:`cancel` method on
 | 
	
		
			
				|  |  | +  :class:`CancelableAsyncResult` instances, to request abortion.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +* Consumers (workers) should periodically check (and honor!) the
 | 
	
		
			
				|  |  | +  :meth:`is_cancelled` method at controlled points in their task's
 | 
	
		
			
				|  |  | +  :meth:`run` method. The more often, the better.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +The necessary intermediate communication is dealt with by the
 | 
	
		
			
				|  |  | +:class:`CancelableTask` implementation.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +Usage example
 | 
	
		
			
				|  |  | +-------------
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +In the consumer:
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.. code-block:: python
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +   from celery.contrib.cancelable import CancelableTask
 | 
	
		
			
				|  |  | +   def MyLongRunningTask(CancelableTask):
 | 
	
		
			
				|  |  | +       def run(self, **kwargs):
 | 
	
		
			
				|  |  | +           logger = self.get_logger()
 | 
	
		
			
				|  |  | +           results = []
 | 
	
		
			
				|  |  | +           for x in xrange(100):
 | 
	
		
			
				|  |  | +               # Check after every 5 loops..
 | 
	
		
			
				|  |  | +               if x % 5 == 0:  # alternatively, check when some timer is due
 | 
	
		
			
				|  |  | +                   if self.is_cancelled(**kwargs):
 | 
	
		
			
				|  |  | +                       # Respect the cancelled status and terminate
 | 
	
		
			
				|  |  | +                       # gracefully
 | 
	
		
			
				|  |  | +                       logger.warning('Task cancelled.')
 | 
	
		
			
				|  |  | +                       return None
 | 
	
		
			
				|  |  | +               y = do_something_expensive(x)
 | 
	
		
			
				|  |  | +               results.append(y)
 | 
	
		
			
				|  |  | +           logger.info('Task finished.')
 | 
	
		
			
				|  |  | +           return results
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +In the producer:
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.. code-block:: python
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +   from myproject.tasks import MyLongRunningTask
 | 
	
		
			
				|  |  | +   def myview(request):
 | 
	
		
			
				|  |  | +       async_result = MyLongRunningTask.delay()
 | 
	
		
			
				|  |  | +       # async_result is of type CancelableAsyncResult
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +       # After 10 seconds, cancel the task
 | 
	
		
			
				|  |  | +       time.sleep(10)
 | 
	
		
			
				|  |  | +       async_result.cancel()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +       ...
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +After the `async_result.cancel()` call, the task execution is not
 | 
	
		
			
				|  |  | +aborted immediately. In fact, it is not guaranteed to abort at all. Keep
 | 
	
		
			
				|  |  | +checking the `async_result` status, or call `async_result.wait()` to
 | 
	
		
			
				|  |  | +have it block until the task is finished.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +"""
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +from celery.task.base import Task
 | 
	
		
			
				|  |  | +from celery.result import AsyncResult
 | 
	
		
			
				|  |  | +from multiprocessing import Process, Event
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +""" Task States
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +.. data:: CANCELLED
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    Task is cancelled (typically by the producer) and should be
 | 
	
		
			
				|  |  | +    cancelled as soon as possible.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +"""
 | 
	
		
			
				|  |  | +CANCELLED = "CANCELLED"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class CancelableAsyncResult(AsyncResult):
 | 
	
		
			
				|  |  | +    """Represents a cancelable result.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    Specifically, this gives the AsyncResult a cancel() method, which sets
 | 
	
		
			
				|  |  | +    the state of the underlying Task to "CANCELLED".
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    """
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def is_cancelled(self):
 | 
	
		
			
				|  |  | +        """Returns ``True'' if the task is (being) cancelled."""
 | 
	
		
			
				|  |  | +        return self.backend.get_status(self.task_id) == CANCELLED
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def cancel(self):
 | 
	
		
			
				|  |  | +        """Set the state of the task to cancelled.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        TODO: Be more descriptive. What does this mean for the worker?
 | 
	
		
			
				|  |  | +        TODO: What does the method return?
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        # TODO: store_result requires all four arguments to be set, but only status should be updated here
 | 
	
		
			
				|  |  | +        return self.backend.store_result(self.task_id, result=None, status=CANCELLED, traceback=None)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class CancelableTask(Task):
 | 
	
		
			
				|  |  | +    """A celery task that serves as a base class for all :class:`Task`'s
 | 
	
		
			
				|  |  | +    that support aborting during execution.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    All subclasses of :class:`CancelableTask` must call the
 | 
	
		
			
				|  |  | +    :meth:`is_cancelled` method periodically and act accordingly when
 | 
	
		
			
				|  |  | +    the call evaluates to ``True''.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    """
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @classmethod
 | 
	
		
			
				|  |  | +    def AsyncResult(cls, task_id):
 | 
	
		
			
				|  |  | +        """Returns the accompanying CancelableAsyncResult instance."""
 | 
	
		
			
				|  |  | +        return CancelableAsyncResult(task_id, backend=cls.backend)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def is_cancelled(self, **kwargs):
 | 
	
		
			
				|  |  | +        """Checks against the backend whether this
 | 
	
		
			
				|  |  | +        :class:`CancelableAsyncResult` is :const:`CANCELLED`.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        Always returns ``False'' in case the `task_id` parameter refers
 | 
	
		
			
				|  |  | +        to a regular (non-cancelable) :class:`Task`.
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        Be aware that invoking this method will cause a hit in the
 | 
	
		
			
				|  |  | +        backend (for example a database query), so find a good balance
 | 
	
		
			
				|  |  | +        between calling it regularly (for responsiveness), but not too
 | 
	
		
			
				|  |  | +        often (for performance).
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        result = self.AsyncResult(kwargs['task_id'])
 | 
	
		
			
				|  |  | +        if not isinstance(result, CancelableAsyncResult):
 | 
	
		
			
				|  |  | +            return False
 | 
	
		
			
				|  |  | +        return result.is_cancelled()
 | 
	
		
			
				|  |  | +
 |