Como resolvi problema timeout ao gerar um PDF no Google App Engine (GAE) com PHP + Laravel
Eu tenho uma aplicação escrita em PHP + Laravel rodando no Google App Engine aproximadamente 1 ano, esta aplicação se trata em leitura e assinatura de documentos digitais (PDF).
Introdução
Minha aplicação funciona da seguinte forma, uma pessoa faz upload de 1 ou mais documentos em PDF e envia para uma determinada pessoa realizar a assinatura, vamos chamar de signatário, este signatário visualiza este documento e para assinar ele cria suas assinaturas (touch), envia fotos dos documentos e faz uma selfie com documentos em mãos, que são as evidências digitais, na última folha do PDF eu acrescento estas evidências digitais.
Problema
No final da assinatura é possível realizar o download do PDF assinado, porém quando esses documentos possuem muitas páginas, ocorre uma lentidão no processamento e em alguns casos dá um timeout.
Problema 2
O Google Cloud te cobra por tempo e processamento, então quanto maior o documento mais caro fica seu processamento.
1ª Solução — Google Cloud Functions
Durante a conversão dos arquivos sigo as seguintes etapas, converto arquivos do PDF original em imagem, edito as imagens inserindo marca d’água da aplicação e código da criptografia, então resolvi realizar esta conversão quando realizar o upload.
Primeira tentativa de resolver esse problema foi utilizando o Google Cloud Functions, onde criei um script em Node, quando um PDF é inserido no Google Cloud Storage, é chamado uma trigger onde converte o PDF em PNG e quando eu gero o PDF só monto as imagens já pré processadas (DOMPDF).
Funcionou perfeitamente nos primeiros meses porém tive um usuário que enviou um PDF com uma fonte incomum, até achei a fonte e iria inserir no servidor, porém o Google Cloud Functions cria uma máquina padrão onde não consigo editar e instalar nada nela, então fui para segunda solução.
2ª Solução — ConvertAPI
No desespero para resolver o problema contratei o serviço de conversão o convertAPI esse funcionou perfeitamente, ele cobra por segundos, exemplo vc compra um pacote de 6000 segundos, e de acordo com o tamanho dos seus arquivos ele desconta o tempo de processamento.
Mas aí surgiu um novo problema, percebi que meus arquivos dariam uma média de R$ 0,50 por documento, não pagaria o custo de envio, teríamos um grande prejuízo futuro, aí fui para a terceira solução.
3ª Solução — Serviço de conversão próprio
Criei minha própria API, no conceito de micro serviço, primeiro desacoplei totalmente a dependência de exibir o contrato se ele não tivesse sido convertido, ou seja, utilizei o plugin PDF.js.
Minha API criei o serviço de conversão, e hospedei na Digital Ocean, custo bem mais acessível do que o Google Cloud, ele funciona da seguinte forma:
- Usuário faz o upload dos arquivos que deseja assinatura
- Usuário seleciona os signatários
- Usuário envia o documento para assinar, nesse momento realizo uma chamada para meu serviço de conversão onde ele baixa o PDF, converte e posta as imagens convertidas no Google Cloud Storage.
Minha aplicação principal antes de exibir os documentos em PDF checa se existem arquivos de imagem convertidos, se existir ele exibe as imagens, caso contrário ele exibe o PDF com o plugin PDF.js.
E a conversão do PDF?
Como já tenho as imagens convertidas, a cada nova assinatura é disparado uma chamada para minha API, e ela gera o PDF e envia uma cópia para o email de quem assinou.
Mas… temos outro problema, e se 1000 pessoas assinarem ao mesmo tempo, o servidor cai!?
R: Não, para resolver isso estou utilizando as queues do laravel, onde limito a quantidade de processamentos simultâneo.
E quando clica em download?
R: Mesmo padrão é realizada uma chamada para minha API, e enviada uma mensagem para o usuário informando quando arquivo dele estiver pronto ele receberá uma notificação por e-mail, e esse processamento tbm entra na fila de conversão.
Resultado
- Reduzi o custo de conversão de $ 100 USD para $ 20 USD, com qualidade e velocidade.
- Conversão ficou escalável.
- Futuramente irei implementar o mesmo conceito baseado em instâncias com balanceamento de carga.