Written by: Helio Loureiro
Category: Python
Hits: 1877

Foto do aerporto GRU no dia em que emigrei do Brasil.

Um dos trabalhos que faço como voluntário é manter alguns serviços "alternativos" na empresa.  Todos baseados em software livre.

Dos que são mantidos temos um mediawiki, um encurtador yourls e um etherpad-lite.   E esse último foi o que precisei mexer pra transferir pra um servidor novo.

Muitas pessoas gostam do etherpad-lite e o usam, mas devo dizer que por trás é um lixo.  Serviço porco.  Ele usa uma só tabela no MySQL/MariaDB com dois campos:

mysql> show tables;
+-----------------+
| Tables_in_paddb |
+-----------------+
| store           |
+-----------------+
1 row in set (0.00 sec)

mysql> desc store;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| key   | varchar(100) | NO   | PRI |         |       |
| value | longtext     | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

Sério.  2 campos.  E só.  Um é uma chave toscamente preparada pra ser chave primária e o resto... é valor.  Então o uso do DB só cresce, sem chances de uma manutenção decente.

Enquanto o uso do etherpad-lite é um dor nas costelas, o assunto é mais da migração dos dados.  Então continuando o assunto, o nosso DB chegou ao incrível valor de 13 GB.   Daí como faz a migração?  O básico é tirar um dump do DB antigo com mysqldump e carregar usando o comand mysql mesmo.

Algo como isso:

# mysql --host=remote-server.mysql.internal.com --port=1234 --user=sqluser --password=sqlpassword mydb < etherpad-migration-backup.sql

 que pra todos efeitos funciona.  O único problema foi que depois de passar 15 horas carregando o arquivo...

ERROR 2013 (HY000) at line 19057418: Lost connection to MySQL server during query

Dizem que não tem dor maior que a dor do parto.  Tem sim e chama-se carregar um dump de 13 GB por 15 horas e falhar.  Assim.

E o que restou fazer.  Bom... eu sabia a linha onde estava o arquivo, mas já tinham sido 15 horas num arquivo serial, que faz linha por linha.   Então decidi quebrar o dump em vários arquivos menores.  Dei um rápido "wc -l" no dump e vi que tinham exatamente 28993313 linhas.  Então era possível quebrar em 28 arquivos de 1 milhão de linhas cada.  E foi o que fiz.

Assim eu sabia que podia continuar do arquivo 20 em diante.  E depois resolvia como fazer com o que faltava.

# split -l 1000000 -d etherpad-migration-backup.sql etherpad-migration-backup.sql.
# ls -1 pad-migration-backup.sql.??
etherpad-migration-backup.sql.00
etherpad-migration-backup.sql.01
etherpad-migration-backup.sql.02
etherpad-migration-backup.sql.03
etherpad-migration-backup.sql.04
etherpad-migration-backup.sql.05
etherpad-migration-backup.sql.06
etherpad-migration-backup.sql.07
etherpad-migration-backup.sql.08
etherpad-migration-backup.sql.09
etherpad-migration-backup.sql.10
etherpad-migration-backup.sql.11
etherpad-migration-backup.sql.12
etherpad-migration-backup.sql.13
etherpad-migration-backup.sql.14
etherpad-migration-backup.sql.15
etherpad-migration-backup.sql.16
etherpad-migration-backup.sql.17
etherpad-migration-backup.sql.18
etherpad-migration-backup.sql.19
etherpad-migration-backup.sql.20
etherpad-migration-backup.sql.21
etherpad-migration-backup.sql.22
etherpad-migration-backup.sql.23
etherpad-migration-backup.sql.24
etherpad-migration-backup.sql.25
etherpad-migration-backup.sql.26
etherpad-migration-backup.sql.27
etherpad-migration-backup.sql.28
etherpad-migration-backup.sql.29

Com isso eu tive vários arquivos que eu podia subir em paralelo.  E foi o que fiz.  O resultado?  Não só um mas vários erros depois de algumas horas carregando.   Eu queria chorar.  No chuveiro.  Em posição fetal.  Só isso.

O maldito do comando mysql não te permite dar um replay descartando o que já existisse no DB, o que seria uma mão na roda nessas situações.  Então fiz isso com python.  Mas achei que seria lento demais manter serializado.  Então era um bom momento pra testar o asyncio, que usei pouquíssimo até hoje.  E valeu muito a pena.  Esse é o script final:

#! /usr/bin/python3

import sys
import pymysql.cursors
import asyncio

connection = pymysql.connect(host="remote-server.mysql.internal.com",
        port=1234,
        user="sqluser",
        password="sqlpassword",
        db="mydb",
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor)

cursor = connection.cursor()
sema = asyncio.Semaphore(value=10)

async def commit_line(line):
    await sema.acquire()
    print(line)
    try:
        cursor.execute(line)
        connection.commit()
    except:
        print("Line (", line[:10],") already inserted")
        pass
    sema.release()


with open(sys.argv[1]) as sqlfile:
    loop = asyncio.get_event_loop()
    for line in sqlfile.readlines():
        loop.run_until_complete( commit_line(line) )
    loop.close()

Não está dos mais polidos, e com senha dentro, mas era uma coisa rápida pra resolver meu problema.  E resolveu.

Eu criei uma fila de 10 processos em paralelo pra rodar com: sema = asyncio.Semaphore(value=10)

o controle de acesso ao processo pra rodar é feito com sema.acquire() e sema.release().  Muito fácil.  Nem precisei criar um objeto Queue.

Dentro do loop do commit_line() eu sabugue um "enfia essa linha lá ou então continua".  Simples assim.  E funcionou.

Eu já tinha deixado o tmux aberto com várias janelas, uma pra cada arquivo, então foi só rodar o mesmo em cada uma que falhou.

Levou mais umas 2 ou 3 horas mas carregou tudo.

Foi lindo, não foi?