Quando eu cuidava de gatos, aprendi que a chave para parecer legal é fingir que cometeu erros de propósito. Ou, como muitos professores dizem, "Bem, quem de vocês percebeu meu erro proposital?"
Durante todo o projeto pseudo, houve muitos inícios falsos interessantes, bugs estranhos e outras experiências instrutivas. Algumas delas são casos muito estranhos e fora de contexto; outras são coisas que eu não acredito que um dia funcionaram. Felizmente, não me lembro das centenas de ocasiões nas quais terminei com um código que nem mesmo compilava. Elevei um simples erro de digitação e uma forma de arte.
Não vamos fazer e dizer que fizemos
Na versão funcional inicial do pseudo, operações de renomeação não funcionavam. Isso não causou nenhuma consequência no mundo real, pois o daemon do pseudo corrigiu as entradas do banco de dados automaticamente em acessos posteriores.
Uma das limitações do mecanismo SQLite SQL é que ele não pode usar
índices para comparações LIKE . Ao renomear um
diretório, obviamente, você deseja renomear os arquivos dentro do diretório.
Portanto, se você estiver renomeando o diretório /foo para
/bar, lembre-se de substituir a cadeia de caractere
/foo por /bar em
cada caminho que comece com /foo/.
Se você fizer isso usando a cláusula SQL
(path LIKE ? || '/'), no entanto, não será possível usar o
índice, e é terrivelmente lento. Navegando por aí, encontrei uma solução
deliciosamente perversa:
(path > (? || '/') AND path
< (? || '0').
Supondo um sistema ASCII, isso é precisamente equivalente a
path/ seguido por qualquer outra coisa, mas
por serem apenas operadores relacionais, o índice foi usado. Isso produziu um
fator de aceleração de aproximadamente vinte mil mesmo em pequenos sistemas de arquivos.
No entanto, durante a conversão para isso, cometi um pequeno errinho. O saldo
é que alterei a ordem das ligações de parâmetro, de tal forma que,
se você renomear /foo para
/bar, acaba substituindo
/bar por /foo em
todos os caminhos que começam com /foo/. O que não
causou nada, mas pelo menos o fez rapidamente.
Devido à paranoia e verificações de sanidade do pseudo, isso nunca causou resultados estranhos, apenas um monte de avisos em arquivos de log.
Uma suposição inicial sobre o pseudo era que não haveria problemas de serialização, pois todas as operações eram serializadas no servidor, e não era possível obter duas operações consecutivas de um determinado cliente fora da ordem. Não era algo tão grave quanto "640K deve ser o suficiente para todos", mas certamente era um erro grave.
No design original, foram tentadas operações subjacentes e reportadas ao servidor no caso de êxito. Para um único programa isso sempre funcionou. No entanto, com vários programas, houve uma possível condição de corrida.
O processo A cria um arquivo temporário, com o número inode 12345. Em seguida, o processo A remove esse arquivo temporário. Depois disso, ele é removido, o Processo B cria um novo arquivo, que reutiliza o número inode 12345. No entanto, quando isso acontece, o daemon do pseudo vê a mensagem de criação do Processo B antes de ver a mensagem de desvinculação do Processo A. O que acontece?
Ao receber a mensagem de criação de B, o daemon do pseudo percebe que há uma entrada antiga no banco de dados (arquivo temporário de A) com o mesmo número inode; ele registra a discrepância e remove a entrada. Em seguida, ele cria uma nova entrada no banco de dados. No entanto, as coisas pioram. Quando a mensagem de exclusão chega, o daemon percebe que há uma entrada antiga no banco de dados (arquivo de B) com o mesmo número inode. Ele remove a entrada falsa e também tenta remover o arquivo de temporário de A do banco de dados. Ao final, o arquivo de B não está mais registrado no banco de dados.
Minha primeira tentativa de corrigir isso foi um fracasso deprimente; modifiquei a operação
UNLINK para retornar a entrada anterior
do banco de dados para o arquivo e fiz o cliente enviar uma mensagem
UNLINK e vinculei novamente um arquivo se a
chamada do sistema subjacente tiver falhado. Isso eliminou a condição de corrida.
No entanto, isso criou um modo de fracasso ainda pior:
rmdir(2) em um diretório com arquivos
excluiu as entradas do banco de dados para todos os arquivos (pois remover um diretório
significa remover todo seu conteúdo).
A adição do sinalizador "deleting" aos arquivos e a adição das mensagens
MAY_UNLINK,
DID_UNLINK e
CANCEL_UNLINK finalmente corrigiu isso.
Essas mensagens permitem que o banco de dados registre se um arquivo está preste
a ser excluído, de modo que as mensagens de criação não gerem erros.
Em seguida, uma mensagem DID_UNLINK excluirá um arquivo somente
se o arquivo tiver o sinalizador deleting definido. Dessa forma, acredito que esse problema
foi finalmente derrotado.
Enfrentamos um problema misterioso ao renomear um diretório fazendo com que os arquivos nesse diretório fossem esquecidos. Esse problema foi o resultado de três bugs distintos; a correção de qualquer um deles corrigia o comportamento problemático.
Ao renomear um diretório, o pseudo verifica se o diretório já é conhecido no banco de dados do pseudo e, se não for, ele cria um diretório com esse nome de modo que a operação de renomeação possa ocorrer normalmente (e, dessa forma, renomeia qualquer arquivo contido nesse diretório e que já era conhecido pelo pseudo). Isso poderia ocorrer se, por exemplo, você criasse um diretório fora do ambiente do pseudo e criasse arquivos dentro desse diretório durante a execução no ambiente do pseudo.
O problema ocorreu pela combinação de três opções. A primeira era que ao vincular um arquivo, o pseudo desvinculava qualquer arquivo existente com o mesmo nome. A segunda era que, ao desvincular um diretório, o pseudo desvinculava conteúdo desse diretório. A combinação dessas opções com o vínculo implícito de uma renomeação significava que, ao renomear um diretório não registrado anteriormente no banco de dados, o pseudo perdia todas as entradas dos arquivos nesse diretório que haviam sido registradas no banco de dados.
Isso, por si só, não teria aparecido em nosso sistema de criação. O que fez isso disparar foi
minha decisão completamente inexplicável de tentar aprimorar o controle
no caso em que rename(3) renomeia um arquivo
nos sistemas de arquivo. Na verdade, isso não pode acontecer, ainda assim por alguma razão, eu não apenas
tentei implementar o suporte, mas o fiz de uma maneira muito ruim, de tal forma que
o wrapper de renomeação acabava sempre tentando vincular o nome antigo no
banco de dados antes de renomear. O resultado foi que ao mover um diretório
que continha arquivos, os arquivos eram sempre removidos do
banco de dados.
Corrigimos esses erros principais. A ação de desvincular implícita realizada por uma operação
LINK agora remove apenas o arquivo nomeado,
não quaisquer arquivos que pareçam estar contidos nele. As operações de renomeação não
tentam mais criar vínculos. O resultado é que a renomeação de um
diretório não acaba mais com as coisas.
um caso de vértice com cinco dimensões
Você já ouviu falar de casos de parâmetros operacionais extremos e de casos de parâmetros fora da operação normal. Este é o único caso de vértice com cinco dimensões que já vi em todos os anos em que trabalho com software.
Quando o sinalizador "deleting" foi adicionado, isso significou uma mudança na estrutura de dados usada pelo pseudo para IPC. Como eu nunca defini a versão, é teoricamente possível que um cliente e um servidor discordem sobre a versão da mensagem IPC que estão usando. No entanto, isso nunca aconteceu; nosso sistema de criação garante a recriação dos componentes ao mesmo tempo.
Ainda assim, tivemos um problema muito estranho no qual um único programa falhava às vezes em um ponto específico da criação. Por "falhar" eu quero dizer "travava indefinidamente esperando uma resposta do daemon". Enquanto isso, o daemon aguardava a entrada do soquete.
Talvez seja necessário detalhar um pouco mais sobre o protocolo do pseudo. Quando um
cliente é iniciado, a primeira coisa que ele faz é enviar uma mensagem
PSEUDO_MSG_PING para o servidor. As
informações nessa mensagem incluem o PID do cliente, o nome do binário
do cliente e uma mensagem "tag" opcional para uso no registro de eventos
desse cliente. Se não houver uma mensagem tag, ela é simplesmente omitida. (O nome
e a tag são enviadas como o "caminho", com seu tamanho indicado no
campo pathlen.)
O travamento estava ocorrendo durante o ping. Ele ocorreu apenas no computador de um desenvolvedor, e apenas temporariamente. No entanto, conseguimos rastreá-lo eventualmente.
A mudança que fizemos aumentou o tamanho da estrutura da mensagem do pseudo em quatro bytes. O servidor é inteligente com relação às leituras que ultrapassam o tamanho da estrutura base, mas a parte inicial, ele apenas presume que sempre receberá uma leitura completa. (Ainda não corrigi esse bug.)
Se você conseguisse de alguma forma executar o novo daemon do pseudo com estrutura quatro bytes maior com um cliente pseudo antigo, ele não receberia tantos dados quanto o esperado. O cliente também envia o nome do caminho e a tag. Portanto, a falha em questão aconteceria apenas se o executável em execução tivesse um nome com quatro caracteres (era sed) e não tivesse uma tag definida. Mesmo assim, como obter o cliente pseudo antigo e o daemon pseudo novo?
Em nosso sistema de criação, é possível ter ferramentas de hospedagem pré-criadas, espelhadas no diretório de criação como uma árvore de links simbólicos (usando lndir) e, então, qualquer ferramenta que precise ser recriada para receber novas versões é recriada. O desenvolvedor em questão tinha ferramentas de hospedagem antigas, incluindo o daemon do pseudo e biblioteca cliente, que foram espelhadas, e criou novas ferramentas, incluindo um novo daemon e uma nova biblioteca cliente, no diretório do projeto.
Como nós definimos LD_LIBRARY_PATH para apontar para o
diretório do projeto, selecionados consistentemente as novas bibliotecas e tudo
correu bem. Ainda assim, houve uma pequena falha. É possível definir um caminho de pesquisa de vinculador
em um executável, e há duas maneiras de fazer isso. A configuração moderna e simples
RUNPATH é usada da maneira esperada. No entanto, a configuração mais antiga e menos simples
RPATH tem a característica incomum de ser
processada antes de
LD_LIBRARY_PATH. O binário em questão tinha sido
criado com RPATH definido como
$ORIGIN/../lib:$ORIGIN/../lib64. Um excelente cookie
$ORIGIN expande para o diretório
contendo o binário.
Lembra-se que eu disse que as ferramentas eram espelhadas com links simbólicos? O processamento
do cookie $ORIGIN segue links simbólicos. Por isso,
ao executar esse executável específico, o vinculador dinâmico acabava
procurando, não em LD_LIBRARY_PATH, mas no
diretório da biblioteca para as pré-criações, fazendo com que recebesse a biblioteca cliente pseudo
antiga. Como o nome do executável continha três caracteres,
isso resultou em um travamento em vez de uma falha ou um diagnóstico.
Para reproduzir esse bug, será necessário ter:
- Uma versão pré-criada do pseudo com no mínimo uma semana de idade
- Uma árvore de origem que recriaria a versão mais recente
- Um executável na árvore de pré-criação que não precisasse ser recriado
- ... com um nome com no máximo três caracteres
- ... que especificasse um caminho de pesquisa de biblioteca usando
$ORIGIN, usandoRPATH
O rastreamento disso tudo demorou muito tempo. A correção de longo prazo envolve a adição
da versão às mensagens (idealmente usando algum indicador que nunca possa
ocorrer nas mensagens atuais) e vários outros aprimoramentos. Também
envolve não usar mais RPATH para indicar caminhos de
vínculo, e possível copiar os binários em vez de torná-los links simbólicos.
Em alguns computadores com Linux recentes, os arquivos copiados com o antigo
/bin/cp acabavam com bits de permissão
incorretos. No final das contas, a família de funções
getxattr()/setxattr()
pode ser usada para consultar ou definir modos POSIX, não apenas atributos
estendidos. Em um sistema específico, isso é realizado
em vez de usar o
chmod(). Em seguida, a especificação exige
retornar para chmod() se as funções
*xattr() falharem e, até o momento, o pseudo
as intercepta e falha, definindo errno como
ENOTSUP. Talvez seja necessário corrigir isso posteriormente.
Da mesma forma, durante uma fase importante de refatoração, muitos dos wrappers do pseudo
foram reimplementados como funções triviais que apenas chamavam outras funções; por
exemplo, usando open() com
O_CREAT para implementar
creat(). Especificamente, muitas funções que tinham variantes de
*at() foram implementadas chamando
a função *at() com
AT_FDCWD como o parâmetro
dirfd . Isso funcionou muito bem até
que experimentamos em um computador que não fornecia
openat().
Provavelmente, à medida que o tempo passa, teremos que desenvolver um controle mais completo para sistemas que oferecem uma gama diferente de suporte de API.
Lições aprendidas e direções futuras
Muitos dos problemas que encontramos durante o desenvolvimento inicial e manutenção contínua do pseudo foram relativamente fáceis de rastrear e diagnosticar. A decisão de se concentrar na robustez e no bom registro em log no início definitivamente compensou. Por outro lado, o conjunto de testes que planejamos escrever "em breve" foi uma falha importante e perceptível; a criação e uso de mais suporte para teste logo no início teria economizado muito tempo.
Embora seja sempre bom usar um código e projetos existentes quando eles correspondem ao que você está fazendo, não tenha medo de concluir que o problema que você está solucionando realmente é um novo problema. Acontece. Não com muita frequência (acho que é a primeira vez que isso acontece comigo), mas acontece, e quando acontecer, esteja preparado.
Para trabalhos futuros, ainda temos muitos aprimoramentos de robustez e de diagnóstico para fazer, mas o próximo campo importante de questionamento poderá ser o desempenho; pois tudo o que o pseudo faz e deveria fazer razoavelmente bem, o faz inegavelmente de forma mais lenta do que o fakeroot, e provavelmente nós podemos melhorar um pouco isso. Ele nunca será tão rápido para armazenar coisas em um formato de banco de dados estável em disco de forma a mantê-las apenas na memória, mas ainda já muito espaço para acelerar as coisas.
Aprender
- O projeto pseudo foi
desenvolvido inteiramente para atender necessidades internas, mas também foi lançado como software
livre.
- Podcasts do developerWorks: escute entrevistas e explicações interessantes para desenvolvedores de software
- developerWorks: fique atualizado em relação aos briefings ao vivo do developerWorks.
- DeveloperWorks no Twitter: siga-nos para acompanhar as últimas notícias.
- Eventos interessantes: confira futuras conferências, exposições e webcasts interessantes para desenvolvedores de software livre IBM.
- Zona de software livre do developerWorks: você encontra informações amplas sobre instruções, ferramentas e atualizações de projetos para ajudá-lo a desenvolver com tecnologias de software livre e usá-las com produtos da IBM, bem como os nossos artigos e tutoriais mais populares.
- demos gratuitas on demand do developerWorks: Acompanhe nossas demos gratuitas e saiba mais sobre as tecnologias IBM e de software livre e funções dos produtos.
Obter produtos e tecnologias
- Versão de teste do software IBM: inove o seu próximo projeto de desenvolvimento de software livre usando software para teste, disponível para download ou em DVD.
Discutir
- comunidade do developerWorks: Entre em contato com outros usuários do developerWorks e explore os blogs, fóruns, grupos e wikis voltados para desenvolvedores.
