Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Events API: Difference between revisions

From MoodleDocs
m (→‎Verb list: - Addition of evaluated, assessed, reevaluated and reassessed)
No edit summary
Line 3: Line 3:
|state = Implementation in progress
|state = Implementation in progress
|tracker = MDL-39797 , MDL-39952, MDL-39846
|tracker = MDL-39797 , MDL-39952, MDL-39846
|discussion = https://moodle.org/mod/forum/discuss.php?d=229425
|discussion = https://moodle.org/mod/forum/discuss.php?d=229139
|assignee = Backend Team
|assignee = Backend Team
}}
}}


= What are events? =
= Locks =


Events are atomic pieces of information describing something that happened in Moodle. Events are primarily the result of user actions, but could also be the result of the [[:en:Cron|cron]] process or administration actions [[:en:Administration via command line|undertaken via the command line]].
Locking is required whenever you need to prevent 2 processing accessing the same resource at the same time. The prime candidate for locking in Moodle is cron. This will allow multiple cron processes to work on different parts of cron at the same time with no risk that they will conflict (work on the same job at the same time).


When an action takes place, an event is created by a [[Core APIs|core API]] or [[Plugins|plugin]]. The Events system then disseminates this event information to observers registered for this event. In this way, the events system acts as a communication backbone throughout the Moodle system.
= When to use locking =


Event observers can not modify event data or interrupt the dispatching of events, it is a one way communication channel.
When you want to prevent multiple requests from accessing the same resource at the same time. Accessing a resource is a vague description, but it could be e.g. running a slow running task in the background, running different parts of cron etc.
 
= Why is a new events system needed? =
 
The need to improve the Events system was prompted by a need for a richer and more efficient logging system, however the benefits of this improvement will be useful to other parts of Moodle that observe event information.
 
* The events need to be more strictly defined if we want to use them for new logging and other advanced use cases. They need to contain a lot more information in a standardised way (such as most fields from current log table and log_actions table).
* Complex data types were allowed in old events which was causing major problems when serialising/storing/unserializing the data.
* The logging and events contain similar information and are tiggered at the same places, new events would remove this code duplication. All events should be loggable and all current log entries should be triggered as events.
* The logging system will become an event observer, listening to all events and directing them to logging storage plugins in a controllable way.
* It will be possible to subscribe to '*' event, which would allow a system to potentially observe, and selectively deal with, all events. Current handlers do not get event name which makes this problematic.
* Current event handlers may trigger exceptions during site upgrade which would lead to fatal upgrade problems. The new design eliminates this.
* Failure in handlers blocked dispatching of subsequent events. Instead problems in new observers would be only logged and execution would continue normally.
* Current execution of external handlers during DB transactions blocks other handlers. This would be eliminated by in-memory buffer for external events.
* It would good to have observer priority.
* Nested events are not dispatched sequentially, it would change the order of events received in lower priority handlers.


= Performance =
= Performance =
Some basic profiling has been conducted.
Locking is not meant to be fast. Do not use it in code that will be triggered many times in a single request (e.g. MUC). It is meant to be always correct - even for multiple nodes in a cluster. This implies that the locks are communicated among all the nodes in the cluster, and hence it will never be super quick.
 
There is a general plan to complete pre- and post-implementation testing as development happens. The new system will be imemented in parallel with the old one which should help with comparison of new and old logging and event triggering performance on each page.
 
Our aim is to trigger more events and log more information, which is going to impact on performance. We hope to offset that impact by improving log storage, simplifying event dispatching and adding other core performance improvements. The proposed class structure of the base event should allow some new advanced techniques, which may also improve performance in some scenarios.
 
More details will be added to this section soon.
 
= Events API =
 
Each plugin will define the events that it can report (trigger) by extending an abstract base class, once for each possible event. This approach will have several benefits.
; Events will be active objects
: When they are triggered and possibly after they are reinstantiated (say, when they are retrieved from a log), an event object will be able to provide callback functions for various purposes (such as capability checks).
; Automatic inclusion
: Event class definitions will be automatically included when needed, without having to maintain lists of known event types. New event definitions can be added without the need to upgrade, only purging of MUC cache is required after adding new observer.
; Maintainability
: It will be easy to add new events and migrate existing events. Code review will be simplified because there will be less duplication of code when triggering events and all event related information/code will be concentrated in one file in fixed locations.
; Self documenting
: The behaviour of events will be combined with the definition of events in one place (file). It will be easy for event observer writers to know what events a plugin can trigger. This includes support for autocompletion and code inspection in modern IDEs.
; Quick, self-validating data structure
: As events are instantiated objects, the PHP processor will validate the structure and type of event classes. This does not ensure data value validity, but does give some assurance of consistency and it also detects common typos.
 
== Backwards compatibility and migration ==
 
Events:
* Moodle core and standard plugins will replace all calls to the events_trigger() function with new events classes.
* For events that already exist in Moodle 2.5 the additional legacy information should be added to the event data (in properties 'legacyeventname' and 'legacyeventdata'.
* The function events_trigger() will continue working as before, but it will be called automatically after a new event is processed using the 'legacyeventname' and 'legacyeventdata'.
* The legacy events handling code will be maintained  separately and will continue being supported in Moodle 2.x. New legacy events will not be added.
* Existing legacy event handlers will be migrated to new event handlers accepting new event class instances.
* More subsystems may be migrated to events-handlers, ex.: gradebook history.
 
Logging:
* Function add_to_log() and all logging internals will continue working as before.
* Existing add_to_log() parameters will be migrated inside new events method get_legacy_log_data() and core_event_base::trigger() will call add_to_log() automatically (this will depend on $CFG->loglifetime setting for performance reasons).
 
== Event dispatching and observers ==
 
The new event dispatching system is completely separate from the old events code. Original event handlers are now called observers with the description stored in the same db/events.php file, but as a new array with a different format.
 
=== Event observers ===
 
The observers are described in db/events.php in the array $observers, the array is not indexed and contains a list of observers defined as an array with the following properties;
* eventname - event class name or "*" indicating all events. All events must use namespace, ex.: ''\plugintype_pluginname\event\something_happened''.
* callback - PHP callable type.
* includefile - optional. File to be included before calling the observer. Path relative to dirroot.
* priority - optional. Defaults to 0. Observers with higher priority are notified first.
* internal - optional. Defaults to true. Non-internal observers are not called during database transactions, but instead after a successful commit of the transaction.
 
<code php>
 
$observers = array(
 
    array(
        'eventname'  => '\core\event\sample_executed',
        'callback'    => 'core_event_sample_observer::observe_one',
    ),
 
    array(
        'eventname'  => '\core\event\sample_executed',
        'callback'    => 'core_event_sample_observer::external_observer',
        'priority'    => 200,
        'internal'    => false,
    ),
 
    array(
        'eventname'  => '*',
        'callback'    => 'core_event_sample_observer::observe_all',
        'includefile' => null,
        'internal'    => true,
        'priority'    => 9999,
    ),
 
);
 
</code>
 
=== Event dispatching ===
 
A list of available observers is constructed on the fly directly from all available events.php files. Previously handlers were installed only during installation and upgrade. There is no risk of performance regression because the list is already cached in MUC. Observers get events before installation or any upgrade, however observers are not notified during the initial installation of moodle core tables.
 
Developers of observers must make sure that execution does not end with a fatal error under any condition (before install, before upgrade or normal operation). Exceptions are automatically captured, logged in the PHP error log, and notification of other observers continues. Current handlers must not throw any exceptions at any time.
 
Observers are notified sequentially in the same order in which events were triggered. This means that events triggered in observers are queued in FIFO buffer and are processed after all observers are notified.
 
=== Differences from old event handling ===
 
# New events contain a lot more structured information.
# New event data must not contain any PHP classes.
# There is separate context cache which may be used when deleting data or for observer performance improvements.
# No database access in new event dispatching code.
# There is no support for cron execution - this eliminates performance problems, simplifies events dispatching and prevents abuse of cron events.
# Events triggered in observers are processed in a different order.
# External events are buffered when a transaction is in progress, instead of being sent to the cron queue.
# It is possible to define multiple observers for one event in one events.php file.
# It is possible to subscribe an observer to all events.
# The new event manager is using frankenstyle autoloading - smaller memory footprint when events are not used on the current page.
 
== Triggering events ==
 
* All event descriptions are objects extending the \core\event\base class.
* Events are triggered by creating a new instance of the class event and executing $event->trigger().
* Each event class name is a unique identifier of the event.
* Class names and namespace follow the identifier scheme \'''frankenstyle_component'''\event\'''some_object_action'''. Core events have prefix 'core_'.
* Plugins define each event class in a separate file. File name and location must match the class name, for example: '''plugindir'''/classes/event/'''something_happened'''.php
* The event identifier suffix has the form ''some_object_action''  ('''something_happened''' in the example above) and should follow our standard naming convention. The last word after underscore is automatically parsed as action, the rest of words is object.
 
Decision:[[#Verb_list| Recommended verb list]]
 
Examples: \core\event\course_completed, \mod_assign\event\submission_commented, \mod_forum\event\post_shared, \mod_forum\event\post_responded, etc.
 
* Ideally, it should be possible to trigger an event without gathering additional information for the event. To reduce the cost of data gathering, specifically the cost of database reads, at least the minimal values needed to trigger an event should be already available in variables.
 
An example of triggering an event:
<code php>
$event = \mod_myplugin\event\something_happened::create(array('context' => $context, 'objectid' => YYY, 'other' => ZZZ));
// ... code that may add some record snapshots
$event->trigger();
</code>
 
 
=== Use of php autoloading ===
 
All new OOP APIs in Moodle 2.6 are going to use class auto loading - see [[Automatic class loading]]. New events use PHP strictly defined namespaces which concentrate all events classes in classes/event/ subdirectory.
 
=== Why separate classes? ===
There were two alternatives proposed on how to define the event structure. The first is a separate class for each event (extending the base class), the other being each event is based on a generic event instance of the base class.
<pre>
Decision: Use separate class for each event.
</pre>
'''Each plugin creates its own event class for each event'''
 
Pros
* Maintainability - It is much easier to review, debug, integrate.
* Self documenting, behaviour is combined with definition.
* It is extremely flexible for plugin developers and core devs too.
* Automatically lists events without being installed - PHPDocs as events documentation.
* It is included only when needed using autoloading.
* Self-validating data structure (by PHP).
* Some developers will find it easier to copy whole class files as templates.
Cons
* Big learning curve for developers without OOP skills (all other new subsystems in Moodle already use OOP, you can not code without these skills any more).
* Some developers may find it harder to copy-and-paste examples because they will need to create new class first and use it afterwards (this can be viewed as a benefit because it forces developers to think more about events).
 
 
'''Each plugin defines events in a list based on a generic object'''
 
Pros
* Easier for some developers (this can be eliminated with developer documentation).
* Some developers think it gives more control of event structure in core (we can easily solve that with private access and validation in the base class).
Cons
* It is not flexible enough. PHP code gives developers more freedom.
* It would not be possible to implement any performance hacks in custom methods. All data would have to be calculated even if it is not used.
* It would be necessary to define access control callbacks in other code.
* It would be harder and slower to integrate legacy logging.
* It would be harder and slower to implement support for legacy events.
* Event observers could not use event class names as reliable identifiers.
* Event object and action could not be parsed from class name, it would have to be stored in event properties every time you trigger event.
* Requires upgrade/install to register an event in DB table with MUC cache. Events could not be triggered earlier.
* The localised descriptions and names would have to be stored as properties, it would not be possible to store them in any definition.
* The implementation of an events infrastructure would be significantly more complex and error prone.
 
== Information contained in events ==
 
Events have to contain as much information as they can, but this should not affect the performance. That's why part of the information is available in properties, and the rest via methods. This allows for delaying the computation of the data at the time it is really needed, if it ever is.
 
=== Properties ===
 
List of properties that the developer has to pass to the event upon creation, or automatically generated when possible and cost free. Some of those properties not mandatory.
 
{| class="nicetable"
|-
! Property name
! Title
! Type
! Comment
|-
| ''eventname''
| Event name
| ''static, automatic from class name''
| Automatically computed by copying class name
|-
| ''component''
| Component
| ''static, automatic from top namespace''
| Component declaring the event, automatically computed from class name.
|-
| ''action''
| Action
| ''static, automatic from last word in class name''
| Can be automatically computed from class name.
|-
| ''target''
| target of action
| ''static, automatic from class name''
| Target on which the action is taken, can be automatically computed from class name.
|-
| objecttable
| Database table name
|
| optional database table name where is/was the object stored. Never use a relationship table here.
|-
| objectid
| Object ID
|
| optional id of the object record from objecttable
|-
| '''''crud'''''
| Transaction type
| ''static mandatory''
| One of [crud] letters. Statically declared in the event class method init().
|-
| '''''level'''''
| Level
| ''static mandatory''
| Level of educational value of the event. Statically declared in the event class method init(). (See below for more details)
|-
| contextid
| Context ID
| mandatory
|
|-
| contextlevel
| Context level
| automatic from context
| This tells you if it was a course, activity, course category, etc.
|-
| contextinstanceid
| Context instanceid
| automatic from context
| Based on context level this may be course id , course module id, course category, etc.
|-
| userid
| User ID
| defaults to current user
| User ID, or 0 when not logged in, or -1 when other (System, CLI, Cron, ...)
|-
| courseid
| Affected course
| defaults to course context from context
| This is used only for contexts at and bellow course level - this can be used to filter events by course (includes all course activities)
|-
| relateduserid
| Affected user
|
| Is this action related to some user? This could be used for some personal timeline view.
|-
| other
| All other data
|
| Any other fields needed for event description - scalars or arrays, must be serialisable using json_encode()
|-
| timecreated
| Time of the event
| automatic
|
|}
 
* ''static'': It is the same for all event instances of this class.
* '''mandatory''': Is required in order to trigger the event.
 
==== Level property ====
 
The level property helps defining the educational value of the event. It is intentional that the list is limited to only 3 different constants, having too many options would make it harder for developers to pick the right one(s). We also have to keep in mind that this is not supposed to answer all the questions about a particular event, other event properties like the courseid, the context, the component name can be used with the level to get more granular reports.
 
Remember that this is event based. If the user that has triggered the event is not really "participating" because he is an admin, or a manager, then it is the job of the report to filter those. The event itself has a static educational level.
 
'''Teaching''' (LEVEL_TEACHING: 1)
 
Any event/action that is performed by someone (typically a teacher) and has a teaching value (anything that is affecting the learning experience/environment of the students). This should not be combined with "Participating" events.
 
Valid events:
 
* A teacher grading a student
* A teacher modifying the course settings
* A teacher adding a new section to the course page
* A teacher modifying a module settings
* A teacher adding a page to course
* A teacher leaving a feedback
 
INVALID events:
 
* A teacher posting in a forum (it might affect the learning experience, but not necessarily, so the teacher is just participating)
 
'''Participating''' (LEVEL_PARTICIPATING: 2)
 
Any event/action that is performed by a user, that is related (or could be related) to his learning experience.
 
Valid events:
 
* A user posting to a forum
* A user submitting an assignment
* A user blogging
* A user reading someone's blog
* A user posting a comment
* A user chatting on a chat activity
* A user viewing the course page
* A user deletes a blog post
 
INVALID events:
 
* A user updating his profile
* A user visiting someone's profile
* A user viewing his /my/ page
* A user sending a message to another one
 
'''Other''' (LEVEL_OTHER: 0)
 
Any other action, whether they are related to the site administration, or are specific to user. They do not have any educational value.
 
=== Methods ===
 
The computation of this data is not required by default, but can be accessed by any event observer if need be.
 
{| class="nicetable"
|-
! Method
! Comment
|-
| get_name()
| Returns localised name of the event, it is the same for all instances.
|-
| get_description()
| Returns localised description of one particular event.
|-
| can_view($user)
| Can the specified user view the event?
|-
| get_url()
| Returns Moodle URL where the event can be observed afterwards.
|-
| get_legacy_eventname()
| Information necessary for event BC.
|-
| get_legacy_eventdata()
| Information necessary for event BC.
|-
| get_legacy_logdata()
| Information necessary for logging BC.
|}
 
===Record caching===
 
The standard event data may not contain all the information observers need. The built-in record snapshot support in events allows developers to attach more auxiliary information when triggering events, it may be for example course record, some record that was just deleted, etc. The snapshot is meant to be a full database record, as it will be automatically fetched from get_record_snapshot() if not set previously and assuming the property ''objecttable'' is set. Please be aware that the snapshots are not stored in the event, and cannot be restored.
 
<code php>
    $event = \core\event\role_assigned::create(
        array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
            'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
    $event->add_record_snapshot('role_assignments', $ra);
    $event->trigger();
</code>
 
<code php>
    $event = \core\event\role_unassigned::create(
        array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
            'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
    $event->add_record_snapshot('role_assignments', $ra);
    $event->trigger();
</code>
 
The related methods are:
* public function add_record_snapshot($tablename, $record)
* public function get_record_snapshot($tablename, $id)
 
=== Rejected properties and methods ===
 
{| class="nicetable"
|-
! Property
! Title
! Why
|-
| URL
| relevant page URL
| These URLs can be constructed on the fly from other data, external log plugins may use get_url() method.
|-
| version
| Event specification number
| The event specification should never change
|-
| type
| Type of event (action, error, ...)
| Event are not intended for error logging or debugging.
|-
| actor
| Whether current execution is cron, cli, user, ...
| Impossible to track down at a low level
|-
| severity
| Severity following [http://tools.ietf.org/html/rfc5424#section-6.2.1 logging standards]
| Our logging does not match this, as we will not (at present) log errors
|-
| coursecatname
| Category name
| Might be costly to retrieve for little gain
|-
| coursename
| Course name
| Might be costly to retrieve for little gain
|-
| cmname
| Course module name
| Might be costly to retrieve for little gain
|-
| categoryid
| Course category id
| Categories are a tree structure, we can not identify them by one integer. It would have to be a path.
|-
| cmid
| Course module id
| Can be derived from contextlevel and contextinstanceid
|-
| associatedobject
| Associated object
| Object associated to the main object. Ie: The user to whom a message is sent.
|-
| associatedobjectid
| Associated object ID
| Identifier of the associated object
|-
| realuserid
| Real User ID
| Will be tracked by log plugins only - user who "logged in as", stores the real user ID
|-
| origin
| Origin of the event
| Will be tracked by log plugins only - CLI, cron, Webservice, ... (optionally with IP address)
|}
 
 
{| class="nicetable"
|-
! Method
! Comment
! Why
|-
| get_all_affected_users()
| Returns all the users affected by this event
| It is expensive to fetch all users and it changes in time, so it would be unreliable too. For now we store only one user who is related to each event.
|-
| get_objecturl()
| Returns the URL to view the object
| There is usually only one URL where event changes may be observed. The URL may depend on current user capabilities too.
|-
| get_associatedobjecturl()
| Returns the URL to view the associated object
| No associated user property is present.
|-
| get_currenturl()
| Returns the current URL, uses $PAGE.
| This information may be added by logger, current page info is not part of events data.
|-
| get_useripaddress()
| Returns the User IP address
| Page access method is not part of events API, this should be implemented as logdata properties in loggers.
|}
 
== Events naming convention ==
 
Clear event names help developers when reading what events are triggered, and defining the events properties when defining the event class.
 
Decision: \<component>\event\<some_object>_<verb>
 
=== Structure of existing events ===
 
List of existing events called in 2.5, along with their future name and the decomposition of the new name into ''action'' and ''object''.
 
{| class="nicetable"
|-
! 2.5 name
! New name
! Subject
! Action
! Object
! <relationship>
! Object 2
! Comment
|-
| activity_completion_changed
| '''core_event_activity_completion_updated'''
| Someone
| changed
| completion
| of
| activity
|-
| assessable_content_uploaded
| '''mod_*_event_assessablecontent_uploaded'''
| Someone
| uploaded
| assessablecontent
|-
| assessable_files_done
| '''mod_*_event_assessablecontent_processed'''
| Someone
| processed
| assessable content
|
|
|
|-
| assessable_file_uploaded
|
| Someone
| uploaded
| assessable_file
|
|
| ''To be deprecated MDL-35197''
|-
| assessable_submitted
| '''mod_*_event_assessablecontent_submitted'''
| Someone
| submitted
| assessable content
|-
| blog_entry_added
| '''mod_blog_event_entry_created'''
| Someone
| added
| entry
|-
| blog_entry_deleted
| '''mod_blog_event_entry_deleted'''
| Someone
| deleted
| entry
|-
| blog_entry_edited
| '''mod_blog_event_entry_updated'''
| Someone
| updated
| entry
|-
| cohort_added
| '''core_event_cohort_created'''
| Someone
| created
| cohort
|-
| cohort_deleted
| '''core_event_cohort_deleted'''
| Someone
| deleted
| cohort
|-
| cohort_updated
| '''core_event_cohort_updated'''
| Someone
| updated
| cohort
|-
| cohort_member_added
| '''core_event_cohort_member_added'''
| Someone
| added
| member
| to
| cohort
|-
| cohort_member_removed
| '''core_event_cohort_member_deleted'''
| Someone
| deleted
| member
| from
| cohort
|-
| course_category_deleted
| '''core_event_coursecat_deleted'''
| Someone
| deleted
| coursecat
|-
| course_completed
| '''core_event_course_completed'''
| Someone
| completed
| course
|-
| course_content_removed
| '''core_event_course_purged'''
| Someone
| purged
| course
|-
| course_created
| '''core_event_course_created'''
| Someone
| created
| course
|-
| course_deleted
| '''core_event_course_deleted'''
| Someone
| deleted
| course
|-
| course_restored
| '''core_event_course_restored'''
| Someone
| restored
| course
|-
| course_updated
| '''core_event_course_updated'''
| Someone
| updated
| course
|-
| groups_group_created
| '''core_event_group_created'''
| Someone
| created
| group
|-
| groups_group_deleted
| '''core_event_group_deleted'''
| Someone
| deleted
| group
|-
| groups_grouping_created
| '''core_event_grouping_created'''
| Someone
| created
| grouping
|-
| groups_grouping_deleted
| '''core_event_grouping_deleted'''
| Someone
| deleted
| grouping
|-
| groups_groupings_deleted
|
| Someone
| deleted
| groupings
|
|
| ''To deprecate''
|-
| groups_groupings_groups_removed
|
| Someone
| removed
| groups
| from
| groupings
| ''To deprecate''
|-
| groups_grouping_updated
| '''core_event_grouping_updated'''
| Someone
| updated
| grouping
|-
| groups_groups_deleted
|
| Someone
| deleted
| groups
|
|
| ''To deprecate''
|-
| groups_group_updated
| '''core_event_group_updated'''
| Someone
| updated
| group
|-
| groups_member_added
| '''core_event_group_member_added'''
| Someone
| added
| member
| to
| group
|-
| groups_member_removed
| '''core_event_group_member_deleted'''
| Someone
| deleted
| member
| from
| group
|-
| groups_members_removed
|
| Someone
| removed
| members
| from
| group
| ''To deprecate''
|-
| lti_unknown_service_api_call
| '''mod_lti_event_unknownservice_called'''
| Someone
| called
| unknown service
|-
| mod_created
| '''core_event_module_created'''
| Someone
| created
| module
|-
| mod_deleted
| '''core_event_module_deleted'''
| Someone
| deleted
| module
|-
| portfolio_send
|
|
|
|
|
|
| ''This is a hack...''
|-
| role_assigned
| '''core_event_user_role_added'''
| Someone
| added
| role
| to
| user
| ''Or ..._role_assigned''
|-
| role_unassigned
| '''core_event_user_role_removed'''
| Someone
| removed
| role
| from
| user
| ''Or ..._role_unassigned''
|-
| quiz_attempt_started
| '''mod_quiz_event_attempt_started'''
| Someone
| started
| attempt
| of
| quiz
|-
| test_cron
|-
| test_instant
|-
| user_created
| '''core_event_user_created'''
| Someone
| created
| user
|-
| user_deleted
| '''core_event_user_deleted'''
| Someone
| deleted
| user
|-
| user_enrolled
| '''core_event_user_enrolment_created'''
| Someone
| added
| enrolment
| of
| user
|-
| user_enrol_modified
| '''core_event_user_enrolment_updated'''
| Someone
| updated
| enrolment
| of
| user
|-
| user_unenrolled
| '''core_event_user_enrolment_deleted'''
| Someone
| removed
| enrolment
| of
| user
|-
| user_logout
| '''core_event_loggedout'''
| Someone
| loggedout
|-
| user_updated
| '''core_event_user_updated'''
| Someone
| updated
| user
|-
| workshop_viewed
| '''mod_workshop_event_viewed'''
| Someone
| viewed
| workshop
|-
|}
 
=== Verb list ===
All events must use a verb from this list. New verbs should be added to this list if required.
{| class="nicetable"
|-
!verb
!Explanation
!Source
|-
|accepted
|Example: Accepting a statement when submitting an assignment.
|Moodle
|-
|added
| Used to represent "something that already exists is now part of/bound to another entity". Examples: "Admin added role to user X", "Admin added user X to group A". Wrong example: "User added course in category" because it is a 'move' action, except if a course can be part of multiple categories. The good examples work because: A user can have multiple roles, a user can be in multiple groups.
|Moodle
|-
|answered
| Indicates the actor responded to a Question
|Tincan
|-
|assessed
| Some submitted material has been assessed
|Moodle
|-
|attempted
|
|Tincan
|-
|awarded
| ex:-teacher awarded student a badge.
|Moodle
|-
|backedup
| When a backup has been performed.
|Moodle
|-
|called
|When a call to something is made like an API @see unknown_service_api_called.php
|Moodle
|-
|commented
|Offered an opinion or written experience of the activity. Can be used with the learner as the actor or a system as an actor. Comments can be sent from either party with the idea that the other will read and react to the content.
|Tincan
|-
|completed
|To experience the activity in its entirety. Used to affirm the completion of content. This can be simply experiencing all the content, be tied to objectives or interactions, or determined in any other way. Any content that has been initialized, but not yet completed, should be considered incomplete. There is no verb to 'incomplete' an activity, one would void the statement which completes the activity."
|Tincan
|-
|created
| Used to represent "something new has been created".
|Moodle
|-
|deleted
| Used to indicate the object in context was deleted.
|Moodle
|-
|duplicated
|For something that has been copied.
|Moodle
|-
|evaluated
| Material has been evaluated.
|Moodle
|-
|failed
|Learner did not perform the activity to a level of pre-determined satisfaction. Used to affirm the lack of success a learner experienced within the learning content in relation to a threshold. If the user performed below the minimum to the level of this threshold, the content is 'failed'. The opposite of 'passed'.
|Tincan
|-
|graded
|Used to represent an activity was graded.
|Moodle
|-
|imported
|The act of moving an object into another location or system.
|Tincan
|-
|loggedin/loggedout
| For login and logout.
|Moodle
|-
|loggedinas
| If user is logged in as different user. This is used by only one event (user_loggedinas). Adding this verb makes event name more clear, then using loggedin verb.
|Moodle
|-
|locked
|
|Moodle
|-
|moved
| Used to indicate the object in context was moved.
|Moodle
|-
|passed
|Used to affirm the success a learner experienced within the learning content in relation to a threshold. If the user performed at a minimum to the level of this threshold, the content is 'passed'. The opposite of 'failed'.
|Tincan
|-
|previewed
|Something has been previewed.
|Moodle
|-
|reassessed
| Submitted material has been assessed again.
|Moodle
|-
|reevaluated
| Material has been evaluated again.
|Moodle
|-
|submitted
|This is very close to "Attempted". Depends on context which one should be used. For example:- "Admin submitted a form. Student attempted a quiz."  is correct, however some cases might not be as clear as the previous example. We can say both "Student submitted an assignment" or "student attempted an assignment". We need to make the difference clear.
|Moodle
|-
|suspended
| Suspend something. (example a user)
| Tincan (However the context is different)
|-
|switched
|Something has been switched. For example:- The workshop phase has been switched to assessment"
|Moodle
|-
|viewed
|Something has been viewed. For example:- "Student viewed chapter 1 of book 1."
|Moodle
|-
|registered
|Indicates the actor registered for a learning activity
|Tincan
|-
|removed
|By opposition to "Added". This does not mean that the object has been deleted, but removed from the entity, or not bound to it any more.
|Moodle
|-
|restored
| When restoring a backup. Rolling back to a previous state.
|Moodle
|-
|reset
| Sets one or more properties back to the default value.
|Moodle
|-
|revealed
|Example: Identities revealed after blind marking.
|Moodle
|-
|unlocked
|
|Moodle
|-
|upgraded
|Something was upgraded, some module probably
|Moodle
|-
|updated
|Used to indicate the object in context was updated. Simple example is "Admin updated course xyz".
|Moodle
|-
|}
 
=== Rules ===
 
==== Use singular ====
 
Plurals must be used on objects when it's a ''One to Many'' relationship. Ex: bulk import, mass deletion, ... In any other case, use the singular.
 
==== Ends with a verb ====
 
The last word (after the last underscore) must be a verb.
 
== Shared events ==
 
Decision: Not supported at this stage.
 
In Moodle 2.5 we have a good example of a shared event: 'assessable_content_uploaded' which is triggered in ''forum'', 'assignment'' and ''workshop''.
 
The problem with shared events is that we cannot easily track what component triggered them. Of course we could add a new property to the event to keep track of that, but we would soon need more information and more properties. Also, in the case of a logger, the event received would be unique, where in fact it should be considered different depending on the component firing it.
 
In our first implementation, we will create one specific event per module. This flexibility does not prevent any observer from capturing them, but still makes sure that the consistency and specificity of each event is maintained.
 
It could happen that some events are defined in core and shared, but this should not really happen as low-level APIs should trigger the event, and a module should call that low API instead of doing the job itself.
 
== One to many ==
 
Decision: Each event should have a one to one relationship. We can reconsider this at a later stage, if the performance hit is extremely high.
 
In 2.5, some events are triggered when an action happens on multiple objects. We have to decide whether we want to keep supporting ''One to Many'' events or not.
 
Keeping a list of all changes for multiple actions may be problematic because you would have to keep them all in memory until all things are processed. This might also result in the order of events being incorrect. The only correct solution seems to be to trigger each item individually and then many things at the end. Performance needs to be improved elsewhere...
 
=== Accuracy ===
 
When uploading a bunch of users using the CSV upload feature, if only one event is triggered, it means that the observers of ''user_created'' won't be triggered. And so some functionality can be lost as, as a plugin developer, I expect this ''user_created'' to be triggered regardless of the way they have been uploaded. Of course, the developer could observe the event ''bulk_user_imported'', but that means that he could miss some relevant observers.
 
This applies to existing events.
 
=== Performance ===
 
Triggering one event is cheaper then repeating the same events x number of times...
 
=== Information tracking ===
 
A ''bulk'' event, might not be verbose enough to allow for proper logging afterwards. Though this is the responsibility of the logger, we probably want to make it easy to store relevant information.
 
=== Double event ===
 
In the case of a bulk user import, if we were to trigger an event per user created, we probably want to trigger one event 'user_bulk_upload_started' when the action starts.
 
== Unit Testing ==
 
With unit testing for this system we want to assert the following:
 
* That event strict validation and custom validation works.
* Missing event data is auto filled with accurate data.
* Typos in properties passed to ::create() are captured (if we decide to validate).
* The legacy methods return the expected values.
* The class properties are correctly overridden (crud, level, action, object, ...).
* The properties automatically generated (component, name, ...) are correct.
* Events are dispatched to the corresponding observers.
* Events are dispatched to the corresponding legacy handlers.
* Events are dispatched to the * observers.
* Events perform an add_to_log() if it has legacy log data.
* 'Events restore' restored the whole event data, and does not miss any information.
* 'Events restore' does not generate any extra information.
 
= Example events =
 
== Assignment ==
 
Assumption: Course contains groups with students in each group.
 
'''1.''' Teacher creates an assignment with group mode set to 'Separate groups' and Feedback type set to comments and files.
*Event: User 'Teacher' has created assignment 'B' in course 'C101'.
'''2.''' A student views the assignment.
*Event: User 'Student' has viewed assignment 'B' in course 'C101'.
'''3.''' A member from one of the groups submits an assignment
*Event: User 'Student' has added a submission for assignment 'B' for group 'C' in course 'C101'.
*Event: Email sent to the teacher and all students in that group.
'''4.''' User 'Adrian' adds some changes to the assignment and updates it.
*Event: User 'Adrian' has updated the submission for assignment 'B' for group 'C' in course 'C101'.
*Event: Email sent to the teacher and all students in that group.
'''5.''' Teacher views the assignment.
*Event: User 'Teacher' has viewed assignment 'B' in course 'C101'.
'''6.''' Teacher clicks on 'View/grade all submissions'
*Event: User 'Teacher' has viewed the assignment 'B' grade area in course 'C101'.
'''7.''' Teacher clicks to grade the student's submission.
*Event: User 'Teacher' has viewed the submission for user 'student' for assignment 'B' in course 'C101'.
'''8.''' Teacher marks the assignment with the setting 'Apply grades and feedback to entire group' set to 'Yes' leaving a comment and a file.
*Event: User 'Teacher' has marked assignment 'B' for group 'C' in course 'C101'.
*Event: User 'Teacher' has left a comment for assignment 'B' for group 'C' in course 'C101'.
*Event: User 'Teacher' has uploaded a feedback file for assignment 'B' for group 'C' in course 'C101'.
*Event: User 'Teacher' has uploaded a file to the course 'C101'.
*Event: Email sent to user 'Student' notifying them their submission for assignment 'B' has been marked. - This is done for all users in the group.
'''9.''' User 'Adrian' views the feedback.
*Event: User 'Adrian' has viewed assignment 'B' in course 'C101'.
'''10.''' User 'Adrian' opens the feedback file.
*Event: User 'Adrian' has viewed the file 'A' for assignment 'B' in course 'C101'.
'''11.''' User 'Adrian' adds some changes to the assignment insulting the teachers marking and updates it.
*Event: User 'Adrian' has updated the submission for assignment 'B' for group 'C' in course 'C101'.
*Event: Email sent to user 'Teacher' notifying them that user 'Adrian' has updated the submission for assignment 'B'.
*Event: Email sent to user 'Student' notifying them their submission for assignment 'B' has been updated. - This is done for all users in the group.
'''12.''' The teacher clicks directly on the link in the email to be taken to the grading page.
*Event: User 'Teacher' has viewed the submission for user 'student' for assignment 'B' in course 'C101'.
'''13.''' The teacher is upset due to the harsh comments and decides to mark Adrian down, but not the rest of the group by setting 'Apply grades and feedback to entire group' set to 'No'.
*Event: User 'Teacher' has marked assignment 'B' for user 'Adrian' in course 'C101'.
*Event: User 'Teacher' has left a comment for assignment 'B' for user 'Adrian' in course 'C101'.
*Event: User 'Teacher' has uploaded a feedback file for assignment 'B' for user 'Adrian' in course 'C101'.
*Event: Email sent to user 'Adrian' notifying them their submission for assignment 'B' has been marked.
 
= FAQs =
 
; Why not use create events in core subsystems? : Because we could not see all core events in one place, it would create problems when naming event classes and finally subsystems are incomplete, we would have to add more now because we could not move the events in the future.
 
= Development stages =
 
== Stage 1 ==
 
* Finish class loader spec and implement basic Frankenstyle class loader.
* Describe new observer definition - just few new flags in current db/events.php
* Describe new event dispatcher.
* Describe core_event_base class.
* Current (= legacy) events triggering:
** Re-factor current event handling code to new self-contained class - do not change functionality, keep events_trigger().
** Create new event handler management class that deals with installation and upgrades of both legacy and new handlers.
* New events:
** Create new core_event_base class.
** Create new self-contained event dispatcher class with '*' handler support.
** In function core_event_base::trigger() check if the  event has property 'legacyeventname' execute events_trigger($this->legacyeventname, $this->legacyeventdata) after triggering new event.
** Write unit tests for all new events code.
* No changes to be made to the current logging system yet.
 
After completing this stage everything should continue to work as it did before and we can start parallel work on further stages.
 
== Stage 2 (requires completion of Stage 1) ==
 
* Create event classes and replace existing calls to events_trigger() and with new event classes containing legacy information properties.
* Add more events throughout the standard Moodle package in places where we have add_to_log(). Implement some_event::get_legacy_log_data() which returns original parameters of add_to_log() and remove it. Old add_to_log() function is called in core_event_base::trigger() automatically with original parameters.
* Add even more new events all over the place.
 
The difficult part is defining the new event classes properly because we must not change them after the 2.6 release.
 
== Stage 3 (requires partial completion of Stage 2) ==


* Migrate current legacy event handlers to new handlers with one event class instance parameter, ex.: enrol plugins.
= Usage =


== Stage 4 (requires partial completion of Stage 2) ==
The locking API is used by getting an instance of a lock_factory, and then using it to retrieve locks, and eventually releasing them. You are required to release all your locks, even on the event of failures.


* Implement an event logging handler.
{code}
* Implement logging storage plugins.
* Define logging apis.
* Create new reports.
* Switch to new logging everywhere after Stage 2 has been completed and new reports are usable.


See [[Logging 2]]
// 5 seconds.
$timeout = 5;
// A namespace for the locks. Must be prefixed with the component name to prevent conflicts.
$locktype = 'mod_assign_download_submissions';
// Resource key - needs to uniquely identify the resource that is to be locked. E.g. If you
// want to prevent a user from running multiple course backups - include the userid in the key.
$resource = 'user:' . $USER->id;


== Stage 5  ==
// Get an instance of the currently configured lock_factory.
$lockfactory = \core\lock\lock_config::get_lock_factory('mod_assign_download_submissions');


* Decide how much backwards compatibility we want for old log tables. Most probably they will get only legacy log data.
// Open a lock.
* Implement some BC solution for old code that reads log tables directly.
$lock = $lockfactory->get_lock($resource, $timeout);


See [[Logging 2]]
// Do slow zip file generation...


== Stage 6 (requires completion of Stage 4 and 5) ==
$lock->release();


Moodle 2.8dev? This is the ultimate end of old logging via the ''log'' table.
{code}


* Deprecate the add_to_log() function with a debug message and do nothing inside.
= Implementing new lock types =
* Remove all legacy logging from event classes.
If you really want to do this you can. I probably wouldn't recommend it - because the core lock types should be very reliable - and the performance is not really a concern.


See [[Logging 2]]
Add a new local_XXX plugin with an autoloaded class that implements \core\lock\lock_factory.
Set $config->lock_factory to the full namespaced path to your class e.g.
{code}
$config->lock_factory = '\\local_redis\\lock\\redis_lock_factory';
{code}
Note: the extra backslashes are just required because it's a string and php insists on moronic syntax for namespaces.

Revision as of 02:50, 24 January 2014

Events 2
Project state Implementation in progress
Tracker issue MDL-39797 , MDL-39952, MDL-39846
Discussion https://moodle.org/mod/forum/discuss.php?d=229139
Assignee Backend Team


Locks

Locking is required whenever you need to prevent 2 processing accessing the same resource at the same time. The prime candidate for locking in Moodle is cron. This will allow multiple cron processes to work on different parts of cron at the same time with no risk that they will conflict (work on the same job at the same time).

When to use locking

When you want to prevent multiple requests from accessing the same resource at the same time. Accessing a resource is a vague description, but it could be e.g. running a slow running task in the background, running different parts of cron etc.

Performance

Locking is not meant to be fast. Do not use it in code that will be triggered many times in a single request (e.g. MUC). It is meant to be always correct - even for multiple nodes in a cluster. This implies that the locks are communicated among all the nodes in the cluster, and hence it will never be super quick.

Usage

The locking API is used by getting an instance of a lock_factory, and then using it to retrieve locks, and eventually releasing them. You are required to release all your locks, even on the event of failures.

{code}

// 5 seconds. $timeout = 5; // A namespace for the locks. Must be prefixed with the component name to prevent conflicts. $locktype = 'mod_assign_download_submissions'; // Resource key - needs to uniquely identify the resource that is to be locked. E.g. If you // want to prevent a user from running multiple course backups - include the userid in the key. $resource = 'user:' . $USER->id;

// Get an instance of the currently configured lock_factory. $lockfactory = \core\lock\lock_config::get_lock_factory('mod_assign_download_submissions');

// Open a lock. $lock = $lockfactory->get_lock($resource, $timeout);

// Do slow zip file generation...

$lock->release();

{code}

Implementing new lock types

If you really want to do this you can. I probably wouldn't recommend it - because the core lock types should be very reliable - and the performance is not really a concern.

Add a new local_XXX plugin with an autoloaded class that implements \core\lock\lock_factory. Set $config->lock_factory to the full namespaced path to your class e.g. {code} $config->lock_factory = '\\local_redis\\lock\\redis_lock_factory'; {code} Note: the extra backslashes are just required because it's a string and php insists on moronic syntax for namespaces.