Expérience de migration de mailman2 à mailman3

J’ai entamé un test d’import des 200Go d’archives et je suis tombé sur un bug qui fait échouer l’import de tout le fichier mbox au lieu de sauter le courriel qui échoue. Un patch a été fait localement et proposé à hyperkitty.

index ff00392..314986e 100644
--- a/hyperkitty/management/commands/hyperkitty_import.py
+++ b/hyperkitty/management/commands/hyperkitty_import.py
@@ -152,8 +152,13 @@ class DbImporter(object):
         for msg in mbox:
             # FIXME: this converts mailbox.mboxMessage to
             # email.message.EmailMessage
-            msg_raw = msg.as_bytes(unixfrom=False)
-            unixfrom = msg.get_from()
+            try:
+                msg_raw = msg.as_bytes(unixfrom=False)
+                unixfrom = msg.get_from()
+            except e:
+                self.stderr.write('Failed to read {} because {}'.format(
+                                   unquote(msg["Message-Id"]), e))
+                continue
             try:
                 message = message_from_bytes(msg_raw, policy=policy.default)

Et encore une autre erreur du même style.

diff --git a/hyperkitty/management/commands/hyperkitty_import.py b/hyperkitty/management/commands/hyperkitty_import.py
index ff00392..29d42d2 100644
--- a/hyperkitty/management/commands/hyperkitty_import.py
+++ b/hyperkitty/management/commands/hyperkitty_import.py
@@ -152,11 +152,16 @@ class DbImporter(object):
         for msg in mbox:
             # FIXME: this converts mailbox.mboxMessage to
             # email.message.EmailMessage
             msg_raw = msg.as_bytes(unixfrom=False)
             unixfrom = msg.get_from()
             try:
                 message = message_from_bytes(msg_raw, policy=policy.default)
-            except UnicodeError as e:
+            except e:
                 self.stderr.write('Failed to convert {} to '
                                   'email.message.Message\n    {}'.format(
                                    unquote(msg["Message-Id"]), e))

Et de façon générale il est suggéré que l’import soit plus robuste qu’il ne l’est actuellement, sans pour autant proposer une solution.

Deux autres erreur d’import ont été révélées (1 et 2). Ce sont des cas exceptionnels et les courriels qui posent problème pourraient être ignorés. Mais comme cela empêche les courriels suivant d’être importés, c’est assez gênant.
Un script est maintenant disponible pour vérifier qu’une mbox peut être lue sans erreur. Mais cela ne couvre pas tous les cas d’échec possible (en particulier celui ci). Il faudrait probablement une option –dry-run à hyperkitty_import.
A défaut d’un correctif upstream, un hack qui consiste à faire un catch de toutes les exceptions et sauter le message fautif est pour l’instant la seule solution.

Petit résumé / synthèse des épisodes précédents parce que sinon je m’y perd avec tout ces rebondissements :stuck_out_tongue:

Installation mailman3

Il n’est pas nécessaire d’installer mailman2 pour faire la migration, il suffit d’avoir les fichiers config.pck qui définissent une liste et les fichiers *.mbox qui contiennent les archives de la liste.

Sur une Debian GNU/Linux buster sur laquelle un serveur SMTP écoute sur le port 25 sans authentification et un reverse proxy nginx qui porte un certificat SSL, l’installation se fait de cette façon:

$ apt-get install mailman3-web # et répondre ok a toutes les questions
$ emacs /etc/mailman3/nginx.conf # pour changer :80 en :8000 et configurer le reverse_proxy pour le diriger vers :8000
$ ln -s /etc/mailman3/nginx.conf /etc/nginx/sites-enabled
$ systemctl restart nginx
$ emacs /etc/mailman3/mailman-web.py # changer la valeur de EMAILNAME
$ systemctl restart mailman3-web
$ django-admin createsuperuser --pythonpath /usr/share/mailman3-web --settings settings --username admin --email loic@dachary.org # se logger, relever le courriel de confirmation et cliquer sur l'URL pour valider, puis se logger

Créer une liste dans mailman3 identique à celle de mailman2

Il faut d’abord prendre les scripts d’import les plus récents, ils vont fonctionner avec une version plus ancienne de mailman.

$ wget -O /usr/lib/python3/dist-packages/mailman/commands/cli_import.py https://gitlab.com/mailman/mailman/-/raw/master/src/mailman/commands/cli_import.py
$ wget -O /usr/lib/python3/dist-packages/mailman/utilities/importer.py https://gitlab.com/mailman/mailman/-/raw/master/src/mailman/utilities/importer.py

Pour créer la liste, avec /tmp/config.pck qui provient de mailman2:

$ mailman create -N spip-dev@mailman.the.re
$ mailman import21 --charset iso8859-1 spip-dev@mailman.the.re /tmp/config.pck

Upgrade hyperkitty 1.3.2

La version de Debian GNU/Linux buster est 1.2.2 il faut donc mettre à jour avec le package qui est dans testing. A cause des dépendances il faut aussi upgrade mailman3-web de la version 0+20180916-8 à 0+20180916-10.

La version 1.3.1 permet de supprimer une archive.

Eviter MySQL server has gone away

Suivre les instructions de cette solution pour éviter un occassionel MySQL server has gone away qui fait échouer l’import d’un message.

Editer /etc/mailman3/mailman-web.py pour y ajouter:

DATABASES = {
    'default': {
...
        'CONN_MAX_AGE': 3600,

Préciser le charset des chaînes MySQL

Lorsque la base de donnée est MySQL, l’import peut échouer parce que le charset est incorrect. Editer /etc/mailman3/mailman-web.py comme expliqué ici pour y ajouter:

'OPTIONS': {'charset': 'utf8mb4'}

Importer dans mailman3 les archives mailman2

Le script d’import est fragile et il faut prendre la toute dernière version pour maximiser les chances qu’il réussisse. Malgré cela il est possible que l’import d’un fichier mbox soit interrompu à cause d’un courriel défecteux. La seule solution est d’enlever manuellement le courriel défectueux et de recommencer, ce qui peut être fastidieux.

$ wget -O /usr/lib/python3/dist-packages/hyperkitty/management/commands/hyperkitty_import.py https://gitlab.com/mailman/hyperkitty/-/raw/master/hyperkitty/management/commands/hyperkitty_import.py
$ cd /usr/share/mailman3-web/
$ python3 manage.py hyperkitty_import --list-address spip-dev@mailman.the.re --since 1980-01-01 --ignore-mtime --no-sync-mailman /home/debian/spip-dev.mbox/*.mbox

Corriger le contenu d’un fichier mbox

Lorsque hyperkitty_import échoue il peut être difficile de déterminer quel est le courriel fautif lorsque l’identifiant du message n’est pas affiché avec le message d’erreur. Le script check_hk_import peut aider a l’identifier.

$ wget https://gitlab.com/mailman/hyperkitty/-/raw/master/hyperkitty/contrib/check_hk_import
$ python3 check_hk_import 200207.mbox 

Indexation plein texte dans mailman3

Pour un volume de plus de 200Mo, il faut utiliser autre chose que l’indexeur par défaut Woosh qui n’est plus maintenu depuis 2016. Sinon les temps d’indexation sont de plusieurs jours pour 2GB de texte, la recherche prend 30 secondes et la RAM nécessaire monte jusqu’à 6GB résident.

Xapian est un backend haystack qui peut être spécifié comme alternative dans le fichier /etc/mailman3/mailman-web.py

HAYSTACK_CONNECTIONS = {
    'default': {
       'ENGINE': 'xapian_backend.XapianEngine',
       'PATH': '/var/lib/mailman3/web/fulltext_index',
    },
}

et installé sur une Debian GNU/Linux buster à partir du package venant de testing en attendant le backport qui corrige un bug bloquant.

$ echo deb http://debian.fr/debian testing main > /etc/apt/sources.list.d/mailman.list
$ apt-get update 
$ apt-get install python3-xapian-haystack

et on peut ensuite indexer avec

$ python3 manage.py update_index_one_list -v 2 spip-dev@mailman.the.re

Controle du nombre de workers django-q / qcluster

Reprendre la valeur de Q_CLUSTER depuis /usr/share/mailman3-web/settings.py et la copier dans /etc/mailman3/mailman-web.py en y ajoutant explicitement le nombre de workers:

Q_CLUSTER = {
    'workers': 5,
    'timeout': 300,
    'save_limit': 100,
    'orm': 'default',
    'poll': 5,
}
1 « J'aime »

En important des listes sur une base MySQL (alors que les essais précédents étaient sur SQLite), cela révèle un problème lorsque la description est plus longue que 255 caractères. Cela mérite un hack et un bug report.

--- importer.py.new     2020-09-25 11:13:18.605591026 +0200
+++ importer.py 2020-09-26 10:42:37.896174302 +0200
@@ -284,6 +284,9 @@
         if key == 'preferred_language' or hasattr(mlist, key):
             if isinstance(value, bytes):
                 value = bytes_to_str(value)
+            if type(value) == str and len(value) > 255:
+                print(f'{key} truncated to the first 255 characters of {value}')
+                value = value[:255]
             # Some types require conversion.
             converter = TYPES.get(key)
             if converter is None:
sqlalchemy.exc.DataError: (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely) (pymysql.err.DataError) (1406, "Data too long for column 'info' at row 1") [SQL: 'UPDATE mailinglist SET created_at=%(created_at)s, next_digest_number=%(next_digest_number)s, digest_last_sent_at=%(digest_last_sent_at)s, volume=%(volume)s, last_post_at=%(last_post_at)s, convert_html_to_plaintext=%(convert_html_to_plaintext)s, forward_unrecognized_bounces_to=%(forward_unrecognized_bounces_to)s, dmarc_mitigate_action=%(dmarc_mitigate_action)s, description=%(description)s, discard_these_nonmembers=%(discard_these_nonmembers)s, first_strip_reply_to=%(first_strip_reply_to)s, info=%(info)s, moderator_password=%(moderator_password)s, preferred_language=%(preferred_language)s, respond_to_post_requests=%(respond_to_post_requests)s, subject_prefix=%(subject_prefix)s WHERE mailinglist.id = %(mailinglist_id)s'] [parameters:
{'created_at': datetime.datetime(2001, 12, 22, 23, 0, 52, 891113), 'next_digest_number': 21, 'digest_last_sent_at': datetime.datetime(2020, 9, 20, 9, 23, 21, 644508), 'volume': 487, 'last_post_at': datetime.datetime(2020, 9, 20, 9, 23, 21, 750535), 'convert_html_to_plaintext': 1, 'forward_unrecognized_bounces_to': 0, 'dmarc_mitigate_action': 1, 'description': 'SPIP : developpement', 'discard_these_nonmembers': b'\x80\x04\x95MP\x00\x00\x00\x00\x00\x00]\x94(\x8c\x18reklama@e-dystrybucja.pl\x94\x8c\x19andre.lefranc@laposte.net\x94\x8c\x1bngouelle@ile-des-medias ... (26458 characters truncated) ... spros.fr\x94\x8c\x1dno.reply@segmentbeast.monster\x94\x8c\x16selvam@madhuoffset.com\x94\x8c\x0ealm@gretopi.re\x94\x8c\x16andreaswang163@163.com\x94e.', 'first_strip_reply_to': 1, 'info': "<b>Attention :</b> cette liste est destinée à discuter de la programmation de SPIP et ses plugins. Le fait que votre problème soit compliqué et impli ... (484 characters truncated) ...  de gestion de
bugs</a>. Ainsi votre intervention sera conservée précieusement plutôt que d\x92être noyée dans un flot de messages divers et variés. ", 'moderator_password': b'36675febd7252eef1c20fe7b6e5f438552115177', 'preferred_language': 'fr', 'respond_to_post_requests': 0, 'subject_prefix': '[spip-dev] ', 'mailinglist_id': 3}] (Background on this error at: http://sqlalche.me/e/9h9h)

Update 24h plus tard: le problème a été corrigé.

J’arrive au bout de l’expérience de migration dont l’essentiel est décrit ici. Maintenant il n’y a-plus-qu’à le faire en vrai ce qui présente aussi quelques difficultés mais c’est hors sujet :wink:

2 « J'aime »

En cas d’erreur lors de l’import dans hyperkitty, il n’est pas possible de supprimer une archive avant la version 1.3.1. Dans Debian GNU/Linux buster la version 1.2.2 est packagée et dans testing c’est la version 1.3.2. A cause des dépendances il faut aussi upgrade mailman3-web de la version 0+20180916-8 à 0+20180916-10 ce qui devrait être ok

L’import des mbox d’une liste pour un total de de 500MB tourne depuis plus de 24h. Au vu de htop ci dessous, je suis tenté de conclure que la machine physique sur laquelle tourne l’import a des problèmes de performances. La CPU et la RAM ne sont pas saturées et les I/O ne totalisent pas plus de 2MB/s. Il y a beaucoup de soft irq (barre grise) ce qui me fait penser que c’est peut être simplement que le disque est saturé.

htop


Update l’import s’est terminé au bout de 25h avec une consomation CPU équivalente à 2h donc effectivement gros soucis d’I/O.

real    1551m16,755s                                                                                                                                                   
user    131m17,600s                                                                                                                                                    
sys     3m46,688s           
1 « J'aime »

Dans un container LXC, je constate un nombre excessif de workers qcluster (69). J’imagine que ça vient de la façon dont la valeur par défaut est calculée: elle doit prendre en compte le nombre de core disponibles sur le host au lieu de prendre le résultat de nproc ou un truc du genre.

Pour contourner le problème j’ai pris la valeur de Q_CLUSTER depuis /usr/share/mailman3-web/settings.py et je l’ai copiée dans /etc/mailman3/mailman-web.py en y ajoutant explicitement le nombre de workers:

Q_CLUSTER = {
    'workers': 5,
    'timeout': 300,
    'save_limit': 100,
    'orm': 'default',
    'poll': 5,
}

En attendant le correctif pour les fantomes de listes j’ai fait le hack suivant dans /usr/lib/python3/dist-packages/hyperkitty/tasks.py:

def _rebuild_mailinglist_cache_recent(mlist_name):
    try:
        mlist = MailingList.objects.get(name=mlist_name)
        for cached_value in mlist.recent_cached_values:
            cached_value.rebuild()
    except Exception as e:
        log.error(f'_rebuild_mailinglist_cache_recent {mlist_name} {e}')

def _rebuild_mailinglist_cache_for_month(mlist_name, year, month):
    try:
        mlist = MailingList.objects.get(name=mlist_name)
        mlist.cached_values["participants_count_for_month"].rebuild(year, month)
    except Exception as e:
        log.error(f'_rebuild_mailinglist_cache_for_month {mlist_name} {e}')

Pour archive la migration est presque fonctionelle mais reste suspendue à la correction de ce bug principalement et d’un backport dans Debian GNU/Linux buster qui n’oblige pas à appliquer les patches decrits dans la procédure d’import pour refaire l’import de zéro.

@Duck m’a indiqué que /var/lib/mailman3/data/postfix_lmtp était créé:

  • soit à l’aide de la commande mailman3 aliases exécutée en tant qu’utilisateur list - le packaging debian fournit la commande mailman-wrapper afin de faciliter l’exécution de la commande.
  • soit à la création d’une liste (car la création d’une liste régénère les aliases)
1 « J'aime »