Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Inter-Event GClasses

RPC-like communication between yunos over the network. A local gobj subscribes to a remote service as if it were local.

Source: kernel/c/root-linux/src/c_ievent_cli.c, c_ievent_srv.c


C_IEVENT_CLI

Inter-event client — connects to a remote yuno and simulates its service as a local gobj. Handles identity-card exchange, authentication, and subscription management.

PropertyValue
StatesST_STOPPED, ST_DISCONNECTED, ST_WAIT_CONNECTED, ST_WAIT_IDENTITY_CARD, ST_SESSION, ST_SUBSCRIBED
Input eventsEV_SEND_MESSAGE, EV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE, EV_TIMEOUT, EV_DROP, EV_STOPPED
Output eventsEV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE

Key attributes

AttributeTypeDescription
wanted_yuno_rolestringRole of the remote yuno to connect to.
wanted_yuno_namestringName of the remote yuno.
wanted_yuno_servicestringService name on the remote yuno.
urlstringConnection URL.
jwtstringJSON Web Token for authentication.
timeout_idackintegerIdentity-card ack timeout in seconds.

Stopping with remote subscriptions open

gobj_stop() closes the transport, but the FSM stays in ST_SESSION until the close arrives, so EV_ON_CLOSE is still published. A subscription added or withdrawn in that window is not sent to the peer. The peer’s C_IEVENT_SRV drops every subscription of the channel when the channel closes, and an added subscription is sent at the next open. Up to 7.25.4 the __unsubscribing__ frame went down to the stopping transport, and C_WEBSOCKET or C_TCP logged “Event NOT DEFINED in state”. Example:

gobj_stop_tree(priv->gobj_remote);                          // the transport starts to close
gobj_unsubscribe_event(priv->gobj_remote, EV_X, 0, gobj);   // not sent, and not an error

C_IEVENT_SRV

Inter-event server — entry gate for authenticated service access. Manages incoming connections, routes inter-event messages to local services, and handles WebSocket upgrade.

PropertyValue
StatesST_STOPPED, ST_DISCONNECTED, ST_WAIT_IDENTITY_CARD, ST_SESSION
Input eventsEV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE, EV_TIMEOUT, EV_DROP, EV_STOPPED
Output eventsEV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE

Key attributes

AttributeTypeDescription
__username__stringAuthenticated username (read-only).
__session_id__stringSession identifier (read-only).
jwt_payloadjsonDecoded JWT payload.
client_yuno_rolestringRole of the connected client yuno.
client_yuno_namestringName of the connected client yuno.
this_servicestringLocal service name this gate serves.
authenticatedboolWhether the connection is authenticated.
max_subscriptionsintegerSubscriptions a peer may hold on the channel (default 5000, 0 no limit). See What a peer may hold.
max_subscription_sizeintegerBytes, as compact json, of the __filter__, of the __global__ and of the routing back (__md_iev__) of a peer’s subscription, each one (default 16384, 0 no limit).
max_pre_session_frameintegerBytes of a frame received before the session (default 65536, 0 no limit). See below.

Lifecycle of a channel

The tree of a channel (C_CHANNEL → C_IEVENT_SRV → C_WEBSOCKET or C_PROT_TCP4H → C_TCP) belongs to its gate, not to one connection. gobj_start_tree() of the gate starts it. When the peer leaves, only its C_TCP stops: the protocol gobj stays running and serves the next connection that the channel accepts.

When the gate stops, all of the tree stops: each layer stops the one below it, and C_IEVENT_SRV stops its protocol gobj since 7.25.5. Up to 7.25.4 a gate stopped with a plain gobj_stop() (the way the yuno stops an autostart service) left each C_WEBSOCKET or C_PROT_TCP4H running, and the yuno exited with “Destroying a RUNNING gobj”. Only an owner that called gobj_stop_tree() on the gate stopped it all.

A channel that is disabled and enabled again (the C_IOGATE commands disable-channel and enable-channel) comes back whole: C_CHANNEL restarts its tree, and C_IEVENT_SRV starts its protocol gobj in its start, the mirror of its stop. The transport C_TCP is manual start: C_TCP_S starts it for each connection it accepts.

ycommand -c 'command-yuno id=<id> service=__input_side__ command=disable-channel channel_name=^input-1$'
ycommand -c 'command-yuno id=<id> service=__input_side__ command=enable-channel channel_name=^input-1$'

channel_name is a regular expression. It selects the channels whose name it matches: ^input-1$ on a gate with input-1 and input-2 selects only input-1. One that matches no channel selects nothing, and the command answers with the header of the view only. (Up to 7.25.4 each of the six commands looped for ever on the first channel that did not match, and blocked the yuno.) A text that is not a valid regular expression is refused with -1, for example channel_name=input-[ answers “<role^name>: channel_name is not a valid regular expression: ‘input-[’” (up to 7.25.4: “regcomp() failed”).

Start and stop the gate as a pair. Either declare it "autostart": true in the config and let the yuno start and stop it, or do both in the owner:

PRIVATE int mt_play(hgobj gobj)
{
    PRIVATE_DATA *priv = gobj_priv_data(gobj);

    priv->gobj_input_side = gobj_find_service("__input_side__", TRUE);
    gobj_subscribe_event(priv->gobj_input_side, 0, 0, gobj);
    gobj_start_tree(priv->gobj_input_side);
    return 0;
}

PRIVATE int mt_pause(hgobj gobj)
{
    PRIVATE_DATA *priv = gobj_priv_data(gobj);

    gobj_stop_tree(priv->gobj_input_side);
    return 0;
}

Subscription authz

A peer subscribes to an event of a local service with a __subscribing__ message. C_IEVENT_SRV accepts it only for a public output event of a service the channel may reach. Since 7.25.5, when the yuno sets enable_subscription_authz (off by default) and the event is flagged EVF_AUTHZ_SUBSCRIBE, the channel’s user also needs the publisher’s permission aliased __subscribe_event__ — read for the EV_TREEDB_NODE_* feed of a treedb and for the EV_TRANGER_RECORD_ADDED feed of a C_TRANGER — or the global __subscribe_event__. A refused subscription is logged (“No permission to subscribe event”) and not made; the channel stays open. The refusal is logged once per service and event on a channel: a peer that repeats it does not write a log line per frame. When the peer withdraws a refused subscription later, that is logged at info level (“its subscription was refused”). A withdrawal that matches no subscription of the channel, and was not refused, is a warning (“UNSUBSCRIBING event matches no subscription of this channel”), with the frame cut to 256 bytes. Both are the peer’s to repeat, so each is written at most once per 10 s per channel, with the count of the ones not written (suppressed). Up to 7.25.4 each frame wrote its line, and the no-match warning the whole frame, as big as the peer made it.

Example: a yuno that enforces the treedb feed permission, in its config:

{
    "yuno": {
        "enable_subscription_authz": true
    }
}

Details, and how a gclass declares a guarded event: YUNO_AUTH.md §4.6.

What a peer may put in a subscription

The kw of a __subscribing__ message can carry __config__, __global__ and __filter__, as a local gobj_subscribe_event() does. C_IEVENT_SRV builds the subscription from those three, and keeps of them only what a peer may set (since 7.25.5):

KeyWhat is kept
__filter__All of it. It decides only the peer’s own deliveries.
__config__The keys in the list below.
__global__The keys of the peer’s own: not one that starts with _ (the framework’s: __md_iev__, __md_yuno__, __service__, ...), and not gbuffer, the binary field of every kw. They come back to the peer in every event of this subscription, and to nobody else.
__local__Nothing. The one __local__ of a remote subscription is the reference to the channel, set by C_IEVENT_SRV.
any other keyNothing.

What is dropped is logged as a warning, “SUBSCRIBING keys a peer may not set, ignored”, at most once per 10 s per channel. A __filter__ or a __global__ bigger than max_subscription_size refuses the subscription (“SUBSCRIBING refused, bigger than max_subscription_size”, same pace).

To route every event of the subscription back to the peer, the gate adds its own back-metadata to the stored __global__: __md_yuno__, and a __md_iev__ built from the TOP record of the frame’s routing stack (the hop of this peer, with the gate’s stamps), reversed. Nothing else of the frame’s __md_iev__ is kept: no key of the peer’s own and no deeper hop. That routing is measured against max_subscription_size too, after it is built: a bigger one (a record with long strings) refuses the subscription, “SUBSCRIBING refused, its routing is bigger than max_subscription_size”, at most once per 10 s. Up to 7.25.4 the frame’s whole __md_iev__ was copied, keys of the peer’s own included, and nothing of a subscription was measured: a peer could store as much as a frame holds in each subscription, with no cap on their number, and got it back with every event.

Up to 7.25.4 only __config__ was filtered, and the publish shared ONE kw with every subscriber, so a peer’s subscription changed the event of every subscriber after it: its __global__ forged keys of the event (a topic_name, a node), its __local__ removed them, the filters of the later subscribers were evaluated on the forged kw, and the back-metadata of the gate (__md_iev__, with the peer’s user name and channel) reached a local subscriber. A gbuffer in the peer’s __global__ was taken for a pointer when the event was serialized back to it, and crashed the yuno. Now gobj_publish_event() gives a subscription with a __local__ or a __global__ a twin of the kw of its own (gobj_publish_event()), and C_IEVENT_SRV changes a kw that somebody else holds only on a copy.

Of __config__ the list of keys a peer may set has one key:

KeyMeaning
__first_shot__Read by the publisher in its mt_subscription_added(): false asks it not to send its current state when the subscription is made.

Any other key is removed before the subscription is made, and logged as a warning (“SUBSCRIBING config keys a peer may not set, ignored”). Three of those keys change how the framework delivers the event, and a peer must never have them:

Up to 7.25.4 the peer’s __config__ went through whole. Also since 7.25.5, the close of a channel removes every subscription it made with force, hard or not.

Example: a SPA subscribes without the first shot, and that key arrives:

gobj_subscribe_event(gobj_remote, "EV_REALTIME_TRACK", {
    __config__: {__first_shot__: false},
    __filter__: {id: device_id}
}, gobj);

A key that a publisher reads from a peer is added to the list in c_ievent_srv.c (peer_subscription_config_keys), and documented here.

A peer withdraws a subscription with an __unsubscribing__ message that repeats what it subscribed. The kw is filtered the same way, and compared with what the peer SENT: the __global__ that C_IEVENT_SRV stores carries its own back-metadata too, which the peer never repeats. Up to 7.25.4 it was compared whole, and a subscription with a __global__ could not be withdrawn until the channel closed.

Example: a C client tags the events of its subscription, and withdraws it:

json_t *kw = json_pack("{s:{s:s}, s:{s:s}}",
    "__filter__", "topic_name", "devices",
    "__global__", "tag", "devices_view"     // comes back in each event
);
gobj_subscribe_event(gobj_remote, EV_REALTIME_TRACK, json_incref(kw), gobj);
gobj_unsubscribe_event(gobj_remote, EV_REALTIME_TRACK, kw, gobj);   // the same kw

What a peer may hold

Every subscription costs a scan of the publisher’s subscriptions when it is made, and one more on every publish of its event. A peer could make them without end: 20000 subscriptions of one peer blocked the event loop for 80 s, and every later publish took 13 ms. So a channel holds at most max_subscriptions of its peer. Beyond it a subscription is refused, logged once (“SUBSCRIBING refused, the peer holds max_subscriptions”), and not again until the peer is under the cap.

A subscription that repeats one the peer holds takes no room:

Both are logged as a warning, at most once per 10 s per channel (“SUBSCRIBING repeated, the one held is kept”, “SUBSCRIBING overrides one held, it is replaced”). A client does not send either: C_IEVENT_CLI withdraws the subscription it replaces first. Up to 7.25.4 every repeated frame went to gobj_subscribe_event(), which deleted the subscription, made it again, and logged a warning with a stack trace and the whole kw.

A gate whose peers subscribe per device (two subscriptions per device, in the SPAs of hidraulia) raises the cap in the kw of the C_IEVENT_SRV of its channel tree:

{
    "name": "input-(^^__range__^^)",
    "gclass": "C_IEVENT_SRV",
    "kw": {
        "max_subscriptions": 20000
    }
}

What the gate stamps

The gate writes who sent a message into the kw it hands on, and overwrites whatever the peer put there under the same key. In a command, a stats request and an event, __username__ is the channel’s authenticated user (set by C_AUTHZ), whatever the peer sent. In the routing stack of an event (__md_iev__), __username__, input_channel and input_service are the gate’s. The command parser’s authz check reads that __username__.

Example: a peer sends list-yunos with a __username__ of its own in the kw of the command:

{"__username__": "admin"}

The service gets "__username__": "bob" when the channel’s user is bob. Up to 7.25.4 kw_set_dict_value() kept a key that already existed, so the peer’s admin reached the service.

A frame the gate cannot route

Every frame of a session carries its routing: __md_iev__ with the ievent stack, whose top record says where the frame comes from (src_yuno, src_role, src_service, strings) and, if it says, where it goes (dst_yuno, dst_role, dst_service, strings). C_IEVENT_CLI (C and JS) pushes it on every request, and an answer copies the one of its request. A well-formed routing (__msg_type__ is optional, a string when present):

"__md_iev__": {
    "ievent_gate_stack": [
        {
            "src_yuno": "", "src_role": "ycommand", "src_service": "ycommand",
            "dst_yuno": "", "dst_role": "yuneta_agent", "dst_service": "agent"
        }
    ],
    "__msg_type__": "__command__"
}

The same frame with "src_role": 7, with an empty ievent_gate_stack, or with no __md_iev__ at all, has no routing, and its channel is closed. The gate reads what the peer sent with plain json calls, never with a kw_get_*() reader that logs a wrong type with a stack. What it refuses, and how:

The frameWhat the gate doesLog (per channel)
No routing, or a malformed oneCloses the channel, as for a frame that is not json. It cannot be answered, nor routed back.“Frame without its routing (__md_iev__ ievent stack), channel closed”, WARNING, MSGSET_PROTOCOL, the kw capped to 256 bytes, at most once per 10 s.
For another yuno (dst_role, dst_yuno)Closes the channel.“It’s not my role, channel closed” / “It’s not my name, channel closed”, WARNING, the routing capped.
For a service the channel may not reachA command or a stats request gets a negative answer and the channel stays; a subscription, a withdrawal or an event closes it.“event ignored, dst_service not authorized for this channel”, WARNING, MSGSET_AUTH, the kw capped, at most once per 10 s.
For a service that does not existThe same.“event ignored, service not found”, WARNING, same pace.
A subscription or a withdrawal of an event that is not publicRefused; the channel stays.“SUBSCRIBING event ignored, not PUBLIC...”, WARNING, same pace.
A command or a stats request that names no command (neither __command__ in the kw nor in its stack), or whose service is not a stringAnswered with an error; the channel stays.“Request without the command or stats it asks, refused”, WARNING, the kw capped, same pace.
A command or a stats request whose service does not exist, or (stats) is one the channel may not reachAnswered with an error; the channel stays.“Service not found” / “Not authorized to request stats of a different service”, WARNING, same pace.

Each of these is the peer’s to repeat, frame after frame, so none writes a line per frame, none carries a stack trace, and none dumps the whole kw. Up to 7.25.4 a frame without routing logged an error with a stack and the whole kw (the lookup was verbose, “TODO check”) and was processed all the same; a command to a service the channel may not reach logged an error, the whole kw, the authorized services and the routing, for each frame; and a subscription to an event that is not public returned an error without closing the channel, which left it connected and deaf.

tests/c/c_ievent_srv_peer_subs.

An identity card the gate refuses

Before its session a peer may send two things: its identity card (EV_IDENTITY_CARD) or EV_GOODBYE. The card carries the same routing as any frame, and it must name this yuno and one of its services:

{
    "jwt": "<token, or empty with the BFF cookie>",
    "__md_iev__": {
        "ievent_gate_stack": [
            {
                "src_yuno": "", "src_role": "ycommand", "src_service": "ycommand",
                "dst_yuno": "", "dst_role": "yuneta_agent", "dst_service": "agent"
            }
        ],
        "__msg_type__": "__identity__"
    }
}

What is refused, each time, is the peer’s: the channel is closed, and the gate writes ONE warning (MSGSET_PROTOCOL, peername, the kw capped to 256 bytes, no stack), with no credential in it. Before a session there is no command table to ask which parameter is SDF_SECRET, so the NAME decides (is_secret_name(), the one list of the SDK: passw, token, secret, jwt, api_key, private_key... any case): at any depth of the kw the value of such a key is shown as ******** (whatever its json type), and in a string (a command line) the value of such a name=value too -- the kernel’s json_mask_secrets(), the same rule as the ievents traces. A command sent before the card:

kw   {"jwt": "eyJ...", "password": "s3cr3t", "kw": {"passw": "x"}, "__command__": "help token=abc"}
log  {"jwt":"********","password":"********","kw":{"passw":"********"},"__command__":"help token=********", ...}

The ievents and ievents2 traces of C_IEVENT_CLI and C_IEVENT_SRV (trace_inter_event(), trace_inter_event2()) mask the same way, in both directions. A command (__command__, v6 or v7) is also masked by the command table of its destination service when that service is in this yuno -- its SDF_SECRET parameters, positional ones too -- as the commands trace does (command_mask_secret_line()). Up to 7.25.20 ievents2 printed a command’s password in clear.

kw   {"__command__": "set-user-pwd username=bob password=hunter2", "password": "hunter2"}
log  {"event": "EV_MT_COMMAND", "kw": {"__command__": "set-user-pwd username=bob password=********", "password": "********"}}
The cardLog
No routing, or a malformed one“Identity card without its routing (__md_iev__ ievent stack), refused”
dst_role is not this yuno’s role“Identity card refused, dst_role NOT MATCH”, with dst_role
dst_yuno given and not this yuno’s name“Identity card refused, dst_yuno NOT MATCH”, with dst_yuno
Empty src_role“Identity card refused, without yuno role”
Empty src_service“Identity card refused, without yuno service”
dst_service is no service of this yuno“Identity card refused, dst_service NOT FOUND in this yuno”, with dst_service
jwt present and not a string“Identity card refused, its jwt is not a string”
Any event but the card or EV_GOODBYE“Event before the identity card, channel closed”, with event
A frame bigger than max_pre_session_frame“Frame before the identity card too big, channel closed”, with size and max; the frame is not parsed, and its bytes are not dumped (unparsed they cannot be masked, and a card carries a jwt)

A frame before the session is not even parsed when it is bigger than max_pre_session_frame. An identity card is a few KB (its jwt the largest part); parsed, a frame of [{},...] takes about 100 times its size (16 MB: 1.7 GB), so up to 7.25.21 the max block of a yuno (200 MB) let a peer nobody had authenticated ask for some 20 GB. A gate whose peers send bigger cards raises it:

{
    "name": "input",
    "gclass": "C_IEVENT_SRV",
    "kw": {
        "max_pre_session_frame": 1048576
    }
}

A card refused by the authentication is answered with a negative EV_IDENTITY_CARD_ACK, logged by the authenticator, and the channel is dropped at timeout_idgot. Up to 7.25.20 each refusal above was an error with the whole kw (its jwt with it), followed by a second error, “event UNKNOWN in not-session state”, with the whole kw again, credentials and all; a jwt that was not a string went on to the authentication.

tests/c/c_ievent_srv_identity_card.