Java EE - Como lidar com transações que envolvem arquivos e file system?

Caros,

Estou com dificuldades no meu projeto Java EE. Dentre outras coisas, meu http endpoint possui uma funcionalidade que envolve upload de arquivos. Para persistir os uploads, vou ficar no feijão com arroz de sempre: guardar metadados a respeito do upload em um banco relacional, mas guardar o arquivo em si no sistema de arquivos do servidor.

Como vocês já devem estar imaginando, quero que tanto a escrita do arquivo no sistema quanto a gravação de dados no DB seja feita atomicamente.

Comecei a implementar manualmente a lógica de negócios em um EJB mas, quando me dei conta, estava com dezenas de linhas de código, sendo que a maioria continha lógica envolvendo a transação (commit, rollback e afins). Me pergunto: afinal, não é para isso que servem os XA resource managers? Comecei a procurar alguma biblioteca que funcionasse como um XA resource manager para file system. Encontrei algumas respostas no StackOverflow, encontrei uma biblioteca chamada XADisk, mas o “problema” é que muitas delas estão bem velhas (datam de 2013), não sei se é seguro utilizá-las.

Dado isso, minhas dúvidas são:

A: Estou pensando da melhor forma? Como lidar com sistema de arquivos do ponto de vista arquitetural?

B: Algum de vocês já utilizou algo parecido? Como eu devo fazer para garantir a saúde de minhas transações distribuidas envolvendo arquivos?

C: E do ponto de vista do JavaEE, como é que eu lido com sistemas de arquivos? Me dá uma sensação bem estranha acessar as pastas do sistema operacional pelo EJB, mesmo colocando o diretório alvo em uma propriedade dentro de um XML de configuração.

Obs: Por favor, se sua resposta envolve microservices, me mostre como pensar a respeito da coordenação da transação (é justamente isso que eu não quero fazer manualmente, quero que o Transaction Manager faça para mim, nada de reinventar a roda!).

Agradeço desde já!

Um EJB transacional deve resolver seu problema perfeitamente, sendo que qualquer RuntimeException lançada pela sua aplicação vai dar rollback, a unica logica extra que possível mente você provavelmente vai ter que fazer é de verificar os arquivos que já foram gravados caso sejam vários arquivos.

Bom dia lvbarbosa

Complicado em…ja tive casos parecidos com outros tipos der recursos também fora de XA…

  1. Opção 1 é tentar usar um XA ai q vc falou…como envolve plataforma, veja se realmente funciona. Tem mesmo? Manda os links ai…nunca via XA para garantir commit de arquivos em disco.

  2. Se primeira não der, vai para mensageria. o EJB comita na transação XA usando mensagem para uma fila ai, no qual vc assincronamente vai garantir a escrita do arquivo. Uso assim em todos os casos que no qual o recurso não tenha XA…e sempre da certo…Vc amarra a excetpions do sistema como garantia de commit de consumo de fila e se caso falhar, a fila cai na politica de RETRY. Se tudo certo na transação…praticamente vc tem o mesmo serviço usando fila transparente. Se ocorrer erro no disco, sua transação de sistema ja foi comitada, e a fila vai tentando gerar o arquivo de tempos e tempo. Ótima arquitetura…

  3. opção seria fazer oq o amigo Henrique falou…vc deixar o código que gera o arquivo como ultima operação do seu EJB…caso gerar exceptions…vc da roolback total na operação…ou até garantir dentro do método transacional uma pesquisa em disco para garantir que o arquivo esta la…ou coisa do tipo…Mas ainda prefiro a opção 2;

O problema é que a quantidade de caminhos que a transação pode tomar me incomoda bastante.

Caso 1: persist no DB dá certo e escrita do arquivo não dá IOException. Beleza, funcionou!

Caso 2: Persist no DB dá errado. Ok, posso usar BMT para saber que a transação deu errado e tomar uma decisão. Nesse caso, nem tento salvar o arquivo.

Caso 3: Persist no DB dá certo, mas escrita no FS dá errado. Posso salvar o arquivo no FS só depois do commit da BMT, se o commit não der rollback (ou mesmo usar CMT e ver no context se a transação foi marcada para rollback, ou ainda capturar uma RollbackException). Mas aí, se por acaso a escrita do arquivo der errado, ferrou, preciso emitir um DELETE pro DB.

Eu consegui implementar isso, porém o código não ficou da melhor maneira possível e acredito que exista uma maneira mais inteligente de fazê-lo! Queria que a lógica de rollback fosse garantida por um XA File System ou algo parecido, o código ficaria muito mais limpo.

Valeu!

Bom dia Fernando!

Respondendo…

1. Eu encontrei esse daqui ó: XADisk. Meu medo é saber se é seguro usar isso mesmo.

2.: Gostei da ideia de async! Vou tentar aqui, obrigado.

3.: Foi o que eu tentei fazer inicialmente! Eu consegui aqui, porem não tenho certeza que consegui pensar em todos os edge cases, isso me incomoda bastante, mas acho que é o jeito hahahahaah

Obrigado!

Mas XA para filesystem não tem que amarrar com a plataforma? Tipo provider para WINDOWS outro para LINUX?
Mesmo sendo velho, deve funcionar…tenta ai…e posta aqui para nos se deu certo!
Achei super legal cara! Vou colocar aqui para usar isso uma hora…

Eu não tenho ideia de como funciona, só abri a página inicial e torci pra alguém aqui já ter usado kkkkkkk.

Vou experimentar! Eu acredito que você deve poder configurar a questão da plataforma, mas sinceramente não sei como é.

Eu tava vendo também a questão de escrever um JCA personalizado, porque aí poderia ser reutilizado né! O negócio é que eu não sei se ainda se usa JCA no Java EE 7, e se vai continuar no 8.

Escrever um JCA acho que complicado em…dai vc perde o foco…Melhor tentar usar esse cara au, ou fazer assíncrono com uma filinha! Tenta ai e me avisa…kkkkk

Salvar um arquivo no filesystem é uma operação atômica. Nunca ouvi falar do arquivo ficar “meio salvo”.

Se esta falando de criar uma transação que dura todo o processo de upload, não acho uma boa idéia. O que impede alguém de tentar enviar vários arquivos de 1gb pro seu endpoint?

Atômica no sentido de que as duas (tanto a persistência no DB quanto a escrita do arquivo) devem ser efetuadas na mesma unidade de trabalho. Se a escrita do arquivo falhar, o registro no DB não deve ser salvo e vice-versa.

O limite do upload pode ser configurado no servidor.

Uma transação de banco leva millisegundos pra executar, enquanto um upload de arquivo leva o que? 10 seg. - 1 min. Enfim, não acho que é uma boa ideia incluir essas duas operações na mesma unidade de trabalho por causa disso. Transações que demoram pra finalizar são mais propícias a sofrerem rollbacks.

Qual problema vc está tentando resolver com isso?

Não havia pensado nisso, você tem razão!

Meu problema não parece ser nada extraordinário. Os administradores do sistema vão poder gerenciar itens. Esses itens possuem imagens (0…*), mas não quero que elas sejam blobs no DB por questão de performance. Pensei em salvar o path no DB, junto com alguns outros meta-dados e utilizar isso para recuperar as imagens no sistema de arquivos.

Será que existe alguma maneira melhor de fazer isso?

EDIT: Estava pensando aqui, o upload é feito antes da transação. Quando chega no EJB e a transação começa, já existe a InputStream apontando pro arquivo enviado, que é passada por argumento pro método transacional. Mas mesmo assim, me parece que o pipe da stream pro FS pode tomar bastante tempo e tornar a transação mais lenta.

O ideal seria começar a transação depois que já recebeu todo o arquivo e verificou que não esta corrompido.

Muda o método transacional pra receber o arquivo, já transferido, e que vc vai usar pra salvar no filesystem durante o commit. Em vez de um inputstream pra ser consumido durante a transação.

Se a transferencia do arquivo falhar, não tem nem porque chamar o método transacional.

Entendi. Acho que não é um problema carregar o arquivo inteiro na memória, o limite vai ser de uns 5mb e não é uma operação que vai ficar acontecendo o tempo inteiro, mas só algumas vezes por mês.