django.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. from __future__ import absolute_import, unicode_literals
  2. import os
  3. import sys
  4. import warnings
  5. from kombu.utils import cached_property, symbol_by_name
  6. from datetime import datetime
  7. from importlib import import_module
  8. from celery import signals
  9. from celery.app import default_app
  10. from celery.exceptions import FixupWarning
  11. if sys.version_info[0] < 3 and not hasattr(sys, 'pypy_version_info'):
  12. from StringIO import StringIO
  13. else: # pragma: no cover
  14. from io import StringIO
  15. __all__ = ['DjangoFixup', 'fixup']
  16. ERR_NOT_INSTALLED = """\
  17. Environment variable DJANGO_SETTINGS_MODULE is defined
  18. but Django is not installed. Will not apply Django fix-ups!
  19. """
  20. def _maybe_close_fd(fh):
  21. try:
  22. os.close(fh.fileno())
  23. except (AttributeError, OSError, TypeError):
  24. # TypeError added for celery#962
  25. pass
  26. def fixup(app, env='DJANGO_SETTINGS_MODULE'):
  27. SETTINGS_MODULE = os.environ.get(env)
  28. if SETTINGS_MODULE and 'django' not in app.loader_cls.lower():
  29. try:
  30. import django # noqa
  31. except ImportError:
  32. warnings.warn(FixupWarning(ERR_NOT_INSTALLED))
  33. else:
  34. return DjangoFixup(app).install()
  35. class DjangoFixup(object):
  36. def __init__(self, app):
  37. self.app = app
  38. if default_app is None:
  39. self.app.set_default()
  40. self._worker_fixup = None
  41. def install(self):
  42. # Need to add project directory to path
  43. sys.path.append(os.getcwd())
  44. self._settings = symbol_by_name('django.conf:settings')
  45. self.app.loader.now = self.now
  46. self.app.loader.mail_admins = self.mail_admins
  47. signals.import_modules.connect(self.on_import_modules)
  48. signals.worker_init.connect(self.on_worker_init)
  49. return self
  50. @property
  51. def worker_fixup(self):
  52. if self._worker_fixup is None:
  53. self._worker_fixup = DjangoWorkerFixup(self.app)
  54. return self._worker_fixup
  55. @worker_fixup.setter
  56. def worker_fixup(self, value):
  57. self._worker_fixup = value
  58. def on_import_modules(self, **kwargs):
  59. # call django.setup() before task modules are imported
  60. self.worker_fixup.validate_models()
  61. def on_worker_init(self, **kwargs):
  62. self.worker_fixup.install()
  63. def now(self, utc=False):
  64. return datetime.utcnow() if utc else self._now()
  65. def mail_admins(self, subject, body, fail_silently=False, **kwargs):
  66. return self._mail_admins(subject, body, fail_silently=fail_silently)
  67. def autodiscover_tasks(self):
  68. try:
  69. from django.apps import apps
  70. except ImportError:
  71. return self._settings.INSTALLED_APPS
  72. else:
  73. return [config.name for config in apps.get_app_configs()]
  74. @cached_property
  75. def _mail_admins(self):
  76. return symbol_by_name('django.core.mail:mail_admins')
  77. @cached_property
  78. def _now(self):
  79. try:
  80. return symbol_by_name('django.utils.timezone:now')
  81. except (AttributeError, ImportError): # pre django-1.4
  82. return datetime.now
  83. class DjangoWorkerFixup(object):
  84. _db_recycles = 0
  85. def __init__(self, app):
  86. self.app = app
  87. self.db_reuse_max = self.app.conf.get('CELERY_DB_REUSE_MAX', None)
  88. self._db = import_module('django.db')
  89. self._cache = import_module('django.core.cache')
  90. self._settings = symbol_by_name('django.conf:settings')
  91. try:
  92. self.interface_errors = (
  93. symbol_by_name('django.db.utils.InterfaceError'),
  94. )
  95. except (ImportError, AttributeError):
  96. self._interface_errors = ()
  97. # Database-related exceptions.
  98. DatabaseError = symbol_by_name('django.db:DatabaseError')
  99. try:
  100. import MySQLdb as mysql
  101. _my_database_errors = (mysql.DatabaseError,
  102. mysql.InterfaceError,
  103. mysql.OperationalError)
  104. except ImportError:
  105. _my_database_errors = () # noqa
  106. try:
  107. import psycopg2 as pg
  108. _pg_database_errors = (pg.DatabaseError,
  109. pg.InterfaceError,
  110. pg.OperationalError)
  111. except ImportError:
  112. _pg_database_errors = () # noqa
  113. try:
  114. import sqlite3
  115. _lite_database_errors = (sqlite3.DatabaseError,
  116. sqlite3.InterfaceError,
  117. sqlite3.OperationalError)
  118. except ImportError:
  119. _lite_database_errors = () # noqa
  120. try:
  121. import cx_Oracle as oracle
  122. _oracle_database_errors = (oracle.DatabaseError,
  123. oracle.InterfaceError,
  124. oracle.OperationalError)
  125. except ImportError:
  126. _oracle_database_errors = () # noqa
  127. try:
  128. self._close_old_connections = symbol_by_name(
  129. 'django.db:close_old_connections',
  130. )
  131. except (ImportError, AttributeError):
  132. self._close_old_connections = None
  133. self.database_errors = (
  134. (DatabaseError,) +
  135. _my_database_errors +
  136. _pg_database_errors +
  137. _lite_database_errors +
  138. _oracle_database_errors
  139. )
  140. def django_setup(self):
  141. import django
  142. try:
  143. django_setup = django.setup
  144. except AttributeError: # pragma: no cover
  145. pass
  146. else:
  147. django_setup()
  148. def validate_models(self):
  149. self.django_setup()
  150. try:
  151. from django.core.management.validation import get_validation_errors
  152. except ImportError:
  153. self._validate_models_django17()
  154. else:
  155. s = StringIO()
  156. num_errors = get_validation_errors(s, None)
  157. if num_errors:
  158. raise RuntimeError(
  159. 'One or more Django models did not validate:\n{0}'.format(
  160. s.getvalue()))
  161. def _validate_models_django17(self):
  162. from django.core.management import base
  163. print(base)
  164. cmd = base.BaseCommand()
  165. try:
  166. cmd.stdout = base.OutputWrapper(sys.stdout)
  167. cmd.stderr = base.OutputWrapper(sys.stderr)
  168. except ImportError: # before django 1.5
  169. cmd.stdout, cmd.stderr = sys.stdout, sys.stderr
  170. cmd.check()
  171. def install(self):
  172. signals.beat_embedded_init.connect(self.close_database)
  173. signals.worker_ready.connect(self.on_worker_ready)
  174. signals.task_prerun.connect(self.on_task_prerun)
  175. signals.task_postrun.connect(self.on_task_postrun)
  176. signals.worker_process_init.connect(self.on_worker_process_init)
  177. self.close_database()
  178. self.close_cache()
  179. return self
  180. def on_worker_process_init(self, **kwargs):
  181. # Child process must validate models again if on Windows,
  182. # or if they were started using execv.
  183. if os.environ.get('FORKED_BY_MULTIPROCESSING'):
  184. self.validate_models()
  185. # close connections:
  186. # the parent process may have established these,
  187. # so need to close them.
  188. # calling db.close() on some DB connections will cause
  189. # the inherited DB conn to also get broken in the parent
  190. # process so we need to remove it without triggering any
  191. # network IO that close() might cause.
  192. try:
  193. for c in self._db.connections.all():
  194. if c and c.connection:
  195. self._maybe_close_db_fd(c.connection)
  196. except AttributeError:
  197. if self._db.connection and self._db.connection.connection:
  198. self._maybe_close_db_fd(self._db.connection.connection)
  199. # use the _ version to avoid DB_REUSE preventing the conn.close() call
  200. self._close_database()
  201. self.close_cache()
  202. def _maybe_close_db_fd(self, fd):
  203. try:
  204. _maybe_close_fd(fd)
  205. except self.interface_errors:
  206. pass
  207. def on_task_prerun(self, sender, **kwargs):
  208. """Called before every task."""
  209. if not getattr(sender.request, 'is_eager', False):
  210. self.close_database()
  211. def on_task_postrun(self, sender, **kwargs):
  212. # See http://groups.google.com/group/django-users/
  213. # browse_thread/thread/78200863d0c07c6d/
  214. if not getattr(sender.request, 'is_eager', False):
  215. self.close_database()
  216. self.close_cache()
  217. def close_database(self, **kwargs):
  218. if self._close_old_connections:
  219. return self._close_old_connections() # Django 1.6
  220. if not self.db_reuse_max:
  221. return self._close_database()
  222. if self._db_recycles >= self.db_reuse_max * 2:
  223. self._db_recycles = 0
  224. self._close_database()
  225. self._db_recycles += 1
  226. def _close_database(self):
  227. try:
  228. funs = [conn.close for conn in self._db.connections.all()]
  229. except AttributeError:
  230. if hasattr(self._db, 'close_old_connections'): # django 1.6
  231. funs = [self._db.close_old_connections]
  232. else:
  233. # pre multidb, pending deprication in django 1.6
  234. funs = [self._db.close_connection]
  235. for close in funs:
  236. try:
  237. close()
  238. except self.interface_errors:
  239. pass
  240. except self.database_errors as exc:
  241. str_exc = str(exc)
  242. if 'closed' not in str_exc and 'not connected' not in str_exc:
  243. raise
  244. def close_cache(self):
  245. try:
  246. self._cache.cache.close()
  247. except (TypeError, AttributeError):
  248. pass
  249. def on_worker_ready(self, **kwargs):
  250. if self._settings.DEBUG:
  251. warnings.warn('Using settings.DEBUG leads to a memory leak, never '
  252. 'use this setting in production environments!')