Mardi dernier, un client en support nous appelle à cause d'un problème d'espace disque. Il est en version 8.2. Il dispose de place libre sur d'autres partitions. Rien de plus simple. Il suffit de créer un tablespace sur une des partitions où il reste suffisamment d'espace et d'y déplacer quelques objets pour faire de la place sur le répertoire principal des données. Tout se passe bien : création du répertoire, ajout du tablespace (avec CREATE TABLESPACE), choix de la table à déplacer, et déplacement de la table (avec un ALTER TABLE). Malheureusement, l'opération prends du temps, beaucoup de temps. Et les performances du serveur ont commencé à en pâtir. Le client a voulu interrompre l'opération, quitte à la reprendre plus tard. Pas de soucis, un simple Ctrl-C doit suffire. Et bien non, Ctrl-C et pg_cancel_backend() nous indiquaient bien que la demande d'annulation avait été envoyée mais les opérations sur le disque continuaient et psql ne nous rendait pas la main. Nous avons donc été forcés d'attendre la fin de l'opération.
Pendant ce temps, j'ai commencé à tester sur mon portable et a fouillé dans le code. Et j'ai fini par me rendre compte que rien ne permettait une prise en compte du signal pendant la copie. Autrement dit, vous lancez un changement de tablespace pour la table X de 50 Go, vous essayez de l'annuler au tout début, il vous faudra quand même attendre la fin de la copie des 50 Go pour que votre demande d'annulation soit prise en compte. Très dommageable. Il faut corriger ça. Tout le code se trouve dans la fonction copy_relation_data du fichier src/backend/commands/tablecmds.c. Une boucle s'occupe de la copie :
for (blkno = 0; blkno < nblocks; blkno++)
{
smgrread(src, forkNum, blkno, buf);
/* XLOG stuff */
if (use_wal)
log_newpage(&dst->smgr_rnode, forkNum, blkno, page);
/*
* Now write the page. We say isTemp = true even if it's not a temp
* rel, because there's no need for smgr to schedule an fsync for this
* write; we'll do it ourselves below.
*/
smgrextend(dst, forkNum, blkno, buf, true);
}
Autrement dit, on lit chaque bloc du fichier source, que l'on copie dans le fichier destination. Une bête copie bloc par bloc, avec aucun moyen de l'interrompre.
De mes lectures dans pgsql-hackers, je me rappelais qu'il existe une fonction appelée CHECK_FOR_INTERRUPTS faisant exactement le travail dont j'avais besoin. J'ai donc uniquement ajouté un appel à cette fonction au début de la boucle, ce qui donne au final :
for (blkno = 0; blkno < nblocks; blkno++)
{
/* If we got a cancel signal during the copy of the data, quit */
CHECK_FOR_INTERRUPTS();
smgrread(src, forkNum, blkno, buf);
/* XLOG stuff */
if (use_wal)
log_newpage(&dst->smgr_rnode, forkNum, blkno, page);
/*
* Now write the page. We say isTemp = true even if it's not a temp
* rel, because there's no need for smgr to schedule an fsync for this
* write; we'll do it ourselves below.
*/
smgrextend(dst, forkNum, blkno, buf, true);
}
Une compilation et quelques tests après, je me suis aperçu que tout fonctionnait comme je le souhaitais. L'annulation se fait exactement au moment où je la demande.
J'ai corrigé aussi le déplacement d'une base de données. Le patch terminé, je l'ai envoyé sur pgsql-hackers pour qu'il puisse être testé, relu et enfin commité. Je l'ai même saisi sur le site commitfest. Robert Haas s'en est occupé et l'a finalement commité vendredi sur toutes les versions, de la 8.0 (version à laquelle les tablespaces ont été ajoutées) à la future 9.0.