La Taverne "The Falling Whale" a connu son lot d'aventures.
Le serveur au comptoir répond aux requêtes des clients depuis un bon moment, et le commis de course sait désormais filer chercher un stock de houblon en urgence sur simple commande.
Mais un problème plus ancien traînait encore dans un coin de la salle : comment faire circuler une information entre deux pièces de la taverne sans que l'une n'attende que l'autre soit disponible ?
C'est là qu'intervient la volière.
Le problème du message qui ne peut pas attendre
Imaginons que le tavernier veuille prévenir la cuisine qu'une nouvelle commande vient d'être passée. Il pourrait envoyer son commis frapper à la porte de la cuisine et attendre une réponse avant de repartir.
Ça fonctionne, mais si la cuisine est occupée, tout le monde attend : le commis, le tavernier, et le client qui commence à trouver le temps long. Et si la cuisine ferme un instant pour un coup de feu imprévu, c'est toute la chaîne qui se bloque.
La volière propose une autre approche. Le tavernier attache son message à la patte d'un corbeau et le lâche. Il n'attend pas de réponse, il n'attend même pas que la cuisine soit prête à le recevoir.
Le corbeau s'envole, se pose sur son perchoir dans la volière, et le cuisinier vient y consulter les parchemins à son propre rythme. Si la cuisine est fermée une heure, les corbeaux patientent simplement sur leur perchoir.
C'est exactement le rôle que joue Apache Kafka dans une application : découpler celui qui produit une information de celui qui la consomme, sans les obliger à se synchroniser ni à connaître mutuellement leur disponibilité.
Les personnages de la volière
La volière de la taverne s'organise autour de plusieurs rôles bien distincts.
Le maître corbeau est celui qui attache les messages et lâche les oiseaux. Dans le vocabulaire Kafka, c'est le producer. Il ne se soucie pas de savoir qui lira le message, ni quand. Son travail s'arrête une fois le corbeau envolé.
Le parchemin est ce que le corbeau porte à sa patte : le contenu même du message. C'est lui l'équivalent du record Kafka, la donnée qui va réellement transiter d'un bout à l'autre du système.
La volière, raven-roost, est l'endroit où tous les corbeaux convergent une fois lâchés. C'est elle qui joue le rôle du topic Kafka : le canal nommé sur lequel les parchemins s'accumulent en attendant d'être lus. Le corbeau, lui, n'a pas vraiment d'équivalent direct dans Kafka : il représente simplement l'acheminement, le trajet du parchemin entre le maître corbeau et la volière.
Le cuisinier est celui qui vient consulter la volière et détacher les parchemins pour les lire. C'est le consumer, celui qui s'abonne à raven-roost et réagit à chaque message reçu.
Une volière unique ne suffit pas si des dizaines de corbeaux affluent en même temps. C'est là qu'interviennent les partitions.
La volière raven-roost n'est pas un perchoir unique, mais plusieurs perchoirs numérotés, chacun capable d'accueillir ses propres corbeaux indépendamment des autres.
En choisissant une clé de partitionnement, par exemple l'expéditeur du message, on peut garantir qu'un corbeau donné se pose toujours sur le même perchoir, ce qui permet à Kafka de conserver l'ordre des messages d'un même expéditeur au sein de cette partition. Cet ordre ne vaut que pour cette partition-là : encore faut-il que la clé de partitionnement soit effectivement renseignée à l'envoi, ce que notre code actuel ne fait pas encore, le message étant publié sans clé explicite.
Et pour que plusieurs cuisiniers puissent se répartir la lecture des corbeaux sans se marcher dessus, Kafka introduit la notion de groupe de consommateurs : au sein d'un même groupe, chaque perchoir n'est surveillé que par un seul cuisinier à la fois, ce qui permet de paralléliser la lecture tout en évitant qu'un message soit traité deux fois par la même équipe.
Cette parallélisation a cependant une limite mécanique : avec trois perchoirs, un quatrième cuisinier du même groupe n'aura tout simplement rien à faire, faute de perchoir disponible à surveiller. Le nombre de partitions d'un topic fixe donc, dès sa création, le degré de parallélisme maximal qu'un groupe de consommateurs pourra jamais atteindre.
Construire la volière avec Quarkus
Quarkus s'intègre à Kafka via l'extension quarkus-messaging-kafka, qui repose sur la spécification MicroProfile Reactive Messaging.
Concrètement, cela se traduit par deux annotations : @Channel côté producer pour injecter un Emitter, et @Incoming côté consumer pour s'abonner automatiquement à un canal.
Le parchemin
public record RavenMessage(String sender, String content, Instant sentAt) {
public static RavenMessage from(String sender, String content) {
return new RavenMessage(sender, content, Instant.now());
}
}
Rien de superflu : un expéditeur, un contenu, un horodatage. Une fois le corbeau lâché, ce message ne bouge plus, comme il se doit pour un objet qui va transiter par un flux.
Encore faut-il que ce record puisse voyager sous forme d'octets sur le réseau. Côté production, Quarkus fournit un ObjectMapperSerializer générique qui s'appuie sur Jackson pour transformer le record en JSON.
Côté consommation, il faut en revanche déclarer un déserialiseur dédié, faute de pouvoir déduire le type cible à la volée :
public class RavenMessageDeserializer extends ObjectMapperDeserializer<RavenMessage> {
public RavenMessageDeserializer() {
super(RavenMessage.class);
}
}
Le maître corbeau (producer)
@ApplicationScoped
public class RavenMasterService {
@Inject
@Channel("raven-outbound")
Emitter<RavenMessage> ravenOutbound;
public RavenMessage dispatch(String sender, String content) {
RavenMessage message = RavenMessage.from(sender, content);
ravenOutbound.send(message);
return message;
}
}
Le cuisinier (consumer)
@ApplicationScoped
public class RavenRoostListener {
@Incoming("raven-inbound")
public void onRavenLanding(RavenMessage message) {
LOG.infof("Un corbeau se pose : message de %s -> \"%s\"",
message.sender(), message.content());
}
}
Le comptoir, pour demander l'envoi d'un corbeau
Pour déclencher tout ça depuis l'extérieur, un point d'entrée REST fait office de comptoir de la taverne : on y dépose sa demande, et c'est lui qui délègue au maître corbeau.
La ressource reste volontairement fine, toute la logique de publication étant portée par le service.
@Path("/ravens")
public class RavenResource {
@Inject
RavenMasterService ravenMasterService;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response dispatch(@Valid @NotNull RavenDispatchRequest request) {
RavenMessage message = ravenMasterService.dispatch(request.sender(), request.content());
RavenDispatchResponse response =
new RavenDispatchResponse(message.sender(), message.content(), message.sentAt());
return Response.accepted(response).build();
}
}
Ce 202 Accepted colle bien à la métaphore : la taverne accepte la demande et lâche le corbeau sans attendre que la cuisine ait traité quoi que ce soit.
Il ne faut cependant pas y lire plus qu'il ne dit : ravenOutbound.send(message) déclenche l'envoi, mais le 202 ne garantit pas à lui seul que le parchemin est déjà durablement posé sur son perchoir au moment où la réponse part.
La sémantique exacte dépend de la façon dont l'Emitter et le connecteur gèrent l'envoi et les accusés de réception côté broker, un sujet qui ouvre justement sur les notions d'acquittement et de garanties de livraison.
Le raccordement au topic
Le lien entre les canaux logiques (raven-outbound, raven-inbound) et le topic Kafka réel se fait dans application.properties, où l'on précise le connecteur, le topic visé et le format de sérialisation :
mp.messaging.outgoing.raven-outbound.connector=smallrye-kafka
mp.messaging.outgoing.raven-outbound.topic=raven-roost
mp.messaging.outgoing.raven-outbound.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
mp.messaging.incoming.raven-inbound.connector=smallrye-kafka
mp.messaging.incoming.raven-inbound.topic=raven-roost
mp.messaging.incoming.raven-inbound.value.deserializer=fr.eletutour.tavern.raven.RavenMessageDeserializer
mp.messaging.incoming.raven-inbound.auto.offset.reset=earliest
Le reste du code, lui, continue de parler corbeaux et parchemins plutôt que producers et consumers.
Quel broker se cache derrière la volière ?
Un point mérite d'être clarifié, car il change selon l'environnement : d'où vient le Kafka auquel l'application se connecte réellement ?
En développement local, aucune configuration de bootstrap.servers n'apparaît volontairement dans application.properties.
C'est Quarkus Dev Services qui prend le relais : au démarrage en mode quarkus:dev, il détecte l'absence de configuration explicite et démarre lui-même un broker Kafka jetable via Testcontainers, en s'appuyant sur Docker.
La volière tourne alors dans un conteneur éphémère, créé et détruit avec l'application, sans qu'on ait à installer ni configurer quoi que ce soit à la main. Sans Docker disponible, Dev Services ne peut simplement pas démarrer son broker : l'application ne dispose alors d'aucun Kafka local prêt à accepter les connexions, et le démarrage échoue tant qu'aucun broker n'est configuré par ailleurs.
Pour les tests automatisés, la stratégie change encore : plutôt que de payer le coût d'un vrai conteneur Kafka à chaque exécution, src/test/resources/application.properties bascule les canaux sur le connecteur smallrye-in-memory.
Le maître corbeau publie alors dans une file en mémoire, directement inspectable via un InMemorySink, ce qui rend les tests rapides et déterministes, sans dépendance à Docker ni au réseau. Il faut avoir en tête ce que ce type de test valide réellement : pas le comportement d'un vrai broker Kafka, mais celui de l'application vis-à-vis de son canal Reactive Messaging.
C'est justement pour ça que cette approche est complémentaire d'un test d'intégration avec un vrai broker, plutôt qu'un substitut à celui-ci.
En production, il ne doit plus y avoir ni Dev Services ni connecteur in-memory : ce sont des mécanismes pensés pour le confort du développement et la rapidité des tests, pas pour un environnement de production. Un vrai cluster Kafka, opéré et supervisé indépendamment de l'application, prend le relais, et sa localisation est fournie explicitement via configuration :
kafka.bootstrap.servers=broker-1.prod.internal:9092,broker-2.prod.internal:9092
Trois volières, donc, pour trois usages :
- celle que Docker construit et démolit à chaque session de développement,
- celle qui n'existe qu'en mémoire le temps d'un test
- celle, bien réelle et surveillée qui sert les messages en production.
Quand le corbeau ne suffit plus
Un vrai corbeau peut se perdre ; un message Kafka, lui, reste disponible dans le topic pendant toute sa durée de rétention, même si aucun consumer ne l'a encore traité.
Cette rétention ne dépend d'ailleurs pas de la consommation : un message peut être lu dix fois et rester en place, ou expirer sans avoir jamais été lu.
Un vrai perchoir ne peut accueillir qu'un oiseau à la fois ; un topic Kafka, grâce à ses partitions, permet à plusieurs corbeaux d'arriver en parallèle sans se gêner. Et là où un message perdu resterait perdu, Kafka propose des mécanismes de relecture et de garantie de livraison bien plus robustes que n'importe quel volatile.
Reste une question : que fait-on d'un corbeau qui porte un message illisible, corrompu, ou que le cuisinier échoue systématiquement à traiter ?
Dans la vraie vie, on ne le relâche pas indéfiniment dans la nature en espérant qu'il finisse par se faire comprendre.
On commence par lui redonner sa chance, avec un délai croissant entre chaque tentative : c'est le principe du retry. Et si, malgré plusieurs tentatives, le message reste imbuvable, on l'isole dans une cage à part pour l'examiner plus tard sans bloquer le reste de la volière : c'est le rôle de la DLQ (Dead Letter Queue).
Les deux mécanismes se succèdent plutôt qu'ils ne se confondent, un peu à la manière d'un message qui échoue une première fois, puis une deuxième, puis une troisième, avant d'être définitivement mis de côté.
Ce sera le sujet du prochain article de la série.
En résumé
Kafka dans Quarkus, ce n'est finalement rien d'autre qu'organiser une volière : un maître corbeau qui lâche des parchemins sans attendre de réponse, une volière découpée en perchoirs qui joue le rôle du topic, et des cuisiniers qui viennent les consulter à leur propre rythme, seuls ou en équipe.
Le découplage entre production et consommation, qui peut sembler abstrait sur le papier, devient nettement plus intuitif une fois qu'on lui donne des ailes, et ce n'est encore que le début de l'histoire.