Expérience de migration de mailman2 à mailman3

L’encodage des listes mailman2 est iso8859-1. Cependant le script d’import suppose que c’est utf-8.

Traceback (most recent call last):
  File "/usr/bin/mailman", line 11, in <module>
    load_entry_point('mailman==3.2.1', 'console_scripts', 'mailman')()
  File "/usr/lib/python3/dist-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python3/dist-packages/mailman/bin/mailman.py", line 69, in invoke
    return super().invoke(ctx)
  File "/usr/lib/python3/dist-packages/click/core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python3/dist-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python3/dist-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/click/decorators.py", line 17, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/usr/lib/python3/dist-packages/mailman/commands/cli_import.py", line 71, in import21                                                                                    
    pickle_file, encoding='utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 55: invalid continuation byte                                                                           `

et ignore les erreurs d’encodage: les accents disparaissent donc. Ca mérite un hack:

diff --git a/cli_import.py.orig b/cli_import.py
index 2d694a3..154bd98 100644
--- a/cli_import.py.orig
+++ b/cli_import.py
@@ -68,7 +68,7 @@ def import21(ctx, listspec, pickle_file):
         while True:
             try:
                 config_dict = pickle.load(
-                    pickle_file, encoding='utf-8', errors='ignore')
+                    pickle_file, encoding='iso8859-1')
             except EOFError:
                 break
             except pickle.UnpicklingError:

et un bug report.

merci @dachary parce que ça me pend aussi au nez cette migration, je suis donc ton post avec attention

1 « J'aime »

Après l’import réussit d’une liste dont les archives (fichiers .mbox) font environ 200Mo, essentiellement du texte (environ 70,000 messages), les archives occupent environ 600Mo

# du -sh /var/lib/mailman3/web/mailman3web.db /var/lib/mailman3/web/fulltext_index/
173M    /var/lib/mailman3/web/mailman3web.db
470M    /var/lib/mailman3/web/fulltext_index/

Il semble qu’il faille compter au pire quatre fois la taille d’une mbox après injection dans mailman3, ce qui semble un peu beaucoup. Par ailleurs je ne vois pas ou il stocke les attachements. Dans le cas de ce test c’est sqlite qui est utilisé mais je doute que la taille des données soit significativement réduite si c’est MySQL ou postgresQL.

La base de donnée de mailman dans laquelle ont été insérées ~400 listes de diffusion fait ~50Mo, c’est négligeable (liste des abonnés, privé-public etc.).

# ls -lh /var/lib/mailman3/data/mailman.db 
-rw-rw---- 1 list list 53M Sep 21 22:38 /var/lib/mailman3/data/mailman.db

Une option a été ajoutée aujourd’hui et on peut maintenant écrire:

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

L’indexation texte intégral d’une liste a consommé trop de mémoire, probablement plus de 2GB sur une machine qui en a 4 au total.

# time python3 manage.py update_index_one_list ?????@mailman.the.re                                                                                                                                         
Indexing 32224 emails                                                                                                                     
MemoryError while preparing object for update                                                                                             
Traceback (most recent call last):                                                                                                        
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/haystack/backends/whoosh_backend.py", line 283, in update                
    writer.update_document(**doc)                                                                                                         
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/writing.py", line 1024, in update_document                        
    self._record("update_document", args, kwargs)                                                                                         
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/writing.py", line 1001, in _record                                
    getattr(self.writer, method)(*args, **kwargs)                                                                                         
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/writing.py", line 483, in update_document                         
    with self.searcher() as s:                                                                                                            
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/writing.py", line 297, in searcher                                
    return Searcher(self.reader(), **kwargs)                                                                                              
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/writing.py", line 639, in reader                                  
    self.generation, reuse=reuse)                                                                                                         
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/index.py", line 535, in _reader                                   
    readers = [segreader(segment) for segment in segments]                                                                                
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/index.py", line 535, in <listcomp>                                
    readers = [segreader(segment) for segment in segments]                                                                                
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/index.py", line 524, in segreader                                 
    generation=generation)                                                                                                                
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/reading.py", line 620, in __init__                                
    self._terms = self._codec.terms_reader(self._storage, segment)                                                                        
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/codec/whoosh3.py", line 120, in terms_reader                      
    tifile = storage.open_file(tiname)                                                                                                    
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/filedb/filestore.py", line 333, in open_file                      
    return self.a.open_file(name, *args, **kwargs)                                                                                        
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/filedb/compound.py", line 121, in open_file                       
    f = BufferFile(buf, name=name)                                                                                                        
  File "/home/debian/venv-hyperkitty/lib/python3.7/site-packages/whoosh/filedb/structfile.py", line 357, in __init__                      
    self.file = BytesIO(buf)                                                                                                              
MemoryError                                                                                          

J’augmente la mémoire pour avoir 8Go et on va voir si ça passe :crossed_fingers:

Et en effet, après avoir relancé le process d’indexation sur une machine avec plus de mémoire, on voit rapidement que ça monte à 3.1Go.

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                              
1246 www-data  20   0 3965716   3,1g   1,5g R  97,0  41,0   1:07.00 python3 /usr/share/mailman3-web/manage.py runjobs hourly     

et ça fluctue, je suppose que ça dépend de la taille du message indexé à un moment. S’il contient de gros attachements, ą peut monter fort. Quelques minutes plus tard le process retombe à 1.3Go.

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                              
216 root      20   0 1686936   1,3g 606016 R  99,0  17,4   8:48.91 python3                                                              

La mise à jour de l’index s’est terminée sans erreur.

(venv-hyperkitty)  (eǝ) root@mailman-host:/usr/share/mailman3-web# time python3 manage.py update_index_one_list ????@mailman.the.re
Indexing 32224 emails

real    260m40,567s
user    134m33,045s
sys     120m35,858s

L’index occupe environ 1.5Go ce qui semble cohérent avec le fait que la liste contient des attachements volumineux qui ne sont pas indexés.

Je remarque que la taille de /var/lib/mailman3/web/mailmanweb.db (9Go) est inférieure à celle des mbox décompressées de la liste qui vient d’être importée (11Go). Ce qui veut dire qu’il y a de la compression quelque part ou bien que certains messages et/ou attachements ont été ignorés.

Quand on fait une recherche dans les archives (un mot) dont le volume est

(venv-hyperkitty)  (eǝ) root@mailman-host:/usr/share/mailman3-web# du -sh /var/lib/mailman3/web/*
1,5G    /var/lib/mailman3/web/fulltext_index
8,2G    /var/lib/mailman3/web/mailman3web.db
7,5M    /var/lib/mailman3/web/static

L’occupation mémoire du process qui fait la recherche est importante (~4.5Go).

PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                              
1020 www-data  20   0 5697352   4,4g   2,0g R  75,7  57,9   0:48.92 /usr/bin/uwsgi --plugin python3 --ini /etc/mailman3/uwsgi.ini        

Aucune idée si c’est proportionel à la taile de l’index text intégral (1.5Go) ou si c’est aussi fonction du nombre de messages que la liste contient. Ca n’augmente pas linéairement avec le nombre de résultats.

Les attachements sont décodés par hyperkitty via django-mailman3 et stockés dans des blobs binaires et prennent donc moins de place que leur équivalent encodé.

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 »