Protheus e AdvPL ASP – Parte 01

Introdução

Nos posts anteriores sobre O Protheus como servidor HTTP – Parte 01O Protheus como servidor HTTP – Parte 02 , vimos como configurar o Protheus Application Server como servidor HTTP de páginas estáticas. Agora, vamos ver como fazer ele processar páginas dinâmicas — o AdvPL ASP. É importante ler os posts anteriores para partir para o AdvPL ASP.

AdvPL ASP

ASP, acrônimo para Active Server Pages, foi o primeiro tipo de script criado pela Microsoft para gerar conteúdo dinâmico de páginas para Internet, onde o script é processado no servidor mediante a requisição do Browser, e a página HTML é gerada no servidor ao executar o script, e retornada ao Browser. O arquivo que contém o Script é composto por conteúdo HTML, e dentro dele separadores especiais delimitam as partes de código / programa que são executadas.

O AdvPL ASP é uma forma similar ao ASP de criação de páginas dinâmicas proprietária do Protheus Server. As duas diferenças básicas são: O código delimitado para execução dentro da página é AdvPL, e uma página (ou arquivo) AdvPL ASP é compilado no repositório de objetos, sendo executado internamente como uma função AdvPL, ao invés de ser interpretado no momento em que for solicitado. Isso inclusive não “expõe”o arquivo original na pasta de publicações do HTTP. Isso mesmo, o arquivo contendo AdvPL ASP pode ficar em uma pasta não publicada na WEB, como uma pasta de fontes do Projeto.

Arquivo APH

Um arquivo contendo AdvPL ASP deve possuir a extensão “.aph“, para ele ser compilado diferenciadamente pelo IDE/TDS e pelo Application Server Protheus. Dentro dele, usamos as tags <% e %> para delimitar código AdvPL que deve ser executado, e <%= <expressão %> para qualquer expressão AdvPL que deve ser acrescentada na página final a ser retornada ao Browser.

Um arquivo chamado teste.aph, ao ser acrescentado em um projeto do AdvPL e compilado, vai virar dentro do repositório uma nova função, chamada H_TESTE() — Prefixo H_ seguido do nome do arquivo. Portanto, atenção ao dar nomes para os arquivos APH, por hora eles não devem ter mais que 8 letras no nome, devem sempre começar com uma letra alfabética, e pode conter a partir de então apenas letras e números — a mesma regra usada para nomear funções do AdvPL.

A função criada a partir da compilação do arquivo APH basicamente cria um retorno em uma variável string, declarada internamente na função, e concatena dentro tela todo o conteúdo estático e dinâmico gerado durante a chamada da função. Isto também significa que, eu posso usar um arquivo APH não apenas para HTML, mas para qualquer outra geração de string dinâmica, mas isso veremos mais para a frente. Por hora, apenas crie um arquivo index.aph no seu ambiente, com o seguinte conteúdo:

<html><body>
<p>Olá Mundo AdvPL ASP</p>
<p>Agora são <%=Time()%></p>
</body></html>

Configuração do Protheus Server para AdvPL ASP

Legal, vamos lembrar como configuramos o nosso site estático no Protheus Server como HTTP:

[http]
enable=1
port=80
path=c:\Protheus12LG\Http
defaultpage=index.html

Com esta configuração — sem configurar hosts ou pastas virtuais — qualquer requisição que chegar na porta HTTP (80) do Protheus Server será atendida considerando o path raid de publicação WEB  a partir da pasta C:\Protheus12LG\Http

Agora, vamos acrescentar uma chave a mais na seção http, e uma nova seção no appserver.ini, para ele ficar assim:

[http]
enable=1
port=80
path=c:\Protheus12LG\Http
defaultpage=index.html
responsejob=webaspthreads

[webaspthreads]
type=webex
environment=envlight
instances=1,2
onstart=U_ASPInit
onconnect=U_ASPConn

Muito bem, vamos por partes: webaspthreads foi o nome que eu dei para a seção de configuração de um POOL de processos AdvPL — também chamado de Working Threads — do tipo WEBEX — para processamento de AdvPL ASP — que vai atender a todas as requisições de AdvPL ASP que o HTTP Server receber. Eu poderia chamar ela de outros nomes, como poolwebjob01 ou o que lhe for conveniente, apenas não pode ser usado nenhum nome de configuração reservada ou outro que conflite com outra seção já existente no arquivo de configuração.

No meu servidor, meu environment de compilação e execução de código AdvPL chama-se “envlight“. Ao usar esta configuração no seu Protheus Server, use o nome do seu environment.

A configuração de instances indica qual é o número de processos AdvPL mínimo e máximo que serão gerenciados internamente pelo POOL de atendimento de requisições de páginas dinâmicas do AdvPL ASP. Por hora, minimo de 1 e máximo de 2 processos está ótimo.

As configurações onstart e onconnect especificadas estão apontando para duas funções que nós vamos criar, pois elas serão chamadas internamente pelo Protheus em momentos distintos, para compor o pool de processos AdvPL para atender a requisições de páginas AdvPL ASP. Pra não deixar pra depois, crie um arquivo no seu projeto de testes, chamado por exemplo AspThreads.prw, e dentro dele as seguintes funções:

#include 'protheus.ch'

User Function ASPInit()
conout("ASPINIT - Iniciando Thread Advpl ASP ["+cValToChar(ThreadID())+"]")
SET DATE BRITISH
SET CENTURY ON
Return .T.

USER Function ASPConn()
Local cReturn := ''
Local cAspPage 
Local nTimer
cAspPage := HTTPHEADIN->MAIN
If !empty(cAspPage)
  nTimer := seconds()
  cAspPage := LOWER(cAspPage)
  conout("ASPCONN - Thread Advpl ASP ["+cValToChar(ThreadID())+"] "+;
         "Processando ["+cAspPage+"]")
  do case 
  case cAspPage == 'index'
    // Execura a página INDEX.APH compilada no RPO 
    // A String retornada deve retornar ao Browser
    cReturn := H_INDEX()
  otherwise
    // retorna HTML para informar 
    // a condição de página desconhecida
    cReturn := "<html><body><center><b>"+;
               "Página AdvPL ASP não encontrada."+;
               "</b></body></html>"
  Endcase
  nTimer := seconds() - nTimer
  conout("ASPCONN - Thread Advpl ASP ["+cValToChar(ThreadID())+"] "+;
         "Processamento realizado em "+ alltrim(str(nTimer,8,3))+ "s.")
Endif
Return cReturn

Fazendo a requisição AdvPL ASP

Uma vez tudo configurado corretamente, e os fontes index.aph e o aspthreads.prw compilados no repositório do ambiente configurado, vamos à ultima informação: Como chamar a página AdvPL ASP pelo Browser.

A extensão “.apw” foi reservada para indicar ao servidor HTTP do Protheus, que ele está recebendo uma requisição de processamento de AdvPL ASP. Logo, abra o browse no seu equipamento, e informe a URL http://localhost/index.apw — se tudo deu certo, você deve ver esta mensagem no seu Browser.

Advpl ASP - Ola Mundo

E, no log de console do Protheus Server, você deve ver as seguintes mensagens:

ASPINIT - Iniciando Thread Advpl ASP [1560]
ASPCONN - Thread Advpl ASP [1560] Processando [index]
ASPCONN - Thread Advpl ASP [1560] Processamento realizado em 0.002s.

Resumo para fixação

APH = Extensão de arquivo AdvPL ASP, que ao ser compilado e acrescentado ao projeto, gera no repositório de objetos do ambiente uma função, chamada H_<nomedoarquivo>, para ser usada para compor páginas de HTML dinâmico. O arquivo APH não deve ser colocado dentro da pasta raiz de publicações WEB — como ele é compilado, ele é um arquivo que faz parte de um Projeto AdvPL. Seu conteúdo será gerado mediante requisição de processamento de página dinâmica.

APW = Extensão de LINK para ser usado no Browser, para pedir ao servidor Protheus HTTP uma solicitação de uma página dinâmica. Uma solicitação através desse link será redirecionada a um POOL de threads pré-configurado dentro do Protheus Server, chamando a função configurada no parâmetro ONCONNECT do POOL.

Working Threads do tipo WEBEX = Pool de threads de trabalho, criado para atender a requisições de links de páginas dinâmicas AdvPL ASP (url com terminação .apw) , onde uma ou mais threads AdvPL são colocados em execução, inicializados com a função especificada na configuração ONSTART, que ficam em modo de espera para atender uma requisição. Quando um processo é alocado para atender uma requisição, a função ONCONNECT é chamada, usando o contexto do processo já existente, que permanece alocado (ocupado) até o final do processamento, quando o HTTP Server retorna a string gerada ao Browser, e o processo usado torna-se disponivel novamente para atender outra requisição de extensão APW, que pode vir do mesmo ou de qualquer outro Web Browser.

Inteligência do Pool de Threads

Lembra-se do parâmetro INSTANCES, configurado no Pool de Threads?  Então, no nosso exemplo usamos 1,2. Isto significa que, este pool de processos inicia com somente um processo para atender as requisições de links APW. Se, em um determinado momento, todos os processos estiverem ocupados em atendimento, e chegar uma nova requisição de AdvPL ASP, o Protheus Server vai colocar essa requisição em uma fila com timeout de alguns segundos, e caso o número de processos total do pool ainda não tenha atingido o máximo, ele sobe mais um processo.

Isso garante por exemplo, que somente quando o site começar a receber forte concorrência de requisições, e os processos em execução estiverem todos ocupados, mais processos serão colocados no ar, visando economia de recursos. Isso também permite definir um limite máximo de processos, para evitar que uma avalanche de requisições ou algum processo muito demorado faça com que o Protheus Server suba mais processos do que ele aguentaria dar conta — normalmente por causa de excesso de memória consumida ou mesmo de CPU.

Detalhes adicionais

Quer ver algumas coisas interessantes? Crie uma user function — vide modelo abaixo — e vamos ver o que ela vai fazer.

User Function TstAPH()
Local cRet := ''
cRet := H_IndeX()
conout(cRet)
return

Agora execute ela diretamente pelo SmartClient, e veja o resultado no log de console do Application Server. Deve ser mostrado algo assim:

[INFO ][SERVER] [11/11/2018 23:55:19] Starting Program U_TSTAPH Thread 5516 (siga0,NOTE-JULIOW-SSD)
<html><body>
<p>Olß Mundo AdvPL ASP</p>
<p>Agora sπo 23:55:20</p>
</body></html>


[INFO ][SERVER] [Thread 5516] [11/11/2018 23:55:20] Thread finished (siga0, NOTE-JULIOW-SSD, 2.63 MB.)

Na prática, eu obtive a string da página de retorno, executando meu código pelo SmartClient. Isso somente foi possível pois dentro do arquivo index.aph eu não usei nada específico do ADVPL ASP. A infraestrutura de processo oferecida pelo Protheus Server para a execução de links APW usando Working Threads oferece o uso de alias virtuais especiais para recuperar informações vindas como parâmetro de GET e POST do Browser, além de um controle nativo de variáveis de SESSION (por usuário) e COOKIES do Browser. Estes recursos estão detalhados na TDN, vide links nas referências no final deste post.

O diferencial da documentação do TDN é que as configurações das funções que fazem o ONSTART e o ONCONNECT do Pool de Threads indica que você deve usar as funções de LIB e Framework WEB compiladas no repositório do ERP, para usar alguns pseudo-comandos, como WEB EXTENDED INIT … WEB EXTENDED END e outros recursos de encapsulamento. No caso do nosso exemplo, estamos usando funções específicas e customizadas para atender uma necessidade didática. De qualquer modo, os recursos de ALIAS VIRTUAIS documentados nesta seção são oferecidos pelo Protheus Server, não dependem da LIB.

Conclusão

Com isso, começamos uma jornada no mundo do AdvPL ASP, e abrimos portas para criar muitas coisas legais. A titulo de informação, os Web Services Server do ERP Microsiga foram todos implementados em AdvPL, usando como base o POOL de Working Threads do AdvPL ASP, apenas implementando tratamentos específicos para receber um XML SOAP via POST, e retornar um XML de retorno ao invés de retornar um HTML. Os portais do ERP Microsiga foram implementados em duas camadas, uma sobre o AdvPL ASP e outra sobre Web Services. O portal em si é uma aplicação AdvPL ASP responsável por montar a interface dinâmica em HTML, consumindo um grupo de serviços publicados em um Web Services Server. Desse modo as Working Threads do portal são muito leves, pois não tem conexão persistente com Banco de Dados e não mantém tabelas abertas. Ela apenas consome um Web Service hospedado em outro serviço, e usa os retornos para montar os HTMLs dinâmicos para os usuários interagirem com as funcionalidades.

Agradeço novamente as curtidas, compartilhamentos, comentários, dúvidas e afins, e desejo novamente a todos TERABYTES de sucesso !!!

Referências

 

Executando Jobs em AdvPL

Introdução

Em todos os posts anteriores sobre escalabilidade, desempenho e afins, sempre aparece o “tal” do JOB. No AdvPL, genericamente damos o nome de “Job” para um processamento de uma função AdvPL iniciada em um ambiente sem interface com o usuário — ou seja, sem conexão com o SmartClient). Neste post, vamos ver em detalhes algumas formas que o AdvPL permite a execução de JOBs.

JOBS “ONSTART”

No arquivo de configurações do Servidor de Aplicação Protheus Server, conhecido por “appserver.ini”, podemos definir um ou mais JOBS, que por default são iniciados apenas uma vez, no momento que o serviço do Protheus é iniciado.

Basta criar uma nova seção nomeada no appserver.ini para cada JOB ou programa que deve ser executado em JOB — lembrando de usar um nome intuitivo para o job, e nao usar nenhuma palavra reservada de configuração — , depois criar uma seção chamada [ONSTART], e dentro dela colocar a lista de jobs a serem executados no início do serviço do Protheus Server dentro da configuração JOBS, separados por vírgula.

Exemplo 01

[ONSTART]
JOBS=JOBMSG1

[JOBMSG1]
main=conout
environment=envp12
nparms=1
parm1="Meu Primeiro Job"

No exemplo acima, configuramos um JOB para ser executado apenas uma vez, na subida do serviço do Protheus Server. O JOB foi configurado para chamar a função “Conout” do AdvPL, para emitir uma mensagem no log de console. Também foi configurado para passar uma string de parâmetro para a função conout(), contendo o texto “Meu Primeiro Job”.

A passagem de parâmetros para um JOB é opcional, as configurações obrigatórias são:

  • MAIN = Identifica o nome da função a ser chamada
  • ENVIRONMENT = Identifica o ambiente de execução desta função no Protheus.

Exemplo 02

[ONSTART]
JOBS=JOBMSG2

[JOBMSG2]
main=conout
environment=envp12
instances=3
nparms=2
parm1="Meu Primeiro Job Multiplo"
parm2="-------------------------"

No exemplo acima, apenas acrescentamos a configuração INSTANCES=3, para indicar para o Protheus Server que ele deve subir três processos (ao invés de apenas um) e executar em cada um deles a mesma função conout(), porém agora com dois parâmetros (a string “Meu Primeiro Job Múltiplo”, seguido de uma string de mesmo tamanho com uma sequencia de ‘-‘). O Resultado no log de console do AppServer deve ser algo parecido com:

Meu Primeiro Job Multiplo
-------------------------
Meu Primeiro Job Multiplo
-------------------------
Meu Primeiro Job Multiplo
-------------------------

Passagem de parâmetros ao JOB

Como vimos nos exemplos acima, podemos passar um ou mais parâmetros para os JOBS, identificando a quantidade de parâmetros a ser enviada usando a configuração NPARMS, e depois cada parâmetro como uma string, identificada como PARM1, PARM2, etc… Os parâmetros neste caso sempre serão passados para as funções a serem executadas como “C” Caractere. Caso os conteúdos dos parâmetros sejam informados entre aspas duplas, as aspas serão removidas automaticamente dos parâmetros para a passagem do valor ao AdvPL.

Exemplo 03

Vamos criar uma função simples para ser usada no lugar do conout() dos exemplos acima. Compile no seu repositõrio a função abaixo:

USER FUNCTION MYJOB1( cMsg1, cMsg2 ) 
conout("Thread ["+cValToChar(ThreadID())+"] executando ... ")
conout("Thread ["+cValToChar(ThreadID())+"] cMsg1 = "+cValToChar(cMsg1))
conout("Thread ["+cValToChar(ThreadID())+"] cMsg2 = "+cValToChar(cMsg2)
sleep(1000)
conout("Thread ["+cValToChar(ThreadID())+"] saindo ... ")
return

Agora, usando a configuração do Exemplo 02, troque na configuração main= conout por main=U_MYJOB1, compile o fonte acima, pare e suba o Servidor de Aplicação novamente, e vamos ver o resultado no LOG de console:

Thread [7048] executando ...
Thread [7048] cMsg1 = Meu Primeiro Job Multiplo
Thread [7048] cMsg2 = -------------------------
Thread [16432] executando ...
Thread [16432] cMsg1 = Meu Primeiro Job Multiplo
Thread [16432] cMsg2 = -------------------------
Thread [16508] executando ...
Thread [16508] cMsg1 = Meu Primeiro Job Multiplo
Thread [16508] cMsg2 = -------------------------
Thread [7048] saindo ...
Thread [16432] saindo ...
Thread [16508] saindo ...

Exemplo 04

Agora, vamos mexer um pouco na função MYJOB1, para ela “nao sair” …

USER FUNCTION MYJOB1( cMsg1, cMsg2 ) 
conout("Thread ["+cValToChar(ThreadID())+"] executando ... ")
conout("Thread ["+cValToChar(ThreadID())+"] cMsg1 = "+cValToChar(cMsg1))
conout("Thread ["+cValToChar(ThreadID())+"] cMsg2 = "+cValToChar(cMsg2)]
While !killapp()
  conout("Thread ["+cValToChar(ThreadID())+"] Hora Atual = "+time())
  sleep(1000)
Enddo
conout("Thread ["+cValToChar(ThreadID())+"] saindo ... ")
return

O resultado esperado no log de console será algo parecido com isso:

Thread [2928] executando ...
Thread [2928] cMsg1 = Meu Primeiro Job Multiplo
Thread [2928] cMsg2 = -------------------------
Thread [2928] Hora Atual = 01:00:21
Thread [13224] executando ...
Thread [13224] cMsg1 = Meu Primeiro Job Multiplo
Thread [13224] cMsg2 = -------------------------
Thread [13224] Hora Atual = 01:00:21
Thread [15056] executando ...
Thread [15056] cMsg1 = Meu Primeiro Job Multiplo
Thread [15056] cMsg2 = -------------------------
Thread [15056] Hora Atual = 01:00:21
Thread [2928] Hora Atual = 01:00:22
Thread [13224] Hora Atual = 01:00:22
Thread [15056] Hora Atual = 01:00:22
Thread [2928] Hora Atual = 01:00:23
Thread [15056] Hora Atual = 01:00:23
Thread [13224] Hora Atual = 01:00:23
Thread [2928] Hora Atual = 01:00:24
Thread [15056] Hora Atual = 01:00:24
Thread [13224] Hora Atual = 01:00:24

A função KillApp() somente retornará .T. caso o processo atual seja derrubado pelo Protheus Monitor, ou caso o serviço do Protheus Seja finalizado. No nosso exemplo, cada job fica em LOOP mostrando a hora atual no log de console, dorme por 1 segundo, e recomeça o loop.

Vale lembrar que um JOB é colocado no ar apenas na instância do Protheus Server na qual ele foi configurado no arquivo de inicialização. Colocar um JOB na seção [ONSTART] do Aplication Server configurado para ser, por exemplo, o servidor Master para Balanceamento de Carga, fará o JOB rodar apenas naquela instância do Protheus Server.

Evolução dos Exemplos

Conhecendo um pouco mais das funções de comunicação entre processos do AdvPL, como por exemplo as funções IPCWAITEX e IPCGO, com mais algumas linhas de código podemos criar um mecanismo de processamento em JOB assíncrono, onde cada job pode ficar esperando uma requisição de IPC usando um mesmo identificador nomeado, e receber notificações de processamento de um ou mais programas sendo executados neste mesmo Serviço do Protheus.

Exemplo 05

Vamos mexer mais um pouco no nosso JOB, para ele ser capaz de receber o nome de uma função de usuário para ser executada em modo assíncrono. Criamos uma função “Cliente”, que solicita o processamento de uma função especifica, para contar de 1 a 10. Colocamos três jobs no ar disponíveis para realizar as chamadas, e executamos a aplicação client, solicitando um novo envio de processamento a cada 1 segundo, por quatro vezes, e vamos ver o que acontece.

USER Function MyClient()
While MsgYesNo("Envia um processamento a um JOB ?")
  If IpcGo("MYJOB_IPC","U_MYTASK",1,10)
    MsgInfo("Processamento enviado.")
  Else
    MsgSTop("Nao foi possivel enviar a requisicao. Nao há jobs disponiveis.")
  Endif
Enddo
Return

USER FUNCTION MYJOB1( cMsg1, cMsg2 ) 
Local cFN,p1,p2

conout("Thread ["+cValToChar(ThreadID())+"] iniciando ... ")

While !killapp()
  cFN := NIL 
  p1 := NIL
  p2 := NIL 
  conout("Thread ["+cValToChar(ThreadID())+"] Aguardando um chamado ... ")
  If IpcWaitEx("MYJOB_IPC",5000,@cFN,@p1,@p2)
    conout("Thread ["+cValToChar(ThreadID())+"] Executando " + p1)
    &cFN.(p1,p2)
  Endif
Enddo

conout("Thread ["+cValToChar(ThreadID())+"] saindo ... ")
return


USER Function MYTASK(p1,p2)
Local nX
conout("Thread ["+cValToChar(ThreadID())+"] --- Inicio da tarefa ...")
For nX := p1 TO p2 
  conout("Thread ["+cValToChar(ThreadID())+"] --- Contando "+cValToChar(nX)+" ( "+cValToChar(p1)+" a "+cValToChar(p2)+" )")
  Sleep(1000)
Next
conout("Thread ["+cValToChar(ThreadID())+"] --- Final da tarefa ...")
Return

Log de execução

Thread [9664] iniciando ...
Thread [9664] Aguardando um chamado ...
Thread [7368] iniciando ...
Thread [7368] Aguardando um chamado ...
Thread [11852] iniciando ...
Thread [11852] Aguardando um chamado ...
Thread [9664] Aguardando um chamado ...
Thread [7368] Aguardando um chamado ...
Thread [11852] Aguardando um chamado ...
Thread [9664] Executando U_MYTASK
Thread [9664] --- Inicio da tarefa ...
Thread [9664] --- Contando 1 ( 1 a 10 )
Thread [9664] --- Contando 2 ( 1 a 10 )
Thread [9664] --- Contando 3 ( 1 a 10 )
Thread [7368] Executando U_MYTASK
Thread [7368] --- Inicio da tarefa ...
Thread [7368] --- Contando 1 ( 1 a 10 )
Thread [9664] --- Contando 4 ( 1 a 10 )
Thread [7368] --- Contando 2 ( 1 a 10 )
Thread [9664] --- Contando 5 ( 1 a 10 )
Thread [11852] Aguardando um chamado ...
Thread [7368] --- Contando 3 ( 1 a 10 )
Thread [9664] --- Contando 6 ( 1 a 10 )
Thread [11852] Executando U_MYTASK
Thread [11852] --- Inicio da tarefa ...
Thread [11852] --- Contando 1 ( 1 a 10 )
Thread [7368] --- Contando 4 ( 1 a 10 )
Thread [9664] --- Contando 7 ( 1 a 10 )
Thread [11852] --- Contando 2 ( 1 a 10 )
Thread [7368] --- Contando 5 ( 1 a 10 )
Thread [9664] --- Contando 8 ( 1 a 10 )
Thread [11852] --- Contando 3 ( 1 a 10 )
Thread [7368] --- Contando 6 ( 1 a 10 )
Thread [9664] --- Contando 9 ( 1 a 10 )
Thread [11852] --- Contando 4 ( 1 a 10 )
Thread [7368] --- Contando 7 ( 1 a 10 )
Thread [9664] --- Contando 10 ( 1 a 10 )
Thread [11852] --- Contando 5 ( 1 a 10 )
Thread [7368] --- Contando 8 ( 1 a 10 )
Thread [9664] --- Final da tarefa ...
Thread [9664] Aguardando um chamado ...
Thread [11852] --- Contando 6 ( 1 a 10 )
Thread [7368] --- Contando 9 ( 1 a 10 )
Thread [11852] --- Contando 7 ( 1 a 10 )
Thread [7368] --- Contando 10 ( 1 a 10 )
Thread [11852] --- Contando 8 ( 1 a 10 )
Thread [7368] --- Final da tarefa ...
Thread [7368] Aguardando um chamado ...
Thread [11852] --- Contando 9 ( 1 a 10 )
Thread [11852] --- Contando 10 ( 1 a 10 )
Thread [9664] Aguardando um chamado ...
Thread [11852] --- Final da tarefa ...
Thread [11852] Aguardando um chamado ...
Thread [7368] Aguardando um chamado ...
Thread [9664] Aguardando um chamado ...
Thread [11852] Aguardando um chamado ...
Thread [7368] Aguardando um chamado ...
Thread [9664] Aguardando um chamado ...
Thread [11852] Aguardando um chamado ...

O programa cliente enviou três requisições com sucesso, onde cada uma ocupou um dos processos em espera. Cada processo deve ficar ocupado por 10 segundos, logo se o programa cliente tentou enviar uma quarta requisição em menos de 10 segundos depois de ter enviado a primeira, ela não vaio encontrar nenhum JOB aguardando em IPCWAITEX, e a função IPCGO() retorna .F. pois não conseguiu enviar a chamada para frente.

O resultado é lindo, a função cliente apenas disparou um IPCGO() com o que precisa ser feito, e a função foi executada em um job pré-configurado, sem que o cliente precisasse esperar pelo retorno ou pelo fim do processamento. Isso para um teste ou uma prova de conceito, até funciona. Mas no mundo real, normalmente é necessário saber se aquele processamento foi concluído com sucesso, ou se aconteceu alguma coisa durante o processamento, e o que aconteceu.

Fatiando processos

Mas, por hora vamos focar em um ponto importante: — Você não precisa e nem deve preocupar-se com “quantas instancias” serão colocadas no ar para dividir ou dimensionar seu trabalho.

Sim, é isso mesmo. É natural você pensar imediatamente em registros por processos e número de processos, por exemplo: Se eu tenho 50 mil títulos para serem processados, e vou subir 10 jobs, então eu vou mandar 5 mil títulos para cada JOB … Porém isso traz algumas consequências desagradáveis.

A primeira delas é: Mesmo que a velocidade média do processamento de um título seja igual entre os processos, cada job vai ficar no ar ocupado por muito tempo processando um lote muito grande de informações. Qualquer variação no tempo de processamento pode fazer um job terminar a sua parte muito rápido, enquanto outros jobs podem demorar mais tempo para terminar a sua parte … e o seu processamento somente está COMPLETO quando todos os JOBS terminarem. Por exemplo, imagina que a média de processamento de um título seja entre 1,5 e 1 segundo. Isto significa que, um processamento de 5 mil títulos pode demorar de 41 a 82 minutos.

Para evitar isso, o ideal é definir um lote de títulos de um determinado tamanho, de modo que o processamento de um lote demore de 30 a 120 segundos. Depois, você deve subir uma quantidade de jobs no ambiente que não prejudiquem o desempenho deste servidor de aplicação, e esse numero varia de ambiente para ambiente. Mantendo cada JOB ocupado por um período mais curto de tempo, eles vão passar mais tempo rodando em paralelo.

Considerando o caso anterior, vamos simplificar o exemplo dividindo 10 mil registros em 2 jobs, cada um deles já pegou a sua “faixa” de registros para processar … embora os dois tenham iniciados juntos, um deles terminou a parte dele em 45 minutos, e o outro demorou mais 20 minutos, e o processo “pai” teve que esperar todos os processos filho terminarem para obter um resultado. Criando um lote de processamento de, por exemplo 60 títulos por vez, cada job ficará alocado de 30 segundos a um minuto para processar o seu lote, ficando disponível novamente para pegar o próximo lote. Neste caso, os dois jobs ficariam quase todo o tempo rodando em paralelo, ficando um deles no máximo 30 segundos “sozinho” terminando de processar o ultimo lote.

Controlando a execução

Voltando um pouco antes da parte de “Fatiar” um processo, é importante saber se todas as requisicoes enviadas para processamento foram realmente processadas com sucesso. Existem várias formas de se fazer isso, inclusive durante a execução do processo, ou no final dele.

Por exemplo, sabendo que cada processo dedicado deve pegar um lote de 30 títulos — fixos ou determinados por uma faixa de intervalo — o programa principal (pai) deste processamento pode criar uma tabela registrando todas as requisições que foram distribuídas, e cada programa que finaliza o processamento de uma requisição, atualiza o Flag dela na tabela de controle. Assim, no final do processo, quando todos os jobs tiverem terminado de processar e não houver nada mais pendente, o LOG de operação deve ser verificado para ver se está tudo em ordem

Inclusive, existem erros graves que não são interceptados por rotinas de tratamento e recuperação de erro, que geram apenas um log local. Quando pensamos em múltiplos jobs, temos estes pontos a considerar.

A função STARTJOB

Utilizando a função STARTJOB() do AdvPL, podemos subir uma instância para rodar uma função qualquer em tempo de execução. O novo processo será colocado no ar no mesmo serviço do Protheus em uso pelo processo “pai” que iniciou o JOB. Para subir múltiplas instâncias, basta fazer um FOR .. NEXT, e podemos passar nenhum, um ou vários parâmetros para a função a ser executada. Veja a documentação da função na TDN, nas referências no final do post.

Existe JOB remoto ?

Sim, existe. Quando usamos uma conexão RPC nativa do AdvPL entre serviços do Protheus Server, podemos iniciar um JOB no servidor alvo da conexão RPC. Logo, uma rotina em AdvPL, onde você possa mapear e fazer RPC para os serviços do Protheus que devam fazer parte de um grande processamento, seria capaz de subir jobs remotos via RPC, e através do RPC, fazer uma distribuição de tarefas para os jobs dinamicamente, usando por exemplo um algoritmo round robin — pega a lista de objetos RPC conectados, passa uma tarefa para um job do primeiro servidor, vai para o próximo servidor, se a tarefa anterior foi passada com sucesso pega uma nova, senão tenta passar para o servidor atual, acabou a lista de conexões, volta para a primeira, mantem dentro desse loop até acabarem as tarefas, coloca um SLEEP de um segundo caso ele tenha varrido todo o array de objetos RPC e não foi encontrado nenhum job livre, espera os jobs remotos terminarem as suas tarefas, e finaliza a rotina.

Existem outros tipos de JOB ?

Sim, também configurados no appserver.ini, existem JOBS que são configurados como Pools de Working Threads, com ou sem inicialização automatica pela seção ONSTART. Por exemplo, o JOB TYPE=WEBEX, utilizado para Working Threads de Portais, WEBEX (AdvPL ASP) e WebServices.

Scheduler do ERP

Vale lembrar que o ERP Microsiga possui uma funcionalidade chamada “Scheduler”, que permite a execução programa de tarefas agendadas, com muito mais detalhes e controles. O objetivos dos exemplos deste post são direcionados a mostrar formas mais simples e nativas da linguagem AdvPL e do TOTVS Application Server de executar processos em JOB.

Conclusão

Cada linguagem de programação oferece um nível de flexibilidade para criarmos uma solução, o segredo está em buscar o uso de alternativas simples, e usar as alternativas mais complexas onde elas são necessárias, buscando implementá-las também de forma simples. Eu acredito que esse é o desafio constante do desenvolvedor de software.

Desejo a todos(as) TERABYTES de SUCESSO 😀

Referências

ONSTART 
IPCWAITEX
IPCGO 
Round Robin

Dicas valiosas de programação – Parte 01

Introdução

Ao longo do tempo, cada analista de sistemas e programador adquire experiência e proeficiência em algoritmos e soluções de software para atender a necessidade de seus clientes. Normalmente cada linguagem têm os seus “pulos do gato”, muitos são conhecidos e são independentes da linguagem de programação. Neste post vamos abordar os mais conhecidos, e que sempre devem ser lembrados, com ênfase no “porquê” é melhor fazer assim do que assado.

Separe o Processamento da Interface

Basicamente, você escreve um pouco mais de código, visando criar no mínimo duas etapas de processamento em códigos distintos. O primeiro código, responsável pela camada de interface, deve criar para o usuário uma interativa de fornecer os dados e parâmetros para um processamento.

Por exemplo, uma inclusão de novo cliente, ou a emissão de um relatório. Você constrói uma interface onde o usuário possa preencher os campos de cadastro ou os parâmetros a serem considerados na emissão do relatório.

O segundo código é responsável pela realização efetiva do processamento. Porém, este código não deve ter acesso a interface. Ele deve receber os dados para serem processados como argumento da função, e o retorno de sua execução deve indicar se o processamento foi executado com sucesso, e em caso de falha, os detalhes pertinentes da falha — como por exemplo parametrização inválida, tentativa de incluir cliente já existente, etc…

Por que fazer dessa forma ?

  1. Uma rotina escrita para não depender de interface pode ser executada SEM interface, em processamentos do tipo “JOB”. Isso torna possível, por exemplo, a criação de JOBs dedicados para realizar várias inclusões ao mesmo tempo, ou por exemplo a execução do relatório através de um programa do tipo “Agendador de Tarefas” (ou Scheduler) sem a dependência de ser iniciado pelo usuário.
  2. A rotina responsável pelo processamento deve possuir as validações adequadas da parametrização envolvida na sua execução, assim você escreve uma única função de processamento, e pode fazer a chamada dela a partir de várias interfaces ( SmartClient, Telnet, WebService, HTML, REST, etc ), onde a regra de negócio é mantida em apenas um código.
  3. As interfaces de chamada da função de processamento não necessariamente precisam apresentar uma tela ao usuário. Por exemplo, a sua função de processamento recebe parâmetros em array, no formato chave=valor. Com algumas linhas de código é possível escrever uma classe de Web Services Server, responsável por publicar uma funcionalidade e tratar os parâmetros (SOAP ou REST) recebidos, converter em array para a chamada da função, e retornar o status da tarefa.
  4. Mesmo utilizando uma arquitetura Client-Server, como o caso do AdvPL, o contexto de execução de um processo de interface — SmartClient por exemplo — somente é mantido no ar enquanto a aplicação Client está sendo executada. Logo, se durante um processamento de código AdvPL no servidor, sendo executado em um contexto (ou Thread) iniciada a partir de um SmartClient, se o APPlication Server perder o contato com o SmartClient — normalmente por instabilidade na rede ou mesmo um problema na máquina onde o Smartclient está sendo executado ), o processo em execução no servidor é encerrado imediatamente — afinal sua existência é baseada na premissa da interface gráfica com o usuário.
  5. Justamente em processamentos mais longos, como por exemplo processos em lote — ou a emissão de um relatório que demora duas horas … Não vale a pena deixar a interface aberta e travada com uma tela de “aguarde”, e rodar o relatório neste processo. Imagine que falta 10 minutos pro relatório efetivamente ser emitido, e a energia “piscou” no terminal, e o terminal perdeu por um instante a conexão de rede .. lá se foi uma hora e 50 minutos de trabalho jogados fora, em uma infra-estrutura onde o servidor de aplicação normalmente está em um ambiente com rede redundante, alimentação de energia redundante, alta disponibilidade e afins .. o relatório teve que ser emitido novamente, e iniciar “do zero” por que a energia “piscou” na sala onde estava o terminal do sistema.

Então, vamos mudar tudo pra JOB ?

Não, não é assim que funciona. Chamar um processo separado do processo atual para realizar uma tarefa têm custo, exige mais controle(s), tratamento(s) de erro, normalmente exige um algoritmo de gerenciamento de processos, para não deixar o sistema subir mais processos do que ele “aguenta” processar. Um erro de lógica neste mecanismo ou a ausência dele pode levar um sistema a um estado de esgotamento de recursos.

O que colocar em JOB ?

Via de regra, normalmente é recomendável colocar em JOB os seguintes tipos de processamento:

  1. Emissões de relatórios pesados e/ou demorados. Aquele resumo financeiro do dia anterior, que só pode ser executado depois da meia note, que demora pelo menos meia hora para ser emitido, e que você precisa ver no dia seguinte as 8 horas da manhã … coloque em um agendador de tarefas (Scheduler)  para rodar as 5 ou 6 da manhã. Quando você entrar no sistema e abrir o Spool de Impressão,  seu relatório deve estar lá. Se não estiver, o Scheduler deve ter registrado alguma falha.
  2. Tarefas complementares de processamentos, onde não existe a necessidade imediata do resultado para a continuidade do processo. Por exemplo, um envio de e-mail informativo de uma determinada operação. Quando utilizadas em pequenas quantidades, subir um jobs adicional na execução do programa é suficiente. Agora, caso múltiplas rotinas de envio de email sejam chamadas ao mesmo tempo, sob pena de haver um esgotamento de recursos, é prudente criar uma fila de envio de emails — por exemplo uma tabela na aplicação contendo os emails pendentes de envio, e um ou mais JOBS dedicados ao envio de email. Assim, enquanto os processos vão inserindo os emails a serem enviados em uma tabela do banco de dados, um ou mais processos dedicados vão enviando esses emails, um a um, sem sobrecarregar o servidor atual com muitos jobs, e sem sobrecarregar o próprio provedor de email.
  3. Tarefas especializadas concorrentes, onde múltiplos processos concorrem pelo mesmo recurso, que só pode ser usado por um processo por vez. Normalmente estes casos são exemplificados por rotinas de atualização que exigem o bloqueio temporário de um registro. Neste caso, seria mais elegante criar um e apenas um JOB dedicado para fazer esta atualização, e este JOB pegar os dados que devem ser atualizados de uma fila. Assim, outros processos que precisem fazer esta tarefa não precisam esperar o processo dedicado terminar de rodar a requisição.

Conclusão

Escrever a rotina de processamento sem depender de interface, além de naturalmente permitir a execução dessa rotina dentro de um processo de interface, lhe abre a oportunidade de executá-la em um processo separado, mas não o obriga a fazer isso. Se uma rotina já existe, e foi escrita para depender de uma interface, você deve decidir se usar um JOB é a melhor alternativa para as suas necessidades, e avaliar o custo de fazer essa separação. Em linhas gerais, para não sofrer amanhã, comece a separar processamento da interface hoje, nas novas funcionalidades que você implementa, assim a utilização delas em Job, se assim conveniente ou necessário for, será menos traumático.

Desejo a todos(as)  TERABYTES de SUCESSO 😀