Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: utility to reset application state before processing event #277

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Change Log

Unreleased
----------
[9.5.0] - 2024-02-07
--------------------
Added
~~~~~
* Adds utility function to reset application state similar to setup/teardown in Django request/response cycle.

[9.4.0] - 2024-01-29
--------------------
Added
Expand Down
35 changes: 35 additions & 0 deletions docs/how-tos/add-new-event-bus-concrete-implementation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
How to add a new concrete implementation of the event bus
=========================================================

Context
-------

Here is a list of the existing concrete implementations of the event bus:

- `Kafka <https://github.com/openedx/event-bus-kafka>`_
- `Redis Streams <https://github.com/openedx/event-bus-redis>`_

This how-to is to help you add a new concrete implementation, for example using Pulsar or some other technology.

Producing
---------

There should be a producer class that inherits from `EventBusProducer <https://github.com/openedx/openedx-events/blob/cbb59f124ed84afacb9ec99baa82a86381370dcc/openedx_events/event_bus/__init__.py#L66>`_ in openedx-events.

The defined ``send`` method is meant to be called from within a signal receiver in the producing service.

Consuming
---------

At a high level, the consumer should be a process that takes the signals and events from the broker and emits the signal with the event. There should be a consumer class that inherits from `EventBusConsumer <https://github.com/openedx/openedx-events/blob/06635f3642cee4020d6787df68bba694bd1233fe/openedx_events/event_bus/__init__.py#L127>`_ in openedx-events.

The consumer class then needs to implement ``consume_indefinitely`` loop, which will stay running and listen to events as they come in.

We have included an utility function called `prepare_for_new_work_cycle <../../openedx_events/tooling.py#L323>`_ in openedx-events which needs to be called before processing any signal. Currently, it reconnects the db connection if required as well as clears RequestCache and there may be later, more comprehensive changes. These steps mimic some setup/teardown that is normally performed by Django in its request/response based architecture.

Checkout `consumer.py <https://github.com/openedx/event-bus-redis/blob/main/edx_event_bus_redis/internal/consumer.py>`_ in event bus redis implementation.

Abstraction tickets
-------------------

The known remaining work for a fully abstracted event bus is captured in the `Abstraction tickets <https://github.com/orgs/edx/projects/11/views/4?filterQuery=label%3Aevent-bus+-status%3ADone+abstraction>`_
1 change: 1 addition & 0 deletions docs/how-tos/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ How-tos
adding-events-to-a-service
adding-events-to-event-bus
using-events
add-new-event-bus-concrete-implementation
2 changes: 1 addition & 1 deletion openedx_events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
more information about the project.
"""

__version__ = "9.4.0"
__version__ = "9.5.0"
44 changes: 44 additions & 0 deletions openedx_events/tooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from logging import getLogger

from django.conf import settings
from django.db import connection
from django.dispatch import Signal
from edx_django_utils.cache import RequestCache

from openedx_events.data import EventsMetadata
from openedx_events.exceptions import SenderValidationError
Expand Down Expand Up @@ -292,3 +294,45 @@ def load_all_signals():
Loads all non-test signals.py modules.
"""
_process_all_signals_modules(import_module)


def _reconnect_to_db_if_needed(): # pragma: no cover
"""
Reconnects the db connection if needed.

This is important because Django only does connection validity/age checks as part of
its request/response cycle, which isn't in effect for the consume-loop. If we don't
force these checks, a broken connection will remain broken indefinitely. For most
consumers, this will cause event processing to fail.
"""
has_connection = bool(connection.connection)
requires_reconnect = has_connection and not connection.is_usable()
if requires_reconnect:
connection.connect()


def _clear_request_cache(): # pragma: no cover
"""
Clear the RequestCache so that each event consumption starts fresh.

Signal handlers may be written with the assumption that they are called in the context
of a web request, so we clear the request cache just in case.
"""
RequestCache.clear_all_namespaces()


def prepare_for_new_work_cycle(): # pragma: no cover
"""
Ensure that the application state is appropriate for performing a new unit of work.

This mimics some setup/teardown that is normally performed by Django in its
request/response based architecture and that is needed for ensuring a clean and
usable state in this worker-based application.

See https://github.com/openedx/openedx-events/issues/236 for details.
"""
# Ensure that the database connection is active and usable.
_reconnect_to_db_if_needed()

# Clear the request cache, in case anything in the signal handlers rely on it.
_clear_request_cache()
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Core requirements for using this application
-c constraints.txt

edx_django_utils
django
attrs
fastavro
Expand Down
29 changes: 26 additions & 3 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,47 @@ asgiref==3.7.2
# via django
attrs==23.2.0
# via -r requirements/base.in
django==3.2.23
cffi==1.16.0
# via pynacl
click==8.1.7
# via edx-django-utils
django==3.2.24
# via
# -c requirements/common_constraints.txt
# -r requirements/base.in
# django-crum
# django-waffle
# edx-django-utils
django-crum==0.7.9
# via edx-django-utils
django-waffle==4.1.0
# via edx-django-utils
edx-django-utils==5.10.1
# via -r requirements/base.in
edx-opaque-keys[django]==2.5.1
# via -r requirements/base.in
fastavro==1.9.3
# via -r requirements/base.in
newrelic==9.6.0
# via edx-django-utils
pbr==6.0.0
# via stevedore
psutil==5.9.8
# via edx-django-utils
pycparser==2.21
# via cffi
pymongo==3.13.0
# via edx-opaque-keys
pytz==2023.3.post1
pynacl==1.5.0
# via edx-django-utils
pytz==2024.1
# via django
sqlparse==0.4.4
# via django
stevedore==5.1.0
# via edx-opaque-keys
# via
# edx-django-utils
# edx-opaque-keys
typing-extensions==4.9.0
# via
# asgiref
Expand Down
2 changes: 1 addition & 1 deletion requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ packaging==23.2
# via
# pyproject-api
# tox
platformdirs==4.1.0
platformdirs==4.2.0
# via
# tox
# virtualenv
Expand Down
54 changes: 41 additions & 13 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ cachetools==5.3.2
# via
# -r requirements/ci.txt
# tox
certifi==2023.11.17
certifi==2024.2.2
# via
# -r requirements/quality.txt
# requests
cffi==1.16.0
# via
# -r requirements/quality.txt
# cryptography
# pynacl
chardet==5.2.0
# via
# -r requirements/ci.txt
Expand All @@ -47,49 +48,63 @@ click==8.1.7
# -r requirements/quality.txt
# click-log
# code-annotations
# edx-django-utils
# edx-lint
# pip-tools
click-log==0.4.0
# via
# -r requirements/quality.txt
# edx-lint
code-annotations==1.5.0
code-annotations==1.6.0
# via
# -r requirements/quality.txt
# edx-lint
colorama==0.4.6
# via
# -r requirements/ci.txt
# tox
coverage[toml]==7.4.0
coverage[toml]==7.4.1
# via
# -r requirements/quality.txt
# coverage
# pytest-cov
cryptography==42.0.0
cryptography==42.0.2
# via
# -r requirements/quality.txt
# secretstorage
ddt==1.7.1
# via -r requirements/quality.txt
diff-cover==8.0.3
# via -r requirements/dev.in
dill==0.3.7
dill==0.3.8
# via
# -r requirements/quality.txt
# pylint
distlib==0.3.8
# via
# -r requirements/ci.txt
# virtualenv
django==3.2.23
django==3.2.24
# via
# -c requirements/common_constraints.txt
# -r requirements/quality.txt
# django-crum
# django-waffle
# edx-django-utils
django-crum==0.7.9
# via
# -r requirements/quality.txt
# edx-django-utils
django-waffle==4.1.0
# via
# -r requirements/quality.txt
# edx-django-utils
docutils==0.20.1
# via
# -r requirements/quality.txt
# readme-renderer
edx-django-utils==5.10.1
# via -r requirements/quality.txt
edx-lint==5.3.6
# via -r requirements/quality.txt
edx-opaque-keys[django]==2.5.1
Expand Down Expand Up @@ -156,7 +171,7 @@ markdown-it-py==3.0.0
# via
# -r requirements/quality.txt
# rich
markupsafe==2.1.4
markupsafe==2.1.5
# via
# -r requirements/quality.txt
# jinja2
Expand All @@ -172,6 +187,10 @@ more-itertools==10.2.0
# via
# -r requirements/quality.txt
# jaraco-classes
newrelic==9.6.0
# via
# -r requirements/quality.txt
# edx-django-utils
nh3==0.2.15
# via
# -r requirements/quality.txt
Expand All @@ -195,7 +214,7 @@ pkginfo==1.9.6
# via
# -r requirements/quality.txt
# twine
platformdirs==4.1.0
platformdirs==4.2.0
# via
# -r requirements/ci.txt
# -r requirements/quality.txt
Expand All @@ -209,6 +228,10 @@ pluggy==1.4.0
# diff-cover
# pytest
# tox
psutil==5.9.8
# via
# -r requirements/quality.txt
# edx-django-utils
pycodestyle==2.11.1
# via -r requirements/quality.txt
pycparser==2.21
Expand Down Expand Up @@ -247,6 +270,10 @@ pymongo==3.13.0
# via
# -r requirements/quality.txt
# edx-opaque-keys
pynacl==1.5.0
# via
# -r requirements/quality.txt
# edx-django-utils
pyproject-api==1.6.1
# via
# -r requirements/ci.txt
Expand All @@ -255,20 +282,20 @@ pyproject-hooks==1.0.0
# via
# -r requirements/pip-tools.txt
# build
pytest==7.4.4
pytest==8.0.0
# via
# -r requirements/quality.txt
# pytest-cov
# pytest-django
pytest-cov==4.1.0
# via -r requirements/quality.txt
pytest-django==4.7.0
pytest-django==4.8.0
# via -r requirements/quality.txt
python-slugify==8.0.1
python-slugify==8.0.3
# via
# -r requirements/quality.txt
# code-annotations
pytz==2023.3.post1
pytz==2024.1
# via
# -r requirements/quality.txt
# django
Expand Down Expand Up @@ -317,6 +344,7 @@ stevedore==5.1.0
# via
# -r requirements/quality.txt
# code-annotations
# edx-django-utils
# edx-opaque-keys
text-unidecode==1.3
# via
Expand Down Expand Up @@ -351,7 +379,7 @@ typing-extensions==4.9.0
# edx-opaque-keys
# pylint
# rich
urllib3==2.1.0
urllib3==2.2.0
# via
# -r requirements/quality.txt
# requests
Expand Down
Loading
Loading