Escalabilidade e performance – Fila com job

Introdução

Em um tópico anterior, sobre escalabilidade e performance, foi abordado o tema das filas, e a sua importância na busca por desempenho em processos. Hoje, vamos ver mais de perto o que a linguagem AdvPL nos oferece para criarmos um ou mais processos dedicados sem interface visual, os “Jobs”, e com poderíamos usá-los para processar uma fila de requisições de processamento.

Processamento assíncrono

Pelo pouco que vimos nos posts anteriores sobre AdvPL e SmartClient, vimos que a interface visual nativa do AdvPL possui a arquitetura client-server, e que trabalha de modo síncrono, com conexão persistente. E, voltando um pouco nas técnicas para obter performance e escalabilidade, que quando for possível (e vamos entender essa palavra no contexto de “onde for cabível) utilize processos assíncronos.

O exemplo que serve como uma luva neste paradigma é a criação de uma fila, e um processo separado para o processamento das requisições colocadas nesta fila, onde o programa cliente da fila, ao colocar a solicitação de processamento na fila, não precisa esperar uma resposta imediata do processo para dar um retorno para a camada de interface.

Acredito que eu já tenha mencionado isso no tópico a respeito de filas, por exemplo, para o envio de e-mails de notificação ou processos secundários.

Jobs

Antes de nos afundarmos nas filas, vamos ver um pouco sobre os Jobs no AdvPL. Entendemos como “Job” o processo de executar um programa desamarrado da camada de interface. O AdvPL nos fornece algumas formas de fazer isso.

Função StartJob()

Permite, a partir de um programa AdvPL em execução, iniciar um novo contexto de execução de Advpl para a execução de uma função em um determinado ambiente do Application Server, sem relação nenhuma com a camada de interface.

Configuração JOBS da seção ONSTART

O arquivo de configuração do Application Server permite que seja configurados um ou mais jobs, executados no momento da subida ou inicialização do serviço do Application Server. Os jobs não explicitamente configurados com um tipo especial são interpretados como jobs de execução direta, onde inclusive podemos especificar quantas execuções serão disparadas ao mesmo tempo (paralelo) de um determinado job.

Partindo para a prática

No exemplo de hoje, vamos usar a configuração ONSTART, para iniciar um job dedicado a remover itens de uma fila global, e depois algumas funções interessantes para dar mais resiliência no processo. A fila de requisições possui um formato desenhado para ser um container independente de requisições, para armazenar um valor AdvPL qualquer (exceto CodeBlock e Objeto). No nosso exemplo, vamos apenas simular um processamento, que por baixo vai manter o job de processamento de fila em SLEEP() por 5 segundos. A fila vai ser mantida em memória, e independente de quantas instâncias de SmartClient forem usadas para abrir o programa que coloca requisições na fila, um Job será mantido no ar para o processamento dos elementos da fila.

O exemplo abaixo utiliza uma classe de fila global, com escopo por environment e por instância de Application Server. Para fazer o Download do arquivo que contém a classe AdvPL de fila global de exemplo, clique no link a seguir: APGlbQueue

#include 'protheus.ch'
/* ===================================================================
Função U_TSTQueue
Autor Júlio Wittwer
Data 08/02/2015
Desrição Teste de fila global com processamento 
 das requisições em JOB
Configuração de onstart para o Job de processamento
[ONSTART]
Jobs=JOBFILA01
[JOBFILA01]
Environment=NOMEDOSEUAMBIENTE
main=U_FILAPROC
nParms=1
Parm1=FILA01
=================================================================== */
User Function TSTQueue()
Local oDlg
Local oButton1
Local oSay1
Local oSay2
Local oQueue
Local nStatus
Local cMsg1 := space(40)
Local cMsg2 := space(40)
Local cMsg3 := space(40)
 
// Cria o objeto da fila global 
// Informa o nome da fila global e 
// a quantidade máxima de elementos
oQueue := APGlbQueue():New( "FILA01", 10 )
DEFINE DIALOG oDlg TITLE "Cliente da Fila" FROM 0,0 TO 160,300 PIXEL
// Botao para acrescentar na fila uma requisicao de processamento
@ 10,10 BUTTON oButton1 PROMPT "&Inserir requisição" ;
 ACTION ( InsertReq(oQueue,oSay1,oSay2) ) ;
 SIZE 080, 013 of oDlg PIXEL
@ 30,10 SAY oSay1 VAR cMsg1 SIZE 080, 013 of oDlg PIXEL
@ 40,10 SAY oSay2 VAR cMsg2 SIZE 080, 013 of oDlg PIXEL
ACTIVATE DIALOG oDlg CENTER ;
 VALID ( MsgYesNo("Deseja encerrar a aplicação ?") )
Return
/* ----------------------------------------------------------------
Inserção de requisição na fila, disparada pelo botão de interface
Atualiza valor na tela com o total de requisicoes enviadas 
---------------------------------------------------------------- */
STATIC Function InsertReq(oQueue,oSay1,oSay2)
Local nValue 
Local nStatus
// Requisicao fixa, 5 segundos ( 5000 ms ) 
nValue := 5000
nStatus := oQueue:Enqueue( nValue )
If nStatus < 0 
 // Falha ao inserir na fila
 MsgStop(oQueue:GetErrorStr(),"Falha ao acrescentar elemento na fila")
Else
 // Inseriu com sucesso, atualiza informaçoes na tela
 oSay1:SetText("Requisições inseridas ... "+cValToChar(oQueue:nEnqCnt))
 oSay2:SetText("Itens na fila ........... "+cValToChar(nStatus))
endif
Return
/* --------------------------------------------------------------
JOB dedicado para retirar e processar elementos colocados na fila
Deve ser iniciado na subida do servidor, no [ONSTART] como um
job, informando como parametro o ID da Fila Global
------------------------------------------------------------- */
USER Function FILAPROC( cQueueId )
Local nStatus
Local oQueue
Local xRequest := NIL
// Coloca observação do processo para Protheus Monitor
PtInternal(1,"Job de Processamento - Fila "+cQueueId)
If empty(cQueueId)
 // Se esta funcao foi chamada sem o ID da Fila ... 
 UserException("Missing QueueId configuration for U_FILAPROC")
Endif
conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Iniciado.")
// Cria a instância para manutenção da fila
oQueue := APGlbQueue():New( cQueueId )
While !KillApp() 
 
 // Loop de processamento permanece em execução
 // desde que esta thread nao tenha recebido
 // uma notificação de finalização 
 
 // Tenta remover o primeiro item da fila
 nStatus := oQueue:Dequeue( @xRequest )
 
 If ( nStatus >= 0 )
 
 conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Tamanho da fila = "+cValToChar(nStatus))
 
 // Pegou o primeiro item da fila
 // e retornou o numero de itens pendentes
 // neste momento na fila
 
 // Informa no console que vai fazer um "Sleep"
 conout( "["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Processando ... " )
 
 // Aqui eu poderia chamar qqer coisa
 // neste exemplo será feito um sleep mesmo 
 Sleep(xRequest)
 
 conout( "["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Fim de processamento " )
Else
 
 // Nao pegou nenhum item ... 
 // Falha de lock ou Fila vazia
 conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] "+oQueue:GetErrorStr())
Endif
 
Enddo
conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Finalizado.")
Return

Funcionamento

Todas as explicações específicas sobre o que cada parte do código fazem estão dentro do próprio código. Aqui vamos avaliar a execução da aplicação como um todo. Após compilar o fonte client de exemplo, e compilar o fonte que gerencia a fila global em memoria, e configurar o JOB para ser executado na subida do Totvs Application Server, o programa U_FILAPROC será colocado em execução via Job, sem interface, e ficará em loop fazendo Dequeue() da lista.

Ao iniciarmos a aplicação U_TSTQUEUE através do SmartClient, cada acionamento do botão “Inserir requisição” acrescenta na fila de requisições global uma requisição de processamento, que contém um número de milissegundos que a função de processamento deve permanecer em Sleep(). Podemos subir mais de um programa client ao mesmo tempo. O programa usa uma classe de exemplo, APGlbQueue, que realiza as operações de empilhamento e desempilhamento, que internamente garante a semaforização da operação de enqueue() e dequeue() (acrescentar e remover requisição da fila, respectivamente). A fila utilizada é do tipo FIFO (First In, First Out), isto é, o primeiro elemento acrescentado será o primeiro a sair da fila (e ser processado).

Cada acionamento da aplicação Client atualiza informações na tela, mostrando quantas requisições foram acrescentadas pela interface atual e quantas requisições ainda estão pendentes na fila de requisições. Podemos acompanhar o processamento da fila através das mensagens mostradas no log de console ( console.log) do Application Server. Caso a fila receba uma inserção, mas o numero de elementos na fila já está no máximo (no nosso caso, 10 elementos), a operação de Enqueue() retorna um erro, indicando que a fila está “cheia”.

Como cada requisição mantém o Job de processamento (Dequeue()) ocupado por 5 segundos, basta aguardar um pouco para a fila ficar menor, e a operação de acrescentar um novo item será possível. Mesmo que todos os programas de interface sejam encerrados, o job de processamento está em um looping separado e independente da interface, e continua em execução.

A classe de fila global criada para este exemplo utiliza um container de armazenamento global de memória do Appplication Server, onde basicamente usamos duas funções ( Set / Get ) para armazenar conteúdo nomeado na memória da instância atual de um TOTVS Application Server, e recuperá-lo dentro de qualquer job ou aplicação em execução no mesmo Appplication Server.

Considerações sobre Balanceamento de Carga

Caso este recurso seja colocado por exemplo, em um ambiente com balanceamento de carga no SmartClient, o servidor “Master” poderá redirecionar a conexão para qualquer um dos serviços configurados para balanceamento. Logo, neste caso cada serviço slave precisaria ter na seção OnStart uma chamada para colocar o job de Dequeue() em execução, senão o client vai acrescentar elementos na fila, que nunca serão processados, até ele recusar novos elementos caso a fila atinja 10 itens, ou ainda apenas um serviço seja configurado para fazer o processamento da fila, com um ou mais instâncias do job, e a aplicação client da fila utilize um RPC por exemplo, para conectar-se com o serviço que processa a fila, para acrescentar na memória deste serviço as novas requisições.

Considerações sobre a persisência da fila

Uma vez que a fila, no nosso exemplo, está sendo persistida em memória, se o serviço do Application Server for finalizado, o processamento pode ser encerrado antes de todas as requisições da fila terem sido processadas, então este exemplo não pode ser usado para etapas importantes de processo, que exijam por exemplo que a requisição desta fila não possa se perder em caso de uma falha grave ou finalização anormal do serviço. Caso seja necessária uma persistência segura, o correto a fazer é criar uma classe que persista a informação da fila em um banco de dados, para que quando o processo seja recolocado no ar, as requisições pendentes sejam processadas. E, remover uma requisição da fila não quer dizer que o processamento foi feito com sucesso, mas sim que alguém removeu para processar. Podem ser necessários mais mecanismos para garantir que o processo tenha chego ao final com sucesso. Vamos abordar mais destas questões em um exemplo um pouco mais rebuscado do que este, em um próximo post.

Conclusão

Este post demorou quase 2 semanas para sair, afinal eu queria publicar algo realmente diferente, e relativamente simples, para aplicar conceitos como o das filas, paralelismo e jobs, e este exemplo teve pelo menos duas refatorações até estar “no ponto” de ser servido !!! Até o próximo post, pessoal 😉

Anúncios

9 comentários sobre “Escalabilidade e performance – Fila com job

  1. Julião, parabéns pelo post. Só fiquei com uma dúvida e não sei se posso sanar por aqui. Não entendi o trecho abaixo:

    “A fila de requisições possui um formato desenhado para ser um container independente de requisições, para armazenar um valor AdvPL qualquer (exceto CodeBlock e Objeto).”

    Obrigado.

    Claudio

    Curtido por 1 pessoa

    • Opa, perfeitamente. Acho que exagerei no gramatiquês …risos … No fonte de exemplo, foi usado um valor numérico para ser acrescentado na lista. Na verdade, eu posso acrescentar na lista qualquer valor Advpl, como um array, string, data, booleano, etc… apenas nao pode ser usado como parametro para enfileiramento os tipos de varíaveis “B” (CodeBlock) e “O” (Objeto).

      Abraços

      Curtir

    • Olá Bruno 😀

      Esta classe ( ApGlbQueue ) também foi feita em Advpl, eu não coloquei o fonte dela como parte do texto do post para não ficar muito grande. O fonte da classe ApGlbQueue está disponivel em um link de download no post, logo acima do fonte client ( … Para fazer o Download do arquivo que contém a classe AdvPL de fila global de exemplo, clique no link a seguir: APGlbQueue … — https://siga0984.files.wordpress.com/2015/02/apglbqueue.docx )

      Abraços

      Curtir

  2. Olá, fiz um programa para ler algumas informações de tabelas e gerar um arquivo XML para ser gravado em um diretório local. Se executado via menu funciona corretamente a geração do arquivo no diretório indicado, porém quando executo via Schedule não gera o arquivo. Alguém tem alguma ideia do que pode estar errado?

    Segue trecho do código:

    nHdlXml:= FCreate(cAnexo,0)
    If nHdlXml > 0
    FWrite(nHdlXml,cXmlDados)
    FClose(nHdlXml)
    Endif

    Curtido por 1 pessoa

    • Olá Evaldo… verifique exatamente o nome completo do arquivo informado. Se você está trabalhando com um JOB, o acesso aos arquivos somente vai funcionar se o arquivo acessado estiver em uma pasta dentro do rootpath, e neste caso você deve endereçá-lo sem usar nenhuma unidade de disco. por exemplo, se o seu rootpath é c:\Protheus11\ap_Data”, e seu arquivo está na pasta “c:\Prothes11\ap_data\trocas\meuarquivo.txt”, você deve informar para a função apenas “\trocas\meuarquivo,txt” 😀

      []s

      Curtir

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s