Skip to content

Universal trigger registry #3988

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

Open
Tracked by #4505
drewdzzz opened this issue Jan 15, 2024 · 0 comments
Open
Tracked by #4505

Universal trigger registry #3988

drewdzzz opened this issue Jan 15, 2024 · 0 comments
Labels
3.0 reference [location] Tarantool manual, Reference part triggers [area] Related to triggers

Comments

@drewdzzz
Copy link

drewdzzz commented Jan 15, 2024

Related dev. issues: tarantool/tarantool#8656

Product: Tarantool
Since: 3.0
Root document: new page - https://www.tarantool.io/en/doc/latest/reference/reference_lua/trigger/
SME (except for transactional triggers): @ drewdzzz
SME (transactional triggers): @ Gumix
https://www.tarantool.io/en/doc/latest/release/3.0.0/#triggers
https://habr.com/ru/companies/vk/articles/782318/

Universal trigger registry

The new Lua module trigger was introduced. It is storage of events - each event has its own unique name and a list of named triggers. Each trigger has unique name within the event in which it is registered.

The module has following methods:

  1. trigger.set(event_name, trigger_name, handler_f) - insert the named handler to the beginning of the trigger list or update a trigger by name (if the name is already taken). Handler can be any callable Lua object.
  2. trigger.del(event_name, trigger_name) - delete a trigger by name from the event. No-op if there is no such trigger.
  3. trigger.call(event_name, arg1, arg2, ...) - call all the triggers registered on the event, from the beginning of the list to its end. The execution is stopped after the first exception. Returned values are ignored, all the arguments are passed without copying and any other preprocessing. For advanced scenarios, pairs method can be used (for example, if you want to process returned values of each trigger).
  4. trigger.pairs(event_name) - an iterator over triggers, registered on event, iterates from the beginning to the end. Iterator yields two values - name of the trigger (string) and its handler (callable object).
  5. trigger.info([event_name]) - return key-value map {event: {{trigger_name1, handler1}, {trigger_name2, handler2}, ...}}, all the triggers inside one event are ordered in call order. If argument event_name is passed, map contains only one event with name event_name, if it has any registered triggers.

The idea of this module - any developer, including us, can create an event. He documents how registered triggers are called and provides this behavior using methods call and pairs. Example: for a trigger like space:on_replace, method call can be used - trigger.call("box.space.space_name.on_replace", old_tuple, new_tuple). But one cannot implement space:before_replace trigger using call method - this trigger passes returned value from current trigger as new_tuple to the next one. For such "advanced" semantics developer needs to use method pairs.

On the other side, users of events just set and delete triggers. Every event has its own behavior, so the user must thoroughly read the documentation of a particular event to set his triggers on it.

Naming

Every name (event_name or trigger_name) is a string, virtually divided into namespaces. Each namespace or name can be a string or a number. To address a sub namespace with a string identifier, one must use dot ("namespace.subnamespace"), in the case of a number identifier, square brackets must be used ("namespace[512]").

Example:

local trigger = require('trigger')
trigger.set('box.space[512].on_replace', 'my_name.on_replace_trigger', function(...) ... end)

Reserved namespaces

Root namespaces tarantool and box (names like tarantool.* and box.*) are reserved for tarantool triggers and internal purposes - creating user triggers or events with such names can lead to negative consequences.

Tarantool triggers

Almost all the tarantool triggers were moved to the trigger registry, old way to set triggers still works.

List of tarantool triggers in the trigger registry:

  1. box.session.on_connect - the new place for this trigger.
  2. box.session.on_disconnect the new place for this trigger.
  3. box.session.on_auth - the new place for this trigger.
  4. box.session.on_access_denied - the new place for this trigger.
  5. box.ctl.on_election - the new place for this trigger.
  6. box.ctl.on_recovery_state - the new place for this trigger.
  7. box.ctl.on_schema_init - the new place for this trigger.
  8. box.ctl.on_shutdown - the new place for this trigger.
  9. Space triggers - described below in a separate section.
  10. New transactional triggers - described below in a separate section.
  11. tarantool.trigger.on_change - described below in a separate section.

Space triggers

NB: behavior of triggers, set with old space trigger API, is not changed. Only the new triggers has new behavior.

In the new trigger system, each space has four types of triggers:

  1. box.space.test.on_replace - works in the same way as old on_replace triggers, but isn't fired on recovery.
  2. box.space.test.before_replace - works in the same way as old before_replace triggers, but isn't fired on recovery.
  3. box.space.test.on_recovery_replace - works in the same way as box.space.test.on_replace, but fired only on recovery and has two additional arguments - xrow header and xrow body of type MsgPack object, both MsgPacks are maps with integer keys.
  4. box.space.test.before_recovery_replace - works in the same way as box.space.test.before_replace, but fired only on recovery and has two additional arguments - xrow header and xrow body of type MsgPack object, both MsgPacks are maps with integer keys.

Also, each trigger can be set by id and by name. For example, we have a space named test with id = 512. We can set an on_replace trigger in two ways:

  1. trigger.set('box.space[512].on_replace', 'my_name.my_on_replace_trigger', function(...) ... end)
  2. trigger.set('box.space.test.on_replace', 'my_name.my_on_replace_trigger', function(...) ... end)

All the triggers, set by id, are fired before the triggers, set by name.

All the restriction are the same as for old space triggers.

New transactional triggers

NB: old transactional triggers are not moved to the trigger registry and still have old behavior.

The new transactional triggers:

  • box.before_commit - triggered when a transaction is ready to commit;
  • box.on_commit - triggered when a transaction is committed;
  • box.on_rollback - triggered when a transaction is rolled back.

Each of them have 3 versions, e.g. `box.on_commit' event has:

  • box.on_commit - global version, called for all transactions;
  • box.on_commit.space.test - called for transactions that write to
    space "test";
  • box.on_commit.space[512] - called for transactions that write to
    space with id 512.

One of the main advantages of the new triggers is that they can be set for all transactions, rather than setting them within each transaction.

Space-specific triggers are called prior to global ones. If a trigger-function fails, the remaining triggers for this event (including global) are not executed.

If a space-specific trigger is added to the registry within an active transaction, it may or may not be called on commit/rollback of this transaction (the behavior is unspecified).

The trigger-function for each event may take an iterator parameter. Similar to old transactional triggers, the iterator goes through the effects of every request that changed a space during the transaction.

box.before_commit trigger-function restrictions:

  • Yield is allowed (on memtx without mvcc it will abort the txn);
  • Allowed to write into database spaces;
  • If the function raises an error, the transaction is rolled back.

box.on_commit and box.on_rollback trigger-function restrictions:

  • Yield is forbidden (it will crash Tarantool);
  • The function should not access any database spaces;
  • If the function raises an error, the error is simply logged.

tarantool.trigger.on_change

In the trigger registry, there is event named 'tarantool.trigger.on_change' which is called when any event is modified (trigger.set or trigger.del is called). All the handlers are called with one argument - name of the modified event. Returned value of each handler is ignored. Handlers are fired after the event is changed (the event contains inserted trigger, if any, and does not contain deleted one, if any). All thrown errors are logged with error level and do not stop execution of triggers.

@xuniq xuniq added the 3.0 label Jan 18, 2024
@xuniq xuniq self-assigned this Jan 18, 2024
@xuniq xuniq added reference [location] Tarantool manual, Reference part triggers [area] Related to triggers labels Jan 18, 2024
sergepetrenko pushed a commit to tarantool/tarantool that referenced this issue Feb 13, 2024
This patch allows to override IPROTO request handlers by setting triggers
on the corresponding events after the initial `box.cfg{}' call.

Part of #8138

@TarantoolBot document
Title: Document iproto override using event triggers
Product: Tarantool
Since: 3.1
Root document: New page - https://www.tarantool.io/en/doc/latest/reference/reference_lua/trigger/

Since Tarantool 3.1 there are 2 ways to override iproto request handlers:

 1. Using `box.iproto.override()`, introduced in Tarantool 2.11:
    https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_iproto/override/

 2. Using universal trigger registry: tarantool/doc#3988

To override an iproto request handler for the given request type, one can
set a trigger (or multiple triggers) on the corresponding event.

There are 2 types of iproto-overriding events:
 1. set by request type id, e.g.:
     - box.iproto.override[1]
     - box.iproto.override[-1]
 2. set by request type name (the name must be in the lowercase), e.g.:
     - box.iproto.override.select
     - box.iproto.override.unknown

Override-by-id allows to set a handler for a particular request type, that
is not known by the given version of Tarantool. This is not possible with
override-by-name, where a type name must be known by Tarantool. Also there
are a special type name "unknown" and a type id box.iproto.type.UNKNOWN
(== -1) that allow to set a single handler for all unknown request types.

Multiple triggers can be associated with a single event. The triggers are
called in reverse order of their installation, however triggers set by id
are called before triggers set by name.

If a trigger returns `false`, the next trigger in the list is called, or a
system handler if there are no more triggers. If a trigger returns `true`,
no more triggers or system handlers are called.

If some request type is overridden by both interfaces (legacy
`box.iproto.override()' and new `trigger.set()'), the order of invocation
of those handlers is unspecified.

Co-authored-by: Andrey Saranchin <[email protected]>
@veod32 veod32 unassigned xuniq Jul 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.0 reference [location] Tarantool manual, Reference part triggers [area] Related to triggers
Projects
None yet
Development

No branches or pull requests

2 participants