csu 2.2.4__tar.gz → 2.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {csu-2.2.4 → csu-2.2.5}/.bumpversion.cfg +1 -1
  2. {csu-2.2.4 → csu-2.2.5}/.cookiecutterrc +1 -1
  3. {csu-2.2.4 → csu-2.2.5}/PKG-INFO +3 -3
  4. {csu-2.2.4 → csu-2.2.5}/README.rst +2 -2
  5. {csu-2.2.4 → csu-2.2.5}/pyproject.toml +1 -1
  6. {csu-2.2.4 → csu-2.2.5}/src/csu/__init__.py +1 -1
  7. {csu-2.2.4 → csu-2.2.5}/src/csu/worker/__init__.py +0 -4
  8. csu-2.2.5/src/csu/worker/asgi.py +74 -0
  9. {csu-2.2.4 → csu-2.2.5}/src/csu/worker/engine.py +5 -3
  10. {csu-2.2.4 → csu-2.2.5}/src/csu/worker/models.py +3 -1
  11. {csu-2.2.4 → csu-2.2.5}/src/csu.egg-info/PKG-INFO +3 -3
  12. csu-2.2.5/tests/test_exampleworker.py +164 -0
  13. csu-2.2.4/src/csu/worker/asgi.py +0 -36
  14. csu-2.2.4/tests/test_exampleworker.py +0 -164
  15. {csu-2.2.4 → csu-2.2.5}/.coveragerc +0 -0
  16. {csu-2.2.4 → csu-2.2.5}/.editorconfig +0 -0
  17. {csu-2.2.4 → csu-2.2.5}/.github/workflows/github-actions.yml +0 -0
  18. {csu-2.2.4 → csu-2.2.5}/.pre-commit-config.yaml +0 -0
  19. {csu-2.2.4 → csu-2.2.5}/.taplo.toml +0 -0
  20. {csu-2.2.4 → csu-2.2.5}/AUTHORS.rst +0 -0
  21. {csu-2.2.4 → csu-2.2.5}/CHANGELOG.rst +0 -0
  22. {csu-2.2.4 → csu-2.2.5}/CONTRIBUTING.rst +0 -0
  23. {csu-2.2.4 → csu-2.2.5}/LICENSE +0 -0
  24. {csu-2.2.4 → csu-2.2.5}/MANIFEST.in +0 -0
  25. {csu-2.2.4 → csu-2.2.5}/ci/bootstrap.py +0 -0
  26. {csu-2.2.4 → csu-2.2.5}/ci/requirements.txt +0 -0
  27. {csu-2.2.4 → csu-2.2.5}/ci/templates/.github/workflows/github-actions.yml +0 -0
  28. {csu-2.2.4 → csu-2.2.5}/pytest.ini +0 -0
  29. {csu-2.2.4 → csu-2.2.5}/setup.cfg +0 -0
  30. {csu-2.2.4 → csu-2.2.5}/src/csu/admin.py +0 -0
  31. {csu-2.2.4 → csu-2.2.5}/src/csu/conf.py +0 -0
  32. {csu-2.2.4 → csu-2.2.5}/src/csu/consts.py +0 -0
  33. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/__init__.py +0 -0
  34. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/auth.py +0 -0
  35. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/fields.py +0 -0
  36. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/forms.py +0 -0
  37. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/perms.py +0 -0
  38. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/phonenumber.py +0 -0
  39. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/serializers.py +0 -0
  40. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/test_auth.py +0 -0
  41. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/test_fields.py +0 -0
  42. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/test_forms.py +0 -0
  43. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/test_phonenumber.py +0 -0
  44. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/test_serializers.py +0 -0
  45. {csu-2.2.4 → csu-2.2.5}/src/csu/drf/views.py +0 -0
  46. {csu-2.2.4 → csu-2.2.5}/src/csu/enums.py +0 -0
  47. {csu-2.2.4 → csu-2.2.5}/src/csu/env.py +0 -0
  48. {csu-2.2.4 → csu-2.2.5}/src/csu/exceptions.py +0 -0
  49. {csu-2.2.4 → csu-2.2.5}/src/csu/export.py +0 -0
  50. {csu-2.2.4 → csu-2.2.5}/src/csu/fixups.py +0 -0
  51. {csu-2.2.4 → csu-2.2.5}/src/csu/forms/__init__.py +0 -0
  52. {csu-2.2.4 → csu-2.2.5}/src/csu/forms/crispy.py +0 -0
  53. {csu-2.2.4 → csu-2.2.5}/src/csu/forms/fields.py +0 -0
  54. {csu-2.2.4 → csu-2.2.5}/src/csu/gettext.py +0 -0
  55. {csu-2.2.4 → csu-2.2.5}/src/csu/gettext_lazy.py +0 -0
  56. {csu-2.2.4 → csu-2.2.5}/src/csu/locale/ro/LC_MESSAGES/django.mo +0 -0
  57. {csu-2.2.4 → csu-2.2.5}/src/csu/locale/ro/LC_MESSAGES/django.po +0 -0
  58. {csu-2.2.4 → csu-2.2.5}/src/csu/logging.py +0 -0
  59. {csu-2.2.4 → csu-2.2.5}/src/csu/management.py +0 -0
  60. {csu-2.2.4 → csu-2.2.5}/src/csu/models.py +0 -0
  61. {csu-2.2.4 → csu-2.2.5}/src/csu/query.py +0 -0
  62. {csu-2.2.4 → csu-2.2.5}/src/csu/routers.py +0 -0
  63. {csu-2.2.4 → csu-2.2.5}/src/csu/service.py +0 -0
  64. {csu-2.2.4 → csu-2.2.5}/src/csu/templates/api_exception.html +0 -0
  65. {csu-2.2.4 → csu-2.2.5}/src/csu/templates/forms/widgets/opaquewidget.html +0 -0
  66. {csu-2.2.4 → csu-2.2.5}/src/csu/test_consts.py +0 -0
  67. {csu-2.2.4 → csu-2.2.5}/src/csu/test_env.py +0 -0
  68. {csu-2.2.4 → csu-2.2.5}/src/csu/test_exceptions.py +0 -0
  69. {csu-2.2.4 → csu-2.2.5}/src/csu/test_logging.py +0 -0
  70. {csu-2.2.4 → csu-2.2.5}/src/csu/test_management.py +0 -0
  71. {csu-2.2.4 → csu-2.2.5}/src/csu/test_service.py +0 -0
  72. {csu-2.2.4 → csu-2.2.5}/src/csu/test_timezones.py +0 -0
  73. {csu-2.2.4 → csu-2.2.5}/src/csu/test_utils.py +0 -0
  74. {csu-2.2.4 → csu-2.2.5}/src/csu/test_xml.py +0 -0
  75. {csu-2.2.4 → csu-2.2.5}/src/csu/timezones.py +0 -0
  76. {csu-2.2.4 → csu-2.2.5}/src/csu/utils.py +0 -0
  77. {csu-2.2.4 → csu-2.2.5}/src/csu/views.py +0 -0
  78. {csu-2.2.4 → csu-2.2.5}/src/csu/worker/admin.py +0 -0
  79. {csu-2.2.4 → csu-2.2.5}/src/csu/worker/enums.py +0 -0
  80. {csu-2.2.4 → csu-2.2.5}/src/csu/worker/job.py +0 -0
  81. {csu-2.2.4 → csu-2.2.5}/src/csu/worker/registry.py +0 -0
  82. {csu-2.2.4 → csu-2.2.5}/src/csu/wsgi.py +0 -0
  83. {csu-2.2.4 → csu-2.2.5}/src/csu/xml.py +0 -0
  84. {csu-2.2.4 → csu-2.2.5}/src/csu.egg-info/SOURCES.txt +0 -0
  85. {csu-2.2.4 → csu-2.2.5}/src/csu.egg-info/dependency_links.txt +0 -0
  86. {csu-2.2.4 → csu-2.2.5}/src/csu.egg-info/requires.txt +0 -0
  87. {csu-2.2.4 → csu-2.2.5}/src/csu.egg-info/top_level.txt +0 -0
  88. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service/test_custom_context.yaml +0 -0
  89. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service/test_custom_context_0_retries.yaml +0 -0
  90. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service/test_decoding.yaml +0 -0
  91. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service/test_error.yaml +0 -0
  92. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service/test_logging.yaml +0 -0
  93. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service/test_redirects.yaml +0 -0
  94. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service_auth/test_async.yaml +0 -0
  95. {csu-2.2.4 → csu-2.2.5}/tests/cassettes/test_service_auth/test_sync.yaml +0 -0
  96. {csu-2.2.4 → csu-2.2.5}/tests/conftest.py +0 -0
  97. {csu-2.2.4 → csu-2.2.5}/tests/exampleworker.py +0 -0
  98. {csu-2.2.4 → csu-2.2.5}/tests/test_models.py +0 -0
  99. {csu-2.2.4 → csu-2.2.5}/tests/test_service.py +0 -0
  100. {csu-2.2.4 → csu-2.2.5}/tests/test_service_auth.py +0 -0
  101. {csu-2.2.4 → csu-2.2.5}/tests/test_views.py +0 -0
  102. {csu-2.2.4 → csu-2.2.5}/tests/test_worker.py +0 -0
  103. {csu-2.2.4 → csu-2.2.5}/tests/testproject/__init__.py +0 -0
  104. {csu-2.2.4 → csu-2.2.5}/tests/testproject/fixtures/testuser.json +0 -0
  105. {csu-2.2.4 → csu-2.2.5}/tests/testproject/models.py +0 -0
  106. {csu-2.2.4 → csu-2.2.5}/tests/testproject/settings.py +0 -0
  107. {csu-2.2.4 → csu-2.2.5}/tests/testproject/urls.py +0 -0
  108. {csu-2.2.4 → csu-2.2.5}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.2.4
2
+ current_version = 2.2.5
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -40,7 +40,7 @@ default_context:
40
40
  sphinx_theme: furo
41
41
  test_matrix_separate_coverage: 'no'
42
42
  tests_inside_package: 'yes'
43
- version: 2.2.4
43
+ version: 2.2.5
44
44
  version_manager: bump2version
45
45
  website: https://blog.ionelmc.ro
46
46
  year_from: '2024'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csu
3
- Version: 2.2.4
3
+ Version: 2.2.5
4
4
  Summary: Clean Slate Utils - bunch of utility code, mostly Django/DRF specific.
5
5
  Author-email: Ionel Cristian Mărieș <contact@ionelmc.ro>
6
6
  License-Expression: BSD-2-Clause
@@ -77,9 +77,9 @@ Overview
77
77
  .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
78
78
  :alt: Supported implementations
79
79
  :target: https://pypi.org/project/csu
80
- .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.4.svg
80
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.5.svg
81
81
  :alt: Commits since latest release
82
- :target: https://github.com/ionelmc/python-csu/compare/v2.2.4...main
82
+ :target: https://github.com/ionelmc/python-csu/compare/v2.2.5...main
83
83
  .. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
84
84
  :alt: Scrutinizer Status
85
85
  :target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
@@ -34,9 +34,9 @@ Overview
34
34
  .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
35
35
  :alt: Supported implementations
36
36
  :target: https://pypi.org/project/csu
37
- .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.4.svg
37
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.5.svg
38
38
  :alt: Commits since latest release
39
- :target: https://github.com/ionelmc/python-csu/compare/v2.2.4...main
39
+ :target: https://github.com/ionelmc/python-csu/compare/v2.2.5...main
40
40
  .. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
41
41
  :alt: Scrutinizer Status
42
42
  :target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
@@ -12,7 +12,7 @@ dynamic = [
12
12
  "readme",
13
13
  ]
14
14
  name = "csu"
15
- version = "2.2.4"
15
+ version = "2.2.5"
16
16
  license = "BSD-2-Clause"
17
17
  license-files = ["LICENSE"]
18
18
  description = "Clean Slate Utils - bunch of utility code, mostly Django/DRF specific."
@@ -2,4 +2,4 @@
2
2
  a.k.a Clean Slate Utils
3
3
  """
4
4
 
5
- __version__ = "2.2.4"
5
+ __version__ = "2.2.5"
@@ -17,7 +17,3 @@ Typical batch processing workload::
17
17
 
18
18
  The database is completely optional.
19
19
  """
20
-
21
- from logging import getLogger
22
-
23
- logger = getLogger("worker")
@@ -0,0 +1,74 @@
1
+ from logging import getLogger
2
+
3
+ import django
4
+ from asgiref.typing import ASGIReceiveCallable
5
+ from asgiref.typing import ASGIReceiveEvent
6
+ from asgiref.typing import ASGISendCallable
7
+ from asgiref.typing import LifespanShutdownCompleteEvent
8
+ from asgiref.typing import LifespanShutdownFailedEvent
9
+ from asgiref.typing import LifespanStartupCompleteEvent
10
+ from asgiref.typing import LifespanStartupFailedEvent
11
+ from django.core.handlers.asgi import ASGIHandler
12
+
13
+ from ..utils import lazy_import_classproperty
14
+
15
+ logger = getLogger(__name__)
16
+
17
+
18
+ class WorkerLifespanASGIHandler(ASGIHandler):
19
+ async def __call__(self, scope, receive: ASGIReceiveCallable, send) -> None:
20
+ if scope["type"] == "lifespan":
21
+ await self.handle_lifespan(receive, send)
22
+ else:
23
+ await super().__call__(scope, receive, send)
24
+
25
+ engine_registry = lazy_import_classproperty("csu.worker.registry.REGISTRY")
26
+
27
+ async def handle_start_engines(self):
28
+ registry = self.engine_registry
29
+ logger.info("Found %s registered engines.", len(registry))
30
+ for module_name, engine in registry.items():
31
+ logger.info("Starting engine for %s: %s...", module_name, engine)
32
+ if engine.shutdown_on:
33
+ raise RuntimeError(f"Engine {type(engine)} shutdown_on should be empty, not {engine.shutdown_on}!")
34
+ await engine.start()
35
+ logger.info(
36
+ "Started %s engines: \n %s",
37
+ len(registry),
38
+ "\n ".join(f"{module_name}: {engine}" for module_name, engine in registry.items()),
39
+ )
40
+
41
+ async def handle_stop_engines(self):
42
+ registry = self.engine_registry
43
+ logger.info("Stopping %s registered engines.", len(registry))
44
+ for engine in registry.values():
45
+ await engine.stop(graceful=True)
46
+
47
+ async def handle_lifespan(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
48
+ while True:
49
+ message: ASGIReceiveEvent = await receive()
50
+ match message["type"]:
51
+ case "lifespan.startup":
52
+ try:
53
+ await self.handle_start_engines()
54
+ except Exception as exc:
55
+ await send(LifespanStartupFailedEvent(type="lifespan.startup.failed", message=str(exc)))
56
+ raise
57
+ else:
58
+ await send(LifespanStartupCompleteEvent(type="lifespan.startup.complete"))
59
+ case "lifespan.shutdown":
60
+ try:
61
+ await self.handle_stop_engines()
62
+ except Exception as exc:
63
+ await send(LifespanShutdownFailedEvent(type="lifespan.shutdown.failed", message=str(exc)))
64
+ raise
65
+ else:
66
+ await send(LifespanShutdownCompleteEvent(type="lifespan.shutdown.complete"))
67
+ return
68
+ case _:
69
+ raise ValueError(f"Unexpected message type: {message['type']!r}", message)
70
+
71
+
72
+ def get_worker_lifespan_application() -> WorkerLifespanASGIHandler:
73
+ django.setup(set_prefix=False)
74
+ return WorkerLifespanASGIHandler()
@@ -18,6 +18,7 @@ from datetime import timedelta
18
18
  from enum import Flag
19
19
  from enum import auto
20
20
  from functools import partial
21
+ from logging import getLogger
21
22
  from random import randint
22
23
  from typing import ClassVar
23
24
  from typing import Protocol
@@ -26,10 +27,11 @@ from datetimerange import DateTimeRange
26
27
 
27
28
  from ..exceptions import InternalServiceError
28
29
  from ..timezones import naivenow
29
- from . import logger
30
30
  from .enums import WorkerState
31
31
  from .job import Job
32
32
 
33
+ logger = getLogger(__name__)
34
+
33
35
 
34
36
  class TaskProtocol(Protocol):
35
37
  job_kwargs: dict
@@ -270,7 +272,7 @@ class AbstractEngine(ABC):
270
272
  workers: list[AbstractWorker]
271
273
  queue: Queue = None
272
274
  queue_sem: Semaphore
273
- shutdown_on: Iterable[int] = (signal.SIGINT, signal.SIGTERM)
275
+ shutdown_on: Iterable[signal.Signals] = (signal.SIGINT, signal.SIGTERM)
274
276
  shutdown_requested = False
275
277
  shutdown_tasks: ClassVar[dict[bool, asyncio.Task]] = {}
276
278
  if sys.platform == "win32":
@@ -322,7 +324,7 @@ class AbstractEngine(ABC):
322
324
  for sig in self.shutdown_on:
323
325
  loop.add_signal_handler(sig, self.shutdown_handler, sig)
324
326
 
325
- def shutdown_handler(self, sig):
327
+ def shutdown_handler(self, sig: signal.Signals) -> None:
326
328
  sig_name = sig.name
327
329
  graceful = not self.shutdown_requested
328
330
  logger.info("%s.shutdown_handler(%s) %s", self, sig_name, "GRACEFUL" if graceful else "FORCED")
@@ -1,6 +1,7 @@
1
1
  from abc import abstractmethod
2
2
  from asyncio import CancelledError
3
3
  from datetime import timedelta
4
+ from logging import getLogger
4
5
  from random import SystemRandom
5
6
 
6
7
  from asgiref.sync import sync_to_async
@@ -11,11 +12,12 @@ from django.db.models import F
11
12
  from django.db.models import Q
12
13
 
13
14
  from ..timezones import utcnow
14
- from . import logger
15
15
  from .engine import AbstractConsumer
16
16
  from .engine import AbstractProducer
17
17
  from .job import Job
18
18
 
19
+ logger = getLogger(__name__)
20
+
19
21
 
20
22
  def get_random_float(system_random=SystemRandom()): # noqa: B008
21
23
  return system_random.random()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csu
3
- Version: 2.2.4
3
+ Version: 2.2.5
4
4
  Summary: Clean Slate Utils - bunch of utility code, mostly Django/DRF specific.
5
5
  Author-email: Ionel Cristian Mărieș <contact@ionelmc.ro>
6
6
  License-Expression: BSD-2-Clause
@@ -77,9 +77,9 @@ Overview
77
77
  .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/csu.svg
78
78
  :alt: Supported implementations
79
79
  :target: https://pypi.org/project/csu
80
- .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.4.svg
80
+ .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-csu/v2.2.5.svg
81
81
  :alt: Commits since latest release
82
- :target: https://github.com/ionelmc/python-csu/compare/v2.2.4...main
82
+ :target: https://github.com/ionelmc/python-csu/compare/v2.2.5...main
83
83
  .. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/ionelmc/python-csu/main.svg
84
84
  :alt: Scrutinizer Status
85
85
  :target: https://scrutinizer-ci.com/g/ionelmc/python-csu/
@@ -0,0 +1,164 @@
1
+ import signal
2
+ import sys
3
+ import time
4
+
5
+ from process_tests import TestProcess
6
+ from process_tests import dump_on_error
7
+ from process_tests import wait_for_strings
8
+
9
+
10
+ def test_noskip():
11
+ with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "4") as target, dump_on_error(target.read):
12
+ wait_for_strings(
13
+ target.read,
14
+ 5,
15
+ "INFO:csu.worker.engine:ExampleConsumer(1/UNKNOWN) started.",
16
+ "INFO:csu.worker.engine:ExampleConsumer(2/UNKNOWN) started.",
17
+ "INFO:csu.worker.engine:ExampleConsumer(3/UNKNOWN) started.",
18
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN) started.",
19
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN).run() started.",
20
+ "PRODUCER fetch_task @ 1 RETURN",
21
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
22
+ "INFO:csu.worker.engine:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
23
+ "PRODUCER fetch_task @ 2 RETURN",
24
+ "INFO:csu.worker.engine:ExampleEngine(4w/1q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=2))",
25
+ "INFO:csu.worker.engine:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=2).done of NoOpTask(id=2).",
26
+ "PRODUCER fetch_task @ 3 RETURN",
27
+ "INFO:csu.worker.engine:ExampleEngine(4w/2q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
28
+ "INFO:csu.worker.engine:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
29
+ "INFO:csu.worker.engine:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
30
+ "INFO:csu.worker.engine:ExampleConsumer(1/WORKING).run() - performing...",
31
+ "CONSUMER 0 perform_work JOB 1 START",
32
+ "INFO:csu.worker.engine:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=2)",
33
+ "INFO:csu.worker.engine:ExampleConsumer(2/WORKING).run() - performing...",
34
+ "CONSUMER 1 perform_work JOB 2 START",
35
+ "INFO:csu.worker.engine:ExampleConsumer(3/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
36
+ "INFO:csu.worker.engine:ExampleConsumer(3/WORKING).run() - performing...",
37
+ "CONSUMER 2 perform_work JOB 3 START",
38
+ "CONSUMER 0 perform_work JOB 1 DONE",
39
+ "CONSUMER 0 save_result JOB 1",
40
+ "CONSUMER 1 perform_work JOB 2 DONE",
41
+ "CONSUMER 1 save_result JOB 2",
42
+ "CONSUMER 2 perform_work JOB 3 DONE",
43
+ "CONSUMER 2 save_result JOB 3",
44
+ "PRODUCER fetch_task @ 4 RETURN",
45
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=4))",
46
+ "INFO:csu.worker.engine:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=4).done of NoOpTask(id=4).",
47
+ "PRODUCER fetch_task @ 5 EXHAUSTED",
48
+ "INFO:csu.worker.engine:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=4)",
49
+ "INFO:csu.worker.engine:ExampleConsumer(1/WORKING).run() - performing...",
50
+ "CONSUMER 0 perform_work JOB 4 START",
51
+ "PRODUCER fetch_task @ 6 EXHAUSTED",
52
+ "CONSUMER 0 perform_work JOB 4 DONE",
53
+ "CONSUMER 0 save_result JOB 4",
54
+ "PRODUCER fetch_task @ 7 EXHAUSTED",
55
+ )
56
+
57
+
58
+ def test_skip():
59
+ with TestProcess(sys.executable, "-mexampleworker", "3", "2", "100", "4") as target, dump_on_error(target.read):
60
+ wait_for_strings(
61
+ target.read,
62
+ 5,
63
+ "INFO:csu.worker.engine:ExampleConsumer(1/UNKNOWN) started.",
64
+ "INFO:csu.worker.engine:ExampleConsumer(2/UNKNOWN) started.",
65
+ "INFO:csu.worker.engine:ExampleConsumer(3/UNKNOWN) started.",
66
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN) started.",
67
+ "INFO:csu.worker.engine:ExampleProducer(1/UNKNOWN).run() started.",
68
+ "PRODUCER fetch_task @ 1 RETURN",
69
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
70
+ "INFO:csu.worker.engine:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
71
+ "PRODUCER fetch_task @ 2 SKIP",
72
+ "INFO:csu.worker.engine:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
73
+ "INFO:csu.worker.engine:ExampleConsumer(1/WORKING).run() - performing...",
74
+ "CONSUMER 0 perform_work JOB 1 START",
75
+ "CONSUMER 0 perform_work JOB 1 DONE",
76
+ "CONSUMER 0 save_result JOB 1",
77
+ "PRODUCER fetch_task @ 3 RETURN",
78
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
79
+ "INFO:csu.worker.engine:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
80
+ "PRODUCER fetch_task @ 4 SKIP",
81
+ "INFO:csu.worker.engine:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
82
+ "INFO:csu.worker.engine:ExampleConsumer(2/WORKING).run() - performing...",
83
+ "CONSUMER 1 perform_work JOB 3 START",
84
+ "CONSUMER 1 perform_work JOB 3 DONE",
85
+ "CONSUMER 1 save_result JOB 3",
86
+ "PRODUCER fetch_task @ 5 EXHAUSTED",
87
+ )
88
+
89
+
90
+ def test_graceful_shutdown():
91
+ with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "100") as target, dump_on_error(target.read):
92
+ wait_for_strings(
93
+ target.read,
94
+ 15,
95
+ "PRODUCER fetch_task @ 1 RETURN",
96
+ )
97
+ target.proc.send_signal(signal.SIGINT)
98
+ wait_for_strings(
99
+ target.read,
100
+ 15,
101
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
102
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).stop(graceful=True)",
103
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) stats: {'ExampleConsumer': {'WORKING': 3}, 'ExampleProducer': {'WAITING': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 0}}",
104
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 1: <__main__.ExampleConsumer object at ",
105
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 2: <__main__.ExampleConsumer object at ",
106
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 3: <__main__.ExampleConsumer object at ",
107
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) worker 4: <__main__.ExampleProducer object at ",
108
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) requesting workers to stop and awaiting 1 producers...",
109
+ "CONSUMER 0 perform_work JOB 1 DONE",
110
+ "CONSUMER 0 save_result JOB 1",
111
+ "INFO:csu.worker.engine:ExampleProducer(1/WAITING).on_exit(",
112
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) flushing queue (0 items) and awaiting consumers...",
113
+ "INFO:csu.worker.engine:ExampleConsumer(1/SHUTDOWN).on_exit(",
114
+ "INFO:csu.worker.engine:ExampleConsumer(2/SHUTDOWN).on_exit(",
115
+ "INFO:csu.worker.engine:ExampleConsumer(3/SHUTDOWN).on_exit(",
116
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) shutdown complete.",
117
+ "DONE: {'ExampleConsumer': {'EXITED': 3}, 'ExampleProducer': {'EXITED': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 2}}",
118
+ )
119
+
120
+
121
+ def test_forced_shutdown():
122
+ with TestProcess(sys.executable, "-mexampleworker", "3", "0", "2000", "100") as target, dump_on_error(target.read):
123
+ wait_for_strings(
124
+ target.read,
125
+ 15,
126
+ "PRODUCER fetch_task @ 1 RETURN",
127
+ )
128
+ target.proc.send_signal(signal.SIGINT)
129
+ time.sleep(1)
130
+ target.proc.send_signal(signal.SIGTERM)
131
+ wait_for_strings(
132
+ target.read,
133
+ 15,
134
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
135
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).stop(graceful=True)",
136
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q) requesting workers to stop and awaiting 1 producers...",
137
+ "INFO:csu.worker.engine:ExampleEngine(4w/0q).shutdown_handler(SIGTERM) FORCED",
138
+ "CONSUMER 0 perform_work JOB 1 DONE",
139
+ "CONSUMER 1 perform_work JOB 2 DONE",
140
+ "CONSUMER 2 perform_work JOB 3 DONE",
141
+ "DONE: {'ExampleConsumer': {'CANCELED': 3}, 'ExampleProducer': {'CANCELED': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 0}}",
142
+ )
143
+
144
+
145
+ def test_sem_rollback():
146
+ with TestProcess(sys.executable, "-mexampleworker", "1", "2", "10", "10") as target, dump_on_error(target.read):
147
+ wait_for_strings(
148
+ target.read,
149
+ 15,
150
+ "PRODUCER fetch_task @ 1 RETURN",
151
+ "CONSUMER 0 perform_work JOB 1 START",
152
+ "CONSUMER 0 perform_work JOB 1 DONE",
153
+ "CONSUMER 0 save_result JOB 1",
154
+ "PRODUCER fetch_task @ 2 SKIP",
155
+ "PRODUCER fetch_task @ 3 RETURN",
156
+ "CONSUMER 0 perform_work JOB 3 START",
157
+ "CONSUMER 0 perform_work JOB 3 DONE",
158
+ "CONSUMER 0 save_result JOB 3",
159
+ "PRODUCER fetch_task @ 4 SKIP",
160
+ "PRODUCER fetch_task @ 5 RETURN",
161
+ "CONSUMER 0 perform_work JOB 5 START",
162
+ "CONSUMER 0 perform_work JOB 5 DONE",
163
+ "CONSUMER 0 save_result JOB 5",
164
+ )
@@ -1,36 +0,0 @@
1
- from asgiref.typing import ASGIReceiveCallable
2
- from asgiref.typing import ASGISendCallable
3
- from asgiref.typing import Scope
4
- from django.core.asgi import get_asgi_application
5
-
6
- from . import logger
7
-
8
-
9
- def get_worker_lifespan_application():
10
- django_application = get_asgi_application()
11
-
12
- async def application_with_worker_lifespan(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
13
- if scope["type"] == "lifespan":
14
- try:
15
- from .registry import REGISTRY # noqa:PLC0415
16
-
17
- while True:
18
- message = await receive()
19
- message_type = message["type"]
20
- if message_type == "lifespan.startup":
21
- logger.info("Found %s registered engines.", len(REGISTRY))
22
- for module_name, engine in REGISTRY.items():
23
- logger.info("Starting engine for %s: %s...", module_name, engine)
24
- await engine.start()
25
- await send({"type": "lifespan.startup.complete"})
26
- if message_type == "lifespan.shutdown":
27
- for engine in REGISTRY.values():
28
- await engine.stop()
29
- await send({"type": "lifespan.shutdown.complete"})
30
- return
31
- except Exception as exc:
32
- logger.exception("Failed setting up lifespan: %r", exc)
33
- else:
34
- await django_application(scope, receive, send)
35
-
36
- return application_with_worker_lifespan
@@ -1,164 +0,0 @@
1
- import signal
2
- import sys
3
- import time
4
-
5
- from process_tests import TestProcess
6
- from process_tests import dump_on_error
7
- from process_tests import wait_for_strings
8
-
9
-
10
- def test_noskip():
11
- with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "4") as target, dump_on_error(target.read):
12
- wait_for_strings(
13
- target.read,
14
- 5,
15
- "INFO:worker:ExampleConsumer(1/UNKNOWN) started.",
16
- "INFO:worker:ExampleConsumer(2/UNKNOWN) started.",
17
- "INFO:worker:ExampleConsumer(3/UNKNOWN) started.",
18
- "INFO:worker:ExampleProducer(1/UNKNOWN) started.",
19
- "INFO:worker:ExampleProducer(1/UNKNOWN).run() started.",
20
- "PRODUCER fetch_task @ 1 RETURN",
21
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
22
- "INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
23
- "PRODUCER fetch_task @ 2 RETURN",
24
- "INFO:worker:ExampleEngine(4w/1q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=2))",
25
- "INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=2).done of NoOpTask(id=2).",
26
- "PRODUCER fetch_task @ 3 RETURN",
27
- "INFO:worker:ExampleEngine(4w/2q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
28
- "INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
29
- "INFO:worker:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
30
- "INFO:worker:ExampleConsumer(1/WORKING).run() - performing...",
31
- "CONSUMER 0 perform_work JOB 1 START",
32
- "INFO:worker:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=2)",
33
- "INFO:worker:ExampleConsumer(2/WORKING).run() - performing...",
34
- "CONSUMER 1 perform_work JOB 2 START",
35
- "INFO:worker:ExampleConsumer(3/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
36
- "INFO:worker:ExampleConsumer(3/WORKING).run() - performing...",
37
- "CONSUMER 2 perform_work JOB 3 START",
38
- "CONSUMER 0 perform_work JOB 1 DONE",
39
- "CONSUMER 0 save_result JOB 1",
40
- "CONSUMER 1 perform_work JOB 2 DONE",
41
- "CONSUMER 1 save_result JOB 2",
42
- "CONSUMER 2 perform_work JOB 3 DONE",
43
- "CONSUMER 2 save_result JOB 3",
44
- "PRODUCER fetch_task @ 4 RETURN",
45
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=4))",
46
- "INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=4).done of NoOpTask(id=4).",
47
- "PRODUCER fetch_task @ 5 EXHAUSTED",
48
- "INFO:worker:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=4)",
49
- "INFO:worker:ExampleConsumer(1/WORKING).run() - performing...",
50
- "CONSUMER 0 perform_work JOB 4 START",
51
- "PRODUCER fetch_task @ 6 EXHAUSTED",
52
- "CONSUMER 0 perform_work JOB 4 DONE",
53
- "CONSUMER 0 save_result JOB 4",
54
- "PRODUCER fetch_task @ 7 EXHAUSTED",
55
- )
56
-
57
-
58
- def test_skip():
59
- with TestProcess(sys.executable, "-mexampleworker", "3", "2", "100", "4") as target, dump_on_error(target.read):
60
- wait_for_strings(
61
- target.read,
62
- 5,
63
- "INFO:worker:ExampleConsumer(1/UNKNOWN) started.",
64
- "INFO:worker:ExampleConsumer(2/UNKNOWN) started.",
65
- "INFO:worker:ExampleConsumer(3/UNKNOWN) started.",
66
- "INFO:worker:ExampleProducer(1/UNKNOWN) started.",
67
- "INFO:worker:ExampleProducer(1/UNKNOWN).run() started.",
68
- "PRODUCER fetch_task @ 1 RETURN",
69
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=1))",
70
- "INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=1).done of NoOpTask(id=1).",
71
- "PRODUCER fetch_task @ 2 SKIP",
72
- "INFO:worker:ExampleConsumer(1/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=1)",
73
- "INFO:worker:ExampleConsumer(1/WORKING).run() - performing...",
74
- "CONSUMER 0 perform_work JOB 1 START",
75
- "CONSUMER 0 perform_work JOB 1 DONE",
76
- "CONSUMER 0 save_result JOB 1",
77
- "PRODUCER fetch_task @ 3 RETURN",
78
- "INFO:worker:ExampleEngine(4w/0q).push(ExampleJob(done=<Future pending>, started=<Future pending>, id=3))",
79
- "INFO:worker:ExampleProducer(1/WORKING).run() awaiting ExampleJob(done=<Future pending>, started=<Future pending>, id=3).done of NoOpTask(id=3).",
80
- "PRODUCER fetch_task @ 4 SKIP",
81
- "INFO:worker:ExampleConsumer(2/READY).run() working on: ExampleJob(done=<Future pending>, started=<Future finished result=True>, id=3)",
82
- "INFO:worker:ExampleConsumer(2/WORKING).run() - performing...",
83
- "CONSUMER 1 perform_work JOB 3 START",
84
- "CONSUMER 1 perform_work JOB 3 DONE",
85
- "CONSUMER 1 save_result JOB 3",
86
- "PRODUCER fetch_task @ 5 EXHAUSTED",
87
- )
88
-
89
-
90
- def test_graceful_shutdown():
91
- with TestProcess(sys.executable, "-mexampleworker", "3", "0", "1000", "100") as target, dump_on_error(target.read):
92
- wait_for_strings(
93
- target.read,
94
- 15,
95
- "PRODUCER fetch_task @ 1 RETURN",
96
- )
97
- target.proc.send_signal(signal.SIGINT)
98
- wait_for_strings(
99
- target.read,
100
- 15,
101
- "INFO:worker:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
102
- "INFO:worker:ExampleEngine(4w/0q).stop(graceful=True)",
103
- "INFO:worker:ExampleEngine(4w/0q) stats: {'ExampleConsumer': {'WORKING': 3}, 'ExampleProducer': {'WAITING': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 0}}",
104
- "INFO:worker:ExampleEngine(4w/0q) worker 1: <__main__.ExampleConsumer object at ",
105
- "INFO:worker:ExampleEngine(4w/0q) worker 2: <__main__.ExampleConsumer object at ",
106
- "INFO:worker:ExampleEngine(4w/0q) worker 3: <__main__.ExampleConsumer object at ",
107
- "INFO:worker:ExampleEngine(4w/0q) worker 4: <__main__.ExampleProducer object at ",
108
- "INFO:worker:ExampleEngine(4w/0q) requesting workers to stop and awaiting 1 producers...",
109
- "CONSUMER 0 perform_work JOB 1 DONE",
110
- "CONSUMER 0 save_result JOB 1",
111
- "INFO:worker:ExampleProducer(1/WAITING).on_exit(",
112
- "INFO:worker:ExampleEngine(4w/0q) flushing queue (0 items) and awaiting consumers...",
113
- "INFO:worker:ExampleConsumer(1/SHUTDOWN).on_exit(",
114
- "INFO:worker:ExampleConsumer(2/SHUTDOWN).on_exit(",
115
- "INFO:worker:ExampleConsumer(3/SHUTDOWN).on_exit(",
116
- "INFO:worker:ExampleEngine(4w/0q) shutdown complete.",
117
- "DONE: {'ExampleConsumer': {'EXITED': 3}, 'ExampleProducer': {'EXITED': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 2}}",
118
- )
119
-
120
-
121
- def test_forced_shutdown():
122
- with TestProcess(sys.executable, "-mexampleworker", "3", "0", "2000", "100") as target, dump_on_error(target.read):
123
- wait_for_strings(
124
- target.read,
125
- 15,
126
- "PRODUCER fetch_task @ 1 RETURN",
127
- )
128
- target.proc.send_signal(signal.SIGINT)
129
- time.sleep(1)
130
- target.proc.send_signal(signal.SIGTERM)
131
- wait_for_strings(
132
- target.read,
133
- 15,
134
- "INFO:worker:ExampleEngine(4w/0q).shutdown_handler(SIGINT) GRACEFUL",
135
- "INFO:worker:ExampleEngine(4w/0q).stop(graceful=True)",
136
- "INFO:worker:ExampleEngine(4w/0q) requesting workers to stop and awaiting 1 producers...",
137
- "INFO:worker:ExampleEngine(4w/0q).shutdown_handler(SIGTERM) FORCED",
138
- "CONSUMER 0 perform_work JOB 1 DONE",
139
- "CONSUMER 1 perform_work JOB 2 DONE",
140
- "CONSUMER 2 perform_work JOB 3 DONE",
141
- "DONE: {'ExampleConsumer': {'CANCELED': 3}, 'ExampleProducer': {'CANCELED': 1}, 'ExampleEngine': {'JOBS': 3, 'QSIZE': 0, 'SEMVAL': 0}}",
142
- )
143
-
144
-
145
- def test_sem_rollback():
146
- with TestProcess(sys.executable, "-mexampleworker", "1", "2", "10", "10") as target, dump_on_error(target.read):
147
- wait_for_strings(
148
- target.read,
149
- 15,
150
- "PRODUCER fetch_task @ 1 RETURN",
151
- "CONSUMER 0 perform_work JOB 1 START",
152
- "CONSUMER 0 perform_work JOB 1 DONE",
153
- "CONSUMER 0 save_result JOB 1",
154
- "PRODUCER fetch_task @ 2 SKIP",
155
- "PRODUCER fetch_task @ 3 RETURN",
156
- "CONSUMER 0 perform_work JOB 3 START",
157
- "CONSUMER 0 perform_work JOB 3 DONE",
158
- "CONSUMER 0 save_result JOB 3",
159
- "PRODUCER fetch_task @ 4 SKIP",
160
- "PRODUCER fetch_task @ 5 RETURN",
161
- "CONSUMER 0 perform_work JOB 5 START",
162
- "CONSUMER 0 perform_work JOB 5 DONE",
163
- "CONSUMER 0 save_result JOB 5",
164
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes