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: add necessary tooling for Open edX events #7

Merged
merged 1 commit into from
Jul 28, 2021

Conversation

mariajgrimaldi
Copy link
Member

@mariajgrimaldi mariajgrimaldi commented Jun 8, 2021

Description:
This PR adds the tooling needed to create and trigger events in Open edX platform. This PR includes:

  • OpenEdxPublicSignal class: used to define events with the relevant metadata and methods to trigger them on the Open edX platform.
  • Custom Exceptions: raised by the tooling when there's an error sending or instantiating events

The overall implementation was discussed by members of the community in this PR: #4

JIRA:
https://edunext.atlassian.net/browse/PS2021-629?atlOrigin=eyJpIjoiODIwNDA5M2MxZGRlNGQ2NjhjNjJhZWIwNWZiNWNlYjUiLCJwIjoiaiJ9

Testing instructions:

Option 1: using PR #18

With the event STUDENT_REGISTRATION_COMPLETED:

In your Open edX devstack

  1. Go to lms shell make lms-shell
  2. Install openedx-events specifying the events definition branch:
    pip install git+https://github.com/eduNEXT/openedx-events.git@MJG/events_definition#egg=events_definition
  3. Change the Open edX platform branch to eduNEXT:MJG/1st_batch_openedx_events
  4. Connect your custom receiver to the signal as follows:
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED

@receiver(STUDENT_REGISTRATION_COMPLETED)
def callback(**kwargs):
....

We'll add soon an example using our plugin openedx-basic-hooks!

Reviewers:

  • tag reviewer
  • tag reviewer

Merge checklist:

  • All reviewers approved
  • CI build is green
  • Version bumped
  • Changelog record added
  • Documentation updated (not only docstrings)
  • Commits are squashed

Post merge:

  • Create a tag
  • Check new version is pushed to PyPI after tag-triggered build is
    finished.
  • Delete working branch (if not needed anymore)

Author concerns:

@mariajgrimaldi mariajgrimaldi changed the base branch from main to MJG/folder_structure June 8, 2021 14:25
@mariajgrimaldi mariajgrimaldi changed the title Mjg/events tooling [WIP] Mjg/events tooling Jun 8, 2021
@mariajgrimaldi mariajgrimaldi changed the base branch from MJG/folder_structure to MJG/fix_testing June 8, 2021 20:43
@mariajgrimaldi mariajgrimaldi force-pushed the MJG/events_tooling branch 3 times, most recently from 755f544 to 5d4e6fc Compare June 9, 2021 02:37
@mariajgrimaldi mariajgrimaldi force-pushed the MJG/events_tooling branch 2 times, most recently from 67c8323 to d7879cd Compare June 9, 2021 02:45
@mariajgrimaldi mariajgrimaldi changed the title [WIP] Mjg/events tooling feat: add necessary tooling for Open edX events Jun 9, 2021
@mariajgrimaldi mariajgrimaldi marked this pull request as ready for review June 10, 2021 18:31
"""
Getter function used to get the Open edX Events version.
"""
return openedx_events.__version__
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felipemontoya this is what I'm returning for spec version.
The OEP 41 says: is a mandatory field that refers to the version of CloudEvents. We have to use “1.0” to be spec-compliant.
In our case, we'd refer to openedx-events version, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of sending the version of the openedx_events library, but I would not like to mix it with the specversion of CloudEvents.
I'd rather leave specversion alone as "1.0" to not mess around and send another field with the openedx events version.

For the new field I'm thinking events_version, but it could be other key

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also play off the source* convention and call it sourcelib or something, but I'm totally fine with events_version as well.

openedx_events/tooling.py Show resolved Hide resolved
openedx_events/tooling.py Show resolved Hide resolved
openedx_events/tooling.py Outdated Show resolved Hide resolved
@mariajgrimaldi mariajgrimaldi force-pushed the MJG/events_tooling branch 2 times, most recently from effffbb to 0df3e69 Compare June 28, 2021 22:48
@mariajgrimaldi mariajgrimaldi changed the base branch from MJG/fix_testing to main June 29, 2021 11:43
@mariajgrimaldi mariajgrimaldi force-pushed the MJG/events_tooling branch 2 times, most recently from de25a1f to cfa9741 Compare June 30, 2021 20:32
),
)

validate_sender()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @felipemontoya! I hope you're doing great.

I found an interesting case while implementing the POC with the 8 events. When the platform sends the signal UNENROLL_DONE, sends an argument called skip_refund. I thought it was not a big deal, I could send the argument as a keyword... but the problem is this validation -that was discussed in the naming ADR-:

We expect send calls to always send things over as keyword args, and throw an exception if we get any parameters that we don't expect, or any parameters that are missing, based on the constructor call to OpenEdxPublicSignal.

This prevents sending extra arguments not defined in the event definition. And skip_refund is not part of the CourseEnrollment object so it doesn't make sense to include it in the CourseEnrollmentrData class.

Then what can we do? I was thinking about just checking that at least the arguments defined when instantiating the event are sent, what do you think? This way we can actually send sender when needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of the spirit of the discussion we had in the naming ADR is that we precisely dont want to permit extra params. We want to make the events super explicit in what they are passing around.

In my opinion we have 2 options. We add skip_refund to make the event we are creating for unenroll an make it 100% compatible to the current unenroll_done or we don't and we make an event that is not 100% compatible. This means we will add it to the core platform and it will co-exist with the current unenroll_done for a while until we can migrate things.

I'd be inclined to say that we should leave without skip_refund and send two events when skip_refund is false. One for the unenrollment and one for the refund.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean something like:

UNENROLL_DONE_WITH_REFUND
UNENROLL_DONE_WITHOUT_REFUND

And the receivers would be subscribed to both signals, right? I like the idea, but what if more extra arguments are needed in the future? How many variations will we need?

Copy link
Member

@felipemontoya felipemontoya Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean something like:

UNENROLL_DONE-> this is the unenrollment event with skip_refund = true
REFUND -> this is what happens when skip_refunds = false

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would do the following:

UNENROLL_DONE -> this event gets sent on any unenrollment whether or not there is a refund.
REFUND -> this event gets sent if skip_refunds = false.

In the case of a refund, you still want the unenroll message for anyone that cares about enrollments.

@mariajgrimaldi mariajgrimaldi force-pushed the MJG/events_tooling branch 4 times, most recently from 6822ed8 to 7ca8286 Compare July 8, 2021 21:17
openedx_events/tooling.py Outdated Show resolved Hide resolved
openedx_events/tooling.py Outdated Show resolved Hide resolved
"""
return "<OpenEdxPublicSignal: {event_type}>".format(event_type=self.event_type)

def get_signal_metadata(self):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments:

Defining Metadata in data.py
Can the metadata itself be another attrs class that has well defined fields? This could live somewhere independently of the learning events.

Separating CloudEvents serialization specifics.
Let's keep the metadata here in a more natural Python form while it's in memory, and convert it to the serialized version in an explicit CloudEvents layer in the future, when that becomes relevant. In concrete terms, I think that just means that specversion goes away at this layer (it's a fixed constant for CloudEvents), time becomes a UTC datetime instead of a string, and events version might become a tuple of ints so that version comparisons don't have to worry about lexical ordering of strings (e.g. '0.9.0' vs. '0.10.0').

Message Queue version
This isn't something I think you need to do yet, but is the long term plan to make metadata an optional param that's auto-generated in the case of a local send, but populated from a serialized message if the signal is transmitted over a message queue?

UUID?
The CloudEvent will eventually have a UUID. Is it useful to have it in this metadata as well?

Nit: "get" vs. "create/generate"
This is a nit, but for me, get implies something is more or less staying the same and you're accessing it, while create or generate means that you're creating a new thing and handing it back.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there @ormsbee! I hope you're doing good!

I've updated the PR with your recommendations:

Defining Metadata in data.py
I've created this PR showing how I would do this

Separating CloudEvents serialization specifics.
I followed all your recommendations here.

Message Queue version
I think the best thing is to consider this use case already. If a Metadata object exists, then it's not generated. What do you think?

UUID?
I added an ID as part of the metadata object, based on what the OEP-41 said:
https://open-edx-proposals.readthedocs.io/en/latest/oep-0041-arch-async-server-event-messaging.html#id2

Nit: "get" vs. "create/generate"
Thanks for the comment! I fixed it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: we fully adopted defining metadata in data.py

@mariajgrimaldi mariajgrimaldi force-pushed the MJG/events_tooling branch 5 times, most recently from 26ad85c to 80ad2c0 Compare July 20, 2021 12:03
Copy link
Member

@felipemontoya felipemontoya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good work @mariajgrimaldi.

I think this PR already covered all the feedback and the current proposal for the tooling is very robust. On my opinion we are ready to merge this.

@mariajgrimaldi mariajgrimaldi force-pushed the MJG/events_tooling branch 3 times, most recently from 863e3de to e827c18 Compare July 28, 2021 13:56
- Add signal class used to create events
- Add metadata atrr class
- Add metadata generator to class
- Add argument validator to class
- Override signal methods to recommend using the custom send_event
source (str): logical source of an event.
sourcehost (str): physical source of the event.
time (datetime): timestamp when the event was sent.
sourcelib (str): Open edX Events library version.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mariajgrimaldi: Hello Maria. It seems we went with a tuple instead of a string. I'm curious why we did that. I'm working on the event bus, which is going to need to send this metadata across the wire, so I may need to pass in a string and convert it back to a tuple, which will be a bit strange, but would make the type in this comment more correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robrap Hello there, Robert. After some digging, I found this discussion I had with dave when this PR was open: #7 (comment) If you think there's room for improvement, let me know! -besides updating the docstring 😅 -

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants