项目作者: Bahus

项目描述 :
Asynchronous tasks management with UWSGI server
高级语言: Python
项目地址: git://github.com/Bahus/uwsgi_tasks.git
创建时间: 2015-01-11T21:00:37Z
项目社区:https://github.com/Bahus/uwsgi_tasks

开源协议:MIT License

下载


UWSGI Tasks engine

This package makes it to use UWSGI signal framework
for asynchronous tasks management. It’s more functional and flexible than cron scheduler, and
can be used as replacement for celery in many cases.

Requirements

The module works only in UWSGI web server environment,
you also may have to setup some mules or\and spooler processes as described in UWSGI documentation.

Installation

Simple execute pip install uwsgi_tasks

Usage

Mules, farms and spoolers

Use case: you have Django project and want to send all emails asynchronously.

Setup some mules with --mule or --mules=<N> parameters, or some spooler
processes with --spooler==<path_to_spooler_folder>.

Then write:

  1. # myapp/__init__.py
  2. from django.core.mail import send_mail
  3. from uwsgi_tasks import task, TaskExecutor
  4. @task(executor=TaskExecutor.SPOOLER)
  5. def send_email_async(subject, body, email_to):
  6. # Execute task asynchronously on first available spooler
  7. return send_mail(subject, body, 'noreply@domain.com', [email_to])
  8. ...
  9. def my_view():
  10. # Execute tasks asynchronously on first available spooler
  11. send_email_async('Welcome!', 'Thank you!', 'user@domain.com')

Execution of send_email_async will not block execution of my_view, since
function will be called by first available spooler. I personally recommend to use spoolers rather than mules for several reasons:

  1. Task will be executed\retried even if uwsgi is crashed or restarted, since task information stored in files.
  2. Task parameters size is not limited to 64 KBytes.
  3. You may switch to external\network spoolers if required.
  4. You are able to tune task execution flow with introspection facilities.

The following tasks execution backends are supported:

  • AUTO - default mode, spooler will be used if available, otherwise mule will be used. If mule is not available, than task is executed at runtime.
  • MULE - execute decorated task on first available mule
  • SPOOLER - execute decorated task on spooler
  • RUNTIME - execute task at runtime, this backend is also used in case uwsgi module can’t be imported, e.g. tests.

Common task parameters are:

  • working_dir - absolute path to execute task in. You won’t typically need to provide this value, since it will be provided automatically: as soon as you execute the task current working directory will be saved and sent to spooler or mule. You may pass None value to disable this feature.

When SPOOLER backend is used, the following additional parameters are supported:

  • priority - string related to priority of this task, larger = less important, so you can simply use digits. spooler-ordered uwsgi parameter must be set for this feature to work (in linux only?).
  • at - UNIX timestamp or Python datetime or Python timedelta object – when task must be executed.
  • spooler_return - boolean value, False by default. If True is passed, you can return spooler codes from function, e.g. SPOOL_OK, SPOOL_RETRY and SPOOL_IGNORE.
  • retry_count - how many times spooler should repeat the task if it returns SPOOL_RETRY code, implies spooler_return=False.
  • retry_timeout - how many seconds between attempts spooler should wait to execute the task. Actual timeout depends on spooler-frequency parameter. Python timedelta object is also supported.

Use case: run task asynchronously and repeat execution 3 times at maximum if it fails, with 5 seconds timeout between attempts.

  1. from functools import wraps
  2. from uwsgi_tasks import task, TaskExecutor, SPOOL_OK, SPOOL_RETRY
  3. def task_wrapper(func):
  4. @wraps(func) # required!
  5. def _inner(*args, **kwargs):
  6. print 'Task started with parameters:', args, kwargs
  7. try:
  8. func(*args, **kwargs)
  9. except Exception as ex: # example
  10. print 'Exception is occurred', ex, 'repeat the task'
  11. return SPOOL_RETRY
  12. print 'Task ended', func
  13. return SPOOL_OK
  14. return _inner
  15. @task(executor=TaskExecutor.SPOOLER, retry_count=3, retry_timeout=5)
  16. @task_wrapper
  17. def spooler_task(text):
  18. print 'Hello, spooler! text =', text
  19. raise Exception('Sorry, task failed!')

Raising RetryTaskException(count=<retry_count>, timeout=<retry_timeout>) approach can be also used to retry task execution:

  1. import logging
  2. from uwsgi_tasks import RetryTaskException, task, TaskExecutor
  3. @task(executor=TaskExecutor.SPOOLER, retry_count=2)
  4. def process_purchase(order_id):
  5. try:
  6. # make something with order id
  7. ...
  8. except Exception as ex:
  9. logging.exception('Something bad happened')
  10. # retry task in 10 seconds for the last time
  11. raise RetryTaskException(timeout=10)

Be careful when providing count parameter to the exception constructor - it may lead to infinite tasks execution, since the parameter replaces the value of retry_count.

Task execution process can be also controlled via spooler options, see details here.

Project setup

There are some requirements to make asynchronous tasks work properly. Let’s imagine your Django project has the following directory structure:

  1. ├── project/
  2. ├── venv/ <-- your virtual environment is placed here
  3. ├── my_project/ <-- Django project (created with "startproject" command)
  4. ├── apps/
  5. ├── index/ <-- Single Django application ("startapp" command)
  6. ├── __init__.py
  7. ├── admin.py
  8. ├── models.py
  9. ├── tasks.py
  10. ├── tests.py
  11. ├── views.py
  12. ├── __init__.py
  13. ├── __init__.py
  14. ├── settings.py
  15. ├── urls.py
  16. ├── spooler/ <-- spooler files are created here

Minimum working UWSGI configuration is placed in uwsgi.ini file:

  1. [uwsgi]
  2. http-socket=127.0.0.1:8080
  3. processes=1
  4. workers=1
  5. # python path setup
  6. module=django.core.wsgi:get_wsgi_application()
  7. # absolute path to the virtualenv directory
  8. venv=<base_path>/project/venv/
  9. # Django project directory is placed here:
  10. pythonpath=<base_path>/project/
  11. # "importable" path for Django settings
  12. env=DJANGO_SETTINGS_MODULE=my_project.settings
  13. # spooler setup
  14. spooler=<base_path>/project/spooler
  15. spooler-processes=2
  16. spooler-frequency=10

In such configuration you should put the following code into my_project/__init__.py file:

  1. # my_project/__init__.py
  2. from uwsgi_tasks import set_uwsgi_callbacks
  3. set_uwsgi_callbacks()

Task functions (decorated with @task) may be placed in any file where they can be imported, e.g. apps/index/tasks.py.

If you still receive some strange errors when running asynchronous tasks, e. g.
“uwsgi unable to find the spooler function” or “ImproperlyConfigured Django exception”, you may try
the following: add to uwsgi configuration new variable spooler-import=my_project - it will force spooler
to import my_project/__init__.py file when starting, then add Django initialization
into this file:

  1. # my_project/__init__.py
  2. # ... set_uwsgi_callbacks code ...
  3. # if you use Django, otherwise use
  4. # initialization related to your framework\project
  5. from uwsgi_tasks import django_setup
  6. django_setup()

Also make sure you didn’t override uwsgi callbacks with this code
from uwsgidecorators import * somewhere in your project.

If nothing helps - please submit an issue.

If you want to run some cron or timer-like tasks on project initialization you
may import them in the same file:

  1. # my_project/__init__.py
  2. # ... set_uwsgi_callbacks
  3. from my_cron_tasks import *
  4. from my_timer_tasks import *

Keep in mind that task arguments must be pickable, since they are serialized and send via socket (mule) or file (spooler).

Timers, red-black timers and cron

This API is similar to uwsgi bundled Python decorators module. One thing to note: you are not able to provide any arguments to timer-like or cron-like tasks. See examples below:

  1. from uwsgi_tasks import *
  2. @timer(seconds=5)
  3. def print_every_5_seconds(signal_number):
  4. """Prints string every 5 seconds
  5. Keep in mind: task is created on initialization.
  6. """
  7. print 'Task for signal', signal_number
  8. @timer(seconds=5, iterations=3, target='workers')
  9. def print_every_5_seconds(signal_number):
  10. """Prints string every 5 seconds 3 times"""
  11. print 'Task with iterations for signal', signal_number
  12. @timer_lazy(seconds=5)
  13. def print_every_5_seconds_after_call(signal_number):
  14. """Prints string every 5 seconds"""
  15. print 'Lazy task for signal', signal_number
  16. @cron(minute=-2)
  17. def print_every_2_minutes(signal_number):
  18. print 'Cron task:', signal_number
  19. @cron_lazy(minute=-2, target='mule')
  20. def print_every_2_minutes_after_call(signal_number):
  21. print 'Cron task:', signal_number
  22. ...
  23. def my_view():
  24. print_every_5_seconds_after_call()
  25. print_every_2_minutes_after_call()

Timer and cron decorators supports target parameter, supported values are described here.

Keep in mind the maximum number of timer-like and cron-like tasks is 256 for each available worker.

Task introspection API

Using task introspection API you can get current task object inside current task function and will be able to change some task parameters. You may also use special buffer dict-like object to pass data between task execution attempts. Using get_current_task you are able to get internal representation of task object and manipulate the attributes of the task, e.g. SpoolerTask object has the following changeable properties: at, retry_count, retry_timeout.

Here is a complex example:

  1. from uwsgi_tasks import get_current_task
  2. @task(executor=TaskExecutor.SPOOLER, at=datetime.timedelta(seconds=10))
  3. def remove_files_sequentially(previous_selected_file=None):
  4. # get current SpoolerTask object
  5. current_task = get_current_task()
  6. selected_file = select_file_for_removal(previous_selected_file)
  7. # we should stop the task here
  8. if selected_file is None:
  9. logger.info('All files were removed')
  10. for filename, removed_at in current_task.buffer['results'].items():
  11. logger.info('File "%s" was removed at "%s"', filename, removed_at)
  12. for filename, error_message in current_task.buffer['errors'].items():
  13. logger.info('File "%s", error: "%s"', filename, error_message)
  14. return
  15. try:
  16. logger.info('Removing the file "%s"', selected_file)
  17. # ... remove the file ...
  18. del_file(selected_file)
  19. except IOError as ex:
  20. logger.exception('Cannot delete file "%s"', selected_file)
  21. # let's try to remove this one more time later
  22. io_errors = current_task.buffer.setdefault('errors', {}).get(selected_file)
  23. if not io_errors:
  24. current_task.buffer['errors'][selected_file] = str(ex)
  25. current_task.at = datetime.timedelta(seconds=20)
  26. return current_task(previous_selected_file)
  27. # save datetime of removal
  28. current_task.buffer.setdefault('results', {})[selected_file] = datetime.datetime.now()
  29. # run in async mode
  30. return current_task(selected_file)

Changing task configuration before execution

You may use add_setup method to change some task-related settings before (or during) task execution process. The following example shows how to change timer’s timeout and iterations amount at runtime:

  1. from uwsgi_tasks import timer_lazy
  2. @timer_lazy(target='worker')
  3. def run_me_periodically(signal):
  4. print('Running with signal:', signal)
  5. def my_view(request):
  6. run_me_periodically.add_setup(seconds=10, iterations=2)
  7. run_me_periodically()