JobMonitor: mudanças entre as edições

De BIS Wiki
Ir para navegação Ir para pesquisar
Linha 78: Linha 78:


Um ponto a se observar aqui é que, embora o JobStatus possa ser recuperado e ser único na memória quando estamos dentro da mesma JVM, é preciso observar que ao devolver o JobStatus através de uma fachada utilize o [[SessionManager#SMInterceptor]] ele será clonado justamente para prevenir esse tipo de apontamento e modificação "no objeto original". Assim, não mais representará o mesmo objeto na memória. Ao retornar o objeto pela fachada, o outro lado deve utilizar o jobUUID para recuperar o objeto correto do JobMonitor para receber atualizações diretamente.}}
Um ponto a se observar aqui é que, embora o JobStatus possa ser recuperado e ser único na memória quando estamos dentro da mesma JVM, é preciso observar que ao devolver o JobStatus através de uma fachada utilize o [[SessionManager#SMInterceptor]] ele será clonado justamente para prevenir esse tipo de apontamento e modificação "no objeto original". Assim, não mais representará o mesmo objeto na memória. Ao retornar o objeto pela fachada, o outro lado deve utilizar o jobUUID para recuperar o objeto correto do JobMonitor para receber atualizações diretamente.}}
= Ciclo de Vida do Job =
Abaixo os passos do ciclo de vida do Job para exemplificar como tudo funciona:
# Criamos a tarefa instanciando a classe Job: new Job(...);
## Já no construtor, '''Job''' se registra no '''JobMonitor''' e ganha seu '''jobUUID'''.
## Com o '''jobUUID''' recebido, '''Job''' cria o objeto final '''JobStatus'''.
## Se o Job criado estiver uma Thread que tenha uma sessão do '''SessionManager''', a Thread do Job é anexada à mesma Sessão do usuário.
##: Não confundir a sessão se segurança com a Transaction do Container. O Job continuará rodando em uma Tread fora da Transaction, a única situação aqui é que ao chamar SessionManager.getSession() teremos uma identificação/autenticação de segurança.
# A partir da instância de '''Job''' que fora criado, podemos obter o '''JobStatus''' e o '''jobUUID''' através dos respectivos métodos: '''job.getJobStatus()''' e '''job.getJobStatus().getJobUUID()'''
# Iniciamos a tarefa disparando a Thread do job com '''job.start()''';
## Neste momento a Thread do Job começa a ser executada.
# O método deve (provavelmente) retornar o '''JobStatus''' ou o '''jobUUID''' para que o Caller consiga acompanhar a tarefa e solicitar seu cancelamento.
== Dentro do JobMonitor ==
Dentro do JobMonitor, quando a tarefa se registrou, o JobMonitor cria um Timer de segurança que verifica se o Job não foi esquecido dentro do JobMonitor e não teve seu .start() chamado. Passado o tempo do timer (no momento 1h) se o Job não estiver sendo executado, forçaremos a limpeza do Job como um vazamento de recursos. Caso esteja rodando, faremos um Log de que a tarefa está demorando demais, tarefas do JobMonitor não devem ser demoradas desta forma, tarefas de manutenção devem ser executadas pelo Scheduler. JobMonitor se destina mais para tarefas do usuário que possam demorar um pouco mais que o desejado para deixar a tela travada, não para execuções de longo prazo desse jeito.
== Dentro do Job ==
* Quando instanciado o Job tem o seu atributo '''step''' definido como '''IDLE''', indicando que a tarefa está inerte por ainda não ter sido iniciada. Ao chamar o '''.start()''';
* Antes da chamada do método '''runJob(...)''' o '''step''' é atualizado para '''RUNNING''', e deve ficar neste passo enquanto a implementação do método é executada.
* Caso este método retorno normalmente, o objeto retornado é salvo dentro do '''JobStatus''', no atributo '''jobReturn''', e o '''step''' é alterado para '''FINISHED''' indicando que a tarefa terminou sua execução com sucesso.
* Caso o método lance qualquer Exception o '''step''' é alterado para '''EXCEPTION''' e a Exception lançada é salva no atributo '''exception''' do '''JobStatus'''.
Ao finalizar o Job, independente do sucesso, o Job notifica o '''JobMonitor''' de que o Job terminou. Ao realizar essa tarefa, o JobMonitor cancela o Timer que ele criou no registro (explicado na sessão anterior). No entanto ele cria um novo timer (atualmente de 30min) que eliminará por completo qualquer outra referência do '''Job''' de dentro do '''JobMonitor''' para garantir que todos os recursos serão liberados e estarão disponíveis para o GC.
O '''JobMonitor''' não excluí imediatamente os recursos porque essa notificação ocorre no momento em que a tarefa se extinguiu, e outras Thread ainda podem precisar encontrar o '''JobStatus''' tanto para recuperar o retorno do método (ou a Exception) para notificar o usuário.
Uma vez que a Thread cliente recupera o "último" JobStatus, obtendo seu retorno, é possível (e recomendado) acelerar a liberação dos recursos chamando o método '''JobMonitor.cleanJob(jobUUID)'''. Assim, tanto os recursos utilizados pelo Job, quando o Timer criado serão liberados. E a Thread cliente fará o que for necessário, salvando ou não, o JobStatus e seus retornos.


= JobChecker =
= JobChecker =

Edição das 15h21min de 31 de outubro de 2020

O JobMonitor é o serviço do BISFW que permite que tarefas sejam executar em uma Thread paralela para que a thread principal não fique bloqueada esperando. Esse recurso é extremamente útil para tarefas demoradas, uma vez que permite retornar status sobre a execução da tarefa enquanto ela acontece, mesmo que seja pela interface WEB.

Criando o Job

Para colocar uma tarefa em background basta implementar a classe Job e iniciar a execução dentro do método runJob().

Exemplo Tarefa em Execução em Background
Job job = new Job("Título da Tarefa") { //O título da tarefa é colocado no título da thread, o que ajuda no DEV.
  @Override
  public Object runJob(Job job, JobStatus jobStatus) throws Throwable {
    //Código para execução da tarefa...
  }
};
job.start(); //inicia a tarefa


Quando a tarefa iniciar, o método runJob() receberá dois parâmetros:

  • job - referência da própria instância do Job criado.
  • jobStatus - Instância do objeto de status do job. Dentro desse objeto a tarefa deve atualizar as propriedades como "mensagens", percentual de tarefa realizada, etc..


JobStatus é passado por referência
Note que este objeto é passado como referência de memória, isto é, ele não precisa ser retornado o conteúdo interno dele é "atualizado" automaticamente para todos que tiverem a mesma referência. O objeto JobStatus é um objeto final dentro da classe Job.

Este objeto é tanto o que é passado para dentro dos métodos, quanto para o métodos que solicitam informação sobre o andamento do Job.

Deve-se tomar cuidado quando esse objeto for enviado para outras partes do sistema, como por exemplo atravessar uma fachada, ou qualquer outra situação em que o objeto é clonado e não mais usada a mesma instância da memória. Nestes casos alterações no novo objeto não refletirão no objeto original.


Interrompendo o Job

O cancelamento do Job em segundo plano pode ser solicitado pelo usuário. Para que isso ocorra a interface deve chamar o método interrupt() do JobStatus. Ao chamar esse método o JobStatus ficará marcado que a tarefa obteve a solicitação de cancelamento de sua execução.


Note que depende do Job Implementado verificar se essa solicitação foi realizada e parar a execução da tarefa tomando as providências necessárias (como dar rollback em tarefas pela metade ou concluí-las e cancelar depois, etc.). Há duas maneiras do Job verificar esse cancelamento:

  • Verificação Manual - Na verificação manual o desenvolvedor pode utilizar o método getInterruptResquested() e obter o valor do atributo. Caso true, indica que a solicitação de cancelamento foi realizada e deste ponto em diante direcionar o método para os procedimentos de cancelamento, e lançar a exception.
    Recomendado lançar a exception:
    throw new BISValidationException("FW_ERROR_000004");
    Este Código de exception é utilizado para identificar que a operação foi cancelada pelo usuário.
  • Verificação Automática - Neste modo basta o desenvolvedor incluir a chamada do método checkInterrupt() do JonMonitor. Este método verifica o status do atributo e se tiver em true já lança a exception como citada acima (Validation com o código 'FW_ERROR_000004'). Só é preciso tomar cuidado para que o próprio código do Job não trate a exception e deixe que ela vaze para fora do método do Job.
    Note que neste método a exception é lançada diretamente, assim, o método que verifica o status deve ser bem posicionado. Ou realizar as tratativas no catch/finally.

JobMonitor

O JobMonitor é uma classe estática, como um Singleton, que controla e mantém todos os Jobs do Sistema. Ao criar uma instância de Job ele automaticamente se registra no JobMonitor. Mesmo antes de iniciar a tarefa o JobMonitor já tem a referência da classe e monitora seu status.

Quando o Job é criado, ela cria também uma instância final da JobStatus que tem um identificador único (UUID). Este UUID é o identificador do Job e pode ser utilizado para realizar operações no JobMonitor.


JobMonitor por JVM
Uma vez que JobMonitor é uma classe estática, ela é única por JVM e só será possível encontrar referência do Job se estivermos procurando dentro da mesma JVM em que o Job foi criado.

Em casos em que o Job é criado em outra JVM, será preciso implementar métodos de acesso ao JobMonitor remoto para realizar as operações desejadas.


Ao criar um Job, devemos nos preocupar em salvar o JobStatus, ou ao menos o UUID para que seja possível referenciar a tarefa em background a partir do JobMonitor.


Exemplo de Criação do Job e Obtenção do Status a partir do JobMonitor
//Implementação do Job
Job job = new Job("Título da Tarefa") { ... }; 

//Recuepra o objeto que mantém o status de progresso diretamente da tarefa
JobStatus status = job.getStatus();

//Obtemos e salvamos para referência futura o UUID do Job
String uuid = status.getUuid();

//Obtendo o JobStatus a partir do JobMonitor com o UUID do job.
JobStatus status2 = JobMonitor.getJobStatus(uuid);


Note que no exemplo acima o .start() do job pode ser dado a qualquer momento. O job não precisa estar sendo executado para que todo o restante funcione.


JobStatus Referência em Memória
Note que o JobStatus é um objeto final dentro do Job, e que não é criada uma nova instância para cada alteração. Isto quer dizer que o objeto pode ser manipulado dentro do Job e as alterações serem lidas concorrentemente em outra Thread. De certa forma o JobStatus funciona apenas como "ponteiros de memória" indicando onde as informações estão sendo escritas.


Um ponto a se observar aqui é que, embora o JobStatus possa ser recuperado e ser único na memória quando estamos dentro da mesma JVM, é preciso observar que ao devolver o JobStatus através de uma fachada utilize o SessionManager#SMInterceptor ele será clonado justamente para prevenir esse tipo de apontamento e modificação "no objeto original". Assim, não mais representará o mesmo objeto na memória. Ao retornar o objeto pela fachada, o outro lado deve utilizar o jobUUID para recuperar o objeto correto do JobMonitor para receber atualizações diretamente.


Ciclo de Vida do Job

Abaixo os passos do ciclo de vida do Job para exemplificar como tudo funciona:

  1. Criamos a tarefa instanciando a classe Job: new Job(...);
    1. Já no construtor, Job se registra no JobMonitor e ganha seu jobUUID.
    2. Com o jobUUID recebido, Job cria o objeto final JobStatus.
    3. Se o Job criado estiver uma Thread que tenha uma sessão do SessionManager, a Thread do Job é anexada à mesma Sessão do usuário.
      Não confundir a sessão se segurança com a Transaction do Container. O Job continuará rodando em uma Tread fora da Transaction, a única situação aqui é que ao chamar SessionManager.getSession() teremos uma identificação/autenticação de segurança.
  2. A partir da instância de Job que fora criado, podemos obter o JobStatus e o jobUUID através dos respectivos métodos: job.getJobStatus() e job.getJobStatus().getJobUUID()
  3. Iniciamos a tarefa disparando a Thread do job com job.start();
    1. Neste momento a Thread do Job começa a ser executada.
  4. O método deve (provavelmente) retornar o JobStatus ou o jobUUID para que o Caller consiga acompanhar a tarefa e solicitar seu cancelamento.

Dentro do JobMonitor

Dentro do JobMonitor, quando a tarefa se registrou, o JobMonitor cria um Timer de segurança que verifica se o Job não foi esquecido dentro do JobMonitor e não teve seu .start() chamado. Passado o tempo do timer (no momento 1h) se o Job não estiver sendo executado, forçaremos a limpeza do Job como um vazamento de recursos. Caso esteja rodando, faremos um Log de que a tarefa está demorando demais, tarefas do JobMonitor não devem ser demoradas desta forma, tarefas de manutenção devem ser executadas pelo Scheduler. JobMonitor se destina mais para tarefas do usuário que possam demorar um pouco mais que o desejado para deixar a tela travada, não para execuções de longo prazo desse jeito.


Dentro do Job

  • Quando instanciado o Job tem o seu atributo step definido como IDLE, indicando que a tarefa está inerte por ainda não ter sido iniciada. Ao chamar o .start();
  • Antes da chamada do método runJob(...) o step é atualizado para RUNNING, e deve ficar neste passo enquanto a implementação do método é executada.
  • Caso este método retorno normalmente, o objeto retornado é salvo dentro do JobStatus, no atributo jobReturn, e o step é alterado para FINISHED indicando que a tarefa terminou sua execução com sucesso.
  • Caso o método lance qualquer Exception o step é alterado para EXCEPTION e a Exception lançada é salva no atributo exception do JobStatus.

Ao finalizar o Job, independente do sucesso, o Job notifica o JobMonitor de que o Job terminou. Ao realizar essa tarefa, o JobMonitor cancela o Timer que ele criou no registro (explicado na sessão anterior). No entanto ele cria um novo timer (atualmente de 30min) que eliminará por completo qualquer outra referência do Job de dentro do JobMonitor para garantir que todos os recursos serão liberados e estarão disponíveis para o GC.

O JobMonitor não excluí imediatamente os recursos porque essa notificação ocorre no momento em que a tarefa se extinguiu, e outras Thread ainda podem precisar encontrar o JobStatus tanto para recuperar o retorno do método (ou a Exception) para notificar o usuário.


Uma vez que a Thread cliente recupera o "último" JobStatus, obtendo seu retorno, é possível (e recomendado) acelerar a liberação dos recursos chamando o método JobMonitor.cleanJob(jobUUID). Assim, tanto os recursos utilizados pelo Job, quando o Timer criado serão liberados. E a Thread cliente fará o que for necessário, salvando ou não, o JobStatus e seus retornos.

JobChecker

JobChecker é a implementação de uma Thread que verifica periodicamente a tarefa sem segundo plano e notifica conforme atualização do objeto de status.

Para criar o JobChecker basta passar o UUID do job na sua construção:

JobChecker verificando o progresso da tarefa
JobChecker jobChecker = new JobChecker(uuid, true) {
  @Override
  public void updateStatus(JobStatus jobStatus) {
    // Código para lêr o conteúdo do JobStatus e atualizar qualquer sistema

    // Abaixo um código exemplo para atualizar o conteúdo da tela do Vaadin.
    // É necessário obter a UI do usuário e criar uma classe de acesso primeiro
    ui.access(new Runnable() {
      @Override
      public void run() {
        // Código do Vaadin para atualizar a UI do usuário
      }
    });
  }
};

Note que no construtor foi passado além da UUID um booleano 'true'. Esse boleano indica se quando o JobChecker detectar que a tarefa finalizou ele mesmo se encarrega de avisar o JobMonitor que ele pode descartar a tarefa e seu status.

Isso é importante porque mesmo que o Job termine, o JobMonitor não sabe até quando deve guardar a referência do JobStatus para ser consultado, assim é importante que o descarte seja autorizado para economizar recursos.

Para forçar esse descarte manualmente, caso não se use o JobChecker ou se passe o atributo como false, deve ser chamado o método:

JobMonitor.cleanJob(uuid);

Caso esse método não seja chamado, no momento em que este documento é escrito, o JobMonitor tem um tempo limite de 6h para manter o Job em memória desde o momento em que a instância do Job é criado. Quando o job é removido de forma forçada, um logImprovement() é chamado com o título do Job.

Job & SessionManager

Ao criar um novo objeto Job, ele mesmo tenta detectar se estamos em um ambiente com uma Session associada a Thread atual. Se encontrar ele automaticamente transfere a sessão de autenticação atual para a nova Thread do Job. A vantagem dessa operação é que o método SessionManager.getSession() passará a funcionar dentro do método run() do Job, bem como permitirá que outros serviços como FWLogger encontre a sessão do usuário.

Transferir Session não Mantém a Transação CMT
Note que o SessionVO passa a fazer parte da nova Thread, mas caso a Thread principal retorne e "volte a fachada de acesso" o container que controla a transação CMT fechará a transação dando commit(). Ou seja, mesmo que o Job tenha acesso ao SessionVO, não indica que ele poderá executar acessos à banco de dados ou outros serviços que dependem de uma Transaction. Esses tipos de chamadas precisam que o Job faça outra chamada através da fachada, mas podem obter o UUID ou Token da sessão recebida para realizar nova autenticação na fachada.