Executando verificação de segurança...
2

Ajuda: Uma decisão difícil para um Framework Web baseado em Rust e Lua

A alguns dias tenho pensado como seria divertido criar um ecossistema do zero com Rust e lua, criando um framework similar a Next.js ou Sveltekit, porém executando tudo no server é claro. A ideia seria simples criar uma biblioteca Rust que possui um server (Axum para ser exato) e mapeia scripts lua com base em uma estrutura de diretórios. Cada script recebe seja por variável global ou parâmetro de função, o contexto da requisição, e responde de forma apropriada.

No momento lua está rodando embarcado no binário final por meio da biblioteca mlua. E aí que entra meu problema:

O problema:

Como chamar esses scripts de forma assíncrona ou multithread. Para começar os estados de lua não são thread safe por motivos obveis e não podem ser compartilhados entre threads.

Algumas soluções me vieram a mente:

  • A imediata é: criar um state para cada requisição que vai morrer no fim dela. Parece uma solução horrível, porém era assim que Scripts PHP executavam, subiam, retornavam e morriam.
  • Outra solução é manter um pool de estados, um para cada thread digamos. Assim cada request será respondida de forma sincrona. Isso limita o gasto de recursos, porém não achei nenhuma solução opensource que utilize essa abordagem, por favor me corrijam se estiver errado.
  • Por último, bolar um event loop para tornar o call de cada script assíncrona. Esse pode ser usado em conjunto com o state pool, porém não é exatamente fácil de implementar.

Um detalhe importante: os Scripts não possuem estado, são funções puras, recebem o contexto, serviços injetados e respondem a request. Ponto.

E aí? O que você acha? Que sou maluco? Bom, isso é claro. Mas adoraria testar os limites de um projeto como esse no meu tempo livre. Portanto gostaria de uma solução, mesmo que não ideal.

Carregando publicação patrocinada...
3

Vou primeiro passar pelas soluções que vc mesmo propôs:

  1. criar um state para cada requisição que vai morrer no fim dela: a única parte "horrível" que consigo ver aqui é o desempenho que seria impactado significativamente por ter de criar e carregar o lua state a cada requisição. Algo que pode aliviar isso seria manter um pool de estados em branco pré-criados que só são consumidos quando há requisições. Este pool poderia ser preenchido constantemente em uma thread separada, isso aliviaria mais ainda o problema de desempenho e acredito que não teria problemas com threads, uma vez que ele nunca é usado simultaneamente em dois threads diferentes.

  2. outra solução é manter um pool de estados: Bem eu devia ter lido esse antes de escrever meu ponto acima :) Mas acho que a diferença é que vc não precisa separar por threads. Não importa em qual thread o lua state é criado, mesmo que seja utilizado em outro thread. Isso não acarreta em problemas de multithreading pois ele nunca é usado ao mesmo tempo em dois threads. Quanto à solução opensource que utilize essa abordagem: também não sei dizer.

  3. por último, bolar um event loop para tornar o call de cada script assíncrona: não conheço a linguagem Rust pra poder afirmar, mas imagino que vc teria que implementar todo o core de event loop e async em todos lugares que fazem I/O. Me parece extremamente complexo. Eu sei que existe o Tokio (https://tokio.rs/) para Rust, mas como nunca usei não posso dizer muito; talvez ele te ajude aqui, mas talvez vc precisaria re-implementar toda biblioteca de I/O do Lua, pra que leve em conta o assincronismo e isso me parece um trabalho absurdamente gigante!

Agora deixa eu questionar a primeira solução que vc deu e que eu complementei. Por que o state precisa morrer ao final da requisição? Não poderia ele ser long-lived, isto é, servir várias requisições no decorrer da vida dele, sem que precise morrer e ser re-criado a cada requisição? Isso é um requisito que vc tem que definir, mas estou só questionando o porquê dele. Se vc quer garantir absolutamente que não haverá estado compartilhado entre uma requisição e outra, então faz sentido. Mas talvez também faça sentido assumir que o programa Lua que rodar não vá alterar o estado -- é que claro que daí cabe ao programa respeitar isso; a garantia não é tão grande quanto re-criar o estado todas as vezes. Aí vc quem decide, já que o requisito é vc quem está dando.

Se não estou enganado, os servidores que rodam através de Fork (e.g. Apache) funcionam assim: eles sempre rodam a aplicação dentro de um fork, uma vez. Portanto, quando o processo forkado termina, o estado é eliminado. Semelhante ao seu requisito, mas usando CoW (copy-on-write) do próprio OS+hardware pra fazer o equivalente que vc faria de criar o lua state toda vez. Vc também poderia fazer desta forma!

Já outros servidores, tipo o puma do Ruby (falo dele pois conheço relativamente bem), compartilham o estado entre requisições mesmo e deixam para a aplicação a resposabilidade de não mexer no estado compartilhado. Mais suscetível à bugs, porém é comum rodar sistemas em produção dessa forma.

1

Foi bem elucidativo, obrigado. O requisito do estado morrer ao fim de cada requisição é pelo próprio Rust não permitir o compartilhamento do state entre threads. Apesar de que ainda não testei algumas ideias que podem resolver isso.

Para criar uma solução assíncrona realmente pensei no Tokio, até por que ele já é a base do servidor web. Vou dar uma olhada para ver o quão tranquilo seria implementar isso.

Vou manter em mente a solução de consumir a pool. No caso de não conseguir algo mais eficiente já é um bom começo, bem melhor que criar um por conexão.

Muito obrigado por comentar.

2

A solução imediata de criar e destruir estados Lua para cada solicitação é prática e sensata. Ela oferece simplicidade, isolamento e segue um modelo comprovado. Não tem nada de 'horrível'.

Embora as preocupações sobre o desperdício de recursos sejam válidas, é preciso avaliar e se certificar por meio de profiling, antes de considerar abordagens mais complexas. As soluções alternativas, embora potencialmente mais eficientes, introduzem complexidade e portanto devem ser justificadas com dados sólidos e necessidades reais de desempenho.

1

Muito obrigado. Realmente, acho que uma boa qualidade de testes e benchmarks realmente sejam necessários e eu possa mitigar essa otimização até ser necessária. Mas vou manter as soluções em mente para o futuro.