Sem Comentários. E agora?

Tradicionalmente, considera-se uma boa prática comentar o código. Há algum tempo, tem-se revisto este conceito. Na Liferay, por exemplo, seguimos uma política de não comentar código. Pessoalmente, sou um entusiasta desta filosofia. Mas não quero apresentar ou defender esta estratégia: há muito material bom sobre isto. Quero discutir uma questão em aberto.

Quem comenta quer transmitir alguma informação importante. Que informação é essa? E, mais importante ainda, onde podemos registrá-la? Vejamos algumas alternativas.

O que essas linhas fazem?

Os nomes de funções são excelentes para explicar o que o código faz. Se um bloco de código precisa de um comentário, considere extraí-lo para uma função ou classe. O nome da entidade já esclarecerá seu propósito.

Observe, por exemplo, as linhas abaixo, retiradas desta classe de testes:

Assert.assertNotNull(recurrence);
Assert.assertNull(recurrence.getUntilJCalendar());
Assert.assertEquals(0, recurrence.getCount());Code language: CSS (css)

Essas linhas verificam se a RRule de um evento tem certas propriedades: ela deve existir, ter um “untilCalendar nulo e uma contagem de zero.

Os conceitos são complexos; eu mesmo me confundiria ao reler estes asserts. Um comentário poderia explicá-los. Mas este commit já esclareceu tudo ao mover essas linhas para um método e invocá-lo:

assertRepeatsForever(recurrence);

Aquelas asserções verificavam se o evento se repete eternamente! Nenhum comentário foi necessário — felizmente, pois estes asserts estavam em vários testes.

O que está acontecendo?

Se o comentário iria explicar algo relevante em tempo de execução, considere transformá-lo em uma mensagem de log! Note o exempo abaixo.

if (Validator.isBlank(serviceAccountKey)) {
	// If no credentials are set for GCS Store, the library will
	// use Application Default Credentials.
	_googleCredentials =
		ServiceAccountCredentials.getApplicationDefault();
}
else {
	_googleCredentials = ServiceAccountCredentials.fromStream(
		new ByteArrayInputStream(serviceAccountKey.getBytes()));
}Code language: JavaScript (javascript)

Este comentário pode ser relevante para quem lê o código. Contudo, seria crucial para alguém investigando um problema de autenticação. Por isso, na prática, escolhi logar uma mensagem:

if (Validator.isBlank(serviceAccountKey)) {
	if (_log.isInfoEnabled()) {
		_log.info(
			"No credentials set for GCS Store. Library will use " +
				"Application Default Credentials.");
	}

	_googleCredentials =
		ServiceAccountCredentials.getApplicationDefault();
}
else {
	_googleCredentials = ServiceAccountCredentials.fromStream(
		new ByteArrayInputStream(serviceAccountKey.getBytes()));
}Code language: JavaScript (javascript)

Por que este código está aqui?

Comentários para explicar por que algumas linhas estão ali também são comuns. Um local melhor para compartilhar essas informações são as mensagens de commits.

Estes dias, por exemplo, me pediram para ajudar com um código em que trabalhei anos atrás. Lendo uma JSP — lembre-se, anos atrás — eu encontrei essas linhas:

<liferay-portlet:renderURL portletName="<%= KaleoDesignerPortletKeys.KALEO_DESIGNER %>" var="viewURL">
	<portlet:param name="mvcPath" value="/designer/view_kaleo_definition_version.jsp" />
	<portlet:param name="redirect" value="<%= currentURL %>" />
	<portlet:param name="name" value="<%= kaleoDefinitionVersion.getName() %>" />
	<portlet:param name="draftVersion" value="<%= kaleoDefinitionVersion.getVersion() %>" />
</liferay-portlet:renderURL>Code language: HTML, XML (xml)

Esta tag está gerando uma URL para ser utilizada em outro lugar. Mas meus olhos treinados acharam estranho aquele parâmetro portletName. Este valor costumar ser definido automaticamente.

Um git blame esclareceu tudo, quando encontrei este commit. A mensagem é clara:

LPS-74977 / LPS-73731 By making the render URL explicitly use the Kaleo Designer name, it will be valid when rendered in another portlet.

Entendi! Este código provavelmente vai ser invocado por algum outro portlet. Neste caso, o valor seria automaticamente setado pela outra aplicação, e por alguma razão queremos evitar isso.

(Por esta razão, aliás, prefiro commits pequenos: eles facilitam descobrir a razão de trechos de código bem específicos. É como se todas as linhas de código tivessem um comentário! Não é uma posição unânime, porém: há quem prefira commits maiores.)

A razão da linha foi esclarecida. Mas por que ela pode ser invocada de outra aplicação? Isto não é usual…

Por que esta mudança foi feita?

Um código bem escrito explica como algo foi implementado. A mensagem de commit esclarece o porquê, mas em um contexto local. Como explicar a motivação mais ampla por trás de um código sem recorrer a comentários?

Os tíquetes do issue tracker são excelentes para isto. Normalmente escritos para guiar o desenvolvimento, esses documentos ajudam demais na interpretação do código. Se adicionarmos a chave do tíquete à mensagem de commit, podemos rastrear as razões.

Voltando ao exemplo acima. Descobrimos que uma linha permite usar o mesmo código em vários portlets. Mas isso raramente é necessário. Por que precisamos reutilizar o código neste caso? Por sorte, a mensagem menciona dois tíquetes. Fui verificar o mais antigo; cheguei a LPSA-64324:

[Information Architecture] EE – As a portal admin, I would like to access all workflow portlets from the control panel section under the same tab.

O título já ajuda, e o texto esclarece de vez. Por razões de usabilidade, três aplicações diferentes passaram a aparecer em abas de um mesmo portlet. Faz todo sentido!

Os comentários que a gente gosta

É importante destacar que tentamos evitar comentários desorganizados, que se entrelaçam no código e tentam explicar trechos difíceis de entender. Há vários comentários, frequentemente com formatos padronizados, que não atrapalham a leitura. Um exemplo óbvio são os cabeçalhos de copyright.

Outra maneira de usar comentários efetivamente é a programação letrada. Neste estilo de programação, os comentários são a estrela do show: o código-fonte contem mais prosa do que código executável. Isto é útil quando explicar o algoritmo é mais importante do que lê-lo, como em pesquisas acadêmicas e análise de dados. Não por acaso, é o paradigma de ferramentas populares como Jupyter Notebook e Quarto.

Mais relevante ainda, ferramentas como Javadoc, JSDoc, Doxygen etc. leem comentários em um formato específico para gerar documentação. Estes comentários não afetam a legibilidade. Pelo contrário: javadocs são ótimos para explicar como usar estas entidades. Combinados com ferramentas como meu querido Doctest, temos até garantias de acurácia e atualidade!

Um mundo de possibilidades

Esses são apenas alguns exemplos de alternativas aos comentários. Há muitas outras opções, como wikis, blogs. Já encontrei a explicação para um código que escrevi no Stack Overflow! Podemos pensar em ainda mais soluções para atender a diferentes necessidades. O ponto principal é que, tendo estas ferramentas à nossa disposição, adicionar comentários diretamente ao código torna-se desnecessário.

Naturalmente, evitar comentários é apenas uma das formas para se escrever código legível. Comentários não são proibidos; de fato, há estratégias que podem torná-los eficazes. No entanto, na minha experiência, comentar indisciplinadamente leva a piores resultados, e essas técnicas ajudam a documentar informações importantes que não cabem diretamente no código.

Você é um adepto da estratégia “sem comentários”? Se sim, que outros meios você usa para transmitir infirmações? Se não, como você faz para ter comentários efetivos? Que tipo de comentário você não vê sendo substituído por essas abordagens? Adoraria escutar suas opiniões.

Dez anos de Liferay

Dias atrás, recebemos aqui em casa um pacote inesperado. O que encontramos dentro dele foi ainda mais surpreendente! O que estaria acontecendo?

Uma caixa de iPad com um cartão sobre ela. O cartão tem o número "10" escrito em cor dourada.

Bem, acontece que há alguns meses atrás, eu completei incríveis dez anos trabalhando na Liferay! Isso não é apenas um long período, mas também uma jornada que me proporcionou muito crescimento. Morei em duas cidades, viajei para algumas outras ao redor do mundo, aprendi a trabalhar remotamente, lidei com inúmeras tecnologias e testemunhei o crescimento da filial da LATAM, que saiu de um punhado de pessoas para centenas.

Um cartão com um cabeçalho escrito: "Happy Liferay Anniversary."

Abaixo, escrito à mão:

"Adam,
É uma honra escrever este cartão para vocÊ comemorar seus 10 anos de Liferay.
Um trabalho feito com comprometimento e dedicação sempre gera bons frutos.
Tenho muitok orgulho de ter feito parte da sua história. Que você continue sendo inspiração para todos nós.
Feliz 10 anos de LIferay!! Que venham muito mais..."

Hoje em dia, é raro permanecer tanto tempo no mesmo lugar, especialmente em uma carreira na área de tecnologia. No entanto, a Liferay é realmente um local agradável para trabalhar, onde sempre há coisas novas para aprender e desafios, sejam da tecnologia, do trabalho em equipe ou do cuidado com o cliente. Sem dúvida, cresci muito e, ao que parece, ainda tenho espaço aqui para evoluir ainda mais!

Portanto, agradeço a todos pelo presente, mas, o que é mais importante, agradeço pelo ótimo momento, pelo crescimento e pelos desafios. E preparem-se, pois pretendo ser uma “incomodação” encantadora entre todos vocês por muitos e frutíferos anos que estão por vir! 😄🎉

Faturando o débito técnico

Um tempo atrás, o homem, o mito, a lenda Fabrício Buzeto fez esta esta interessante pergunta:

Out of curiosity. Does your team keep a list of technical debt? Does it make you feel joy?
Só por curiosidade. Seu time mantém uma lista de débitos técnicos? Isso faz vocês felizes?

Isso me fez lembrar algumas coisas. Por alguns anos, fui responsável pelos portlets Liferay Calendar e Kaleo Designer. Eram aplicativos complexos, construídos em ritmo acelerado quando o conceito de SPAs ainda estava em evolução: muitas escolhas exigiam uma revisão.

Então comecei a escrever tíquetes do JIRA para débitos técnicos. Quando um desses problemas tornava uma demanda mais difícil, eu convertia esse tíquete em uma subtarefa da demanda. Como gosto de dizer, eu estava “faturando o débito na feature“.

Comentei isso e então ele me fez uma pergunta crucial:

Why not treat them like any other card in the backlog then?

De fato, por quê?

Bem, a princípio, nós tentamos! Eu apresentava as issues de débito técnico em nossas reuniões de priorização. Ter os problemas escritos ajudou muito a chamar a atenção dos gestores, aliás.

Débito técnicos são um peixe difćil de vender, no entanto. As pessoas são compreensivelmente cautelosas ao investir em algo cujo valor não é evidente. Ainda assim, as alterações demoravam cada vez mais para serem entregues e os bugs de regressão continuavam aparecendo. Precisávamos corrigir a raiz dos problemas.

É por isso que comecei a trabalhar em débitos como parte das tarefas que agregam valor. Quando consertávamos um débito para facilitar uma demanda, ficava evidente de que o trabalho extra valia a pena. Aquela refatoração não foi apenas uma ideia aleatória: ela nos trouxe valor.

Essa é a primeira razão para lidar com débito técnico como subtarefas de outras demandas: ao vincular a dívida a uma tarefa que entrega valor, é mais fácil justificar o esforço extra para as partes interessadas.

No início, esse “faturamento de débito” era apenas um dispositivo de comunicação. Mas houve um efeito colateral interessante: os problemas mais gritantes eram naturalmente resolvidos primeiro. Faz sentido: como trabalhávamos neles quando causam problemas, os que causassem mais problemas eram resolvidos primeiro. Como priorização é sempre um desafio (e priorizar débito técnico é ainda mais difícil), isso foi muito útil!

Ainda tínhamos uma pilha de tarefas de débito, mas muitas já não eram relevantes. Algumas, já haviam sido resolvidas. Outras eram ideias elegantes no passado, mas não faziam mais sentido. Em retrospectiva, boa parte do “débito” eram preferências pessoais, ou suposições que não eram mais verdadeiras após alguma evolução do produto.

Esta é a segunda razão para o faturamento da dívida: trabalhar na “saúde do código” como parte de demandas é uma maneira eficaz de priorizar em qual débito merece o esforço.

Veja que ótimo! Tivéssemos resolvido o débito técnico sozinho — por exemplo, em uma força-tarefa —, talvez fizéssemos mudanças que poderiam, de fato, dificultar a evolução futura. A cobrança de dívidas permitiu-nos confirmar que solicitações se adequavam aos nossos objetivos. E houve uma consequência mais sutil e importante.

Nós, desenvolvedores, temos opiniões fortes, e isso é bom. Geralmente tentamos transformar essas opiniões em um objetivo. Mas é difícil saber se um objetivo é o correto. Uma vez que usamos essas ideias para ajudar em algo mais claramente relevante, esse objetivo se transforma em uma ferramenta. Ferramentas são muito mais fáceis de avaliar!

Esse é um terceiro motivo para faturar o débito: quando o débito técnico está atrelada à entrega de valor, a força criativa da equipe se alinha com os objetivos da organização.

Nossa experiência com essa estratégia foi bastante eficaz. Todos sabiam que suas sugestões seriam avaliadas: as tarefas de saúde não seriam mais uma obrigação a priorizar, mas um conjunto de ferramentas que nossos colegas buscariam para ajudar em seus desafios O backlog de débito técnico não era mais apenas um poço dos desejos.

Os aplicativos também ficaram melhores. Quando comecei a trabalhar no Calendário, por exemplo, ele era visto como um portlet especialmente problemático. O primeiro lançamento não podia agendar eventos! Quando saí daquele time, o Calendário não tinha bug de prioridade 3 ou superior (níveis que temos que corrigir). E entregamos uma boa quantidade de recursos, mesmo alguns ausentes nos líderes da concorrência. Nada mal para um produto que fora um exemplo de como não funcionar!

Por tudo isso, sempre me pareceu bom pagar o débito técnico como parte das demandas, mas nunca pensei muito sobre por que. Então, obrigado pela pergunta, Fabricio! Foi uma prazer pensar nisso.

PS: Acabei de lembrar que Ron Jeffries escreveu um excelente post sobre quando refatorar. Ele na verdade é contra algo que defendi aqui, mas vi bastante semelhanças, e naturalmente ele explica muito melhor. Vale muito a leitura!

(Esta é uma tradução de Billing the Technical Debt, um post em Suspension of Disbelief.)

Importando Módulos ES6 em CommonJS

Aqui na Liferay, alguns dias atrás, necessitávamos utilizar o pacote p-map. Só tinha um problema: esta aplicação em específico utilizava módulos no padrão CommonJS, e p-map utiliza módulos ES6. Até algumas das melhores referências que encontrei (por exemplo, este post) deixavam claro que não seria possível importar módulos ES6 a partir de CommonJS.

A boa notícia é que isto não é mais verdade! Usando import dinâmico, podemos carregar módulos ES6 a partir de CommonJS. Vejamos um exemplo.

Neste projeto, o arquivo importer.js tenta utilizar require() para importar um módulo ES6:

const pmap = require('p-map');

exports.importer = () => {
  console.log('Yes, I could import p-map:', pmap);
}Code language: JavaScript (javascript)

Naturalmente, isto não funciona:

$ node index.js 
internal/modules/cjs/loader.js:1102
      throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
      ^

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /home/adam/software/es6commonjs/node_modules/p-map/index.js
require() of ES modules is not supported.
require() of /home/adam/software/es6commonjs/node_modules/p-map/index.js from /home/adam/software/es6commonjs/importer.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /home/adam/software/es6commonjs/node_modules/p-map/package.json.

    at new NodeError (internal/errors.js:322:7)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1102:13)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Module.require (internal/modules/cjs/loader.js:974:19)
    at require (internal/modules/cjs/helpers.js:101:18)
    at Object.<anonymous> (/home/adam/software/es6commonjs/importer.js:1:14)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32) {
  code: 'ERR_REQUIRE_ESM'
}
Code language: Bash (bash)

A solução é converter o require() em um import dinâmico. Mas tem um detalhe: import dinâmicos retornam uma Promise. Há várias maneiras de lidar com isso; a mais simples provavelmente é tornar nossa função assíncrona, como nessa versão:

exports.importer = async () => {
  const pmap = await import('p-map');
  console.log('Yes, I could import p-map:', pmap);
}Code language: JavaScript (javascript)

Agora nossa pequena aplicação funciona!

$ node index.js 
ok
Yes, I could import p-map: [Module: null prototype] {
  AbortError: [class AbortError extends Error],
  default: [AsyncFunction: pMap],
  pMapSkip: Symbol(skip)
}Code language: Bash (bash)

Outros ajustes podem ser necessários; eu mesmo precisei ajustar as configurações do eslint. O importante é que isto é possível. E não é uma gambiarra: a própria documentação de Node recomenda essa abordagem.

Então, não se assuste com informações desatualizadas: você não vai precisar reescrever sua aplicação toda como módulos ES 6, ao menos por enquanto.

Bandeiras de mobilização e botões vermelhos

É melhor trabalhar numa organização hierarquizada, top-down, ou numa mais solta, bottom-up? Depois de alguns anos, aprendi que ambos os estilos têm vantagens e desvantagens, funcionam e podem ser igualmente agradáveis.

Naturalmente, em ambos os estilos, a boa liderança depende de uma miríade de fatores. Como contribuidor individual, porém, notei duas atitudes de meus superiores que fazem uma diferença especial.

Nas organizações bottom-up é bem comum contratar pessoas competentes e deixá-las fazer sua mágica. Nesses cenários, sempre me ajudou muito o líder desenhar bem a missão. Não é definir o que vai ser feito, mas sim objetivos concretos com o qual podemos nos conectar. Sem isso, o subordinado pode se perder em tarefas irrelevantes. Já aconteceu muito comigo: dediquei muito esforço a bugs mínimos.

Isso é algo que tende a não acontecer em ambientes hierarquizados. Mesmo que nos dediquemos a algo irrelevante, a “culpa” não será nossa. Por isso mesmo, porém, frequentemente recebemos instruções francamente perigosas. Eu lembro de uma funcionalidade que me solicitaram. Eu sabia claramente como fazer — já tinha feito muitas similares — mas meu chefe me ordenou outra abordagem. Soube que, anos depois, após perdas de dados e duas reescritas, fizeram exatamente o que eu faria no primeiro dia.

Enfim, se você for dar liberdade, dê também uma missão clara, uma bandeira de mobilização que nos guie e nos inspire a tentar. Se for dar instruções rigidamente, disponibilize um botão vermelho para alertas que venham de baixo. Liderança vai além disso, mas essas duas atitudes já ajudam bastante.

Dois problemas num Jenkinsfile

Ah, as sutilezas do shell script…

Já usei vários servidores Jenkins aqui na Liferay, sempre como desenvolvedor. Na Liferay Cloud, porém, configurar e rodar Jenkins é uma tarefa rotineira. Resolvi então reforçar meu conhecimento seguindo os passos de Guided Tour to Jenkins. Foi quando aconteceu uma coisa curiosa.

O processo de build no Jenkins é definidos por arquivos Jenkinsfile. Um Jenkinsfile contém, entre outras coisas, vários estágios do processo de build. Cada estágio possui um ou mais passos (isto é, comandos a serem executados).

A sessão sobre passos múltiplos me apresentou o retry (que executa um um passo até ele ser bem-sucedido) e timeout (que limita o tempo que um passo pode tomar). Para testá-los, escrevi o seguinte no meu Jenkinsfile:

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
                      [ $((R % 3)) -eq 0 ] && sleep 10
          '''
         }
       }
    }
  }
}Code language: PHP (php)

Mesmo que você não conheça Jenkinsfiles, é possível ter uma ideia do que se passa:

  • No retry(3), coloquei um script shell que, a cada três execuções, falha. Assim, poderia ver Jenkins fazendo novas tentativas de executá-lo.
  • No passo de timeout(5, SECONDS) há um script que, a cada três execuções, demora dez segundos para terminar. Como há um tempo-limite de 5 segundos, essas execuções demoradas vão falhar.

Mandei o aquivo para GitHub configurei Jenkins para “buildar” o repositório e… todos os builds falharam! Isto não estava certo, eu esperava que o build falhasse pouco mais de um terço das vezes. O que estava acontecendo?

O problema é que, na minha cabeça, a condição do teste [ $((R % 3)) -eq 0 ] && exit 1 seria verdadeira um terço das vezes, fazendo com que o script encerrasse com status 1. (Todo status não-nulo é considerado falha.) Só que, quando a condição era falsa, o próprio teste falhava. Era o status do teste que Jenkins verificava!

Entendido o problema, foi fácil resolver: adicionei, ao final do script, um operador OU. Por quê? Porque se o teste passar, o exit 1 é executado e o script falha; se o teste não passar, porém, o sh vai executar o comando depois do ||, que sempre vai retornar zero.

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            [ $((R % 3)) -eq 0 ] && sleep 10 || echo 'slow ok'
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Funcionou! Exceto que agora o build raramente falhava, e nunca por tempo excedido. Por que será?

É que concatenei || echo 'slow ok' também na frente do passo do timeout. O script desse passo de fato levava 10 segundos para executar um terço das vezes, e Jenkins de fato cancelava o script.

O problema é que Jenkins “mata” o comando sleep 10, que então retorna um status de erro. Só que o echo 'slow ok' também “come” esse status de falha quando o build é cancelado. Por isso, mesmo quando abortado, o build aparecia como bem-sucedido. Por outro lado, eu não poderia simplesmente deixar o teste falhar, como antes.

A solução é aquela tão comum: parar de usar truquezinhos engenhosos e escrever código claro. Troquei os && e || por um condicional if. Convenhamos que até mais legível ficou, né?

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            if [ $((R % 3)) -eq 0 ] ; then 
              sleep 10
            fi
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Às vezes sinto que estamos na era CVS das ferramentas de CI, aguardando um Subversion que simplifique nossa vida. Mas confesso estar feliz que tenhamos, hoje essas ferramentas disponíveis. Por ora, vou focando em aprender com esses incidentes.

Busca textual no Liferay Portal com Elasticsearch – Parte 2: Arquitetura

Quando migramos de Lucene para Elasticsearch, Liferay manteve o mesmo processo: ao salvar uma entidade, indexamos seus campos. Só que, agora, no Elasticsearch.

Contudo, Elasticsearch não é uma biblioteca: é um sistema servidor altamente distribuído, rodando em clusters. Isto nos forçou a tomar novas decisões, e nos presenteou com algumas lições valiosas. Para entendê-las, convém revisar como Elasticsearch pode ser usado.

O modelo de programação de Elasticsearch

No Elasticsearch, os dados são salvos como pares de chave-valor, agrupados em documentos. Os documentos representam as instâncias das entidades.

Um índice contem vários documentos. Para efeitos práticos, todos os documentos de um índice são mesmo tipo (ao contrário de SGBDs, que aceitam várias tabelas em um banco de dados). O tipo define que campos são encontrados nos documentos e é usualmente dinâmico, permitindo que campos novos sejam adicionados por mera inserção.

Vários índices para vários portais

É muito comum trabalhar, no Elasticsearch, com vários índices diferentes em um mesmo cluster, para o mesmo formato de dados. Como seriam os índices do Liferay. Para responder isso, é necessário ter em mente que Liferay é multi-tenant.

Sabe quando você entra, digamos, em wordpress.com ou blogspot.com e cria um blog? Seu blog é totalmente independente de qualquer outro no mesmo site. O mesmo se aplica ao Liferay Portal, que permite criar instâncias independentes em uma mesma instalação.

Os engenheiros decidiram então que cada instância teria seu próprio índice no Elasticsearch. Separar os dados desta maneira nos permitiu controlar melhor o acesso de uma instância, assim como nos deu flexibilidade para otimizar Elasticsearch.

Por outro lado, embora houvesse vários índices, todos teriam o mesmo formato. Como seria esse formato?

Tudo junto e misturado

Seguindo as recomendações da Elastic, cada índice teria um só tipo. Neste tipo, indexamos as mais diversas entidades.

Na hora em que uma entidade é salva no banco de dados, os valores de suas colunas são indexados em um só tipo de documento. Assim, o post de blog, o artigo do CMS, o comentário no fórum… todos serão representados pelo mesmo formato. O título do blog, do evento, da wiki etc. se tornarão o campo title de vários documentos. O mesmo se passa com campos como content, summary, description

Estes campos representam características comuns a várias entidades. Quando todas elas são indexadas em um só índice, com um schema uniforme, a busca se torna mais simples: basca procurar pela string nestes documentos.

Os documentos Elasticsearch também contêm bastante metadados, sendo alguns dos mais importantes o campo entryClassName (o tipo da entidade que o documento representa, geralmente o nome de uma classe Java) e entryClassPK (identificador da entidade na tabela SQL). Desta forma, podemos encontrar o que o usuário procura e, a partir daí, recuperar a entidade correta do bando de dados.

Esta arquitetura é uma herança da busca com Lucene mas, nada surpreendentemente, funcionou perfeitamente com Elasticsearch. Esta foi uma de nossas primeiras lições:

Para implementar busca texutal, junte vários tipos de unidade em um só tipo de documento.

Decidida a arquitetura dos dados, a próxima questão a resolver era: como indexar e buscar as entidades no índice da instância. Este será o tema da parte 3 desta série.

Protected: Por que eu provavelmente não usaria Haskell em um projeto profissional

This content is password protected. To view it please enter your password below:

Busca textual no Liferay Portal com Elasticsearch – Parte 1: História

Qual a aplicação indispensável de um portal web?

Liferay Portal é uma plataforma de portais web, referência de mercado, open source e em Java. Um portal web é um software que suporta várias aplicações, chamadas portlets; portlets podem inclusive estar na mesma página web. O Liferay Portal, por exemplo, inclui CMS, blog, wiki, fóruns, calendário, formulários, workflow, e-commerce e muito mais, por padrão. Ainda é possível criar suas próprias aplicações e acrescentá-las ao portal.

Por que é difícil fazer buscas em um portal

Uma aplicação obrigatória em todo portal é a pesquisa global. Usuários querem uma barra de busca simples, que procure por dados de todas as diferentes aplicações.

Isto é desafiador em um portal baseado em SQL como Liferay. Cada aplicação salva seus dados em tabelas próprias. SQL “tradicional” não é uma solução razoável para estes casos: as queries seriam imensas e difíceis de construir.

Além disso, faltam a SQL funcionalidades básicas de busca textual. Alguns dialetos de SQL possuem capacidades extras, mas Liferay Portal pode rodar em mais de quarenta SGBDs diferentes. Não poderíamos utilizar extensões de vendors.

Outra solução era necessária.

Bibliotecas como solução

Logotipo da Lucene, em que a palavra "lucene" aparece escrita em letras minúsculas cursivas, e o "l" do começo possui algumas "franjas" apontando para trás, dando a sensação de velocidade, movimento para frente.
Logotipo de Lucene

Essa outra solução era Apache Lucene, a biblioteca Java de indexação e busca. Lucene tornou-se praticamente a escolha natural quando se trabalha com pesquisa textual. Mesmo soluções mais robustas como Solr e Elasticsearch são construídos ao redor dele.

Como utilizávamos a biblioteca? Quando o Liferay salvava uma entidade buscável no banco SQL, passava seus campos para o Lucene. Lucene indexava o conteúdo (em um formato eficiente para busca) e o salvava no disco.

Havia porém um desafio. Em produção, portais Liferay sempre rodam em clusters. Como o Lucene salvava os dados no disco local, o conteúdo era indexado apenas no nó do cluster Liferay que recebeu a requisição. Para superar esta limitação, sincronizávamos os índices de Lucene com JGroups, uma biblioteca de mensageria que enviava mensagens entre os vários nós.

Utilizamos esta solução por vários anos… até adotarmos Elasticsearch.

O que é Elasticsearch?

Logotipo atual do Elasticsearch, em que três figuras geométricas formam um "E" meio arredondado.
Logotipo atual do Elasticsearch. (Confesso que achava o antigo mais fofinho.)

Resumidamente, Elasticsearch é banco de dados NoSQL criado com ênfase em para busca textual.

Baseado em Lucene, Elasticsearch é extremamente escalável, distribuído e simples. Hoje, além de ser quase um padrão de facto para busca textual, Elasticsearch é popular para análise de dados, monitoramento e segurança, especialmente em combinação com o resto da Elastic Stack. Atualmente utilizamos para muitas destas outras funções na Liferay, mas hoje focaremos aqui na busca textual.

As razões da migração

Por que abandonamos nossa solução e adotamos Elasticsearch? Por várias razões.

  1. Lucene é uma biblioteca de baixo nível, o que tornava a manutenção do código mais trabalhosa e cara. Elasticsearch é mais poderoso, simples e flexível.
  2. Elasticsearch roda e suas próprias VMs e cluster, enquanto Lucene rodava na JVM de Liferay. Mover a busca para fora das VMs do Liferay reduziu os recursos necessários nos servidores Liferay.
  3. As JVMs separadas do Elasticsearch também permite otimizá-las para busca sem conflitos com as configurações ideais para portal das VMs do Liferay.
  4. Clusterização e sincronização já eram partes de Elasticsearch. Pudemos deletar milhares de linhas de código!

Além de tudo, Elasticsearch se tornava padrão de mercado. Foi uma escolha natural.

Neste processo, aprendemos muito sobre Elasticsearch. Veremos um pouco do que descobrimos na próxima parte.

Ajustando a Cartela de Kanban em Validação

Em Kanban, não se pode voltar uma tarefa pra fases anteriores. Como toda regra, há situações em que não se aplica. Quais? Veja nesse excelente post do grande Eduardo Zoby!

Um exemplo em especial me fez pensar.

Uma pulga atrás da orelha

O que fazer quando bugs críticos são encontrados em uma tarefa que satisfaz os critérios para revisão? Zoby explica que o time deixa a cartela em validação. Enquanto isso, subtarefas são criadas e executadas.

É uma maneira correta e funcional de modelar o processo. Como desenvolvedor, porém, sinto certo incômodo com este cenário. Por razões bobas, confesso, mas interessantes:

  1. Trabalhar em algo que está “em validação” soa impreciso. Nós estamos desenvolvendo quando corrigimos falhas.
  2. Pessoalmente, fico ansioso quando tenho de esperar pelo resultado de uma validação. Não há diferença prática, mas há uma psicológica. Temos agência sobre o trabalho que estamos desenvolvendo, mas não sobre o trabalho nosso que estão revisando.
  3. Falta clareza na posse da tarefa. Quem é o responsável, o desenvolvedor ou o validador? O tíquete define a quem está atribuído, mas a própria coluna acaba sendo dividida entre dois papeis.

Mas… como seria uma alternativa?

Imaginando uma fábrica

Sendo Kanban inspirado por processos de manufatura, talvez um retorno às origens pudesse oferecer alguma alternativa.

Me imaginei (porque não sei nada de engenharia de produção) em uma fábrica de carros. O que acontece com um automóvel em montagem se um de seus componentes se provou defeituoso? O carro não volta na esteira de produção, mas tampouco vai ser testado. Mais crucialmente, o componente também não volta para ser consertado: ele é descartado.

Não dá para mapear um processo industrial para engenharia de software, mas a ideia de descartar o componente é instigante. Descartar os “componentes” da nossa história é um bom caminho.

Não se conserta o amortecedor enquanto se testa o carro

Como seriam os “componentes” de nossa cartela. Seriam subtarefas, mas apenas se satisfizerem duas condições:

  1. são bem definidas o suficiente para serem avaliáveis;
  2. são pequenas o suficiente para que possam ser descartadas.

Durante o desenvolvimento, subtarefas seriam criadas prodigamente, inclusive e especialmente se a tarefa estiver “pronta” (naquele sentido obscuro que nós programadores damos ao termo). Uma vez terminada, cada subtarefa é enviada para validação; caso falhe, é fechada e outra subtarefa é aberta. Enquanto isso, a história continua em desenvolvimento.

Imagino várias vantagens neste processo:

  1. as alterações são revisadas e mergeadas gradativamente — bem à moda do trunk-based development.
  2. regressões seriam descobertas mais perto da mudança que as causou.
  3. estatísticas seriam provavelmente mais precisas: trabalho em bugs descobertos não seria mais computado como validação.

Certamente há pontos em aberto. O que fazer, por exemplo, se ao final a história não for aprovada? Há que se pensar. Eu tornaria a própria a revisão em uma subtarefa, mas nunca fiz isso. Como não sou engenheiro de produção, não sei o que fazem com os carros que falham ao final — talvez haja alguma inspiração la!

Um caso de uso peculiar do passado remoto

Isso tudo é um exercício mental, mas à medida que pensava, lembrei que já fiz algo similar.

Cuidei do Calendário da Liferay por anos, como uma “euquipe.” Comunicar-me com todos os stakeholders e codificar ao mesmo tempo era um desafio. Resolvi isto criando muitas subtarefas — alguns tíquetes chegaram a ter mais de trinta. Enquanto isso, os bugs e histórias permaneciam em desenvolvimento.

Às vantagens acima, somaram-se outras:

  1. os pull requests menores que eram revistos rapidamente.
  2. os stakeholders podiam ver o que eu estava fazendo, se quisessem.

Para um componente não-estratégico, estes detalhes foram fundamentais!

Vale a pena?

Deixar em validação ou em desenvolvimento a tarefa a corrigir é um detalhe semântico. Ainda assim, acho que pensar no assunto pode ser produtivo. Como não sou agilista, não tenho tanto know-how em definir workflows de times, mas o ponto de vista dos contribuidores individuais podem ser úteis. Quem sabe este aqui não é?