Notification des mises à jour

Imaginez que vous concevez un service de messagerie à l’aide de CouchDB. Chaque utilisateur dispose d’une boîte aux lettres sous la forme d’une base de données ; les expéditeurs envoient leurs messages en déposant un document dans cette base. Quand les utilisateurs désirent relever leur courrier, ils peuvent ouvrir leur boîte et lire les messages qu’elle contient.

Jusque-là, c’est simple. Toutefois, vous vous retrouvez avec vos utilisateurs effectuant des rafraîchissements fréquents pour découvrir si un nouveau message est arrivé. C’est ce que l’on appelle couramment la scrutation (ou attente active, de l’anglais polling). Dans ce cas, un nombre important d’utilisateurs génère un nombre conséquent de requêtes qui, la plupart du temps, ne renvoient rien de nouveau ; juste la liste que l’utilisateur avait déjà consultée.

Ne serait-il pas judicieux de demander à CouchDB de notifier l’utilisateur quand un nouveau message arrive ? L’API _changes est là pour cela !

Ce scénario traite le problème de l’invalidation de l’antémémoire, c’est-à-dire de savoir quand ce que j’affiche ne représente plus fidèlement les données sous-jacentes. Toute invalidation d’antémémoire, qu’elle soit ou non backend/frontend-related, peut se baser sur l’API _changes.

_changes est aussi conçue pour extraire un fil d’activité d’une base de données, que ce soit pour afficher l’information ou pour réagir en conséquence à la création ou modification d’un document.

La beauté des systèmes qui implémentent l’API de scrutation réside dans leur découplage. En effet, un programme qui s’intéresse uniquement aux dernières mises à jour n’a pas besoin de connaître les programmes qui créent des documents, et réciproquement.

Voici à quoi ressemble un objet changes :

{"seq":12,"id":"foo","changes":[{"rev":"1-23202479633c2b380f79507a776743d5"}]}

Il y a trois champs :

seq
L’update_seq de la base de données qui a été généré lors de la création ou de la mise à jour du document id.
id
L’identifiant du document.
changes
Un tableau de champs qui, par défaut, inclut l’identifiant de la révision du document. Il peut aussi contenir des informations sur les conflits ou d’autres choses.

L’API de scrutation est disponible pour chaque base de données. Une seule requête vous permet d’obtenir les modifications se produisant sur une seule base. Toutefois, vous pouvez facilement envoyer plusieurs requêtes pour interroger plusieurs API sur plusieurs bases si vous en avez besoin.

Créons une base de données que nous utiliserons par la suite :

> HOST="http://127.0.0.1:5984"
> curl -X PUT $HOST/db
{"ok":true}

Il existe trois manières d'obtenir des notifications : scruter (par défaut), scruter à la ligne [NdT : comme la pêche à la ligne, on pose la ligne et on attend] et scruter de manière continue. Chacune correspond à un besoin différent que nous expliciterons ci-après.

Scruter les modifications

Dans l’exemple précédent, nous tentions d’éviter le recours à la scrutation. Cependant, c’est très simple et cela s’avère parfois la seule méthode convenable pour résoudre un problème. Comme il s’agit du cas le plus aisé, c’est celui qui est choisi par défaut dans l’API de scrutation.

Voyons à quoi ressemble l’API dans notre base. Tout d’abord, la requête (nous utilisions de nouveau curl) :

curl -X GET $HOST/db/_changes

Le résultat est limpide :

{"results":[

],
"last_seq":0}

Il n’y a rien à voir, car nous n’avons rien mis dans la base — guère surprenant. Vous pouvez deviner où vous verriez des modifications s’il y en avait. Créons un document :

curl -X PUT $HOST/db/test -d '{"name":"Anna"}'

CouchDB répond :

{"ok":true,"id":"test","rev":"1-aaa8e2a031bca334f50b48b6682fb486"}

Regardons à nouveau l’API :

{"results":[
{"seq":1,"id":"test","changes":[{"rev":"1-aaa8e2a031bca334f50b48b6682fb486"}]}
],
"last_seq":1}

Nous obtenons une notification de notre nouveau document. C’est sympa ! Sauf que… nous avons déjà eu ces informations en retour de la requête de création du document. Pourquoi donc voudrions-nous soumettre une nouvelle requête pour l’obtenir à nouveau ? Souvenez-vous qu’il s’agit de créer des systèmes découplés. Le programme qui crée le document n’est probablement pas celui qui scrutera les changements puisqu’il les connaît déjà. Pour vous embrouiller un peu, ce même programme pourrait désirer connaître les modifications apportées par les autres.

Dans les coulisses, nous avons créé un nouveau document. Regardons l’effet :

{"results":[
{"seq":1,"id":"test","changes":[{"rev":"1-aaa8e2a031bca334f50b48b6682fb486"}]},
{"seq":2,"id":"test2","changes":[{"rev":"1-e18422e6a82d0f2157d74b5dcf457997"}]}
],
"last_seq":2}

Voyez-vous la nouvelle ligne qui représente le nouveau document ? En plus, le premier document créé est de nouveau listé. Il faut dire que, par défaut, l’API de modification conserve l’historique de tous les changements effectués sur la base.

Comme nous avons déjà vu la modification de "seq":1, elle ne nous intéresse plus. Aussi, nous pouvons demander à l’API de le masquer à l’aide du paramètre since=1 :

curl -X GET $HOST/db/_changes?since=1

Cela renvoie les modifications survenues après le seq spécifié par since :

{"results":[
{"seq":2,"id":"test2","changes":[{"rev":"1-e18422e6a82d0f2157d74b5dcf457997"}]}
],
"last_seq":2}

Puisque nous en sommes aux options, utilisez donc style=all_docs pour obtenir davantage de révisions et d’informations sur les conflits dans le tableau changes de chaque enregistrement. Si vous exigez explicitement le comportement par défaut, la valeur du paramètre est main_only.

Scruter à la ligne

La technique de scrutation à la ligne (long polling en anglais) a été inventée pour aider les navigateurs web à résoudre le problème majeur de la scrutation traditionnelle : elle n’envoie pas de requêtes si rien n’a changé. Cela fonctionne de la manière suivante : lorsqu’une requête part vers l’API de scrutation à la ligne [NdT : vous posez votre ligne de pêche], vous ouvrez une connexion HTTP à CouchDB jusqu’à ce qu’un nouvel enregistrement apparaisse dans l’API [NdT : qu’un poisson morde à l’hameçon] ; en attendant, vous et le serveur conservez la connexion ouverte. Dès qu’un résultat apparaît, la connexion est fermée.

Cela fonctionne bien quand les mises à jour sont rares. Dans le cas où un grand nombre de modifications affectent un client, vous vous retrouvez à soumettre de nombreuses requêtes, ce qui réduit d’autant l’intérêt de cette approche par rapport à la scrutation traditionnelle. Une autre conséquence de cette technique est de devoir maintenir une connexion HTTP ouverte. CouchDB en est capable, puisqu’il est conçu pour traiter de nombreuses requêtes en parallèle. Toutefois, vous devez vous assurer que votre système d’exploitation permet à CouchDB d’utiliser au moins autant de sockets que vous avez de pêcheurs à la ligne (en sus d’un nombre complémentaire pour les requêtes usuelles).

Pour forger une requête de scrutation à la ligne, ajouter le paramètre feed=longpoll. Pour ce listage, nous avons ajouté l’horodatage pour vous montrer quand les choses se sont passées.

00:00: > curl -X GET "$HOST/db/_changes?feed=longpoll&since=2"
00:00: {"results":[
00:10: {"seq":3,"id":"test3","changes":[{"rev":"1-02c6b758b08360abefc383d74ed5973d"}]}
00:10: ],
00:10: "last_seq":3}

À 00:10, nous créons un autre document à votre insu et CouchDB nous indique promptement la modification. Notez que nous avons spécifié since=2 pour éviter les notifications précédentes. Notez aussi que nous devons utiliser les doubles guillemets droits pour la commande curl, car nous utilisons une esperluette, laquelle est un caractère spécial pour notre shell.

L’option style fonctionne de la même manière pour les requêtes de scrutation et de scrutation à la ligne.

Les réseaux sont des bêtes délicates et, quelques fois, vous ne savez pas si vous ne recevez rien car il n’y a rien à recevoir, ou parce que votre connexion est morte. Si vous ajoutez un autre paramètre, heartbeat=N, où N est un nombre, CouchDB vous enverra un retour chariot toutes les N millisecondes. Tant que vous recevez des retours chariot, c’est qu’il n’y a pas eu de nouveaux changements et que CouchDB est toujours prêt à vous notifier dès que cela survient.

Scruter en continu

La scrutation à la ligne est pratique, mais vous vous retrouvez tout de même à ouvrir une connexion HTTP par notification. Pour les navigateurs web, c’est le seul moyen d’éviter le fléau de la scrutation régulière. Mais les navigateurs ne sont pas les seuls clients qui peuvent communiquer avec CouchDB. Si vous utilisez Python, Ruby, Java, ou quelque autre langage, vous disposez d’une autre possibilité.

L’API de scrutation continue vous permet de recevoir les modifications comme elles surviennent, à travers une seule connexion HTTP. Vous soumettez une requête à l’API et CouchDB et vous-même tiendrez la connexion ouverte pour « toujours ». CouchDB vous enverra des retours chariot lors des notifications et, contrairement à la scrutation à la ligne, ne fermera pas la connexion et attendra d’envoyer d’autres modifications.

C’est pratique aussi bien pour les notifications fréquentes que rares, et cela emporte la même conséquence que la scrutation à la ligne : vous allez avoir de nombreuses connexions HTTP de longue durée. Une fois encore, CouchDB le supporte bien.

Utilisez le paramètre feed=continuous pour rendre une scrutation continue. Ci-dessous se trouve le résultat, à nouveau avec l’horodatage. À 00:10 et 00:15, nous créerons un nouveau document :

00:00: > curl -X GET "$HOST/db/_changes?feed=continuous&since=3"
00:10: {"seq":4,"id":"test4","changes":[{"rev":"1-02c6b758b08360abefc383d74ed5973d"}]}
00:15: {"seq":5,"id":"test5","changes":[{"rev":"1-02c6b758b08360abefc383d74ed5973d"}]}

Notez bien que la réponse de l’API de scrutation continue n’encapsule pas ses réponses dans un objet JSON pour lequel chaque notification est un élément du tableau. Elle consiste en une ligne brute par notification. Notez aussi que les lignes ne s’achèvent plus par des virgules. Contrairement aux deux autres modes de scrutation où le résultat est contenu dans un objet JSON, l’API de scrutation continue renvoie un objet JSON valide par ligne. Cette différence permet de faciliter le traitement des réponses par les clients. Par ailleurs, les paramètres style et heartbeat fonctionnent comme attendu dans cette API.

Filtres

L’API de scrutation, avec ses trois modes d’opération, vous offre déjà de nombreuses possibilités d’obtention et de traitement des modifications opérées dans CouchDB. Vous pouvez y ajouter des filtres et ainsi gagner en souplesse. Illustrons : les messages précédents ont un niveau de priorité et un utilisateur ne s’intéresse qu’aux messages de priorité haute (high).

Bienvenue dans les filtres ! À l’instar des vues, un filtre est une fonction JavaScript définie dans un design document et exécutée par CouchDB. Ils se trouvent dans le membre spécial filters et portent le nom que vous leur donnez. Par exemple :

{
  "_id": "_design/app",
  "_rev": "1-b20db05077a51944afd11dcb3a6f18f1",
  "filters": {
    "important": "function(doc, req) { if(doc.priority == 'high') { return true; }
    else { return false; }}"
  }
}

Pour interroger l’API de scrutation avec ces filtres, utilisez le paramètre filter=designdocname/filtername dans la requête :

curl "$HOST/db/_changes?filter=app/important"

Le résultat se restreint désormais aux enregistrements qui désignent des documents pour lesquels le filtre a renvoyé true — dans notre cas, lorsque la propriété priority est positionnée à la valeur high. Voilà qui est bien pratique ! mais ce n’est pas tout. CouchDB va plus loin.

Revenons à l’exemple initial où les utilisateurs peuvent s’échanger des messages. Plutôt que d’avoir une base de données par utilisateur, laquelle représente la boîte aux lettres, nous avons une unique base pour tout le monde. Dès lors, comment un utilisateur peut-il souscrire aux seules arrivées de messages qui le concernent ?

Nous pouvons désigner un paramètre à la fonction de filtrage :

function(doc, req)
{
  if(doc.name == req.query.name) {
    return true;
  }

  return false;
}

Si vous soumettez une requête en y adjoignant le paramètre ?name=Steve, le filtre ne retournera que les enregistrements désignant des documents dont le champ name est positionné à « Steve ». Pour changer d’utilisateur, vous n’avez qu’à changer la valeur du paramètre (name=Joe).

Toutefois, qu’est-ce qui empêcherait Steve de placer name=Joe dans sa requête pour accéder au contenu de sa boîte ? Pas grand-chose. CouchDB peut-il apporter une solution ? Dans le cas contraire, évoquerions-nous le sujet ?

Le paramètre req de la fonction de filtrage inclut un membre userCtx : le contexte utilisateur. Il contient les informations transmises par l’utilisateur lors de son authentification HTTP. Plus précisément, req.userCtx.name contient le nom de l’utilisateur qui soumet la requête de scrutation. Nous pouvons être confiants dans cette valeur car elle a été validée par le moteur d’authentification de CouchDB. De cette manière, nous n’avons même plus besoin du paramètre dynamique dans notre requête (bien qu’il puisse être utile dans d’autres cas).

Si vous avez configuré CouchDB pour exiger l’authentification des requêtes, un utilisateur devra soumettre une requête d’authentification dont le résultat sera disponible dans notre fonction de filtrage :

function(doc, req)
{
  if(doc.name) {
    if(doc.name == req.userCtx.name) {
      return true;
    }
  }

  return false;
}

Résumé

L’API de scrutation vous permet de concevoir des schémas de notification utiles dans bien des cas, notamment en présence de composants asynchrones qui suivront les mêmes évènements. En y associant le mécanisme de réplication, cette API est la base nécessaire à la réalisation de grappes CouchDB distribuées, hautement disponibles et à haute performance.