Mirajv1.0
FR

20. Événements de changement : LISTEN, NOTIFY, WAIT FOR CHANGES

Éditions Entreprise et Cluster. En édition Express, ces instructions rendent l'erreur 9001.

Une application qui veut savoir ce qui a changé (écran de stock, tableau de bord, cache) n'a pas besoin d'interroger la base en boucle : elle s'abonne aux tables ou aux canaux qui l'intéressent, puis attend sur une connexion dédiée. MIRAJ lui rend les changements dès leur validation :

-- Connexion dédiée de l'application
LISTEN TABLE articles COLUMNS (qte, prix);
WAIT FOR CHANGES TIMEOUT 30;
seqcommit_seqeventdbnamepkcolumnspayloadsession_idat
17902944000000421790294400000042UPDATEstockarticles[25]prixNULL172026-09-25 10:41:07.318

Principes :

  • Aucun déclencheur à écrire : MIRAJ relève lui-même la clé primaire et les colonnes modifiées de chaque ligne écrite dans une table écoutée.
  • Publication à la validation : un événement n'est rendu qu'une fois la transaction validée, jamais après un ROLLBACK. Les écritures d'une même transaction sont fusionnées : une ligne insérée puis modifiée donne un seul INSERT, et une ligne insérée puis supprimée ne donne rien.
  • Reprise après une coupure : chaque événement porte un numéro croissant. Un client qui se reconnecte reprend là où il s'était arrêté.
  • Un écrivain n'attend jamais un abonné : un abonné lent ou absent ne ralentit pas les écritures. S'il prend trop de retard, il reçoit RESYNC et doit tout relire.
  • Aucun pilote particulier : WAIT FOR CHANGES est une instruction ordinaire qui rend un jeu de résultats. Tout client SQL l'utilise (Delphi, PHP, outils graphiques, miraj-cli).

20.1 S'abonner#

LISTEN TABLE [base.]table [COLUMNS (c1, …)] [WHERE condition] [WITH ROW] [FROM n]
LISTEN [CHANNEL] canal [FROM n]
UNLISTEN TABLE [base.]table
UNLISTEN [CHANNEL] canal
UNLISTEN *
  • LISTEN TABLE suit les écritures d'une table : insertions, modifications, suppressions, vidages et changements de structure. Il demande le privilège SELECT sur la table, vérifié au moment du LISTEN.
    • COLUMNS (…) : seuls les UPDATE qui changent au moins une de ces colonnes sont rendus. Les insertions et suppressions sont toujours rendues.
    • WHERE condition : seules les lignes qui satisfont la condition sont rendues. La condition porte sur les colonnes de la table (l'image d'après, ou l'image d'avant pour une suppression). Elle ne peut contenir ni sous-requête, ni agrégat, ni variable, sinon l'erreur 1235 est rendue.
    • WITH ROW : la colonne payload contient la ligne entière, en objet JSON.
  • LISTEN canal écoute un canal nommé, alimenté par NOTIFY (§20.4). Il ne demande aucun privilège.
  • LISTEN rend une ligne à une colonne seq : la position de la session dans la suite des événements (§20.3).
  • Une session peut avoir jusqu'à 64 abonnements (change_events_max_listeners) ; au-delà, l'erreur 9008 est rendue. Un nouveau LISTEN sur la même cible remplace le précédent.
  • UNLISTEN * retire tous les abonnements de la session. La déconnexion et KILL les retirent aussi.
  • Une table sans clé primaire peut être écoutée, avec l'avertissement 9009 : ses écritures ne donnent que des événements BULK, sans clé.
  • Les tables temporaires ne sont jamais écoutées.

LISTEN, UNLISTEN et WAIT FOR CHANGES sont refusés dans une procédure, une fonction, un déclencheur ou un événement planifié (erreur 1314).

20.2 Attendre les changements#

WAIT FOR CHANGES [TIMEOUT secondes] [LIMIT n]
  • Rend, dans l'ordre, les événements publiés depuis la position de la session qui correspondent à ses abonnements, au plus LIMIT (1 000 par défaut), puis avance la position : un événement rendu ne l'est plus.
  • S'il n'y a rien à rendre, l'instruction attend le premier événement, au plus TIMEOUT secondes (par défaut @@change_events_wait_timeout, 30 s). Le délai écoulé, elle rend un résultat vide.
  • TIMEOUT 0 ne bloque pas : on relève ce qui est arrivé et on rend la main aussitôt.
  • L'attente ne tient aucun verrou : elle ne retarde ni les écritures ni les ALTER TABLE.
  • KILL QUERY l'interrompt (erreur 1317), tout comme max_statement_time (erreur 1969). SHOW PROCESSLIST montre la session à l'état Waiting for changes.
  • WAIT FOR CHANGES est refusé dans une transaction ouverte (erreur 9006) et sans abonnement (erreur 9007). Avec autocommit = 0, une transaction implicite qui n'a fait que lire est fermée d'office.

Colonnes rendues :

ColonneContenu
seqNuméro de l'événement, croissant.
commit_seqNuméro commun à tous les événements d'une même validation. Il permet de regrouper les changements d'une transaction.
eventINSERT, UPDATE, DELETE, BULK, TRUNCATE, SCHEMA, NOTIFY ou RESYNC (voir le tableau ci-dessous).
dbBase de la table ou du canal.
nameNom de la table, ou du canal pour NOTIFY.
pkClé primaire de la ligne, en tableau JSON ([25], ["FR", 2026]) ; les valeurs binaires sont écrites "0x…", les dates au format SQL. NULL pour un événement de table.
columnsPour un UPDATE : colonnes dont la valeur a changé, séparées par des virgules.
payloadCharge d'un NOTIFY ; ligne en JSON avec WITH ROW ; motif d'un événement de table (1500 rows, ALTER TABLE, RENAME TABLE TO base.nouveau…).
session_idSession à l'origine de l'écriture, pour qu'une application ignore ses propres modifications. NULL sur un secondaire de cluster (§20.7).
atDate et heure de publication, à la milliseconde.

Événements :

ÉvénementQuand
INSERT, UPDATE, DELETEUne ligne ajoutée, modifiée ou supprimée. Une clé primaire modifiée donne DELETE de l'ancienne clé puis INSERT de la nouvelle ; un REPLACE qui remplace une ligne donne UPDATE.
BULKPlus de change_events_bulk_rows lignes (1 000 par défaut) d'une table modifiées par une même transaction, ou table sans clé primaire : un seul événement, sans clé, qui invite à relire la table.
TRUNCATETRUNCATE TABLE ou DELETE sans condition.
SCHEMAALTER TABLE, RENAME TABLE, DROP TABLE ou DROP DATABASE de la table écoutée. L'abonnement reste : une table recréée sous le même nom est de nouveau suivie.
NOTIFYNotification d'un canal écouté (§20.4).
RESYNCDes événements ont été perdus pour cette session (§20.3) : relire les données.

20.3 Reprise après une coupure#

Chaque session a une position : le numéro du dernier événement qu'elle a reçu. Le premier LISTEN la place sur le dernier événement publié, et chaque WAIT FOR CHANGES l'avance.

Pour reprendre après une coupure (redémarrage de l'application, réseau coupé), le client garde le dernier seq traité et se réabonne avec FROM :

LISTEN TABLE articles FROM 1790294400000042;
WAIT FOR CHANGES;   -- rend les événements qui suivent 1790294400000042

Les événements sont gardés en mémoire dans une file de taille bornée (100 000 événements et 64 Mio par défaut, change_events_buffer_events et change_events_buffer_size). Une table reste écoutée pendant change_events_retention (10 minutes par défaut) après la déconnexion de son dernier abonné, pour que ses changements soient retrouvés à la reconnexion. En revanche, un UNLISTEN explicite arrête le suivi aussitôt.

La session reçoit une seule ligne RESYNC quand la reprise est impossible :

  • les événements attendus ont quitté la file (abonné trop en retard, ou absent plus longtemps que la rétention) ;
  • le serveur a redémarré depuis : les événements ne sont pas conservés sur disque ;
  • le numéro vient d'un autre serveur, par exemple d'un autre nœud du cluster (§20.7).

Après RESYNC, relire les données. Pour un rechargement sans trou, relever d'abord la position :

SELECT MIRAJ_CHANGE_SEQ();          -- par exemple 1790294400000108
SELECT * FROM articles;             -- rechargement complet
LISTEN TABLE articles FROM 1790294400000108;

Les numéros sont propres à chaque démarrage du serveur : ils partent de l'heure du démarrage, en microsecondes. Un numéro d'un démarrage précédent ne peut donc pas être confondu avec un numéro du démarrage en cours.

20.4 Notifications applicatives : NOTIFY#

NOTIFY canal [, 'charge']
SELECT MIRAJ_NOTIFY('canal', 'charge');   -- 1
  • NOTIFY envoie une notification aux sessions qui écoutent le canal. La charge est un texte libre de 8 000 octets au plus (change_events_max_payload), au-delà l'erreur 9010 est rendue.
  • Dans une transaction, la notification n'est publiée qu'au COMMIT, avec les changements des tables et sous le même commit_seq. Un ROLLBACK l'annule. Deux notifications identiques (même canal, même charge) d'une transaction n'en font qu'une.
  • NOTIFY et MIRAJ_NOTIFY() sont permis dans les procédures, les fonctions et les déclencheurs. Dans un déclencheur, on écrit par exemple SET @n = MIRAJ_NOTIFY('stock_bas', NEW.id);. Une instruction rejouée après un conflit d'écriture ne publie sa notification qu'une fois.

20.5 Suivi#

SHOW LISTENERS;
SHOW STATUS LIKE 'Miraj_change_events_%';
SELECT MIRAJ_CHANGE_SEQ();
  • SHOW LISTENERS liste les abonnements : Id, User, Kind, Target, Columns, Filter, With_row, Cursor, Lag (événements publiés que la session n'a pas encore reçus), Waiting et Time. Avec le privilège PROCESS, il montre toutes les sessions, sinon celles de l'utilisateur.
  • SHOW STATUS : Miraj_change_events_published, _buffered, _buffer_bytes, _evicted, _resyncs, _bulk, _listeners, _waits, _last_seq.

20.6 Réglages#

VariableOption du serveurDéfautRôle
change_events_buffer_events--change-events-buffer-events <n>100 000Événements gardés en mémoire pour la reprise.
change_events_buffer_size--change-events-buffer-size <octets>64 MioTaille maximale de ces événements.
change_events_bulk_rows--change-events-bulk-rows <n>1 000Au-delà, par table et par transaction, un seul BULK.
change_events_max_payload--change-events-max-payload <octets>8 000Taille maximale d'une charge de NOTIFY.
change_events_max_listeners--change-events-max-listeners <n>64Abonnements par session.
change_events_wait_timeout--change-events-wait-timeout <secondes>30Attente de WAIT FOR CHANGES sans TIMEOUT (variable de session et globale).
change_events_retention--change-events-retention <secondes>600Suivi d'une table après la déconnexion de son dernier abonné ; 0 l'arrête aussitôt.

Les options fixent la valeur au démarrage ; elles s'écrivent aussi, sous le même nom avec des soulignés, dans miraj_config.xml (chapitre 11, §11.2). SET GLOBAL les change ensuite jusqu'au prochain redémarrage. Une valeur invalide empêche le serveur de démarrer. En édition Express, ces options sont acceptées et sans effet.

20.7 Sur un cluster (édition Cluster)#

Un abonné peut se connecter au primaire ou à un secondaire : chaque nœud publie les changements de ses propres tables.

  • Sur le primaire, les événements sont publiés à la validation, comme sur un serveur seul.
  • Sur un secondaire, ils sont publiés quand le secondaire applique le journal reçu du primaire. Il rend les mêmes événements que le primaire, dans le même ordre, avec un commit_seq commun à chaque transaction ; ils arrivent seulement avec le retard de réplication (LAG_SECONDS, §12.5.2). Les changements de structure (SCHEMA) y sont aussi rendus.
  • Sur un secondaire, session_id vaut NULL : la session qui a écrit est sur le primaire.
  • Les filtres (COLUMNS, WHERE, WITH ROW) s'appliquent de la même façon sur tous les nœuds.
  • Les tables partitionnées s'écoutent sous leur nom, sur tous les nœuds, quelle que soit la partition écrite. Une ligne qui change de partition change de clé : elle donne DELETE puis INSERT. Le seuil change_events_bulk_rows s'entend par partition. Un TRUNCATE de la table, ou ALTER TABLE … TRUNCATE PARTITION, donne un BULK (motif TRUNCATE PARTITION) par partition vidée plutôt qu'un TRUNCATE.
  • NOTIFY reste local au nœud qui l'exécute : il ne passe pas par la réplication. Comme les écritures, et donc les NOTIFY des déclencheurs, s'exécutent sur le primaire, une application qui écoute un canal doit s'abonner sur le primaire.
  • Chaque nœud a sa propre numérotation et ses propres réglages change_events_…. Un numéro obtenu sur un nœud n'a pas de sens sur un autre.
  • Bascule : après une promotion, un client qui se reconnecte à un autre nœud avec LISTEN … FROM n reçoit RESYNC et relit ses données. Un client déjà abonné au secondaire promu n'est pas touché : le nœud garde sa numérotation et continue de publier.

Stratégie recommandée : abonner les écrans et caches en lecture sur un secondaire, pour décharger le primaire, et les canaux NOTIFY sur le primaire. Sur RESYNC, relire les données puis se réabonner (§20.3).

Écarts entre nœuds, rares en pratique :

  • Avec le modèle de concurrence pessimiste (--concurrency pessimistic), une écriture hors transaction peut être décrite plus finement par un secondaire que par le primaire. Le secondaire rend un UPDATE de clé primaire en DELETE + INSERT, ne cite que les colonnes réellement changées, et détaille ligne à ligne certains DELETE que le primaire résume en BULK. Le modèle multiversion, par défaut, donne les mêmes événements sur tous les nœuds.
  • Deux nœuds réglés avec des change_events_bulk_rows différents ne résument pas les mêmes transactions en BULK.
  • Les lignes d'un CREATE TABLE … SELECT sont rendues par un secondaire si un abonnement porte déjà sur ce nom de table (par exemple resté après un DROP TABLE), pas par le primaire.

20.8 Limites#

  • Les événements sont gardés en mémoire seulement : un redémarrage les perd, et le client reçoit RESYNC.
  • Une session a une seule position pour tous ses abonnements : si des événements quittent la file avant qu'elle les lise, elle reçoit RESYNC, même si ces événements concernaient d'autres tables.
  • Après RENAME TABLE, l'abonnement reste sur l'ancien nom (le motif de l'événement SCHEMA donne le nouveau) : se réabonner sous le nouveau nom.
  • Modèle pessimiste, écriture hors transaction : un UPDATE de la clé primaire est rendu sous la nouvelle clé seulement ; une suppression déjà compactée donne BULK.
  • Si l'horloge du serveur a reculé entre deux démarrages, un numéro ancien peut, très rarement, retomber dans la plage du démarrage en cours.