JobMonitor: mudanças entre as edições

De BIS Wiki
Ir para navegação Ir para pesquisar
 
(9 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 15: Linha 15:
job.start(); //inicia a tarefa
job.start(); //inicia a tarefa
</syntaxhighlight>}}
</syntaxhighlight>}}




Linha 29: Linha 30:
}}
}}


== Session Context do Job ==
Quando o job é utilizado para executar uma tarefa no CRUD, é preciso se atentar que depois que o Job é disparado, a Thread principal do usuário é finalizada retornando o JobStatus para o usuário. Uma vez que a chamada é retornada, o contexto de sessão do EJB é finalizado, e o Job não conseguirá mais acessar o banco de dados ou outros recursos que dependam do contexto.
Para resolver isso é preciso que o Job faça a chamada através da fachada para que um novo contexto seja criado para ele.
Nestes casos a recomendação é que dentro da própria fachada ('''BISFacade''') seja criado o Job que chama o método correspondente da fachada privada ('''BISFacadePrivate'''). Para uma chamada simples não vale a pena tumultuar os arquivos do CRUD com um método só para isso.
Siga o exemplo que foi utilizado para o método de geração do SPED:
{{java|Exemplo Tarefa em Execução em Background|<syntaxhighlight lang="java">
@Override
@Security(action = SecurityAction.HASKEY, key = BISSystem.SECKEY_COMPANY_FISCAL_SPED_GENERATE)
public JobStatus generateSPED(String uuid, SPEDOptionBeanVO optBean) throws BISException {
  Job job = new Job("Generate SPED") {
    @Override
    public Object runJob(Job job, JobStatus jStatus) throws Throwable {
      return KernelCrud.getBISFacadePrivate().generateSPEDBackground(uuid, optBean, jStatus.getJobUUID());
    }
  };
  job.start();
  return job.getJobStatus();
}
</syntaxhighlight>}}
=== Padrão de Nomenclatura ===
Os métodos que trabalham em background (orientados por um Job), devem ter o sufixo "Background" no nome. Facilitando assim a pesquisa e entendimento sobre as funções do método. Além de receberem o jobUUID como parâmetro.
{{stop|Não Passar o JobMonitor pela Fachada|Ao passar o JobMonitor pela fachada o objeto é clonado e a referência perdida. Por isso passe sempre o jobUUID e recupere o JobStatus dentro do método.}}


== Interrompendo o Job ==
== Interrompendo o Job ==
Linha 72: Linha 104:




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.
Note que no exemplo acima o '''.start()''' do job pode ser dado a qualquer momento. O job não precisa estar em execução para que todo o restante funcione.




Linha 89: Linha 121:
# Iniciamos a tarefa disparando a Thread do job com '''job.start()''';
# Iniciamos a tarefa disparando a Thread do job com '''job.start()''';
## Neste momento a Thread do Job começa a ser executada.
## 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.
# O método do usuário deve retornar o '''JobStatus''' ou o '''jobUUID''' para que o Caller consiga acompanhar a tarefa e solicitar seu cancelamento.
 


== Dentro do JobMonitor ==
== Dentro do JobMonitor ==
Linha 95: Linha 128:
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.
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.
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 ==
== Dentro do Job ==
Linha 158: Linha 191:




Para suprir a necessidade de encontrar o Job em um JobMonitor de outra JVM, é possível criar o '''JobChecker''' passando uma implementação do '''JobStatusSupplier''' que seja responsável por realizar a ponte até o JobMonitor correto.
{{nota|JobChecker é sempre chamado!|Note que dependendo da velocidade em que a tarefa é terminada, o JobChecker pode ser iniciado depois que a tarefa já esteja no passo '''FINISHED''' ou '''EXCEPTION'''. Ainda assim é garantido que o método '''updateStatus(...)''' será chamado ao menos uma única vez.


Quando o '''updateStatus''' for chamado e o passo do '''JobStatus''' recebido for '''FINISHED''' ou '''EXCEPTION''' esta será a última vez (ou única) que será chamado. Quando detectado que chegamos em um desses dois passos o '''JobChecker''' se encerra.}}


{{nota|JobChecker é sempre chamado!|Note que dependendo da velocidade em que a tarefa é terminada, o JobChecker pode ser iniciado depois que a tarefa já esteja no passo '''FINISHED''' ou '''EXCEPTION'''. Ainda assim é garantido que o método '''updateStatus(...)''' será chamado ao menos uma única vez.


Quando o '''updateStatus''' for chamado e o passo do '''JobStatus''' recebido for '''FINISHED''' ou '''EXCEPTION''' esta será a última vez (ou única) que será chamado. Quando detectado que chegamos em um desses dois passos o '''JobChecker''' se encerra.}}
== JobStatusSupplier ==
 
Para suprir a necessidade de encontrar o Job em um JobMonitor de outro ClassLoader, é possível criar o '''JobChecker''' passando uma implementação do '''JobStatusSupplier''' que seja responsável por realizar a ponte até o JobMonitor correto.
 
O FW fornece uma implementação padrão '''JobStatusSupplierDefault''' que faz a ponte para o '''JobMonitor''' dentro do mesmo ClassLoader. Essa implementação também é utilizada internamente quando o JobChecker é instanciando passando ''null'' no argumento do Supplier.


Já no BISVaadin, para que o JobChecker possa acessar as tarefas criadas no Core, é oferecida a implementação '''JobStatusSupplierBISImpl''' que faz a chamada através da fachada e questiona o '''JobMonitor''' do Core.


= JobMonitorComponent =  
= JobMonitorComponent =  

Edição atual tal como às 15h20min de 12 de fevereiro de 2022

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...
    //E atualizações do andamento da tarefa no objeto jobStatus
  }
};
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.


Session Context do Job

Quando o job é utilizado para executar uma tarefa no CRUD, é preciso se atentar que depois que o Job é disparado, a Thread principal do usuário é finalizada retornando o JobStatus para o usuário. Uma vez que a chamada é retornada, o contexto de sessão do EJB é finalizado, e o Job não conseguirá mais acessar o banco de dados ou outros recursos que dependam do contexto.

Para resolver isso é preciso que o Job faça a chamada através da fachada para que um novo contexto seja criado para ele.

Nestes casos a recomendação é que dentro da própria fachada (BISFacade) seja criado o Job que chama o método correspondente da fachada privada (BISFacadePrivate). Para uma chamada simples não vale a pena tumultuar os arquivos do CRUD com um método só para isso.

Siga o exemplo que foi utilizado para o método de geração do SPED:

Exemplo Tarefa em Execução em Background
@Override
@Security(action = SecurityAction.HASKEY, key = BISSystem.SECKEY_COMPANY_FISCAL_SPED_GENERATE)
public JobStatus generateSPED(String uuid, SPEDOptionBeanVO optBean) throws BISException {
  Job job = new Job("Generate SPED") {
    @Override
    public Object runJob(Job job, JobStatus jStatus) throws Throwable {
      return KernelCrud.getBISFacadePrivate().generateSPEDBackground(uuid, optBean, jStatus.getJobUUID());
    }
  };
  job.start();
  return job.getJobStatus();
}

Padrão de Nomenclatura

Os métodos que trabalham em background (orientados por um Job), devem ter o sufixo "Background" no nome. Facilitando assim a pesquisa e entendimento sobre as funções do método. Além de receberem o jobUUID como parâmetro.


Não Passar o JobMonitor pela Fachada
Ao passar o JobMonitor pela fachada o objeto é clonado e a referência perdida. Por isso passe sempre o jobUUID e recupere o JobStatus dentro do método.

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 JobStatus.interrupt(). 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 ClassLoader e só será possível encontrar referência do Job se estivermos procurando dentro da mesma ClassLoader em que o Job foi criado.

Em casos em que o Job é criado em outra ClassLoader, 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 em execução 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.
  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 do usuário deve 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 10min) 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 em segundo plano e dispara eventos de alteração no formato de listeners.

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

JobChecker verificando o progresso da tarefa
JobChecker jobChecker = new JobChecker(uuid);
checker.addListener(new JobCheckerListener() {
  @Override
  public void updateStatus(JobStatus jobStatus, boolean lastCall) {
    // 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 interface 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
      }
    });
  }
};


No método do listener são recebidos 2 parâmetros:

  • JobStatus - o objeto JobStatus da tarefa com as últimas informações disponíveis.
  • lastCall - um boleano indicando se esta é a última chamada do listener. Esse atributo só é true quando o step do Job já for FINISHED ou EXCEPTION. E permite que algumas ações sejam realizadas só quando a tarefa terminou.


Dentro de um mesmo JobChecker é possível colocar vários listeners que manipulem e executem partes diferentes do sistema. Como listeners para atualizar a tela e o progresso da tarefa, bem como o listener específico para disparar novas tarefas em cadeia.


LastCall e Step do Job
Note que quando LastCall for true será de fato a última fez que o listener será chamado. No entanto, mesmo com lastCall for false a tarefa pode estar já nos passos FINISHED ou EXCEPTION. Isso ocorre porque, caso o JobStatus utilizado seja o mesmo objeto de memória em que a tarefa esteja atualizando, entre a definição do lastCall e a chamada do listener o step pode ter sido atualizado. Mesmo asim, o listener será chamado novamente passando o lastCall como true.

Em resumo:

  • Quando lastCall for true:
    • O passo da tarefa será FINISHED ou EXCEPTION
    • Será a última chamada do listener
    • A tarefa já terminou sua execução.
  • Quando lastCall for false:
    • O passo da tarefa pode estar em qualquer estágio, inclusive terminado.
    • Teremos pelo menos uma chamada do listener. Mesmo que a tarefa já tenha terminado.


JobChecker é sempre chamado!
Note que dependendo da velocidade em que a tarefa é terminada, o JobChecker pode ser iniciado depois que a tarefa já esteja no passo FINISHED ou EXCEPTION. Ainda assim é garantido que o método updateStatus(...) será chamado ao menos uma única vez.

Quando o updateStatus for chamado e o passo do JobStatus recebido for FINISHED ou EXCEPTION esta será a última vez (ou única) que será chamado. Quando detectado que chegamos em um desses dois passos o JobChecker se encerra.


JobStatusSupplier

Para suprir a necessidade de encontrar o Job em um JobMonitor de outro ClassLoader, é possível criar o JobChecker passando uma implementação do JobStatusSupplier que seja responsável por realizar a ponte até o JobMonitor correto.

O FW fornece uma implementação padrão JobStatusSupplierDefault que faz a ponte para o JobMonitor dentro do mesmo ClassLoader. Essa implementação também é utilizada internamente quando o JobChecker é instanciando passando null no argumento do Supplier.

Já no BISVaadin, para que o JobChecker possa acessar as tarefas criadas no Core, é oferecida a implementação JobStatusSupplierBISImpl que faz a chamada através da fachada e questiona o JobMonitor do Core.

JobMonitorComponent

O JobMonitorComponent é um componente do pacote BISFW.Vaadin, que permite que uma tarefa seja monitorada e exibida em um pequeno quadro com uma barra de progresso e alguns botões para manipulação.

O componente encapsula dentro dele um JobChecker que faz o trabalho da verificação da mudança de estados do Job. Assim o JobMonitorComponente por ser instanciado basicamente de 2 maneiras:

  • Passando o jobUUID e um JobStatusSupplier (opcional) - neste modo o componente instancia e configura seu próprio JobChecker para verificar a tarefa.
  • Passando um JobChecker já criado - Neste modo o componente aproveita o JobChecker já criado e só coloca seu listener para obter os eventos e atualizar seus componentes.


Este componente além de atualizar a barra de progresso e mensagem da tarefa, ainda é capaz de exibir 3 botões de forma automática:

  • Solicitação de Cancelamento - Enquanto o Job estiver em execução o botão é exibido e executa a chamada do JobStatus.interrupt(), solicitando o cancelamento da tarefa;
  • Visualização do Relatório - Quando o JobStatus tiver um relatório (.hasReport() for true) o botão é exibido e ao clicar exibe o conteúdo do relatório da tarefa em um Popup que também se atualiza automaticamente.
  • Exception - Caso a tarefa tenha terminado em exception, o botão é exibido e quando clicado a exception é exibida para o usuário.


Além desses, outros botões podem ser adicionados ao componente para personalizar sua aparência através do método

.addButton()