Rotinas

Introdução

As features do tipo Job fazem parte da camada de negócio da aplicação.

As regras de negócio devem ser implementadas em classes Java conforme o disposto na seção Negócio, que explana sobre o padrão Feature Service.

Toda feature possui uma classe principal @Service, que contém o ponto de entrada da transação de negócio. Essa classe, conforme o padrão Feature Service, encapsula as regras de negócio relativas à funcionalidade.

Após desenvolvimento da classe de serviço @Service, é feito seu mapeamento na forma de rotina. Esse mapeamento é responsabilidade da classe adicional de estereótipo @Job, localizada no mesmo pacote da feature.

A classe @Job implementa os métodos do fluxo de execução da rotina, conforme o paradigma que for mais conveniente para a funcionalidade, podendo ser o iterativo ou o Produtor-Consumidor. Este último caso inclui o subtipo Produtor-Consumidor ininterrupto, projetado para filas contínuas de processamento.

A classe @Job injeta e consome a classe de serviço. Não é função da classe @Job implementar regras de negócio, tão somente adequar o serviço para o fluxo do tipo de rotina desejado.

As rotinas são executadas em background pelo motor provido pelo Cloudsupport, conforme detalhado nas seções seguintes.

Além do estereótipo de classe @Job, a arquitetura oferece @Schedule, para classes dedicadas à configuração de agendamento da rotina. A classe de agendamento fica também localizada no pacote da feature.

As rotinas podem ser gerenciadas:

  • Via API Java, através de métodos disponíveis para iniciar e parar Jobs, consultar andamentos, consultar históricos, ativar ou desativar agendamentos, dentre outras opções.

  • Via API Webservice, através de endpoints disponibilizados no Spring Boot Actuator, que permitem o gerenciamento remoto das rotinas, como bem a integração com ferramentas de monitoramento e interfaces gráficas.

As próximas seções documentam os tipos de rotina iterativo e Produtor-Consumidor, as APIs de gerenciamento e detalhes do ciclo de vida do motor de execução do Cloudsupport.

Vantagens e Desvantagens

Destacam-se as seguintes vantagens da solução de rotinas do Cloudsupport:

  • Não requer softwares ou ferramentas adicionais no ambiente computacional, o que reduz o custo de infraestrutura e de operação, que envolve gestão, monitoramento, disponibilidade, segurança e outros fatores.

  • Ultra lightweight, com consumo mínimo de RAM e apenas, aproximadamente, 100 KB de Metaspace.

  • Baixa curva de aprendizado: Todo o conhecimento necessário pelo desenvolvedor consta neste capítulo.

  • Atende a uma ampla quantidade de casos de uso em função do modelo Produtor-Consumidor, que inclui o modo batch (produz-se e consome-se todos os itens) e o modo contínuo (produção iterada).

  • Permite o acompanhamento das execuções e auditoria via banco de dados, pela persistência JDBC, via API Java e via endpoints webservices.

  • Produtividade: Basta criar uma classe Java e implementar 1 (um) ou poucos métodos, conforme o desejado. Requer zero configurações.

São desvantagens do presente framework:

  • Não é projetado para processos distribuídos. Nestes casos, orienta-se no sentido de utilizar soluções existentes, baseadas em ferramentas como RabbitMQ ou Kafka, Spring Cloud, Spark Streaming, gerenciadores de filas via SaaS, dentre outras.

Conceitos

Visão Geral

Os Jobs ou rotinas são funcionalidades que rodam em background mediante uma solicitação de execução ou conforme configurações de agendamento.

Cada rotina pode ser implementada segundo a técnica que melhor atender ao negócio:

  1. Processo iterativo: Neste modelo, o método execute() da classe @Job é executado repetidamente pelo motor do Cloudsupport até que ele retorne FINISHED. Trata-se de um fluxo mais simples e genérico. O processo pode ser executado de maneira multithreaded, em que um pool de threads é criado para invocar execute() concorrentemente.

  2. Processo Produtor-Consumidor: Neste modelo, o Cloudsupport primeiramente executa o método produce() da classe @Job, que deve retornar uma lista de itens. De posse da lista, o Cloudsupport então executa o método consume(item), também da classe @Job, para cada item da lista. Esse esquema se torna conveniente quando se habilita o consumo multithreaded dos itens, que permite aumentar substancialmente a performance da rotina. O motor da arquitetura garante que todos os itens serão processados e que não haverá repetição do mesmo item. Essas garantias envolvem uma programação mais avançada, motivo pelo qual estão inclusas na arquitetura. O desenvolvedor da funcionalidade se restringe à implementação das regras de negócio para o processamento de cada item, sem precisar se preocupar com controles de sincronia de programação paralela. O modelo Produtor-Consumidor é útil para uma série de casos de uso, por exemplo:

    • Processar um lote de registros pendentes;

    • Realizar a baixa de pedidos;

    • Enviar notificações para o cliente.

Os modelos de execução serão detalhados nas seções seguintes.

O motor de rotinas suporta execução full JVM, através da opção In-Memory Repository, ou com repositório do tipo JDBC, em que o estado de execução das rotinas é persistido em banco de dados relacional.

Ciclo de Vida

Todas as rotinas seguem um ciclo de vida geral, independente de seu modelo de execução, seja iterativo ou Produtor-Consumidor.

As possíveis situações da execução de uma rotina são:

  1. ACTIVE: Situação do processo quando ele é criado e está em execução.

  2. STOPPING: Indica que foi solicitada a interrupção da rotina e o motor do Cloudsupport está finalizando a execução do processo. Tecnicamente, a finalização consiste em não executar novas operações na classe @Job e aguardar o término das threads em andamento.

  3. TERMINATED: Processo não mais se encontra em execução.

  4. ABANDONED: Indica que a rotina abortou inesperadamente, como por exemplo falha na infraestrutura computacional. Essa situação é marcada manual e posteriormente.

A rotina que terminou sua execução recebe uma situação de término. As possíveis, situações do término de uma rotina são:

  • COMPLETED: Indica que a rotina executou todo seu trabalho com sucesso. Ocorre somente se o processo terminou por decisão própria, como por exemplo retorno FINISHED do execute() ou consumo de todos os itens retornados no produce(), e não houve nenhuma exceção Java não tratada em quaisquer dos métodos da classe @Job.

  • STOPPED: Indica que a rotina executou seu trabalho com sucesso, porém parcialmente. Ocorre quando há uma solicitada a interrupção da rotina e todas as threads finalizaram suas atividades sem erro.

  • FAILED: A rotina terminou com erro, seja durante sua execução ordinária ou seja durante a fase de STOPPING.

  • UNKNOWN: Indica que a rotina abortou inesperadamente e não há informação sobre sua completude. Essa situação é marcada manual e posteriormente.

Entidades

O motor de rotinas do Cloudsupport envolve as entidades elencadas abaixo. Elas são retornas nas APIs de gerenciamento.

  • JobInstance: Entidade que representa uma solicitação de execução de um Job. Contém os seguintes atributos:

    • uid: Identificação única da solicitação de execução. Esse UID é utilizado nas APIs de gerenciamento da rotina, descritas nas seções seguintes.

    • jobName: Nome da rotina, por exemplo EnviarNotificacoesVencimento.

    • jobParams: Referência ao objeto JobParams descrito abaixo.

    • concurrency: Quantidade de threads para o pool de execução da classe @Job.

  • JobParams: Entidade que contém a lista de parâmetros de um JobInstance, na forma de mapa chave/valor.

  • JobExecution: Entidade que contém o último estado da execução da rotina. Há uma instância de JobExecution para cada JobInstance. Contém os seguintes atributos:

    • uid: Identificação única da execução. Esse UID é interno não é utilizado nas APIs de gerenciamento da rotina.

    • executionStatus: Situação de execução: ACTIVE, STOPPING, TERMINATED ou ABANDONED.

    • exitStatus: Situação de término: COMPLETED, STOPPED, FAILED ou UNKNOWN.

    • startDate: Data e hora de início da execução.

    • lastRepeatDate: Data e hora da execução última iteração, ou seja, da última execução concluída e sem erro do método execute() da classe @Job. (*1)

    • repeatCount: Quantidade de vezes que o processo iterou, ou seja, que o método execute() foi executado e sem erro. (*1)

    • stopRequestDate: Data e hora da solicitação de interrupção da rotina. Neste caso, o campo exitStatus será STOPPED ou FAILED.

    • terminationDate: Data e hora do término da execução. Neste caso, o campo exitStatus será COMPLETED ou FAILED.

    • exitMessage: Erro resumido quando ocorre exceção na execução da rotina. Caso não seja BusinessException, o resumo incluirá o código de rastreamento de log (salvo se cloudsupport.error.conventions.enabled=false).

    • errorDetails: O detalhamento do erro fica disponível no log, que inclui o rastreamento. Esse campo será preenchido com o stack trace caso cloudsupport.error.conventions.enabled=false.

  • JobSchedule: Entidade que contém a lista de definições de agendamento de um determinado Job, Esta entidade não está vinculada a uma execução específica. Contém os seguintes atributos:

    • jobName: Nome da rotina, por exemplo EnviarNotificacoesVencimento.

    • enabled=true|false: Indica se o agendamento está ativo ou não, cujo valor é conforme as configurações de JobConfigurations (vide a seguir).

    • definitions=[JobScheduleDefinition]: Lista de agendamentos. Cada agendamento é uma entidade JobScheduleDefinition, que contém os seguintes atributos, conforme preenchimento da anotação @Scheduled do Spring:

      • cron: Expressão Cron.

      • zone: Fuso horário.

      • fixedDelay: Intervalo fixo entre repetições (número).

      • fixedDelayString: Intervalo fixo entre repetições (String).

      • fixedRate: Taxa fixa de repetição (número).

      • fixedRateString: Taxa fixa de repetição (String).

      • initialDelay: Atraso inicial (número).

      • initialDelayString: Atraso inicial (String).

      • timeUnit: Nome da unidade de medida do intervalo, taxa ou atraso.

  • JobConfigurations: Entidade que contém a lista de configurações de um determinado Job, na forma de mapa chave/valor. Esta entidade não está vinculada a uma execução específica. A única configuração atualmente suportada é:

    • scheduling=true|false: Indica se o agendamento está ativo ou não. Assume-se o valor padrão true.
  • JobHistory: Entidade que contém o registro histórico de uma execução de Job. Contém os atributos de JobInstance mais JobExecution. Este objeto é retornado pela API de consulta de histórico do Cloudsupport. Considerações importantes:

    • Caso a rotina ainda esteja em execução no momento da consulta do histórico, este objeto será um snapshopt do estado e não é mantido atualizado ainda que esteja no contexto da mesma JVM em que a rotina está em execução.

    • O Cloudsupport sempre faz a aquisição dos dados a partir do repositório JDBC. Ou seja, caso o motor de rotinas esteja no modo full JVM, a API de consulta de histórico não terá efeito (sempre retornará lista vazia). Consulte as APIs de gerenciamento para mais informações.

(*1) Observação: O processo Produtor-Consumidor é, internamente, um caso especializado do modelo iterativo, de tal forma que esse campo também se aplica à ele.

Com exceção da entidade JobHistory, as demais são persistidas em banco de dados para fins de auditoria e histórico, salvo se o motor de rotinas estiver no modo full JVM. Vide Configuração.

Estrutura de Pacotes

As features do tipo Job têm a seguinte estrutura de pacotes:

Pacote Descrição
.services.jobs.assunto.nomeFuncionalidade Funcionalidades na forma de rotinas ou processos batch.

O subpacote de assunto é opcional.

Exemplo de estrutura de pacotes de rotinas:

services
   jobs
      avisos
         enviarNotificacoesVencimento
         enviarNotificacoesAquisicao
         enviarNotificacoesNovaOs
      ordemService
         RealizarGlosa

Estereótipos de Classes

@Job

A classe de estereótipo @Job tem o objetivo de realizar o mapeamento na forma de rotina de determinada funcionalidade. Ela injeta a classe de serviço @Service e a executa conforme o modelo de execução desejado.

São diretrizes para classes de mapeamento de Job:

  • Herdam de BaseJob ou BaseProducerConsumerJob e são anotadas com @Job.

    • Utilize BaseJob para implementar rotinas simples, no modelo Iterativo.

    • Utilize BaseProducerConsumerJob para implementar rotinas no padrão Produtor-Consumidor.

  • Têm sufixo Job.

  • Têm escopo de rotina, ou seja, haverá uma instância da classe por execução da rotina (JobExecution).

  • Não controlam ou delimitam transação.

  • Não implementam regras de negócio.

  • São localizadas no pacote da funcionalidade, .services.jobs.[assunto].{nomeFuncionalidade}, junto com as classes de negócio.

Exemplo de uma classe de rotina no modelo Iterativo:

@Job
public class RealizarGlosaJob extends BaseJob {

    @Inject
    private RealizarGlosaService service;

    @Override
    protected RepeatStatus execute(JobParams parameters, long sequence) {

        // Recebe parametro da rotina
        Integer sla = parameters.getInteger("sla");

        // Executa o serviço
        service.glosarAposSla(sla);

        // Continua a rotina
        if (service.existePendencia()) {
            return RepeatStatus.CONTINUABLE;
        }

        // Sinaliza fim da rotina
        else {
            return RepeatStatus.FINISHED;
        }
    }
}

Consulte a seção específica sobre o modelo Produtor-Consumidor.

@Schedule

A classe de estereótipo @Schedule tem o objetivo de configurar o agendamento da rotina.

São diretrizes para classes de agendamento de Job:

  • Herdam de BaseSchedule e são anotadas com @Schedule.

  • Têm sufixo Schedule.

  • São singletons e stateless, não devendo manter estados em seus atributos.

  • Não controlam ou delimitam transação.

  • Não implementam regras de negócio.

  • São localizadas no pacote da funcionalidade, .services.jobs.[assunto].{nomeFuncionalidade}, junto com as classes de negócio.

  • Configuram o agendamento através de um método público anotado com @Scheduled, do Spring Scheduled Tasks. Todas as opções desta anotação podem ser utilizadas.

  • Disparam a rotina via runJob(), herdado da classe mãe.

Exemplo de uma classe de agendamento:

@Schedule
public class RealizarGlosaSchedule extends BaseSchedule {

    @Scheduled(fixedRate = 60000) // cada 1min
    public void schedule() {
        runJob("sla=10");
    }
}

As classes de agendamento devem evitar a injeção de JobManager, vez que o método disponível runJob() possui garantias adicionais:

  • O nome da rotina é obtido automaticamente por convenção, evitando risco de uma determinada classe de agendamento disparar rotina diversa do seu pacote.

  • É síncrono, mitigando risco de tentativas concorrentes de execução da rotina. O Cloudsupport emitirá erro, pois ele não permite concorrência de execução de uma mesma rotina com os mesmos parâmetros.

Consulte exemplos de rotinas disponíveis no projeto arquétipo.

O Modelo Produtor-Consumidor

Esta seção detalha um importante padrão utilizado em rotinas de background, o modelo Produtor-Consumidor.

Para implementar um processo Produtor-Consumidor, a classe @Job deve herdar de BaseProducerConsumerJob<T>, onde T é o tipo do objeto que será produzido e consumido.

Os principais métodos de uma rotina Produtor-Consumidor são:

  • produce(jobParams): Deve retornar uma coleção de T composta dos itens a serem consumidos, de maneira multithreaded, pelo método consume(item). Exemplo: listar os IDs das ordens de serviço elegíveis para glosa. É esperado que o método de produção retorne razoavelmente rápido, e que a carga custosa do processsamento seja implementada no consumidor.

  • consume(item): Deve realizar o processamento de um item. Por exemplo: o cálculo da glosa de uma ordem de serviço.

  • terminate(jobParams): Opcional, será executado após todos os itens serem consumidos com sucesso.

Exemplo de uma classe de rotina no modelo Produtor-Consumidor:

public class RealizarGlosaJob extends BaseProducerConsumerJob<Long> {

    @Inject
    private RealizarGlosaService service;

    @Override
    protected Collection<Long> produce(JobParams jobParams) {
        return service.produzir();
    }

    @Override
    protected void consume(Long item) {
        service.consumir(item);
    }

    @Override
    protected void terminate(JobParams jobParams) {
        logger.info("Ordens de serviço atualizadas com sucesso");
    }
}

Outros métodos podem ser implementados na classe Produtor-Consumidor:

  • start(jobParams): Executado antes da primeira produção. Pode ser utilizado para preparar os dados antes do começo efetivo dos processamentos.

  • onConsumerBusinessException(item, businessException): Executado caso ocorrer exceção do tipo BusinessException durante um consumo. A implementação padrão relança o erro, causando a interrupção da rotina, com existStatus igual a FAILED. Caso este método não lance exceção, a execução da rotina continuará normalmente. Este método pode ser utilizado, por exemplo, para persistir erros no banco de dados.

  • shouldProduceMore(): Indica se o motor de rotinas deve continuar produzindo mais itens. Retorne true, portanto, para que o processo seja ininterrupto. Neste caso, a rotina nunca terminará por si própria.

  • queueThreshold(): O motor de rotinas executará novamente a produção se a quantidade de itens na fila for igual ou abaixo de valor de limiar retornado por este método. Tem efeito somente se shouldProduceMore() for verdadeiro. Por padrão vale -1, que significa que o motor de rotinas irá aguardar o esvaziamento da fila bem como o términdo de todos os consumos antes de realizar nova produção. Esse valor padrão implica que não haverá concorrência entre produção e consumo. Caso seja configurado outros valores de threshold, considere implementar os tratamentos devidos na produção e/ou consumo para evitar inconsistências de dados.

  • productionRetryDelay(): Retorna o interstício, em segundos, após uma produção retornar null ou uma coleção vazia. Por padrão vale 5. Tem efeito somente se shouldProduceMore() for verdadeiro.

  • onStateChange(state): Executado a cada mudança de estado. Os possíveis eventos de mudança de estado, disponívels em state.getEvent(), são:

    • CHUNK_PRODUCED: Ocorreu execução do produtor.

    • ITEM_DISPATCHED: Um item foi encaminhado para consumo.

    • ITEM_CONSUMED: Um item foi consumido sem exceção não tratada.

    • CONSUMPTION_FINISHED: Todos os itens foram consumidos sem exceção não tratada.

    Os atributos do objeto state são:

    • currentQueueSize: Quantidade de itens na fila de processamento.

    • currentProcessingCount: Quantidade de itens em processamento. Limitado à quantidade de threads.

    • countItemsProduced: Total de itens produzidos.

    • countItemsDispatched: Total de itens encaminhados para consumo.

    • countItemsConsumed: Total de itens consumidos sem exceção não tratada.

    • countItemsConsumedSuccessfully: Total de itens consumidos com sucesso, ou seja, sem exceção gerada.

    • countItemsConsumedWithErrorHandled: Total de itens consumidos que tiveram exceção tratada.

Importante:

  1. No modelo de execução Produtor-Consumidor, o nível de concorrência deverá ser igual ou superior a 2, pois sempre 1 (uma) thread é dedicada às produções e demais para consumos. Por padrão a concorrência vale 2, indicando que o consumo será sequencial. Consulte a seção Multithreading para mais informações.

  2. Caso a produção esteja parada devido ao intervado productionRetryDelay, a operação wakeUp fará a rotina acordar imediamente e tentar produzir mais itens. Consulte a API de gerenciamento abaixo. Este comando é útil para implementação de processos de fluxo contínuo em tempo real.

Exemplo de Produtor-Consumidor ininterrupto:

public class RealizarGlosaJob extends BaseProducerConsumerJob<Long> {

    @Inject
    private RealizarGlosaService service;

    @Override 
    protected boolean shouldProduceMore() {
        return true; // ativa o processo infinito
    }

    @Override
    protected int queueThreshold() {
        return 3; // ativa produção antecipada, quando a fila cai para 3 itens 
    }

    @Override
    protected void start(JobParams jobParams) {
        service.preparar();
    }

    @Override
    protected Collection<Long> produce(JobParams jobParams) {
        return service.produzir();
    }

    @Override
    protected void consume(Long item) {
        service.consumir(item);
    }

    @Override
    protected void onConsumerBusinessException(Long item, BusinessException ex) {
      service.registrarErro(item);
    }
}

A classe de serviço possui 4 métodos, preparar, produzir, consumir e registrarErro, utilizados pela rotina. Note que as regras de negócio estão encapsuladas na classe de serviço.

Abaixo tem-se o log do exemplo de processo ininterrupto, que mostra:

  • A quantidade total itens produzidos
  • A quantidade total itens consumidos
  • A quantidade de itens na fila aguardando o início do processamento
  • A quantidade de itens em processamento (pelas threads do pool de execução)
INFO Job [instance 634170898940800] - Starting with no parameters
INFO Produzindo mais 2 item(ns)...  
INFO Evento ITEM_DISPATCHED         produzido: 2  consumido: 0  fila: 1  processando: 1 
INFO Evento ITEM_DISPATCHED         produzido: 2  consumido: 0  fila: 0  processando: 2 
INFO Produzindo mais 3 item(ns)...                                       
INFO Produzindo mais 1 item(ns)...                                       
INFO Evento ITEM_DISPATCHED         produzido: 5  consumido: 0  fila: 2  processando: 3 
INFO Evento ITEM_DISPATCHED         produzido: 5  consumido: 0  fila: 1  processando: 4 
INFO Produzindo mais 0 item(ns)...                                       
INFO Evento ITEM_CONSUMED           produzido: 6  consumido: 1  fila: 2  processando: 3 
INFO Evento ITEM_DISPATCHED         produzido: 6  consumido: 1  fila: 1  processando: 4 
INFO Evento ITEM_CONSUMED           produzido: 6  consumido: 2  fila: 1  processando: 3 
INFO Evento ITEM_DISPATCHED         produzido: 6  consumido: 2  fila: 0  processando: 4 
INFO Evento ITEM_CONSUMED           produzido: 6  consumido: 3  fila: 0  processando: 3 
INFO Evento ITEM_CONSUMED           produzido: 6  consumido: 4  fila: 0  processando: 2 
INFO Evento ITEM_CONSUMED           produzido: 6  consumido: 5  fila: 0  processando: 1 
INFO Evento ITEM_CONSUMED           produzido: 6  consumido: 6  fila: 0  processando: 0 
INFO Produzindo mais 4 item(ns)...                                       
INFO Evento ITEM_DISPATCHED         produzido: 10 consumido: 6  fila: 3  processando: 1 
INFO Produzindo mais 5 item(ns)...                                       
INFO Evento ITEM_DISPATCHED         produzido: 10 consumido: 6  fila: 7  processando: 2 
INFO Evento ITEM_DISPATCHED         produzido: 10 consumido: 6  fila: 6  processando: 3 
INFO Evento ITEM_DISPATCHED         produzido: 10 consumido: 6  fila: 5  processando: 4

Note que:

  1. Ocorre mais produção se a fila atingir tamanho igual ou menor a 3.
  2. A produção sofre um delay caso forem produzidos 0 itens.

Consulte os exemplos disponíveis no projeto arquétipo.

Multithreading

O motor de rotinas do Cloudsupport suporta a execução de processos de maneira multithreaded, tanto para o modelo Iterativo quanto para o Produtor-Consumidor. Este recurso é conveniente para aumentar a performance, especialmente nas rotinas que utilizam múltiplos recursos, como CPU e banco de dados, em ambientes computacionais com mais de um core ou vCPU.

É importante ressaltar que se ativada a concorrência na rotina, suas regras de negócio deverão se implementadas de forma a tratar adequadamente esse cenário e evitar inconsistência de dados.

O padrão:

  1. Rotinas do tipo Iterativo são executadas de maneira sequencial, ou seja, com nível de concorrência igual a 1, ou seja, uma única thread no pool de execução.

  2. Rotinas do tipo Produtor-Consumidor finito são executadas de maneira sequencial, ou seja, com nível de concorrência igual a 2, ou seja, duas threads no pool de execução, sendo uma dedicada a produção e outra para os consumos.

  3. Rotinas do tipo Produtor-Consumidor ininterrupto, onde shouldProduceMore é true, são executadas de maneira sequencial, pois além do nível de concorrência ser 2 (uma thread para produção e outra para os consumos), o valor padrão de queueThreshold é -1, de forma que produção e consumo não ocorrem simultaneamente.

O nível de concorrência da rotina pode ser configurado de duas maneiras:

  1. Estaticamente, através da implementação do método concurrency() na classe @Job, exemplo:

     @Job
     public class RealizarGlosaJob extends BaseProducerConsumerJob<Long> {
    
         // ...
    
         @Override
         protected Integer concurrency() {
             return 5; // Uma thread de produção e quatro para consumo
         }
     }
    
  2. Dinamicamente, no momento da invocação da rotina:

Importante:

  • O método dinâmico é permitido somente caso concurrency() não seja implementado (ou retorne null).

  • É desejável que se configure a concorrência estaticamente, para evitar paralelismo indesejado e eventual inconsistência de dados.

Agendamentos

Os agendamentos de rotinas são configurados em classes @Schedule, conforme descrito na seção de estereótipos.

Por padrão os agendamentos ficam inativos. Consulte a seção Ativação dos Agendamentos para habilitar as classes @Schedule. Sugere-se que os agendamentos sejam ativados em apenas uma instância da aplicação no ambiente de produção, para evitar duplicidade de execução da mesma rotina.

Uma vez ativadas as classes @Schedule, os agendamentos serão executados exceto salvo houver configuração que os desative para determinado Job, conforme APIs de gerenciamento - vide JobConfigurations.

Em agendamentos Cron, evite o caractere * na primeira posição da expressão. Caso o primeiro caractere for *, significa que a Cron está ativa durante todos os segundos do minuto, fazendo o Job ser executado repetidamente até que a Cron se torne inativa (final do minuto).

Mais sobre Cron no Spring: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/support/CronExpression.html

Consulte exemplos disponíveis no projeto arquétipo.

APIs de Gerenciamento

Java

A API Java para gerenciamento das rotinas está disponível por injeção da classe JobManager.

Operações disponíveis:

Método Descrição
start(jobName, jobParams, concurrency) Inicia uma rotina. Esse método é assíncrono. Parâmetro jobName é o nome simples da classe @Job sem o sufixo “Job”. jobParams é a lista de parâmetros no formato “chave=valor” separados por vírgula. concurrency é o número de threads que fará o processamento. Note que no caso de Produtor-Consumidor, a quantidade mínima de threads é 2.
start(class, jobParams, concurrency) Semelhante ao método acima, porém recebe a classe @Job no lugar de jobName.
requestStop(instanceUid) Solicita a interrupção da execução da rotina. Não lança erro se a rotina não estiver em execução. Parâmetro instanceUid pode ser obtido via jobExecution.getJobExecution().getUid().
requestStop(jobExecution) Semelhante ao método acima, porém recebe a entidade jobExecution.
awaitTermination(instanceUid) Aguarda o término da execução da rotina. Bloqueia a thread corrente. Parâmetro instanceUid pode ser obtido via jobExecution.getJobExecution().getUid().
awaitTermination(jobExecution) Semelhante ao método acima, porém recebe a entidade jobExecution.
wakeUp(instanceUid) Acorda as threads da rotina caso estejam eventualmente em wait ou sleep. Útil para processos que precisam continuar imediatamente, como, por exemplo, produzir mais itens em um processo Produtor-Consumidor ininterrupto.
wakeUp(jobExecution) Semelhante ao método acima, porém recebe a entidade jobExecution.
wakeUpAll(jobName) Acorda as threads de todas as execuções de determinada rotina caso estejam eventualmente em wait ou sleep. Útil para processos que precisam continuar imediatamente, como, por exemplo, produzir mais itens em um processo Produtor-Consumidor ininterrupto.
listRegistryEntries() Retorna a lista de rotinas declaradas. A implementação considera os nomes simples de todas as classes @Job, sem o sufixo “Job”.
listRunningJobs() Retorna a lista das execuções de rotinas que estão em andamento. O retorno é um mapa indexado pelo instanceUid.
listJobSchedules() Retorna a lista dos agendamentos de cada rotina, conforme classes @Schedule.
listConfigurations() Retorna a lista de configurações de rotinas. O retorno é um mapa indexado pelo instanceUid.
reloadConfigurations() Força a atualização das configurações de rotinas a partir do banco de dados relacional. Útil caso o banco de dados tenha sido alterado manualmente, sem utilização das APIs de gerenciamento da arquitetura.
setConfiguration(jobName, key, value) Define uma configuração de rotina. No momento apenas a configuração de chave "scheduling" é suportada, que ativa ou desativa o agendamento da rotina informada em jobName. Values possíveis são: "true" (padrão) e "false".
getConfiguration(jobName, key) Obtém o valor de uma configuração de rotina. Retorna null se não houver configuração definida. No momento apenas a configuração de chave "scheduling" é suportada, que indica se o agendamento da rotina informada em jobName está ativo ou não. Caso não houver essa configuração, o motor de rotinas assume o valor padrão "true".
getConfiguration(jobName, key, defaultValue) Semelhante ao método acima, porém retorna defaultValue se não houver configuração definida para a chave informada.
listHistory(…) Retorna o histórico de execução de rotinas. Aplica-se somente quando o repositório é do tipo JDBC (vide Conceitos e Configuração). Os parâmetros de listHistory são todos opcionais: jobName, executionStatus, exitStatus, instanceUid, dateFromInclusive, dateToExclusive, offset, maxResults.

Endpoints Actuator

A API Actuator para gerenciamento remoto das rotinas está disponível via webservices com prefixo /actuator/.

Operações disponíveis:

URL relativa Método equivalente na API Java jobManager
cloudsupportjobs/start?jobName[&jobParams]&concurrency start
cloudsupportjobs/stop?instanceUid requestStop
cloudsupportjobs/wakeup?instanceUid wakeUp
cloudsupportjobs/wakeup?jobName wakeUpAll
cloudsupportjobs/registry listRegistryEntries
cloudsupportjobs/running listRunningJobs
cloudsupportjobs/schedules listJobSchedules
cloudsupportjobs/configurations listConfigurations
cloudsupportjobs/configurations?reload=true reloadConfigurations
cloudsupportjobs/configuration?jobName&key&value setConfiguration
cloudsupportjobs/configuration?jobName&key getConfiguration
cloudsupportjobs/history?[jobName][&executionStatus][&exitStatus][&instanceUid][&dateFrom][&dateTo][&offset][&maxResults] listHistory (com maxResults por padrão 100)

Exemplos de retorno dos endpoints:

URL: cloudsupportjobs/registry

{
    "result": [
        {
            "name": "Exemplo1"
        },
        {
            "name": "Exemplo2"
        },
        {
            "name": "Exemplo3"
        },
        {
            "name": "Exemplo4"
        }
    ]
}

URL: cloudsupportjobs/start?jobName=Exemplo4&concurrency=2&jobParams=param1%3Dvalor1

{
    "result": {
        "instanceUid": 633002179247616
    }
}

URL: cloudsupportjobs/running

{
    "result": [
        {
            "executionStatus": "ACTIVE",
            "startDate": 1715022688857,
            "stopRequestDate": null,
            "terminationDate": null,
            "lastRepeatDate": 1715022703281,
            "repeatCount": 21,
            "exitStatus": null,
            "exitMessage": null,
            "errorDetails": null,
            "terminating": false,
            "instance": {
                "uid": 633001938361856,
                "jobName": "Exemplo4",
                "jobParams": {
                    "param1": "valor1"
                },
                "concurrency": 2
            }
        }
    ]
}

URL: cloudsupportjobs/stop?instanceUid=633001938361856

{
    "result": "stop requested"
}

URL: cloudsupportjobs/schedules

{
    "result": [
        {
            "jobName": "Exemplo1",
            "enabled": false,
            "definitions": [
                {
                    "cron": null,
                    "zone": null,
                    "fixedDelay": null,
                    "fixedDelayString": null,
                    "fixedRate": 10000,
                    "fixedRateString": null,
                    "initialDelay": null,
                    "initialDelayString": null,
                    "timeUnit": "MILLISECONDS"
                }
            ]
        },
        {
            "jobName": "Exemplo2",
            "enabled": true,
            "definitions": [
                {
                    "cron": "0 0/4 * * * *",
                    "zone": null,
                    "fixedDelay": null,
                    "fixedDelayString": null,
                    "fixedRate": null,
                    "fixedRateString": null,
                    "initialDelay": null,
                    "initialDelayString": null,
                    "timeUnit": "MILLISECONDS"
                }
            ]
        }
    ]
}

URL: cloudsupportjobs/configurations

{
    "result": [
        {
            "jobName": "Exemplo1",
            "configurations": {
                "scheduling": "false"
            }
        }
    ]
}

URL: cloudsupportjobs/history

{
    "result": [
        {
            "instanceUid": 633001938361856,
            "jobName": "Exemplo4",
            "jobParams": "param1=valor1",
            "concurrency": 2,
            "executionStatus": "ACTIVE",
            "startDate": 1715022688857,
            "lastRepeatDate": 1715116047010,
            "stopRequestDate": null,
            "terminationDate": null,
            "repeatCount": 40,
            "exitStatus": null,
            "exitMessage": null
        },
        {
            "instanceUid": 633384225154752,
            "jobName": "Exemplo1",
            "jobParams": "param1=valor1, param2=valor2",
            "concurrency": 3,
            "executionStatus": "TERMINATED",
            "startDate": 1715105220595,
            "lastRepeatDate": 1715116020636,
            "stopRequestDate": null,
            "terminationDate": 171511620638,
            "repeatCount": 11,
            "exitStatus": "COMPLETED",
            "exitMessage": null
        }
    ]
}

Configuração

Ativação dos Agendamentos

O Cloudsupport define a seguinte propriedade Spring Boot para ativação dos agendamentos:

cloudsupport.jobs.scheduling.enabled=true

Por padrão seu valor é false.

Seu efeito técnico é ativar a anotação de classe @Schedule, responsável pela criação do beans do Spring que fazem o agendamento de cada rotina.

Caso as classes de agendamento estejam ativas, ainda é possível que determinados agendamentos não funcionem caso existir a configuração específica do Job que desativa seus agendamentos: scheduling=false em JobConfigurations. Vide APIs de gerenciamento para mais informações.

Observação: O Spring Scheduled Tasks possui um limite total de tarefas em paralelo, o que impacta os agendamentos de rotinas, vez que utilizam as anotações do Spring. Certifique-se de que a configuração abaixo esteja adequada ao seu microsservico:

spring.task.scheduling.pool.size=20

Tipo de Repositório

O Cloudsupport define a seguinte propriedade Spring Boot para configuração do tipo de repositório de dados:

cloudsupport.jobs.repository=jdbc

Por padrão seu valor é memory, no qual o motor de rotinas funcionará em modo full JVM, sem persistir informações em banco de dados relacional.

Utilize o valor jdbc para manter o histório de execução em banco de dados (JobInstance, JobParams e JobExecution) bem como para persistir as configurações específicas de cada Job (JobConfigurations).

Será utilizado o dataSource padrão do Spring Boot.

A seção abaixo contém a estrutura de tabelas necessária.

Banco de Dados

A seguir são apresentados scripts SQL ANSI e Liquibase para criação do banco de dados do Cloudsupport Jobs.

Script SQL:

CREATE TABLE jobinstance (
    uid BIGINT PRIMARY KEY,
    jobname VARCHAR(255),
    jobparams VARCHAR(255),
    concurrency INT
);

CREATE TABLE jobexecution (
    uid BIGINT PRIMARY KEY,
    jobinstance_uid BIGINT,
    executionstatus VARCHAR(255),
    startdate TIMESTAMP,
    lastrepeatdate TIMESTAMP,
    stoprequestdate TIMESTAMP,
    terminationdate TIMESTAMP,
    repeatcount BIGINT,
    exitstatus VARCHAR(255),
    exitmessage VARCHAR(255),
    errordetails CLOB,
    FOREIGN KEY (jobinstance_uid) REFERENCES jobinstance(uid)
);

CREATE TABLE jobparam (
    jobinstance_uid BIGINT,
    paramkey VARCHAR(255),
    paramvalue VARCHAR(255),
    FOREIGN KEY (jobinstance_uid) REFERENCES jobinstance(uid)
);

CREATE TABLE jobconfigurations (
    jobname VARCHAR(255) PRIMARY KEY,
    configurations VARCHAR(255),
    lastupdate TIMESTAMP
);

Script Liquibase:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.4.xsd">

    <changeSet id="cloudsupportjobs" author="bernardo.dias@gmail.com">
        <createTable tableName="jobinstance">
            <column name="uid" type="BIGINT">
                <constraints primaryKey="true"/>
            </column>
            <column name="jobname" type="VARCHAR(255)"/>
            <column name="jobparams" type="VARCHAR(255)"/>
            <column name="concurrency" type="INT"/>
        </createTable>
        <createTable tableName="jobparam">
            <column name="jobinstance_uid" type="BIGINT"/>
            <column name="paramkey" type="VARCHAR(255)"/>
            <column name="paramvalue" type="VARCHAR(255)"/>
        </createTable>
        <createTable tableName="jobexecution">
            <column name="uid" type="BIGINT">
                <constraints primaryKey="true"/>
            </column>
            <column name="jobinstance_uid" type="BIGINT"/>
            <column name="executionstatus" type="VARCHAR(255)"/>
            <column name="startdate" type="TIMESTAMP"/>
            <column name="lastrepeatdate" type="TIMESTAMP"/>
            <column name="stoprequestdate" type="TIMESTAMP"/>
            <column name="terminationdate" type="TIMESTAMP"/>
            <column name="repeatcount" type="BIGINT"/>
            <column name="exitstatus" type="VARCHAR(255)"/>
            <column name="exitmessage" type="VARCHAR(255)"/>
            <column name="errordetails" type="CLOB"/>
        </createTable>
        <createTable tableName="jobconfigurations">
            <column name="jobname" type="VARCHAR(255)">
                <constraints primaryKey="true"/>
            </column>
            <column name="configurations" type="VARCHAR(255)"/>
            <column name="lastupdate" type="TIMESTAMP"/>
        </createTable>
        <addForeignKeyConstraint constraintName="fk_jobparam_jobinstance"
                                 baseTableName="jobparam" baseColumnNames="jobinstance_uid"
                                 referencedTableName="jobinstance" referencedColumnNames="uid"/>
        <addForeignKeyConstraint constraintName="fk_jobexecution_jobinstance"
                                 baseTableName="jobexecution" baseColumnNames="jobinstance_uid"
                                 referencedTableName="jobinstance" referencedColumnNames="uid"/>
    </changeSet>
</databaseChangeLog>

Próximos Passos

A próxima leitura sugerida é a seção Integração, que detalha a camada de consumo de serviços remotos.