Le mode pipeline de libpq permet aux applications d'envoyer une requête sans avoir à lire le résultat de la requête précédemment envoyée. En utilisant le mode pipeline, un client attendra moins le serveur car plusieurs requêtes/résultats peuvent être envoyés/reçus dans une seule transaction réseau.
Alors que le mode pipeline offre une amélioration significative des performances, les clients en écriture utilisant le mode pipeline sont plus complexes car cela implique de gérer une queue de requêtes en cours et de trouver quel résultat correspond à quelle requête dans la queue.
Le mode pipeline consomme généralement aussi plus de mémoire sur le client et le serveur, bien qu'une gestion attentionnée et agressive de la queue d'envoi/réception peut mitiger cela. Ceci s'applique que la connexion soit ou non en mode blocage ou non blocage.
Alors que l'API de pipeline de libpq a été introduite dans PostgreSQL 14, c'est une fonctionnalité du côté client qui ne requiert pas de support spécial côté serveur, et fonctionne avec tout serveur qui supporte le protocole de requêtes étendu v3. Pour plus d'informations, voir Section 53.2.4.
Pour exécuter des pipelines, l'application doit basculer la connexion en
mode pipeline, ce qui se fait avec PQenterPipelineMode
.
PQpipelineStatus
peut être utilisé pour tester si
le mode pipeline est actif. Dans le mode pipeline, seules les opérations asynchrones qui utilisent le protocole
de requête étendue sont autorisées, les
chaînes de commandes contenant plusieurs commandes SQL et
COPY
sont interdits. Utiliser les fonctions d'exécution
de commandes synchrones telles que
PQfn
,
PQexec
,
PQexecParams
,
PQprepare
,
PQexecPrepared
,
PQdescribePrepared
,
PQdescribePortal
,
PQclosePrepared
,
PQclosePortal
,
renvoie une erreur. PQsendQuery
est aussi interdite
parce qu'elle utilise le protocole de requête simple. Une fois que toutes
les commandes envoyées ont eu leurs résultats traités, l'application peut
retourner dans le mode sans pipeline avec PQexitPipelineMode
.
Il est préférable d'utiliser le mode pipeline avec libpq dans le mode sans blocage. Si utilisé en mode blocage, il est possible qu'un deadlock client/serveur survienne. [15]
Après être entré en mode pipeline, l'application envoie les requêtes en
utilisant
PQsendQueryParams
ou un des équivalents pour
requêtes préparées PQsendQueryPrepared
. Ces
requêtes sont mises en queue côté client jusqu'au vidage du serveur ;
ceci survient quand PQpipelineSync
est utilisé
pour établir un point de synchronisation dans le pipeline ou quand PQflush
est appelé. Les fonctions
PQsendPrepare
,
PQsendDescribePrepared
,
PQsendDescribePortal
,
PQsendClosePrepared
et
PQsendClosePortal
fonctionnent aussi en mode
pipeline. Le traitement des résultats est décrit ci-dessous.
Le serveur exécute les requêtes et envoie les résultats dans l'ordre dans
lequel le client les a envoyé. Le serveur commencera immédiatement
l'exécution des commandes dans le pipeline. Notez que les résultats sont
mis en tampon du côté du serveur ; le serveur vide ce tampon quand un
point de synchronisation est établi avec soit
PQpipelineSync
soit PQsendPipelineSync
,
ou quand
PQsendFlushRequest
est appelé. Si l'exécution d'une
requête rencontre une erreur, le serveur annule la transaction en cours
et n'exécute pas toute autre commande dans la queue jusqu'au prochain
point de synchronisation ; un résultat
PGRES_PIPELINE_ABORTED
est produit pour chacune de ces
commandes. (Ceci reste vrai même si les commandes dans le pipeline vont
annuler la transaction.) Le traitement des requêtes continue après le
point de synchronisation.
Il est possible qu'une opération dépende des résultats d'une opération précédente ; par exemple, une requête pourrait définir une table que la prochaine requête du même pipeline utilise. De façon similaire, une application pourrait créer une requête préparée nommée et l'exécuter dans les requêtes suivantes du même pipeline.
Pour traiter le résultat d'une requête dans un pipeline, l'application
appelle PQgetResult
de façon répété et gère chaque
résultat jusqu'à ce que PQgetResult
renvoie null.
Le résultat de la prochaine requête dans le pipeline sera alors récupéré
en utilisant de nouveau PQgetResult
et le cycle
sera répété. L'application gère les résultats de requête individuelle
comme d'habitude. Quand les résultats de toutes les requêtes du pipeline
ont été renvoyés, PQgetResult
renvoie un résultat
contenant la valeur de statut PGRES_PIPELINE_SYNC
Le client peut choisir de différer le traitement des résultats jusqu'à ce que le pipeline complet ait été envoyé ou de continuer en envoyant d'autres requêtes dans le pipeline ; voir Section 32.5.1.4.
PQgetResult
se comporte comme pour le traitement
asynchrone normal, sauf qu'il peut contenir les nouveaux types
PGresult
, à savoir PGRES_PIPELINE_SYNC
et PGRES_PIPELINE_ABORTED
.
PGRES_PIPELINE_SYNC
est indiqué exactement une fois
pour chaque PQpipelineSync
ou
PQsendPipelineSync
au point correspondant
dans le pipeline.
PGRES_PIPELINE_ABORTED
est émis à la place d'un
résultat de requête normal pour la première erreur et tous les résultats
suivants jusqu'au prochain PGRES_PIPELINE_SYNC
;
voir Section 32.5.1.3.
PQisBusy
, PQconsumeInput
, etc
opèrent normalement lors du traitement ds résultats du pipeline. En
particulier, un appel à PQisBusy
au milieu d'un
pipeline renvoie 0 si les résultats de toutes les requêtes lancées
jusqu'à maintenant ont été consommés.
libpq ne fournit pas d'informations à
l'application quant à la requête en cours de traitement (sauf que
PQgetResult
renvoie null pour indiquer que nous
commençons l'envoi des résultats de la requête suivante). L'application
doit garder la trace de l'ordre dans lequel elle a envoyé les requête,
pour les associer avec les résultats correspondants. Les applications
utiliseront typiquement une machine à état ou une queue FIFO pur ça.
Du point de vue du client, après que PQresultStatus
renvoie PGRES_FATAL_ERROR
, le pipeline est considéré
comme annulé. PQresultStatus
renverra un résultat
PGRES_PIPELINE_ABORTED
pour chaque opération restant
en queue dans un pipeline annulé. Le résultat pour
PQpipelineSync
ou
PQsendPipelineSync
est rapporté comme
PGRES_PIPELINE_SYNC
pour signaler la fin du pipeline
annulé et le retour au traitement normal des résultats.
Le client doit traiterles résultats avec
PQgetResult
lors de la récupération après erreur.
Si le pipeline utilisait une transaction implicite, alors les opérations
qui se sont déjà exécutées sont annulées et les opérations en queue
après l'opération annulée sont complètement ignorées. Le même
comportement reste si le pipeline commence et valide une seule
transaction explicite (la première requête est BEGIN
et la dernière est COMMIT
) sauf que la session reste
dans un état de transaction annulée à la fin du pipeline. Si un pipeline
contient plusieurs transactions explicites, toutes
les transactions validées avant l'erreur restent validées, la
transaction actuellement en cours est annulée, et toutes les opérations
suivantes sont complètement ignorées, ceci incluant les transactions
suivantes. Si unpoint de synchronisation du pipeline survient avec un
bloc de transaction explicite dans un état annulé, le pipeline suivant
deviendra immédiatement annulé sauf si la commande suivante place la
transaction en mode normal avec ROLLBACK
.
Le client ne doit pas supposer que le travail est validé quand il
envoie un COMMIT
--
seulment quand le résultat correspondant est reçu pour confirmer que la
validation est terminée. Comme des erreurs arrivent en asynchrone,
l'application a besoin d'être capable de recommencer depuis le dernier
changement validé reçu et renvoie le travail
effectué après ce point si quelque chose ne s'est pas bien passé.
Pour éviter des verrous sur des pipelines importants, le client devrait
être structuré autour d'une boucle d'événements non bloquante en
utilisant les possibilités du système d'exploitation, tel que
select
, poll
,
WaitForMultipleObjectEx
, etc.
L'application doit généralement maintenir une queue de travail restant à déployer et une queue de travail qui a déjà été déployée mais dont les résultats n'ont pas encore été traités. Quand le socket est accessible en écriture, il pourrait déployer plus de travail. Quand le socket est lisible, il pourrait lire les résultats et les traiter, les faisant correspondre à la prochaine entrée dans la queue de résultats correspondante. Suivant la mémoire disponible, les résultats provenant du socket doivent être lus fréquemment : il n'est pas nécessaire d'attendre la fin du pipeline pour lire les résultats. Les pipelines doivent être placés en unités logiquesde travail, habituellement (mais pas nécessairement) une transaction par pipeline. Il n'est pas besoin de quitter le mode pipeline et de ré-entrer dans ce mode entre les pipelines ou d'autre la fin d'un pipeline avant d'envoyer le suivant.
Un exemple utilisant select()
et une simple machine
à état pour trace le travail envoyé et reçu se trouve dans
src/test/modules/libpq_pipeline/libpq_pipeline.c
à
partir de la distribution des sources de PostgreSQL.
PQpipelineStatus
#Renvoie le statut actuel du mode pipeline de la connexion libpq.
PGpipelineStatus PQpipelineStatus(const PGconn *conn);
PQpipelineStatus
peut renvoyer une des valeurs
suivantes :
PQ_PIPELINE_ON
La connexion libpq est en mode pipeline.
PQ_PIPELINE_OFF
La connexion libpq n'est pas en mode pipeline.
PQ_PIPELINE_ABORTED
La connexion libpq est en mode pipeline
et une erreur est survenue lors du traitement du pipeline actuel.
Le drapeau d'annulation est effacé quand PQgetResult
renvoie un résultat de type PGRES_PIPELINE_SYNC
.
PQenterPipelineMode
#Cause l'entrée en mode pipeline de la connexion si elle est actuellement inactive ou déjà en mode pipeline.
int PQenterPipelineMode(PGconn *conn);
Renvoie 1 en cas de succès. Renvoie 0 et n'a pas d'effet si la connection est actuellement active, par exemple elle a un résultat prêt ou elle est en attente d'entrée du serveur, etc. Cette fonction n'envoie rien au serveur, elle modifie juste l'état de la connexion libpq.
PQexitPipelineMode
#Cause la sortie du mode pipeline pour la connexion si elle est en mode pipeline avec une queue vide et aucun résultat en attente.
int PQexitPipelineMode(PGconn *conn);
Renvoie 1 en cas de succès. Renvoie 1 et ne fait aucune action si la
connexion n'est pas en mode pipeline. Si l'instruction en cours n'a
pas terminé son traitement ou si PQgetResult
n'a pas été appelé pour récupérer les résultats de toutes les
requêtes envoyées précédemment, renvoie 0 (auquel cas, utilisez
PQerrorMessage
pour obtenir plus d'informations
sur l'échec).
PQpipelineSync
#Marque un point de synchronisation dans un pipeline en envoyant un message sync et en vidant le tampon d'envoi. Ceci sert comme délimiteur d'une transaction implicite et d'un point de restauration après erreur ;voir Section 32.5.1.3.
int PQpipelineSync(PGconn *conn);
Renvoie 1 en cas de succès. Renvoie 0 si la connexion n'est pas dans le mode pipeline ou est en cours d'envoi d'un message sync en échec.
PQsendPipelineSync
#Marque un point de synchronisation dans un pipeline en envoyant un message sync sans vider le tampon d'envoi. Cela sert comme délimiteur d'une transaction implicite et comme point de récupération d'erreur ; voir Section 32.5.1.3.
int PQsendPipelineSync(PGconn *conn);
Renvoie 1 en cas de succès. Renvoie 0 si la connexion n'est pas en mode
pipeline ou si l'envoi du message sync a échoué. Notez que
le message n'est pas lui-même vidé automatiquement du serveur ;
utilisez PQflush
si nécessaire.
PQsendFlushRequest
#Envoie une requête au serveur pour vider son tampon en sortie.
int PQsendFlushRequest(PGconn *conn);
Renvoie 1 en cas de succès. Renvoie 0 en cas d'échec.
Le serveur vide son tampon en sortie automatiquement comme conséquence
de l'appel à PQpipelineSync
ou pour toute requête
quand la connexion n'utilise pas le mode pipeline ; cette fonction
est utile pour forcer le serveur à vider son tampon en sortie dans le
mode pipeline sans établir de point de synchronisation. Notez que la
requête n'est pas elle-même vidée du serveur automatiquement ;
utilisez PQflush
si nécessaire.
Tout comme le mode de requête asynchrone, il n'y a pas de surcharge significative pour les performances lors de l'utilisation du mode pipeline. Il augmente la complexité de l'application cliente, et une grande attention est requise pour éviter les deadlocks client/serveur, mais le mode pipeline peut offrir des améliorations considérables des performances, en échange d'une utilisation mémoire accrue.
Le mode pipeline est plus utile quand le serveur est distant, autrement dit quand la latence réseau (« durée du ping ») est importante, et aussi quand plusieurs petites opérations sont en cours d'exécution en succession rapide. Il y a moins d'intérêt à utiliser les commandes en pipeline quand chaque requête prend plusieurs fois le temps des aller/retours client/serveur pour s'exécuter. Une exécution d'une opération de 100 instructions sur un serveur avec des aller/retour à 300 ms va prendre 30 secondes de latence réseau sans pipeline. Avec le pipeline, celle pourrait prendre 0,3 seconde à attendre les résultats du serveur.
Utilisez les commandes de pipeline quand votre application fait plein de
petites opérations INSERT
, UPDATE
et
DELETE
qui ne peuvent pas facilement être transformées
en opérations sur des ensembles ou en une opération COPY
.
Le mode pipeline n'est pas utile quand l'information d'une opération est requise par le client pour produire la prochaine opération. Dans de tels cas, le client devra introduire un point de synchronisation et attendre un aller/retour complet client/serveur pour obtenir le résultat nécessaire. Néanmoins, il est souvent possible d'ajuster la conception du client pour échanger les informations requises côté serveur. Les cycles lecture/modification/écriture sont tout spécialement de bon candidat ; par exemple :
BEGIN; SELECT x FROM mytable WHERE id = 42 FOR UPDATE; -- result: x=2 -- client adds 1 to x: UPDATE mytable SET x = 3 WHERE id = 42; COMMIT;
pourrait être réalisé plus efficacement avec :
UPDATE mytable SET x = x + 1 WHERE id = 42;
Envoyer dans un pipeline est moins utile et plus complexe quand un seul pipeline contient plusieurs transactions (voir Section 32.5.1.3).
[15] Le client bloquera les tentatives d'envoyer des requêtes au serveur, mais le serveur bloquera en essayant d'envoyer les résultats au client des requêtes qu'il a déjà traité. Ceci survient seulement quand le client envoie suffisamment de requêtes pour remplir à la fois son tampon en sortie et le tampon de réception du serveur avant qu'il bascule pour traiter les entrées du serveur, mais il est difficile de prédire exactement quand cela arrivera.