<!--
Les sources pour ce sujet sont :

Conférence de Tomas Vondra : https://www.youtube.com/watch?v=iCmUhS9XYI0
Slides : https://www.pgevents.ca/events/pgconfdev2025/sessions/session/311/slides/61/fast-path-locking-pgconf-dev-2025.pdf

Discussion sur pgsql-hackers : https://www.postgresql.org/message-id/510b887e-c0ce-4a0c-a17a-2c6abb8d9a5c@enterprisedb.com

Commit : https://git.postgresql.org/pg/commitdiff/c4d5cb71d229095a39fda1121a75ee40e6069a2a

-->

<div class="slide-content">

  * Le mécanisme _fast-path locking_ est amélioré
  * Plus de verrous peuvent être posés via ce mécanisme
  * Configurable via `max_locks_per_transaction` (64 par défaut)
  * Limite dure à 16 384 verrous (contre 16 auparavant)
  * Profite aux _workloads_ de type OLTP avec tables partitionnées

</div>

<div class="notes">

Il est sans doute nécessaire, dans un premier temps, de décrire ce mécanisme relativement peu connu.
Dans ce chapitre, sauf mention explicite, nous parlerons uniquement des verrous au niveau « table »
décrits dans cette partie de la
[documentation](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES).
Ceux-ci concernent les tables ainsi que leurs index éventuels.

Le mécanisme _fast-path locking_ a été ajouté pour la version 9.2 de PostgreSQL, par Robert Haas,
et n'a quasiment pas bougé jusqu'en version 17 incluse. Il permet d'éviter des contentions liées
à l'accès d'une table de hachage en mémoire partagée, qui contient tous les verrous de tous les
_backends_ jusqu'en version 9.1. Celle-ci sera nommée « table des verrous principale » dans la
suite de ce chapitre. À partir de la version 9.2, certains verrous peuvent être enregistrés
dans la structure PGPROC de chaque _backend_ (qui réside en mémoire partagée et permet de régir
les interactions inter-processus en partageant des informations de verrouillage, synchronisation,
etc.) plutôt que dans la table des verrous principale.
Les verrous éligibles sont les verrous « faibles » qui n'entrent pas en conflit avec des verrous du
même type (voir
[ce tableau](https://www.postgresql.org/docs/current/explicit-locking.html#TABLE-LOCK-COMPATIBILITY)) :
`ACCESS SHARE`, `ROW SHARE`, et `ROW EXCLUSIVE`.

Il est aussi possible de stocker un unique verrou de type `vxid` via ce mécanisme, ce qui est
intéressant dans la mesure où les transactions prennent toujours un verrou sur leur propre
_virtual transaction ID_, permettant ainsi à certaines actions d'attendre la fin de celles-ci ;
`CREATE INDEX CONCURRENTLY` utilise notamment cette technique pour attendre
la fin des transactions qui modifient la table, ou celles ayant un snapshot plus ancien,
selon les différentes étapes de la création.
<!-- voir
https://github.com/postgres/postgres/blob/f5a987c0e5f6bbf0cc0420228dc57e7aae4d7e8f/src/backend/commands/indexcmds.c#L1685
https://github.com/postgres/postgres/blob/f5a987c0e5f6bbf0cc0420228dc57e7aae4d7e8f/src/backend/commands/indexcmds.c#L4217
-->


\newpage
Dans l'exemple ci-dessous, le processus 485913 exécute la requête \newline
`CREATE INDEX CONCURRENTLY ON pgbench_accounts(bid);` \newline
mais est bloqué par le processus 485921, qui
a modifié la même table avec la requête \newline
`INSERT INTO pgbench_accounts(aid) VALUES (-1);` \newline
Sur l'avant-dernière ligne,
on voit que le premier a tenté de prendre un verrou de type `virtualxid` en mode `ShareLock`,
sans utiliser le mécanisme _fast-path_ (champ `fastpath`), sur la _virtual transaction_ 23/11 portée par le deuxième.
Il sera intéressant de revenir à cet exemple après avoir lu la suite de ce chapitre.

\tiny
```
bench [488266] # SELECT locktype, relation, virtualxid as vxid, virtualtransaction as vtrans, l.pid,
  mode, granted, fastpath, substring(query from 1 for 12) as "truncated query"
  FROM pg_locks l left join pg_stat_activity s on (l.pid = s.pid and s.query NOT LIKE '%pg_locks%')
  WHERE l.pid != pg_backend_pid();

   locktype    | relation | vxid  | vtrans |  pid   |           mode           | granted | fastpath | truncated query
---------------+----------+-------+--------+--------+--------------------------+---------+----------+-----------------
 virtualxid    |          | 22/41 | 22/41  | 485913 | ExclusiveLock            | t       | t        | CREATE INDEX
 relation      |    16445 |       | 23/11  | 485921 | RowExclusiveLock         | t       | t        | INSERT INTO
 relation      |    16445 |       | 22/41  | 485913 | ShareUpdateExclusiveLock | t       | f        | CREATE INDEX
 transactionid |          |       | 23/11  | 485921 | ExclusiveLock            | t       | f        | INSERT INTO
 virtualxid    |          | 23/11 | 22/41  | 485913 | ShareLock                | f       | f        | CREATE INDEX
 virtualxid    |          | 23/11 | 23/11  | 485921 | ExclusiveLock            | t       | f        | INSERT INTO
```
\normalsize

Même chose ci-dessous, mais cette fois-ci le processus 485921 porte une transaction _Repeatable Read_
qui a juste fait un simple `select 1`, et qui bloque à nouveau la création d'index dans sa phase finale.

\tiny
```
  locktype  | relation | vxid  | vtrans |  pid   |           mode           | granted | fastpath | truncated query
------------+----------+-------+--------+--------+--------------------------+---------+----------+-----------------
 virtualxid |          | 22/43 | 22/43  | 485913 | ExclusiveLock            | t       | t        | CREATE INDEX
 virtualxid |          | 23/12 | 23/12  | 485921 | ExclusiveLock            | t       | f        | select 1;
 virtualxid |          | 23/12 | 22/43  | 485913 | ShareLock                | f       | f        | CREATE INDEX
 relation   |    16445 |       | 22/43  | 485913 | ShareUpdateExclusiveLock | t       | f        | CREATE INDEX
```
\normalsize

\newpage

Le schéma ci-dessous représente les principales structures de données impliquées dans ce mécanisme.

![Principales structures de données](fastpath.png)

On peut se demander à juste titre quel est l'intérêt de stocker un verrou dans une structure de
données propre à un _backend_, étant donné que celui-ci est utile pour la synchronisation entre
les différents _backends_. Mais cette structure de données, comme indiquée plus haut, vit tout
de même en mémoire partagée, et est donc accessible aux autres _backends_. Ainsi, lorsqu'un autre
_backend_ veut poser un verrou « fort » (`SHARE` ou au-dessus), il va pouvoir aller vérifier le
PGPROC de tous les _backends_ existants, pour voir s'ils contiennent un verrou faible
sur la même _relation_ (index ou table). Si c'est le cas, alors le verrou faible est migré dans
la table des verrous principale. Dans tous les cas, un compteur est incrémenté en mémoire partagée
pour indiquer qu'un verrou fort est positionné pour la relation en question ; il existe un tableau
statique de 1024 entiers en mémoire partagée (`FastPathStrongRelationLocks` dans le schéma), pour
tous les verrous forts de toutes les relations
de toutes les bases de l'instance, chaque compteur étant donc utilisé pour possiblement plus d'un
verrou. Quand ce compteur est strictement supérieur à zéro, le mécanisme _fast-path_ n'est pas
disponible pour tous les couples `(base oid, relation oid)` dont le hachage correspond à son index
dans le tableau. Ce dernier est protégé par un _spin lock_, uniquement en écriture, et ne pose donc
pas de problèmes de contention dans le cas nominal du mécanisme _fast-path_.
Pour résumer, le mécanisme _fast-path_ est vraiment peu coûteux lorsqu'on a juste
besoin de prendre un verrou « faible », mais ajoute un certain _overhead_ lorsqu'il est nécessaire
de prendre un verrou « fort ». Typiquement, il y a toujours beaucoup plus de verrous faibles
que forts, et on y gagne donc beaucoup en moyenne.

Jusqu'en version 17, chaque _backend_ peut stocker un maximum de 16 verrous via le mécanisme
_fast-path_, mais cette limite est atteinte assez vite, il suffit d'une table avec 10 partitions
et un index (un par partition), et d'une requête qui ne puisse pas faire de _partition pruning_.
En pratique, on peut observer un effet de contention avec seulement 30 clients qui font des
requêtes en lecture sur une telle table partitionnée.

La version 18 autorise un bien plus grand nombre de verrous, configurable avec le paramètre
`max_locks_per_transaction` qui vaut 64 par défaut. On réutilise donc ici un paramètre déjà
existant, afin de ne pas en rajouter encore un à la longue liste des paramètres PostgreSQL.
Il est possible que cela change par la suite, car la taille de la table des verrous principale
dépend du produit `max_locks_per_transaction × max_connections`, ce qui peut commencer à être
non négligeable si on doit augmenter sensiblement `max_locks_per_transaction` et que
`max_connections` a une valeur relativement haute.
Afin de travailler au mieux avec le cache du CPU (principe de localité), on utilise toujours des
tableaux de 16 identifiants de _relation_ (32 bits chacun, 64 octets en tout, la taille d'un
_CPU cache line_) répartis en plusieurs groupes. Avec `max_locks_per_transaction` positionné à
48, on a donc trois groupes comme sur le schéma ci-dessous. Il suffit de calculer un hachage
de l'identifiant de _relation_ pour déterminer le groupe à utiliser, et de procéder ensuite
à une recherche linéaire dans le tableau, ce qui est efficace pour un petit tableau de 16 éléments.
Le nombre maximal de groupes autorisés dans le code est de 1024, soit 16 384 verrous au total, pour chaque backend.

PGPROC étant une structure, sa taille est fixée à la compilation.
Le tableau contenant les groupes d'oid est
lui dimensionné en fonction de `max_locks_per_transaction`, qui est défini au démarrage
de PostgreSQL. Il ne peut donc pas être stocké dans PGPROC.
Cela explique pourquoi dans le schéma ci-dessous, le tableau est une entité séparée de PGPROC.

![Changements en v18](fastpath_v18.png)

Notons enfin que ce patch a fait sauter un goulet d'étranglement,
mais son auteur, Tomas Vondra, en a identifié un
[autre](https://vondra.me/posts/tuning-the-glibc-allocator-for-postgres/)
juste derrière, lié à l'allocation mémoire.
Il a fallu le résoudre également
afin de pouvoir vraiment bénéficier de plus de performances pour les cas d'usage concernés.

</div>
