Este projeto foi elaborado durante a disciplina de Programação Distribuída na UFRN, ministrada pelo professor Nélio Cacho. Se trata de um sistema distribuído de extração de dados de editais que agrega recursos de observabilidade, padrões de resiliência, agentes inteligentes, etc.
O foco deste projeto não é ter um produto final imediatamente aplicável, mas aprender e aplicar conceitos importantes de sistemas distribuídos. Você pode utilizá-lo como bem entender, vou deixar uma licensa MIT.
São ao todo 7 microsserviços:
- Configuration (/config-server):
- Fornece para os outros microsserviços os
arquivos de configuraçãocentralizados em um repositório.
- Fornece para os outros microsserviços os
- Eureka (/eureka):
- Responsável pela
descoberta de serviços.
- Responsável pela
- Gateway (/gateway);
- Notices (/notices):
- Microsserviço acessível pelo Gateway que permite criar e atualizar editais existentes;
- Vetoriza os documentos enviados.
- Extractions (/extractions):
- Microsserviço interno que realiza de forma assíncrona (não bloqueante) a extração dos dados dos editais;
- Faz uso de um
agente de IApara extrair dados dos editais, usando as ferramentas expostas peloExtractions MCPe recursos deRetrieval-augmented generation (RAG); - Também envia um e-mail quando termina de extrair os dados usando o
Mailtrap MCP:- Como não deixei uma variável para definir o destinatário do e-mail, você pode configurar alterando no arquivo /extractions/src/main/resources/prompts/system_notice.xml.
- Extractions MCP (/extractions-mcp):
- Fornece as ferramentas para que um agente de IA possa extrair os dados de um edital e persistir eles no banco de dados.
- Serverless (/serverless):
- Um microsserviço utilizado para mostrar o funcionamento básico de um microserviço serverless, com apenas uma função registrada e que retorna informações sobre o hardware do computador.
Você vai precisar alterar seu arquivos /hosts. No Ubuntu o caminho dele é /etc/hosts. Sem esse mapeamento, você não conseguirá iniciar com facilidade as instâncias do Eureka. No arquivo, deve conter o conteúdo abaixo (ou similar):
127.0.0.1 localhost
127.0.0.1 server1
127.0.0.1 server2
127.0.0.1 server3Para que a IA consiga enviar o e-mail corretamente, você precisará configurar também o Mailtrap MCP, para isso, preencha adequadamente os campos do arquivo /extractions/src/main/resources/mcp-servers-example.json. Uma vez preenchido, renomei o arquivo para mcp-servers.json.
Na pasta (/config-server/src/main/resources/)[/config-server/src/main/resources/], deixei no arquivo application.properties o servidor configurado para procurar as configurações em um repositório privado. É importante que seja privado, uma vez que há nas configurações várias chaves secretas.
spring.cloud.config.server.git.uri=https://github.com/L-Marcel/distributed-system-with-spring-ai-config.git
spring.security.user.name=root
spring.security.user.password=root
spring.cloud.config.server.git.search-paths={application}/{profile}
spring.cloud.config.server.git.default-label=main
spring.cloud.config.server.git.username=L-MarcelEstou deixando para você um repositório template para as configurações, disponível em Distributed System With AI Config Template. Há instruções extras no readme.md deste mesmo template destinadas exclusivamente ao repositório de configurações.
Além disso, você vai precisar criar um arquivo nessa mesma pasta chamado secret.properties. É nele que há a chave de acesso para o repositório privado.
spring.cloud.config.server.git.password=<ACCESS TOKEN>Você pode conseguir esse token no GitHub, indo em Personal Access Token e criando um token de acesso restrito apenas ao seu repositório de configuração. Cuidado para não deixar esse token vazar! E se vazar, apague-o o quanto antes e crie um novo.
Este projeto não está utilizando o autoconfigure do Spring AI que permite uma troca rápida entre modelos de empresas diferentes. Então, como para este projeto utilizei o model gpt-5-mini da Open AI, se quiser utilizar algum modelo, por exemplo, do Gemini, vai precisar mexer nas configurações manuais que fiz de alguns beans (ou migrar para as configurações automáticas).
Essas configurações manuais estão, geralmente, nos arquivos AiConfiguration, localizados dentro das pastas configurations de cada microsserviço.
Por que não usei as automáticas? Enfrentei muitos problemas de compatibilidade entre o Spring Cloud e Spring AI. Pode soar arrogante, mas acredito ter relação com o fato do Spring AI ainda está meio prematuro no momento em que este projeto foi desenvolvido (por ser um pacote recente). É bem provável que em versões futuras esses problemas, que não vou detalhar aqui, sumam.
Alguns microsserviços não vão iniciar se não conseguir se comunicar, por exemplo, com o banco de dados. Dito isto, deixei um arquivo docker-compose.yml pronto. Você só precisará ter o Docker corretamente instalado na sua máquina e executar o comando:
docker compose up -dEsse comando inicializará os seguintes containers:
- Redis (utilizado para rate-limit do gateway);
- Postgres (banco de dados padrão);
- Prometheus (extração de métricas);
- Grafana (observabilidade);
- Zipkin (rastreamento).
Eu sei que para um sistema distribuído o Postgres pode não ser uma boa ideia, no geral temos que olhar para as propriedades: tolerância a partições, consistência e disponibilidade; pensando no Teorema CAP, ou sua extensão, o Teorema PACELC. Contudo, se trantando de uma aplicação simples e que não vai escalar muito para a finalidade a qual a desenvolvi, escolhi o Postgres unicamente pela familiaridade.
É necessário ter o JDK 21 e o Maven — estou usando a versão 3.8.7, mas não acho que a versão se tornará um problema para você.
Sobre o ambiente, recomendo o uso do VSCode com o Extension Pack for Java. Mas, para a execução de alguns microserviços, que devem iniciar com profiles específicos, como é o caso do Eureka, ele não vai ser muito útil.
Antes de executar quaisquer comandos, dê um olhada na ordem de execução.
Vale destacar, para todos os comandos listados a seguir, deixei uma Task do VSCode configurada em /.vscode/tasks.jon. Utilizando a extensão Task Runner você deve conseguir executar tudo com mais facilidade.
Além disso, alguns comandos seguem parâmetros de configuração para otimização do uso de recursos de memória (principalmente). Logo, talvez você queira mudar um pouco alguns em um cenário de uso real. Segue a lista de comandos:
- Compilar tudo:
mvn clean install
- Executar configuration (/config-server):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar config-server/target/config-server-*.jar - Executar eureka server 1 (/eureka):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar eureka/target/eureka-*.jar --spring.profiles.active=dev,server1 - Executar eureka server 2 (/eureka):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar eureka/target/eureka-*.jar --spring.profiles.active=dev,server2 - Executar eureka server 3 (/eureka):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar eureka/target/eureka-*.jar --spring.profiles.active=dev,server3 - Executar gateway (/gateway):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar gateway/target/gateway-*.jar - Executar extractions MCP (/extractions-mcp):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar extractions-mcp/target/extractions-mcp-*.jar - Executar extractions (/extractions):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar extractions/target/extractions-*.jar - Executar notices (/notices):
java -Xms128m -Xmx256m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar notices/target/notices-*.jar - Executar serverless (/notices):
- Windows:
java -Xms64m -Xmx128m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar serverless/target/serverless-*.jar - Linux (precisa de privilégio de administrador):
sudo java -Xms64m -Xmx128m -XX:TieredStopAtLevel=1 -XX:+UseSerialGC -Xss256k -jar serverless/target/serverless-*.jar
- Windows:
Tive a ideia de utiliza o pacote oshi-core no serverless para que ele retornasse alguma informação mais interessante, neste caso, dados do hardware do computador que está hospedando. O problema é que no método .getPhysicalMemory(), se você olhar a descrição atribuída a ele, está escrito (pelo menos na versão que utilizei):
Physical memory, such as banks of memory.
On Linux, requires elevated permissions. On FreeBSD and Solaris, requires installation of dmidecode.
Não perdi permissão no Windows é que talvez seja estranho, parando para pensar. Segue o trecho do código em que este método é chamado no arquivo /serverless/src/main/java/ufrn/imd/serverless/functions/SystemFunctions.java:
@Configuration
public class SystemFunctions {
@Bean
public Supplier<String> info() {
SystemInfo system = new SystemInfo();
return () -> {
StringBuilder builder = new StringBuilder();
builder.append("Processor: ");
builder.append(system.getHardware().getProcessor().toString());
builder.append("\n\nMemory: ");
builder.append(system.getHardware().getMemory().getPhysicalMemory().getFirst().toString());
return builder.toString();
};
};
};Respeite a ordem de execução e regras estabelecidas abaixo, e sem esquecer de subir os containers do Docker antes.
- Configuration (/config-server):
- Configurado para apenas uma instância;
- Espere ele iniciar corretamente, todos os demais vão tentar se comunicar com ele assim que iniciarem.
- Eureka (/eureka):
- Configurado para exatamente trẽs instâncias;
- Espere ao menos uma instância iniciar corretamente;
- É normal algumas instâncias lançarem um
warningpor não encontrarem as demais em um primeiro momento.
- Espere o banco de dados está disponível.
-
Serverless (/serverless);
-
Extractions MCP (/extractions-mcp):
- Espere o banco de dados está disponível.
- Extractions (/extractions):
- Espere o
Extractions MCPestá disponível peloGateway.
config server - http://localhost:8888/
eureka 1 - http://localhost:8761/
eureka 2 - http://localhost:8762
eureka 2 - http://localhost:8763/
gateway - http://localhost:8080/
notices - http://localhost:8080/notices/
extractions - http://localhost:8080/extractions/
extractions mcp - http://localhost:8080/extractions-mcp/
serverless - http://localhost:8080/serverless/
prometheus - http://localhost:5432/
grafana - http://localhost:8760/
zipkin - http://localhost:9411/
Mande um PUT na rota http://localhost:8080/notices/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 enviando dentro de um multipart/form-data o .pdf do edital no formato application/pdf. Deixe um edital público que pode ser usado como exemplo em /tests/docs.
Por que o PUT vai funcionar em um UUID específico antes de criar qualquer coisa no banco de dados? Bom, deixei dados mockados em /notices/src/main/resources/data.sql.
Você também pode testar o POST na rota http://localhost:8080/notices enviando dentro de um multipart/form-data o .pdf do edital no formato application/pdf. Neste caso, acho que vai precisar de um .pdf com nome diferente do mockado.
curl -X POST http://localhost:8080/notices/actuator/refreshUma vez tendo configurado tudo, e iniciado as instâncias, você pode testar utilizando o Apache Jmeter. Basta importar com ele o arquivo /tests/jmeter.jmx e executar os testes. Há nele também exemplo de requisições para o notices.