Les événements d'une base de données Firebird et middleware(s) orienté(s) messages

Mike Justin

Traducteur

Serge Girard

Les développeurs mettant en œuvre Firebird peuvent utiliser les événements Firebird pour informer les clients des changements dans les données d'une base. Dans cet article, l'auteur nous montre et discute des limites des événements Firebird et compare les contournements possibles avec une solution basée sur un middleware orienté messages.

Commentez

- Titre : Les événements d'une base de données Firebird et middleware(s) orienté(s) messages

- Auteur : Mike Justin

- Traducteur : Serge Girard

- Parution : 4 juillet 2015

- Mise à jour : 13 juillet 2015

- Public visé :

- Licence : Copyright ® 2014 Mike Justin. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Partie 1 : Mise en œuvre avec Firebird

- II. Partie 2 : Utilisation d'un middleware

- III. Critique et conclusion de la part du traducteur

I - Partie 1 : Mise en œuvre avec Firebird

Traduit de Habari Blog : firebird database events and message oriented middleware Part 1

Comment mettre en œuvre une solution où le serveur indique (« push ») qu'il y a de nouvelles données disponibles au lieu que ce soit le client qui en fasse la demande (« pull ») ?

Pour cet article, nous utiliserons une application simple, soit :

- une application client-serveur à deux niveaux ;

- un poste client crée de nouveaux enregistrements de type « bon de commande » ;

- les autres postes clients ont besoin d'être avertis des nouveaux dossiers « bon de commande ».

Dans ce scénario, répéter les lectures sur la table « ordre d'achat » provoquerait un trafic réseau à partir de tous les postes de travail clients (même s'il n'y a pas de nouvelles commandes) et vers tous (et donc trop) les postes clients provoquant ainsi une surcharge du réseau et/ou des ressources du serveur. Du fait que l'application est à deux niveaux, il n'y a pas de couche de service ou de serveur d'application entre la base de données et le logiciel client qui pourrait gérer ces notifications. En conclusion nous devons, côté serveur, envoyer des notifications « push », et ce, afin de ne provoquer un trafic réseau que lorsque de nouvelles données sont envoyées.

Avec les événements Firebird, le code de notification fera partie d'un objet de métadonnées de la base de données (un déclencheur ou une procédure stockée). Par exemple :

Trigger

CREATE TRIGGER PURCHASE_ORDER_INSERT
FOR PURCHASE_ORDER
AFTER INSERT
AS
BEGIN
POST_EVENT 'purchase_order_table_updated';
END

Tout poste client intéressé par cet événement utilisera l'API Firebird pour s'inscrire à l'événement via le nom de ce dernier. Exemple de code, en utilisant TZIBEventAlerter de la Zeoslib :

var
EventAlerter: TZIBEventAlerter;
...
// register (subscribe) to purchase order event
EventAlerter.Events.Add('purchase_order_table_updated');
EventAlerter.RegisterEvents;
...

Chaque fois que le serveur envoie un événement « purchase_order_table_updated », la méthode de gestionnaire d'événements du poste client sera appelée.

Tous les clients connectés ayant souscrit à cet événement seront concernés.

Problème n° 1 : « Puis-je avoir l'ID du nouvel ordre, s'il vous plaît ? »

Il y a un problème avec cette solution simple : les événements Firebird ne sont pas en mesure de fournir de plus amples informations. Les postes clients ne seront informés que du fait qu'il y a de nouvelles données dans la table des commandes. Les postes clients qui reçoivent cet événement devront alors analyser la table des commandes pour identifier les nouveaux enregistrements.

Solution : Une table auxiliaire pour les événements de base de données.

Des recherches Internet proposent des solutions de contournement, par exemple sur Stackoverflow ici ou là. Des tables supplémentaires peuvent être créées sur le serveur afin de stocker des informations sur le nouvel ordre. À chaque événement, le code côté serveur (dans la procédure stockée ou le déclencheur) écrit un enregistrement dans une table auxiliaire avec des données supplémentaires par exemple, l'identification du bon de commande et la date de création de la commande :

begin
/* stocke le numéro de l'ordre ID */
insert into purchase_order_changes values (gen_id(some_generator, 1), New.ID, 'now');
/* et enfin, pousse l'événement */
POST_EVENT 'purchase_order_table_updated';
end
end

Note du traducteur : purchase_order_changes est dans cet exemple notre table auxiliaire.

Problème n° 2 : « Oups, j'étais déconnecté lorsque la nouvelle commande est arrivée ! »

Les événements Firebird sont envoyés sous forme de messages diffusés (broadcasting) : le serveur ne se soucie pas que les postes clients soient à l'écoute ou non. Tous les postes clients déconnectés manqueront ces nouveaux événements. Lorsque le poste client se reconnectera, l'événement manqué ne sera pas là à l'attendre sur le serveur.

Solution : Recenser les nouvelles données afin que le client s'en informe lors de sa reconnexion.

Comme illustré dans le cas précédent, des tables auxiliaires peuvent être utilisées pour stocker les identifiants des nouveaux enregistrements, et une lecture côté client peut récupérer toutes les nouvelles commandes depuis la dernière déconnexion. Techniquement, c'est une solution, mais d'une utilité limitée pour les clients qui se dé/reconnectent fréquemment. Il serait plus efficace que le serveur stocke les identifiants par poste client pour tous les événements ayant lieu alors que celui-ci est déconnecté.

Problème n° 3 : « Je ne dois être notifié que chaque troisième ordre. »

Imaginons qu'il y ait trois postes clients dans le bureau, traitant les commandes entrantes d'achat. Dès qu'une application cliente reçoit un nouvel événement de création d'un ordre, elle va utiliser la table auxiliaire pour récupérer l'identification du nouvel ordre et puis démarrer le flux de traitement de la commande. Évidemment, il serait très inefficace que tous les postes clients traitent la même. Au lieu de cela, le serveur doit tenter de distribuer les nouvelles commandes entre tous les postes clients connectés.

Parce que les événements Firebird sont envoyés sous forme de messages diffusés (« broadcasting »), tous les clients connectés qui ont souscrit à l'événement Firebird recevront le message. Les événements Firebird ne peuvent pas être configurés de sorte qu'ils soient envoyés de manière aléatoire ou circonscrits à un poste client particulier.

Solution : Une logique additionnelle côté serveur pour mettre en œuvre un « équilibrage de charge » (?)

La mise en œuvre de fonctionnalités avancées telles que l'équilibrage de charge des événements Firebird et ce totalement côté serveur (en utilisant des déclencheurs, des procédures stockées, des tables auxiliaires) est sûrement possible, mais laissée en exercice au lecteur.

Problème n° 4 : « Après une mise à jour en masse de tous les dossiers, le programme ne répond plus. »

Considérez ce code de notification dans un déclencheur UPDATE.

CREATE TRIGGER PURCHASE_ORDER_INSERT
FOR PURCHASE_ORDER
AFTER UPDATE
AS
BEGIN
POST_EVENT 'purchase_order_updated';
END

Il informera tous les postes clients connectés inscrits lorsqu'un enregistrement aura été modifié. Maintenant, imaginez qu'une instruction SQL UPDATE modifie un grand nombre d'enregistrements de la table : tous les postes clients abonnés et actifs recevront alors un nombre élevé d'événements « purchase_order_updated ». Est-ce que votre application rafraîchit l'interface utilisateur à chaque événement de base de données ? Si oui, cela peut rapidement surcharger le poste client.

Solution : avec un peu de logique supplémentaire côté client, les messages qui viennent trop rapidement pourraient être éliminés. Mais cela ne réduira pas la charge de la bande passante du réseau - elle restera élevée, sauf si le client décide de résilier son abonnement jusqu'à ce que les « opérations de masse » sur la base de données soient terminées. (Remarque : ce problème # 4 ne se produira que si chaque mise à jour était exécutée dans une nouvelle transaction.)

La partie suivante présente une solution différente, basée sur des produits middleware orientés messages, disponibles en open source et prenant en charge plusieurs langages et plates-formes.

II - Partie 2 : Utilisation d'un middleware

Traduit de Habari Blog : firebird database events and message oriented middleware Part 2

Dans la partie précédente, l'auteur a montré et discuté des limites des événements Firebird. Cette seconde partie présente une solution basée sur un middleware orienté message.

La solution appliquée côté serveur, proposée en première partie, intervient sur les métadonnées de la base de données Firebird, et est mise en œuvre avec le langage procédural de Firebird (PSQL). Les informations qui ne peuvent être apportées via les événements Firebird, telles que l'ID d'enregistrement du nouvel ordre d'achat, peuvent être stockées dans des tables auxiliaires qui doivent ensuite être lues par les clients de la base de données. Donc, d'un point de vue technique, ces solutions ne remplacent que les appels des postes clients déclenchés par minuterie ou par un événement ayant lieu sur le poste client (bouton, menu, etc.). C'est un petit pas en avant, au prix d'une complexité accrue des métadonnées de base de données.

Une nouvelle stratégie : La mise en œuvre de la notification de l'événement sur côté client

De toute évidence, l'application cliente qui insère un nouvel ordre d'achat pourrait également envoyer des notifications aux autres clients. L'avantage serait que le code PSQL côté serveur pourrait alors être remplacé par un code écrit en langage de programmation côté client (Delphi/Free Pascal, C #…).

Si nous mettons en place des notifications dans l'application côté client, un code similaire au trigger du début de l'article doit être exécuté quand une nouvelle commande est insérée dans la table des commandes, et ce, en utilisant une méthode de communication interprocessus. Le code de notification d'événement pourrait ressembler à ceci :

procedure TAppDataModule.PurchaseOrderAfterPost(DataSet: TDataSet);
var
Notification: INotification;
begin
Notification := NotificationService.CreateNotification(PURCHASE_ORDER_TABLE_UPDATED);
Notification.SetIntProperty(PURCHASE_ORDER_ID, PurchaseOrderID.AsInteger);
NotificationService.Send(Notification);
end;

Mais des fonctionnalités avancées telles que l'équilibrage de charge, ou l'information aux clients n'étant pas à l'écoute auraient besoin d'une grande quantité de code personnalisé. Une autre tâche difficile serait l'intégration de clients ou d'autres systèmes d'écriture dans un autre langage de programmation (une application web de saisie de commande basée sur PHP par exemple) - leur intégration avec votre application ne sera possible qu'avec le portage du système de notification.

Présentation des courtiers de messages (message brokers) et de files d'attente (queues)

Les courtiers de messages et de files d'attente sont des solutions logicielles qui mettent en œuvre un middleware orienté message (MOM) pour l'échange de messages. MOM fournit des éléments logiciels qui se trouvent dans tous les composants de communication et se charge des appels asynchrones entre les applications.

Après avoir installé le logiciel de courtage de messages (messages brokers), les applications clientes peuvent utiliser un programme logiciel qui « écoutera » les messages placés dans la file d'attente.

Par exemple, une application C # peut utiliser l'interface IMessageListener du client ActiveMQ NMS pour recevoir un message asynchrone. L'interface IMessageListener déclare une méthode, OnMessage, qui a un paramètre : un message. Si la bibliothèque cliente ActiveMQ reçoit un nouveau message, elle va passer le paramètre message à sa mise en œuvre via le OnMessage de l'application :

using Apache.NMS;
using Common.Logging;

namespace MyApp
{
public class SimpleMessageListener : IMessageListener
{
public void OnMessage(IMessage message)
{
ITextMessage textMessage = message as ITextMessage;
if (textMessage != null)
{
LOG.Info("Texte du message = " + textMessage.Text);
} else
{
LOG.Warn("Ne peut traiter un message de type" + message.GetType());
}
}
}

Recevoir des messages peut être effectué dans un thread d'arrière-plan, de façon asynchrone, comme le montre l'exemple C # ci-dessus, ou avec un appel synchrone, par exemple déclenché en appuyant sur un bouton ou en choisissant une option de menu.

Texte, binaire : La charge « utile » du message

Les messages non seulement peuvent inclure des propriétés définies par l'utilisateur (mises en œuvre comme une liste clé-valeur), mais aussi un texte ou des éléments binaires dans le corps du message. Dans notre exemple de scénario, le corps de message peut être utilisé pour transférer des données image comme un fax numérisé (ou un document EDI bon de commande entrant).

Comment puis-je utiliser un serveur de file d'attente de messages comme un «équilibrage de charge» ?

Dans la première partie, l'exigence troisième option a été intitulée « Je dois seulement être notifié de chaque troisième ordre ». La conclusion était que :  le serveur devait tenter de distribuer les nouveaux enregistrements de commandes entre tous les postes clients connectés.

Si vous avez configuré une pile de messages pour les nouveaux messages d'ordre d'achat, une application cliente peut poster de nouveaux messages au courtier de messages, et toutes les applications peuvent écouter les messages de cette file d'attente. Le serveur va conserver les messages dans la file d'attente jusqu'à ce qu'un client demande au serveur un nouveau message à traiter. Le courtier va alors supprimer le message le plus ancien de la file d'attente et l'envoyer à ce client exclusivement. Un nouveau client faisant la demande recevra alors le prochain message en attente, et ainsi de suite. La diffusion de messages entre les clients est un élément-clé d'une pile de messages, ce qui en fait une solution idéale pour l'équilibrage de charge.

Qu'advient-il si le client est déconnecté ?

Contrairement aux événements Firebird, qui exigent que le poste client soit connecté et à l'écoute (sinon les événements sont perdus), expéditeurs de messages dans file d'attente et récipiendaires n'ont pas à être connectés au courtier en même temps. Les nouveaux messages d'ordre d'achat resteront dans la file d'attente, attendant que les applications clientes se connectent.

Puis-je utiliser les courtiers de messages pour diffuser des messages ?

Les courtiers de messages utilisent un modèle de communication de type publication/abonnement, où un producteur publie des messages à une destination particulière (en fonction du sujet du message). Les abonnés peuvent enregistrer leur intérêt à recevoir des messages sur un sujet particulier. Seuls les clients connectés recevront le message, à la manière d'un récepteur radio.

Exemple de code Delphi/Free Pascal (en utilisant la bibliothèque Habari), côté application émettrice :

émetteur

procedure TAppDataModule.PurchaseOrderAfterPost(DataSet: TDataSet);
var
NewOrderMessage: IMessage;
begin
// crée un nouveau message
NewOrderMessage := Session.CreateMessage;
// Stocke l'ID du nouvel enregistrement dans les propriétés du message
NewOrderMessage.SetIntProperty(PURCHASE_ORDER_ID, PurchaseOrderID.AsInteger);
// envoie le message
MessageProducer.Send(NewOrder);
end;

Les applications de réception peuvent utiliser une réception asynchrone (dans un thread d'arrière-plan) ou un code comme dans l'exemple ci-dessous, qui vérifie pour un nouvel ordre d'achat dans un gestionnaire d'événements de l'interface :

procedure TOrderForm.CheckPurchaseOrder(Sender: TObject);
var
NewOrderMessage: IMessage;
OrderID: Integer;
begin
// Vérifie l'existence de nouveaux messages (attente 100 millisecondes)
NewOrderMessage := MessageConsumer.Receive(100);
if (NewOrderMessage <> nil) then
begin
// Obtient l'ID du nouvel ordre
OrderID := NewOrderMessage.GetIntProperty(PURCHASE_ORDER_ID);
// démarre le processus de traitement de l'ordre
EditOrder(OrderID);
end
else
begin
ShowMessage('Pas de nouvel ordre disponible');
end;
end;

Bibliothèques disponibles pour Delphi et Free Pascal

Des bibliothèques multilangages sont disponibles pour la plupart des systèmes de files d'attente de messages, par exemple :

- ActiveMQ : qui prend en charge les protocoles Java ainsi qu'un grand nombre de langages multiplates-formes C, C ++, C #, Ruby, Perl, Python, PHP ;

- RabbitMQ : qui peut être utilisé avec Java, Ruby, Python, .NET, PHP et d'autres plates-formes de développement.

Pour les développeurs Delphi et Free Pascal, Habarisoft propose des bibliothèques clientes pour les courtiers de messages Open Source ActiveMQ, Apollo, HornetQ, Open MQ et RabbitMQ. Ces bibliothèques enveloppent le protocole de communication des courtiers dans une API de haut niveau, et donnent accès à des fonctions de courtier avancées, telles que les messages persistants, « l'envoi traitement », et plus encore.

III - Critique et conclusion de la part du traducteur

L'auteur, peut-être poussé par l'envie de proposer « sa » solution (c'est-à-dire Habari), n'a sans doute pas assez exploré les possibilités offertes par les tables auxiliaires, laissées à l'imagination du lecteur (solution du problème n° 3). Néanmoins ces deux billets nous permettent d'aborder de façon concrète les événements de Firebird ainsi que les problèmes inhérents et leurs solutions. L'apparition des versions de Delphi (XE2 et +) permettant le multiplate-forme amène un peu plus d'intérêt, s'il en est besoin, à la solution du middleware.

L'auteur publie beaucoup d'autres billets intéressants, n'hésitez pas à consulter son blog si vous êtes intéressé par tout ce qui est traitement de messages en file d'attente.

Merci à Claude Leloup pour sa relecture.