Repository navigation
Conversation
Several libraries in one VM can each own their syn scopes, but syn has one event handler per node. A library that needs its own conflict resolution or callbacks has to chain to the handler set before it, or overwrite it and break the other library. syn:set_event_handler/2 sets a handler for a single scope. syn selects the handler for a scope as a whole, not per callback. A scope with its own handler uses only that module: a callback the module does not export behaves as if no handler were set, which for resolve_registry_conflict/4 means the default rule (keep the later registration, kill the other process with syn_resolve_kill), and the node-wide handler is not called for that scope. A scope without its own handler uses the node-wide handler from the event_handler key, and syn's defaults when there is none. The scope handler lives in persistent_term, so it can be set before syn starts and before the scope is added. A later call for the same scope replaces it.
syn starts the scopes listed under the scopes key inside its own boot,
in syn_sup:init/1. When syn starts as a dependency, those scope
processes exist before host code can call syn:set_event_handler/2, so
their first events, including the sync with other nodes, go to the
node-wide handler or to syn's defaults.
The scope_event_handlers key takes a list of {Scope, Module} tuples.
syn_app:start/2 applies every entry before it starts the supervisor.
In interactive mode it loads each module first, as set_event_handler/2
does; in embedded mode the release has already loaded it. The list is
applied at every start of the application and replaces a handler set
earlier for the same scope with set_event_handler/2. Handlers of other
scopes stay as they are.
The application fails to start when:
- an entry is not a tuple of two atoms, or has undefined as the
module: a function_clause shows the entry;
- the value is not a list: a function_clause shows the value;
- an entry names a module that cannot be loaded: a
{scope_event_handler_not_loaded, Scope, Module} error shows the
scope and the module. Starting anyway would leave the scope without
callbacks and with syn's default conflict resolution.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #91.
Two libraries in one VM that each bring their own
syn_event_handlermodule cannot both have their callbacks run today: syn has one handler per node, and the lastset_event_handler/1call wins.syn:set_event_handler(Scope, Module)sets the handler of one Scope, and thescope_event_handlerskey, a list of{Scope, Module}tuples, does the same for the Scopes that syn starts itself from thescopeskey.Before: one module, chosen for the whole node, runs every callback of every Scope. After: a Scope with its own module uses only that module; a Scope without one uses the node-wide handler from the
event_handlerkey, and syn's defaults when there is none.set_event_handler/1and theevent_handlerkey are unchanged.The handler is chosen per Scope, not per callback. If the Scope's module does not export a callback, syn skips that callback for the Scope and does not fall back to the node-wide handler. Without
resolve_registry_conflict/4, syn applies its default rule: it keeps the later registration and kills the other process. The edoc ofresolve_registry_conflict/4now says that every node must resolve a conflict in a Scope the same way, which in practice means the same module and version.The configuration key exists because the scope processes started from
scopesexist before host code can callset_event_handler/2, so their first events, the sync with other nodes included, would otherwise go to the node-wide handler or to the defaults. syn fails to start when the key is malformed or names a module that cannot be loaded: starting anyway would leave that Scope without callbacks and with default conflict resolution.Per-Scope handlers are stored in
persistent_term, one key{syn_event_handler, Scope}per Scope. The handler is read on every callback and written rarely, at startup; apersistent_termread is constant time and does not copy the term to the heap. One key per Scope means two concurrentset_event_handler/2calls cannot lose each other's entry, as a read-modify-write of one list in the application environment could. It also works before syn starts and survives a restart of the application. The cost thepersistent_termdocs name, a global garbage collection in which all processes scan their heaps when a term is replaced or erased, does not apply here: the stored value is a module name, and deletion of atoms is specially optimized to avoid that GC.It may make sense to move the node-wide
event_handlertopersistent_termas well. This pull request leaves it in the application environment so thatset_event_handler/1and theevent_handlerkey behave exactly as before. The price is on the fallback path. Measured on OTP 27 in a running node with 293 applications: reading the handler from the application environment takes 32 ns, apersistent_termhit 17 ns, and a Scope without its own handler (apersistent_termmiss, then the environment) 41 ns. With the node-wide handler inpersistent_termtoo, that fallback would take 23 ns.OTP:
persistent_term:get/2exists since OTP 21.3. syn declares no minimum OTP version; the code so far needed 21.0 (loggerandClass:Reason:Stacktrace), and CI tested 21.3 until cf2b318 in August 2024. CI now runs 27 and 28.Both suites pass on OTP 25.3.2.8, 27.3.4.11 and 29.0.6 (registry 26 cases, pg 14). Dialyzer is clean on 27.3.4.11 and 29.0.6.
Two commits: the function and the selection, then the configuration key. I can squash them if you prefer one.