Não tem sido muito fácil manter o site atualizado com informações semanalmente como eu planejava, mas eu já esperava por isso. Ao menos tenho escrito com mais frequência que antes.
Um dos motivos é que tenho participados de organização de hackathons (como descrevi uma parte em Rodando desafios de uma hackathon com Python) e ontem foi para palestrar na BSD Day.
Eu não sabia muito bem sobre o que palestrar, então fiz relacionado à programação em Python, em como substituir o que poderia ser feito em shell script por Python. Foi um live coding, que está apresentado aqui. Boa diversão!
Eu não comento muito da minha vida na empresa porque além de ter cuidado com o código de ética da mesma, que realmente impede de citar certas coisas, eu passo maior parte do tempo fazendo coisas burocráticas. Mas desde que mudei da Suécia eu passei a participar dos hackathons internos da empresa. No início como participante e depois como organizador. Hoje em dia eu não organizo muita coisa porque a hackthon é toda online. Mas como organizador propus fazer um desafio de código, no estilo do que é feito no os programadores, como citei em aprendendo a programar através de desafios com o site osprogramadores.
Ontem e hoje fizemos a maratona de código e eu fiquei testando.
Como era uma competição, sem prêmios diga-se de passagem, o formato foi assim:
Bom... vamos começar antes com o container que rodava os desafios, tanto a construção deles, se necessária, quanto sua execução: o container devcon.
FROM ubuntu:18.04
ARG DNS_SERVER
ENV DNS_SERVER ${DNS_SERVER}
ENV DEBIAN_FRONTEND noninteractive
ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE 1
RUN apt-get -y update \
&& apt-get -y dist-upgrade \
&& apt-get -y install apt-transport-https \
apt-utils \
ca-certificates \
curl \
gnupg-agent \
software-properties-common \
&& curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" \
&& mv kubectl /usr/local/bin \
&& curl https://get.helm.sh/helm-v2.17.0-linux-amd64.tar.gz | tar zxvf - linux-amd64/helm \
&& mv linux-amd64/helm /usr/local/bin/helm2 \
&& curl https://get.helm.sh/helm-v3.5.3-linux-amd64.tar.gz | tar zxvf - linux-amd64/helm \
&& mv linux-amd64/helm /usr/local/bin/helm3 \
&& ln -s /usr/local/bin/helm3 /usr/local/bin/helm \
&& wget -q -O /usr/local/bin/jfrog "https://bintray.com/jfrog/jfrog-cli-go/download_file?file_path=1.5.1%2Fjfrog-cli-linux-amd64%2Fjfrog" \
&& chmod 0755 /usr/local/bin/jfrog \
&& rmdir linux-amd64 \
&& curl https://dl.google.com/go/go1.15.7.linux-amd64.tar.gz | tar zxvf - -C /usr/local \
&& ln -s /usr/local/go/bin/* /usr/local/bin \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
&& echo "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable" \
> /etc/apt/sources.list.d/docker.list \
&& curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor > /etc/apt/trusted.gpg.d/microsoft.gpg \
&& echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" \
> /etc/apt/sources.list.d/vscode.list \
&& curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" \
> /etc/apt/sources.list.d/yarn.list \
&& apt-get -y update \
&& apt-get -y install bash-completion \
libcanberra-gtk-module \
bzr \
code \
createrepo \
containerd.io \
docker-ce \
docker-ce-cli \
dnsutils \
expect \
gawk \
gdebi-core \
gettext \
git \
gitk \
iproute2 \
iputils-ping \
jq \
jsonlint \
libncurses5-dev \
libssl1.0-dev \
libterm-ui-perl \
libxss1 \
lynx \
lzip \
make \
man \
meld \
mercurial \
mc \
netcat \
net-tools \
node-gyp \
nodejs-dev \
npm \
openjdk-8-jdk \
openssh-server \
pandoc \
pkg-config \
python \
python-pip \
python3-pip \
python3-setuptools \
python3-jinja2 \
python3-yaml \
rpm \
rsyslog \
runit \
sudo \
shellcheck \
yarn \
vim \
vim-scripts \
vim-syntax-docker \
wget \
cpio \
&& apt-get -y install maven \
&& apt-get clean \
&& pip3 install WeasyPrint \
&& sed -i 's/%sudo\tALL=(ALL:ALL)\ ALL/%sudo\tALL=(ALL:ALL) NOPASSWD:ALL/' /etc/sudoers \
&& echo "X11UseLocalhost no" >> /etc/ssh/sshd_config \
&& mkdir /devel; chmod 777 /devel \
&& echo "Europe/Stockholm" > /etc/timezone \
&& dpkg-reconfigure tzdata \
&& mkdir /go \
&& export PATH=/usr/local/go/bin:$PATH \
&& export GOPATH=/go \
&& export GOBIN=/usr/local/bin \
&& go get -v -u github.com/tebeka/go2xunit \
&& go get -v -u golang.org/x/lint/golint \
&& go get -v -u github.com/go-delve/delve/cmd/dlv \
&& go get -v -u github.com/uudashr/gopkgs/v2/cmd/gopkgs \
&& go get -v -u github.com/ramya-rao-a/go-outline \
&& go get -v -u github.com/cweill/gotests/... \
&& go get -v -u github.com/fatih/gomodifytags \
&& go get -v -u github.com/josharian/impl \
&& go get -v -u github.com/haya14busa/goplay/cmd/goplay \
&& GO111MODULE=on go get golang.org/x/tools/gopls@latest \
&& GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0 \
&& go get github.com/securego/gosec/cmd/gosec \
&& rm -rf /go
Eu removi algumas partes de coisas internas, mas é basicamente isso aí o container. Um ubuntu 18.04 com um go mais recente.
O loop do código era esse aqui, que basicamente entra no diretório de repositórios e busca por diretórios com os nomes "challenge-1", "challenge-2" e "challenge-3".
for directory in get_directories(HACKATHONREPOS):
full_path = f"{directory}/challenge-"
for i in range(1, 4):
if os.path.exists(f"{full_path}{i}"):
challenge(i, f"{full_path}{i}", timestamp)
Bem simples. Mais próximo de um shell script que de um programa. A variável HACKATHONREPOS apontando pro diretório onde estavam as cópias dos repositórios participantes, get_directories( ) retornando os nomes de diretórios de caminho apontado e challenge( ) pra rodar o teste, send o primeiro passo "make all". Antes de rodar o programa eu checo a data de modificação de um arquivo de time stamp pra sabe se o programa é mais novo ou mais velho. Se for mais velho, não preciso rodar. Então a função get_mtime( ) retorna o tempo em segundos (unix time) da data de modificação do arquivo. No update_timestamp( ) eu abro o arquivo, ou crio se não existir, e jogo qualquer coisa dentro. Estou jogando o tempo em segundos com time.time( ), mas realmente não precisa nada.
def update_timestamp():
with open(TIMESTAMP, 'w') as tmpstamp:
tmpstamp.write(str(time.time())
def get_mtime(filename):
return os.stat(filename).st_mtime
if not os.path.exists(TIMESTAMP):
update_timestamp()
timestamp = get_mtime(TIMESTAMP)
for directory in get_directories(HACKATHONREPOS):
full_path = f"{directory}/challenge-"
for i in range(1, 4):
if os.path.exists(f"{full_path}{i}"):
challenge(i, f"{full_path}{i}", timestamp)
update_timestamp()
Pra descrever um pouco mais dos problemas que encontrei, melhor uma olhada mais a fundo na função challenge( ).
def challenge(chl_id, directory, timestamp):
os.chdir(directory)
challenge_nr = os.path.basename(os.path.realpath(directory))
team_name = os.path.basename(os.path.realpath(directory + "/.."))
print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))
if os.path.exists(f"{directory}/Makefile"):
tmstp = get_mtime(f"{directory}/Makefile")
if tmstp > timestamp:
try:
dockerize(directory, "git clean -fdx")
except subprocess.CalledProcessError:
pass
try:
dockerize(directory, "make all")
except subprocess.CalledProcessError as e:
update_results({team_name: {challenge_nr: "failed: to build using make"}})
print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
return
if not os.path.exists(f"{directory}/hacking"):
print(team_name, challenge_nr, "done - binary missing")
return
tmstp = get_mtime(f"{directory}/hacking")
if timestamp >= tmstp:
print(team_name, challenge_nr, "done - old timestamp")
return
container_name = f"{team_name}_{challenge_nr}"
print("container_name:", container_name)
challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
time_start = time.time()
try:
result = dockerize(directory, f"./hacking {challenge_file}", container_name)
except subprocess.CalledProcessError as e:
update_results({team_name: { challenge_nr : "failed: to run challenge: "}})
print(team_name, challenge_nr, "done - failed to run: ", e.output)
return
time_stop = time.time()
print(f"docker ended for {team_name} in {challenge_nr}")
md5 = md5sum(result)
print(f"md5 ended for {team_name} in {challenge_nr}")
if md5 == EXPECTED_RESULTS[challenge_nr]:
update_results({team_name: { challenge_nr : "failed: wrong md5 check"}})
else:
update_results({team_name: { challenge_nr : time_stop - time_start}})
print(team_name, challenge_nr, "done - arrived at the end")
A função challenge está um pouco grande, mas o que ela basicamente faz é olhar se existe um arquivo Makefile com timestamp mais recente e rodar um "make all" pra construir o executável. O acordo era o binário seria nomeado como "hacking" pra facilitar essa função de rodar. Ela então busca por hacking na chamada container_name = f"{team_name}_{challenge_nr}"
e verifica o timestamp. Se for mais novo, roda. Do contrário não faz nada. Ao rodar os programas é chamada a função dockerize( ), que nada mais é que uma chamada pra docker usando subprocess. O tempo de início e fim de execução são capturados em time_start e time_stop. O resutando é verificado com a função md5sum( ), que emula o funcionamento do programa md5sum em Linux.
Bom... já deu pra perceber que o que parecia simples foi ficando bem complicado. Vamos então dar uma olhada na função dockerize.
def dockerize(pwd, command, container_name=None):
#userid = os.getuid()
#groupid = os.getgid()
userid = 1000
groupid = 1000
docker_cmd = [ "docker",
"run" ]
if not container_name is None:
docker_cmd += [ f"--name={container_name}" ]
docker_cmd += [
"--rm",
f"--user={userid}:{groupid}",
"-w",
f"{pwd}",
"-v",
f"{pwd}:{pwd}",
"-v",
f"{ROOTDIR}:{ROOTDIR}",
"hackathon:latest" ]
cmd = docker_cmd + command.split()
print("Running:", " ".join(cmd))
return str(subprocess.check_output(cmd))
Nada muito sofisticado. Mas já deu pra ver que buscar o uid e gid deu problemas. Por quê? Porque eu deixei rodando numa instância de jenkins, que roda com seu próprio usuário. Tentei arrumar as permissões do diretório pro mesmo grupo, mas no fim foi mais fácil deixar o container rodar com uid e gid fixos. Se nunca fez isso em container, experimente. Funciona que é uma beleza:
docker run --rm --user=$(id -u):$(id -g) -v $PWD:$PWD -w $PWD ubuntu:18.04 ls -a
E isso seria tudo do programa. Tem a parte do update_results( ), mas vou comentar depois que é somente salvar os resultados no formato json.
O que deu errado? Muita coisa.
Alguns programas simplesmente travavam. Ficavam lá parados com algum crash de lib ou coisa do tipo.
Qual foi a solução. Bom... a solução foi usar threads. Mas como alguém já disse antes, você tem um problema de dead-lock e quando resolve corrigir usando threads você termina tendo 5 outros problemas. Mas foi o que fiz. O loop inicial então foi modificado pra isso aqui:
for directory in get_directories(HACKATHONREPOS):
full_path = f"{directory}/challenge-"
for i in range(1, 4):
if os.path.exists(f"{full_path}{i}"):
th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp)).start()
ths.append(th)
#challenge(i, f"{full_path}{i}", timestamp)
for th in ths:
th.join()
update_timestamp()
A diferença era que agora rodavam muita instâncias ao mesmo tempo, tudo em paralelo, e acabava usando CPU demais. Então pra resolver um problema, criei outro. Precisei criar um semáforo pra poder dizer quantos threads poderia rodar simultaneamentes. Lembram do update_results( ) que salvava em json? O que acontece quando várias threads tentam escrever no mesmo arquivo ao mesmo tempo? Ou você chega numa estado chamado race condition, ou simplesmente dados aparecem e somem. Então foi preciso criar dois semáforos: um pro número de threads e outro pra salvar o resultado.
O loop principal então ficou assim:
ths = []
q = threading.Semaphore(MAX_THREADS)
qs = threading.Semaphore(1)
for directory in get_directories(HACKATHONREPOS):
#print(directory)
full_path = f"{directory}/challenge-"
#print(full_path)
for i in range(1, 4):
if os.path.exists(f"{full_path}{i}"):
th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp, q, qs)).start()
ths.append(th)
#challenge(i, f"{full_path}{i}", timestamp, q, qs)
for th in ths:
th.join()
update_timestamp()
E já que dois novos parâmetros foram passados pra função challenge( ), como essa ficou internamente? Assim:
def challenge(chl_id, directory, timestamp, q, qs):
q.acquire()
print(chl_id, directory, timestamp)
os.chdir(directory)
challenge_nr = os.path.basename(os.path.realpath(directory))
team_name = os.path.basename(os.path.realpath(directory + "/.."))
print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))
if os.path.exists(f"{directory}/Makefile"):
tmstp = get_mtime(f"{directory}/Makefile")
if tmstp > timestamp:
try:
dockerize(directory, "git clean -fdx")
except subprocess.CalledProcessError:
pass
try:
dockerize(directory, "make all")
except subprocess.CalledProcessError as e:
update_results({team_name: {challenge_nr: "failed: to build using make"} + str(e.output)}, qs)
print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
q.release()
return
if not os.path.exists(f"{directory}/hacking"):
print(team_name, challenge_nr, "done - binary missing")
print(os.listdir("."))
q.release()
return
tmstp = get_mtime(f"{directory}/hacking")
if timestamp >= tmstp:
print(team_name, challenge_nr, "done - old timestamp")
q.release()
return
container_name = f"{team_name}_{challenge_nr}"
print("container_name:", container_name)
challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
time_start = time.time()
try:
result = dockerize(directory, f"./hacking {challenge_file}", container_name)
except subprocess.CalledProcessError as e:
update_results({team_name: { challenge_nr : "failed: to run challenge: " + str(e.output)}}, qs)
print(team_name, challenge_nr, "done - failed to run: ", e.output)
q.release()
return
time_stop = time.time()
print(f"docker ended for {team_name} in {challenge_nr}")
md5 = md5sum(result)
print(f"md5 ended for {team_name} in {challenge_nr}")
if md5 == EXPECTED_RESULTS[challenge_nr]:
update_results({team_name: { challenge_nr : "failed: wrong md5 check"}}, qs)
else:
update_results({team_name: { challenge_nr : time_stop - time_start}}, qs)
print(team_name, challenge_nr, "done - arrived at the end")
q.release()
Basicamente um q.acquire( ) pra começar a rodar e um q.release( ) ao terminar. Tudo lindo. Vamos rodar? Via Jenkinks claro? E... problemas. Algumas dessas threads ficavam paradas. Caso não tenha reparado, ao final do loop principal existe esse pequeno trecho de código:
for th in ths:
th.join()
Ele basica diz o seguinte: pra cada thread nesse vetor de threads, espere a thread terminar. É isso que o join( ) faz. E como os programas travavam em sua execução o que acontecia? Dead-lock de novo.
O que fazer então? Vamos criar um sistema de monitoração chamado... soulkiller! Se jogou Cybepunk 2077 sabe do que estou falando, certo Silverhand? Então o soulkiller fica aguardando um certo tempo pra terminar a execução. Se passar daqui, uma vez sendo container basta simplesmente chamar um "docker kill <nome do container>". E aliás esse foi o motivo de eu passar o nome do container como argumento da função docker( ).
def soulkiller(container_name, timeout=None):
print("soulkiller has started for:", container_name)
time.sleep(3)
if timeout is None:
timeout = TIMEOUT
timeout -= 3
while timeout > 0:
resp = exec("docker ps -a")
if not re.search(container_name, resp):
print("soulkiller exiting since no container found for:", container_name)
return
timeout -= 1
time.sleep(1)
print("soulkiller reached timeout and will kill:", container_name)
exec(f"docker kill {container_name}")
E assim o sistema funcionou durante a hackathon. Era pra ser simples mas... bom... funcionou. Abaixo segue o script inteiro em todo sua beleza. Ou não.
#! /usr/bin/python3
import json
import os
import subprocess
import time
import hashlib
import threading
import queue
import re
MAX_THREADS = 1
TIMEOUT = 30 * 60 * 60
CHALLENGE_INPUTS = {
"challenge-1": "Employees-30M.json",
"challenge-2": "1GB.txt",
"challenge-3": "pi-1M.txt"
}
EXPECTED_RESULTS = {
"challenge-1": "d5c140cdc965be8ed56c35f570eaf83f",
"challenge-2": "2b4fd25f11d75c285ec69ecac420bd07",
"challenge-3": "731fa54d7133f61d4b3fac9b46bda927"
}
ROOTDIR = "/usr/local/tmp/hackathon"
TIMESTAMP = ROOTDIR + "/timestamp"
HACKATHONREPOS = ROOTDIR + "/repos"
RESULTS = ROOTDIR + "/results.json"
def get_directories(dir_name):
#print("dir_name:", dir_name)
directories = []
for filename in os.listdir(dir_name):
if filename[0] == ".":
#print(filename, " begins with a dot")
continue
filename = f"{dir_name}/{filename}"
if not os.path.isdir(filename):
#print(filename, " isn't a directory")
continue
#print("appending:", filename)
directories.append(filename)
return directories
def exec(command):
return str(subprocess.check_output(command.split()))
def dockerize(pwd, command, container_name=None):
#userid = os.getuid()
#groupid = os.getgid()
userid = 1000
groupid = 1000
docker_cmd = [ "docker",
"run" ]
if not container_name is None:
docker_cmd += [ f"--name={container_name}" ]
docker_cmd += [
"--rm",
f"--user={userid}:{groupid}",
"-w",
f"{pwd}",
"-v",
f"{pwd}:{pwd}",
"-v",
f"{ROOTDIR}:{ROOTDIR}",
"hackathon:latest" ]
cmd = docker_cmd + command.split()
print("Running:", " ".join(cmd))
return str(subprocess.check_output(cmd))
def update_timestamp():
with open(TIMESTAMP, 'w') as tmpstamp:
tmpstamp.write(str(time.time()))
def get_mtime(filename):
return os.stat(filename).st_mtime
def read_results():
j = json.loads("{}")
if os.path.exists(RESULTS):
#print(RESULTS, "exists")
with open(RESULTS) as results:
j = json.load(results)
return j
def save_results(j):
with open(RESULTS, "w") as output:
json.dump(j, output, indent=4)
def update_results(response_dict, qs):
print("Called update_result:", response_dict)
qs.acquire()
j = read_results()
print("Before:", j)
for team_name_resp in response_dict.keys():
for challenge_id_resp in response_dict[team_name_resp].keys():
value_resp = response_dict[team_name_resp][challenge_id_resp]
if not team_name_resp in j.keys():
j[team_name_resp] = { challenge_id_resp: value_resp }
elif not challenge_id_resp in j[team_name_resp].keys():
j[team_name_resp][challenge_id_resp] = value_resp
else:
if not challenge_id_resp in j[team_name_resp].keys():
j[team_name_resp][challenge_id_resp] = value_resp
else:
previous_value = j[team_name_resp][challenge_id_resp]
if isinstance(previous_value, float) and isinstance(value_resp, float):
if value_resp < previous_value:
j[team_name_resp][challenge_id_resp] = value_resp
else:
j[team_name_resp][challenge_id_resp] = value_resp
print("After:", j)
save_results(j)
qs.release()
def md5sum(message):
hash = hashlib.md5(message.encode())
return hash.digest()
def soulkiller(container_name, timeout=None):
print("soulkiller has started for:", container_name)
time.sleep(3)
if timeout is None:
timeout = TIMEOUT
timeout -= 3
while timeout > 0:
resp = exec("docker ps -a")
if not re.search(container_name, resp):
print("soulkiller exiting since no container found for:", container_name)
return
timeout -= 1
time.sleep(1)
print("soulkiller reached timeout and will kill:", container_name)
exec(f"docker kill {container_name}")
def challenge(chl_id, directory, timestamp, q, qs):
q.acquire()
print(chl_id, directory, timestamp)
os.chdir(directory)
challenge_nr = os.path.basename(os.path.realpath(directory))
team_name = os.path.basename(os.path.realpath(directory + "/.."))
print(directory, challenge_nr, team_name, os.path.realpath(os.path.curdir))
if os.path.exists(f"{directory}/Makefile"):
tmstp = get_mtime(f"{directory}/Makefile")
if tmstp > timestamp:
try:
dockerize(directory, "git clean -fdx")
except subprocess.CalledProcessError:
pass
try:
dockerize(directory, "make all")
except subprocess.CalledProcessError as e:
update_results({team_name: {challenge_nr: "failed: to build using make"}}, qs)
print(team_name, challenge_nr, "done - failed to make: " + str(e.output))
q.release()
return
if not os.path.exists(f"{directory}/hacking"):
print(team_name, challenge_nr, "done - binary missing")
print(os.listdir("."))
q.release()
return
tmstp = get_mtime(f"{directory}/hacking")
if timestamp >= tmstp:
print(team_name, challenge_nr, "done - old timestamp")
q.release()
return
container_name = f"{team_name}_{challenge_nr}"
print("container_name:", container_name)
threading.Thread(target=soulkiller, args=(container_name,), daemon=True).start()
challenge_file = ROOTDIR + "/" + CHALLENGE_INPUTS[challenge_nr]
time_start = time.time()
try:
result = dockerize(directory, f"./hacking {challenge_file}", container_name)
except subprocess.CalledProcessError as e:
update_results({team_name: { challenge_nr : "failed: to run challenge: " + str(e.output)}}, qs)
print(team_name, challenge_nr, "done - failed to run: ", e.output)
q.release()
return
time_stop = time.time()
print(f"docker ended for {team_name} in {challenge_nr}")
md5 = md5sum(result)
print(f"md5 ended for {team_name} in {challenge_nr}")
if md5 == EXPECTED_RESULTS[challenge_nr]:
update_results({team_name: { challenge_nr : "failed: wrong md5 check"}}, qs)
else:
update_results({team_name: { challenge_nr : time_stop - time_start}}, qs)
print(team_name, challenge_nr, "done - arrived at the end")
q.release()
if not os.path.exists(TIMESTAMP):
update_timestamp()
timestamp = get_mtime(TIMESTAMP)
ths = []
q = threading.Semaphore(MAX_THREADS)
qs = threading.Semaphore(1)
for directory in get_directories(HACKATHONREPOS):
full_path = f"{directory}/challenge-"
for i in range(1, 4):
if os.path.exists(f"{full_path}{i}"):
th = threading.Thread(target=challenge, args=(i, f"{full_path}{i}", timestamp, q, qs)).start()
ths.append(th)
#challenge(i, f"{full_path}{i}", timestamp, q, qs)
for th in ths:
try:
th.join()
except:
pass
update_timestamp()
Se estiver lendo com atenção notará com que rodei com... 1 thread só. Como eram desafios que exigiam computação e o parâmetro era tempo pra decidir o vencedor, no fim eu decidi deixar uma thread só pra ser justo.
Toda vez que gravamos o Unix Load On (Canal Unix Load On), eu acabo fazendo uma seleção tosca do que vamos falar. Isso sempre me deu uma coceira de resolver. E nada melhor que o bom e velho Python.
Hoje eu coloquei as mangas de fora e fiz funcionar. No bom e velho modo script: rodo num shell, e pego o resultado. Mas cheguei no ponto em que gostaria de que isso estivesse disponível em modo web, pra eu poder mostrar durante o programa diretamente no browser.
O Python fornece o módulo SimpleHTTPServer, que aliás parece que no Python3 virou uma classe de http.serve. Mas tem. Ele mostra o filesystem via interface http a partir de onde você chama o módulo. Não deveria ser complicado fazer o output do meu script ir pra uma interface http. E realmente foi o que fiz.
def start():
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
client_ip, client_port = self.client_address
reqpath = self.path.rstrip()
print(f"request from {client_ip}:{client_port} for {reqpath}")
article = get_final_article()
title = get_title(article)
link = get_link(article)
response = f"""
<h1>Title: <a href="/{link}">{title}
<h2>Link: <a href="/{link}">{link}</a></h2>
"""
content = bytes(response.encode("utf-8"))
self.wfile.write(content)
Se leu com atenção vai ver que a linha do artigo eu pego em "article = get_final_article()". Isso retorna algo como:
* [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)
Dai o restante é sanitizar cada um pra mostrar corretamente.
O script todo pode ser visto aqui: https://github.com/helioloureiro/homemadescripts/blob/master/random_article.py
O resultado no console é algo parecido com isso aqui:
./random_article.py
127.0.0.1 - - [26/Mar/2021 21:31:27] "GET /newarticle? HTTP/1.1" 200 -
request from 127.0.0.1:46060 for /newarticle?
= Articles =
* /helioloureiro/canalunixloadon/blob/master/pautas/2016030.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016040.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016041.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016050.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016051.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016090.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016100.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016110.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2016120.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017010.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017020.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017030.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017040.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017060.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017061.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017070.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017080.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2017110.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018030.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018050.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018060.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018080.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2018100.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019020.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019040.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019070.md
* /helioloureiro/canalunixloadon/blob/master/pautas/2019110.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20200717.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20200807.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201001.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201015.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201029.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201115.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20201204.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210121.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210205.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210215.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210312.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210325.md
* /helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
Latest: /helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
https://raw.githubusercontent.com/helioloureiro/canalunixloadon/blob/master/pautas/20210410.md
Article selected: * [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)
title: Linux developers get ready to wield the secateurs against elderly microprocessors • The Register
article: * [Linux developers get ready to wield the secateurs against elderly microprocessors • The Register](https://www.theregister.com/2021/01/11/linux_olld_cpus/)
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?
Hoje chegou um mail pedindo pra testar uma mudança de máquinas que saíram da empresa pra irem habitar o cloud. A tarefa era testar máquina e porta. Algumas máquinas com uma porta somente, outras com várias. E todas TCP.
Pra fazer isso rapidamente eu escrevi um script em python3 que basicamente estabelece uma conexão TCP e mostra OK se conectar ou FAIL se não conseguir. Bem básico, mas resolveu meu problema muito mais rápido que se eu fosse testar máquina por máquina, porta por porta provavelmente com o comando telnet.
#! /usr/bin/python3
import socket
servers = [
"helio.loureiro.eng.br:443",
"helio.loureiro.eng.br:389",
"helio.loureiro.eng.br:8081",
"helio.loureiro.eng.br:80",
"helio.loureiro.eng.br:22"
]
for server in servers:
try:
host, port = server.split(":")
port = int(port)
socket.create_connection((host, port), 3)
print(server, "OK")
except:
print(server, "FAIL")
O resultado:
helio.loureiro.eng.br:443 OK helio.loureiro.eng.br:389 FAIL helio.loureiro.eng.br:8081 FAIL helio.loureiro.eng.br:80 OK helio.loureiro.eng.br:22 OK
Boa diversão!