Skip to content

Commit 9cb3fee

Browse files
docs: add how-to add event bus support to an Open edX Event
1 parent a0e1f1e commit 9cb3fee

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
Adding Event Bus Support to an Open edX Event
2+
=============================================
3+
4+
Before sending an event across services, you need to ensure that the event is compatible with the Open edX Event Bus. This involves ensuring that the event, with its corresponding payload, can be emitted by a service through the event bus and that it can be consumed by other services. This guide will walk you through the process of adding event bus support to an Open edX event.
5+
6+
For more details on how the :term:`Event Payload` is structured refer to the :doc:`../decisions/0003-events-payload` decision record.
7+
8+
.. note::
9+
This guide assumes that you have already created an Open edX event. If you haven't, refer to the :doc:`../how-tos/creating-new-events` how-to guide.
10+
11+
Step 1: Does my Event Need Event Bus Support?
12+
----------------------------------------------
13+
14+
By default, Open edX Events should be compatible with the Open edX Event Bus. However, there are cases when the support might not be possible or needed for a particular event. Here are some scenarios where you might not need to add event bus support:
15+
16+
- The event is only used within the same application process and cannot be scoped to other services.
17+
- The :term:`Event Payload` contains data types that are not supported by the event bus, and it is not possible to refactor the :term:`Event Payload` to use supported data types.
18+
19+
When adding support is not possible do the following:
20+
21+
- Add it to the ``KNOWN_UNSERIALIZABLE_SIGNALS`` list in the ``openedx_events/tooling.py`` file so the event bus ignores it.
22+
- Add a ``warning`` in the event's docstring to inform developers that the event is not compatible with the event bus.
23+
24+
If you don't add the event to the ``KNOWN_UNSERIALIZABLE_SIGNALS`` list, the CI/CD pipeline will fail for the missing Avro schema that could not be generated for the :term:`Event Payload`. If you don't add a warning in the event's docstring, developers might try to send the event across services and encounter issues.
25+
26+
Step 2: Define the Event Payload
27+
--------------------------------
28+
29+
An Open edX Event is compatible with the event bus when its payload can be serialized, sent, and deserialized by other services. The payload, structured as attrs data classes, must align with the event bus schema format which in this case is the `Avro`_ schema. This ensures the event can be sent by the producer and be then re-emitted by the same instance of `OpenEdxPublicSignal`_ on the consumer side. For more information on the event bus schema format, refer to the :doc:`../decisions/0005-external-event-schema-format` and :doc:`../decisions/0004-external-event-bus-and-django-signal-events` decision records.
30+
31+
Here is an example of an :term:`Event Payload` structured as attrs data classes that align with the event bus schema format:
32+
33+
.. code-block:: python
34+
35+
@attr.s(frozen=True)
36+
class UserNonPersonalData:
37+
"""
38+
Attributes defined for Open edX user object based on non-PII data.
39+
40+
Arguments:
41+
id (int): unique identifier for the Django User object.
42+
is_active (bool): indicates whether the user is active.
43+
"""
44+
45+
id = attr.ib(type=int)
46+
is_active = attr.ib(type=bool)
47+
48+
@attr.s(frozen=True)
49+
class UserPersonalData:
50+
"""
51+
Attributes defined for Open edX user object based on PII data.
52+
53+
Arguments:
54+
username (str): username associated with the Open edX user.
55+
email (str): email associated with the Open edX user.
56+
name (str): name associated with the Open edX user's profile.
57+
"""
58+
59+
username = attr.ib(type=str)
60+
email = attr.ib(type=str)
61+
name = attr.ib(type=str, factory=str)
62+
63+
@attr.s(frozen=True)
64+
class UserData(UserNonPersonalData):
65+
"""
66+
Attributes defined for Open edX user object.
67+
68+
This class extends UserNonPersonalData to include PII data completing the
69+
user object.
70+
71+
Arguments:
72+
pii (UserPersonalData): user's Personal Identifiable Information.
73+
"""
74+
75+
pii = attr.ib(type=UserPersonalData)
76+
77+
@attr.s(frozen=True)
78+
class CourseData:
79+
"""
80+
Attributes defined for Open edX Course Overview object.
81+
82+
Arguments:
83+
course_key (str): identifier of the Course object.
84+
display_name (str): display name associated with the course.
85+
start (datetime): start date for the course.
86+
end (datetime): end date for the course.
87+
"""
88+
89+
course_key = attr.ib(type=CourseKey)
90+
display_name = attr.ib(type=str, factory=str)
91+
start = attr.ib(type=datetime, default=None)
92+
end = attr.ib(type=datetime, default=None)
93+
94+
The data types used in the attrs classes that the current Open edX Event Bus with the chosen schema are:
95+
96+
Primitive Data Types
97+
~~~~~~~~~~~~~~~~~~~~
98+
99+
- Boolean
100+
- Integer
101+
- Float
102+
- String
103+
- Bytes
104+
105+
Complex Data Types
106+
~~~~~~~~~~~~~~~~~~
107+
108+
- Type-annotated Lists (e.g., ``List[int]``, ``List[str]``)
109+
- Attrs Classes (e.g., ``UserNonPersonalData``, ``UserPersonalData``, ``UserData``, ``CourseData``)
110+
- Types with Custom Serializers (e.g., ``CourseKey``, ``datetime``)
111+
112+
Ensure that the :term:`Event Payload` is structured as `attrs data classes`_ and that the data types used in those classes align with the event bus schema format.
113+
114+
Step 3: Ensure Serialization and Deserialization
115+
------------------------------------------------
116+
117+
Before sending the event across services, you need to ensure that the :term:`Event Payload` can be serialized and deserialized correctly. The event bus concrete implementations use the Avro schema to serialize and deserialize the :term:`Event Payload` as mentioned in the :doc:`../decisions/0005-external-event-schema-format` decision record. The concrete implementation of the event bus handles the serialization and deserialization with the help of methods implemented by this library.
118+
119+
.. For example, here's how the Redis event bus handles serialization before sending a message:
120+
121+
.. .. code-block:: python
122+
.. :emphasize-lines: 4
123+
124+
.. # edx_event_bus_redis/internal/producer.py
125+
.. full_topic = get_full_topic(topic)
126+
.. context.full_topic = full_topic
127+
.. event_bytes = serialize_event_data_to_bytes(event_data, signal)
128+
.. message = RedisMessage(topic=full_topic, event_data=event_bytes, event_metadata=event_metadata)
129+
.. stream_data = message.to_binary_dict()
130+
131+
.. Where `serialize_event_data_to_bytes`_ is a method that serializes the :term:`Event Payload` to bytes using the Avro schema. While the consumer side deserializes the :term:`Event Payload` using the Avro schema with the help of the `deserialize_bytes_to_event_data`_ method:
132+
133+
.. .. code-block:: python
134+
.. :emphasize-lines: 3
135+
136+
.. # edx_event_bus_redis/internal/consumer.py
137+
.. signal = OpenEdxPublicSignal.get_signal_by_type(msg.event_metadata.event_type)
138+
.. event_data = deserialize_bytes_to_event_data(msg.event_data, signal)
139+
.. send_results = signal.send_event_with_custom_metadata(msg.event_metadata, **event_data)
140+
141+
If the :term:`Event Payload` contains types that are not supported by the event bus, you could implement custom serializers for these types. This ensures that the :term:`Event Payload` can be serialized and deserialized correctly when sent across services.
142+
143+
Here is an example of a custom serializer for the ``CourseKey`` type:
144+
145+
.. code-block:: python
146+
147+
# event_bus/avro/custom_serializers.py
148+
class CourseKeyAvroSerializer(BaseCustomTypeAvroSerializer):
149+
"""
150+
CustomTypeAvroSerializer for CourseKey class.
151+
"""
152+
153+
cls = CourseKey
154+
field_type = PYTHON_TYPE_TO_AVRO_MAPPING[str]
155+
156+
@staticmethod
157+
def serialize(obj) -> str:
158+
"""Serialize obj into string."""
159+
return str(obj)
160+
161+
@staticmethod
162+
def deserialize(data: str):
163+
"""Deserialize string into obj."""
164+
return CourseKey.from_string(data)
165+
166+
167+
After implementing the serializer, add it to ``DEFAULT_CUSTOM_SERIALIZERS`` at the end of the ``event_bus/avro/custom_serializers.py`` file:
168+
169+
.. code-block:: python
170+
171+
DEFAULT_CUSTOM_SERIALIZERS = [
172+
# Other custom serializers
173+
CourseKey: CourseKeyAvroSerializer,
174+
]
175+
176+
Now the :term:`Event Payload` can be serialized and deserialized correctly when sent across services.
177+
178+
.. warning::
179+
One of the known limitations of the current Open edX Event Bus is that it does not support dictionaries as data types. If the :term:`Event Payload` contains dictionaries, you may need to refactor the :term:`Event Payload` to use supported data types. When you know the structure of the dictionary, you can create an attrs class that represents the dictionary structure. If not, you can use a str type to represent the dictionary as a string and deserialize it on the consumer side using JSON deserialization.
180+
181+
If your :term:`Event Payload` contains only supported data types, you can skip this step.
182+
183+
Step 4: Generate the Avro Schema
184+
--------------------------------
185+
186+
As mentioned in the previous step, the serialization and deserialization of the :term:`Event Payload` is handled by the concrete event bus implementation with the help of methods implemented in this library. However, ``openedx-events`` ensures the payload of new events can be serialized and deserialized correctly by adding checks in the CI/CD pipeline for schema verification. To ensure this, you need to generate the Avro schema for the :term:`Event Payload`:
187+
188+
1. Run the following command to generate the Avro schema for the :term:`Event Payload`:
189+
190+
.. code-block:: bash
191+
192+
python manage.py generate_avro_schemas org.openedx.learning.course.enrollment.changed.v1
193+
194+
2. The Avro schema for the :term:`Event Payload` will be generated in the ``openedx_events/event_bus/avro/tests/schemas`` directory.
195+
3. Push the changes to the branch and create a pull request or run the checks locally to verify that the Avro schema was generated correctly.
196+
197+
.. code-block:: bash
198+
199+
make test
200+
201+
Step 5: Send the Event Across Services with the Event Bus
202+
---------------------------------------------------------
203+
204+
To validate that you can consume the event emitted by a service through the event bus, you can send the event across services. Here is an example of how you can send the event across services using the Redis event bus implementation following the `setup instructions in a Tutor environment`_.
205+
206+
.. _Avro: https://avro.apache.org/
207+
.. _OpenEdxPublicSignal: https://github.com/openedx/openedx-events/blob/main/openedx_events/tooling.py#L37
208+
.. _attrs data classes: https://www.attrs.org/en/stable/overview.html
209+
.. _serialize_event_data_to_bytes: https://github.com/openedx/openedx-events/blob/main/openedx_events/event_bus/avro/serializer.py#L82-L98
210+
.. _deserialize_bytes_to_event_data: https://github.com/openedx/openedx-events/blob/main/openedx_events/event_bus/avro/deserializer.py#L86-L98
211+
.. _setup instructions in a Tutor environment: https://github.com/openedx/event-bus-redis/blob/main/docs/tutor_installation.rst

docs/how-tos/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ How-tos
77

88
creating-new-events
99
adding-events-to-a-service
10+
adding-event-bus-support-to-an-event
1011
adding-events-to-event-bus
1112
using-events
1213
add-new-event-bus-concrete-implementation

0 commit comments

Comments
 (0)