In my opinion, it is a really cool concept. However, I wanted to query a few things, and I thought that this was the appropriate place to discuss it.
Comment 1 is about the database tables. Martin is proposing two: event_handlers and event_queue. This is not properly normalised, because if you have several handlers for one event, the same (potentially large) eventdata object will be repeated several rows.
We should have three tables: event_handlers as before, event_queued_events with columns id, eventdata, schedule and timecreated, and event_queue_handlers_todo, with columns id, queuedeventid, handlerid, status, timmodified.
When the last row in event_queue_handlers_todo referring to a row in event_queued_events is removed, then the corresponding row from event_queued_events is automatically removed.
In event_queued_events, I suggest we add columns
stackdump text serialized debug_backtrace showing where the event was fired from
userid int(10) $USER->id when the event was fired
Possibly some others. And possibly allow these to be null, and only store them if some config option is turned on.
In event_queue_handlers_todo (someone should think of a better name for this), add an column
errormessage text if an error happened last time we tried to process this event, record it here.
Or perhaps a better way to deal with all this is to have some sort of log table, which is what Martin suggests on the wiki page.
A related notion that's been floating around is that of merging add_to_log into the whole thing, so that all the "events" we currently log suddenly become possible triggers. This would instantly create all kinds of possibilities for interesting handlers.
Storing a backtrace is a good idea.
Then, if it ever makes sense to separate out different types of logs into different types of tables, we can do that within the log subsystem. And, if we use the events to handle the log sub-system, the log sub-system can dispatch specific actions on specific events, possibly even throwing events out in case someone cares and is listening? (Hey! I just go a admin login failure! Anyone care?).
The queue should be pretty short or empty most of the time - it only holds events that have failed or are waiting for the next cron.
However, I don't mind changing it if there's strong support for the idea of three tables. The nice thing is that such things can easily be extended later, because it's entirely internal to the Events API and no crucial data is held in these tables.
I like the proposed design -- and in terms of the 2 vs 3 tables I tend to lean towards martinD's 2-tables, but I don't mind one way or the other.
A couple of notes:
- I think that the API should allow anything in the serialised data (as it does), but that there should be super extra strong encouragement to put the actual data elsewhere. So for the example given in the wikipage, I'd put the data in the grades table and then trigger a (perhaps parameter-less) event that succintly says "new grades!".
- I am not opposed to having support for "immediate" events as well as "queued" events. I do think that queued events is all we should ever use... no. About once a year, it's safe to commit code that needs immediate events...
- The events that Moodle triggers will become a bespoke API -- should we maintain some doco on those, explanining how and when they are triggered?
- How do we discourage cross-module interdependencies that the API will probably foster?
Edit: I see now that 'timing' probably controls queued vs immediate.
For less crucial stuff, or bulk stuff, then 'cron' make sense - in that case it'll be on the queue for up to the cron period (usually 15 minutes).
Martin, you told me about Events for making cross-modules API. I read it out and wonder what is the processing path for 'instant' events.
My goal is making one module (some project dedicated module) ask for data to a potentially not installed module, and to bind such instance of a coursemodule to exacly such other instance, as they are fit to work (potentially) together. Another situation could be plugin a module within another so that part of its data model get shared with part of another. Say, when adding a task module (Jeff Graham's), I can bind that module to use a techproject task outline rather than its internal model, or even just some of the attributes.
As Events are essentially an asynchronous triggering method (seems trivial when croned, that cron framework will consume all queued events), I fear such very synchronous form of cooperation does not exactly fit, as this is not a straight implementation of the Observer pattern in that way I wonder how the triggered event can give data back to another module for displaying in its caller screen.
So my main question : when triggering out an "instant' event, what is the path ? How could I lock and wait for a synchronous answer (supposed it is an answering event) of the triggered module ? (e.g. in order to complete my output with the data I get from that anwser).
In case events are not exactly suited for this kind of cooperation, how to get a real synchronous Listener registration pattern ?
this new concept seems to become a good help to us. we are working on a synchronization between Moodle and an external LMS.
We do have a working implementation. But we put our code into Moodle's core-code. Now we are working on changing our implementation to work as a module to be "update-resistant"
So we need our synchronise-activity-module to be informed of any changes on enrolled students or other basic course information (from Moodle), as well as information of new grades from the external LMS to be written into the Moodle-DB.
First we thought we could use some functionality similar to the Forum-force-subscription, but it seems to be very difficult because of some missing documentation on how this works.
But now we hope, this forthcoming events-API could reduce the complexity of our task.
Therefore I would like to know your opinion to this. do you agree?
And am I right that that the modules can react on either internal events (such as new enrolments or new groups to a course) or events from other modules (like new grades)?
I hope that this was the right place to post this question. I am thankful to any help or advice...
The external system we are going to synchronize with is a third-party open-source mathematical learning tool named MUMIE. Students can work on exercises as applets. These exercises are graded automatically by the system and the grades should be send back to Moodle.
But the whole course, semester and user administration has to come from Moodle via synchronisation. (MUMIE still gots its own database)
In short: We want to integrate MUMIE to behave as a homework-tool into Moodle.
I hope now you get a better understanding of our project.
If there are still any questions or annotations - don's hesitate to write.
You can use any mechanism you like to copy grades from MUMIE to your module's tables, or enrolment information from Moodle to MUMIE (this is unrelated to the event API). Like Martin L said, there are lots of normal functions available to help find this information in Moodle.
The event API will only be important when you want to pump those grades into the gradebook, or trigger other events that might be interesting.
thanks so far. I guess I should take a look at the LAMS module now.
I see, it is possible to get the relevant information (for example students in a course) with the available functions (like get_course_users).
But this functions have to be called out of the activity. Right?
Since we don't want the course-databases (Moodle's and MUMIE's) to be inconsistent, we would like to have any changes in our course (and not just the activity) like enrollments or new trainer or new groups automaticly send to the activity (and we then forward it to MUMIE).
And we are not quite sure how to tell Moodle to always send such information. (We are still trying to understand the force-subscription of the forum...)
And yes.. We do want to get the MUMIE grades to be in the gradebook later because one of our goals is that a student should use MUMIE as a part of Moodle not noticing he is redirected to another system.
1) Why does the 'schedule' option in events_handlers only include 'cron' or 'instant'? How about allowing that to be a number in seconds (or 0=instant) for future expansion i.e. in case there is ever any attempt to shuffle load of cron, so that if the system has a lot of things to do in cron it can drop 'later' ones until next time? (Okay maybe this is not so important really...)
2) Where did the 'schedule' option in events_queue come from if the data is only stored once? (Some handlers might have cron, some might be instant.) There didn't seem to be any option to spec this when creating an event. I don't think it should be there (is this an intentional mistake to check I'm reading?)
3) Re events_queue_handlers; is there a number of times after which retry stops happening? Any logic so that retries become less frequent as the count increases? [What I'm really getting at is that it probably shouldn't keep retrying for ever, notifying somebody would be good!]
4) Should there be a way to define 'provided' events as well as 'requested' events? So that we can for example do debug checks that the requested events match up with those provided somewhere on the system.
(edit) AAAAND one more.
5) Event requests should contain a 'filter' parameter, optional, as serialised PHP object. The semantics of the filter would depend on the specific event. For example, imagine there is a role_assign event, but you only care about assign to courses; could be $filter->contextlevel=50. Events that support filters would need to be sent with a callback function that basically returns true or false depending on a $filter.
(edit) AAAAAND another one more
6) Should be a way to define events as 'overwrite' type i.e. an event supercedes other events of the same type/parameters (because it just notifies something has happened, without including any specific data). This allows for potential later performance improvements such as not adding any events to the queue if a previous similar event already caused all handlers to be added.
1) It's a text field so we can we add more easily (arbitrarily even). Further optimisation in cron might be useful later ... I guess we can add that once we get more familiar with it.
2) This events_queue schedule gets copied from the events_handler ... it's a copy just in case we might want to alter it in the queue (override what the handler said it wanted) for some reason.
3) Yes indeed it would throw a "too many failed attempts" event. (we talked about it but it hadn't made the document yet, sorry. Fixed.).
4) To catch typos in event names, maybe, yes ... perhaps unit tests can do this?
5) Can't all that go in the main $eventdata object?
6) I can't think of an example where a module would send two events for the same thing this way ...
4 - It would also provide psuedo-documentation (standard place to look for events and would be possible to automatically create a list of all available events in moodle, perhaps as an admin report [on which note - I kind of wonder if we need a 'developer' category of admin reports]). For checking, I was thinking along the lines of the check it does for whether capabilities exist, when debugging is turned to developer. I.e. when you send or request an event it checks it exists.
5 - You're right, it would be easier to just check it in the event function and ignore any you don't care about.
6 (ignoring duplicate events) - Well, I can However it may not apply. For example supposing there is a 'role assignments have changed on course x' event. It might be fairly frequent that this event would be sent say 500 times when students are allocated to the course, but really only needs to run once. HOWEVER it may be the intent that this kind of 'something's changed in the database, go look it up' event is never or not normally used and those examples would become 'user y has been added to course x' which would obviously not be repeated. That's certainly how the example works.
are such events just thrown my modules or from core-parts of Moodle, too? I am asking, because we are working on an activity-module that has to keep an external DB konsistant to Moodle's.
So even when such an activity has been put into a course, we want to be informed when for example a new group is created whithin that course.
Unfortunately there is no "if(function_exists..."-call for this situation and we do not want to make extensions to Moodle's core-code. So the possibility of triggering an event could eventually help us a lot.
What do you think??
I got a question on where you are going to place the events_trigger()-calls... More high-level or more low-level...
An example for a better understanding:
We want to keep an external DB synchronized to Moodle's. So we need events triggered, when for example a course has been updated. So we would add an events_trigger()-call into moodle/course/edit.php (near line 111 in Moodle1.9). That's OK.
We do also want to know about groups. So when a group has been deleted we suggest to add an events_trigger()-call into moodle/group/edit.php (near line 86).
But what about a course that have been deleted? Before the course will be deleted all groups within this course will automaticly been deleted before. So should there be an events-trigger('group_deleted')-call in moodle/group/lib/basicgrouplib.php (in function groups_delete_group) or is this too low-level. Our third-party-software could cope with just the info about the deleted course, too. Then it would do exactly the same internal checks and delete-calls as Moodle does.
But what about a user that enrolls into a course via enrollment key that put the user into a group, too. Where to call the events_trigger('group_user_added') then?
What do you think?
1) Is the message_send event implemented in Moodle 1.9.3?
2) Is the message_send event implemented for modules other than forum, and what are the other event names that are triggered for the modules, i.e. is there an event for 'add discussion'?
3) Is there a way to add a custom field to the event data that is thrown by a module. I need to add the id of the post also into the eventdata as a field.