Algoritmos – Conversão Binária – Parte 03

Introdução

No post anterior sobre conversão binária, vimos algumas formas de converter um valor binário representado em uma string de volta a um valor decimal. O algoritmo mais rápido demorou 9,2 segundos para converter 2.560.000 valores. E se eu disser que dá para fazer isso QUATRO VEZES mais rápido, eu devo estar louco ???

Acelerando o AdvPL

No primeiro post sobre conversão binária, o algoritmo têm um desempenho incrível para converter valores de decimal para string binária, pois eu monto um cache com um array armazenando as strings  com o resultado das conversões dos valores de 0 a 255, e ao converter eu acesso diretamente o elemento pela sua posição, o tempo de busca é praticamente o mesmo para qualquer elemento.

Agora, será que é possível aplicar isso da forma inversa ? Usar um array em cache, com as strings binárias e o seu valor decimal correspondente, e buscar o valor desejado a partir da string informada ? Considerando uma string de 8 caracteres ‘0’ ou ‘1’ , com 256 possibilidades diferentes, é mais rápido calcular o número em decimal ou procurar na lista ? Veremos …

Peso do algoritmo de cálculo

Partindo da conversão de uma string binária de 8 caracteres, usando o algoritmo proposto no post anterior, podemos elencar o melhor e pior tempo da rotina, partindo das operações que ela precisa fazer para chegar ao resultado final. Vamos avaliar quantas operações de string, comparação e aritmética são necessárias para converter as strings “00000000” e “11111111” respectivamente:

User Function TST8()
Local nRet 
nRet := BitStoN("00000000")
nRet := BitStoN("11111111")
return

STATIC Function BitsToN(cBitStr)
Local nI, nT := len(cBitStr) 
Local nMult := 1
Local nResult := 0
For nI := nT to 1 STEP -1
    // a linha abaixo é executada 8 vezes, uma para 
    // identificar cada caractere da string binária
    // e compará-lo com '1'
    IF substr(cBitStr,nI,1) = '1'
        // A soma do multiplicador somente é executada para
        // cada caractere '1' encontrado na string. Ela 
        // nao é executada nenhuma vez para "00000000", mas é
        // feita 8 vezes para converter "11111111"
        nResult += nMult
    Endif
    // A Multiplicação abaixo é feita sempre 8 vezes
    nMult *= 2
Next
Return nResult

Passando na ponta do lápis, a busca mais “leve” de uma string binária fará 8 extrações de uma posição da string (substr), 8 comparações de string de 1 caractere e 8 multiplicações de nMult. Para cada dígito encontrado que tenha o  valor ‘1’ ainda será feita uma soma adicionando o valor de nMult no resultado. Logo, com “11111111”, temos mais oito operações aritméticas. Podemos concluir que, o número estimado de operações dessa rotina é de 24 a 32 operações de string e numéricas por conversão.

HASH MAP

Quem se lembra do artigo sobre Performance e Escalabilidade – HASH MAP em AdvPL ? Então, um Hash Map é uma forma otimizada de busca em vetores do tipo chave-valor, que monta internamente um acesso posicional. Se, internamente, a quantidade e peso das operações para fazer a busca usando um HASHMAP em uma lista pré-montada é menor do que calcular o resultado, o HASH vai ser mais rápido. Vamos ao teste:

User Function tststr()
Local cStrBit
Local nTimer
Local nNum, nI , nJ      
Local oHash

// Cria o cache de HASH para a busca ( 0 a 255 )            
oHash := BuildBitHash()

// Roda a carga de teste
nTimer := seconds()
For nJ := 0 to 255              
  // Cria uma string binária para conversão
  cStrBit := nToBit8(nJ)
  For nI := 1 to 10000 
    // Busca o valor no HashMap
    HMGET( oHash,cStrBit ,@nNum)
  Next
Next
// Mostra o tempo do teste
conout(str(seconds()-nTimer,12,3)+"s.")

// Limpa de destroi o HashMap
HMClean(oHash) 
FreeObj(oHash)

Return

// Monta o HashMap com os 256 bytes de cache
STATIC Function BuildBitHash()
Local oHash := HMNew()
For nI := 0 to 255
  // Seta uma tupla chave , valor 
  HMSet(oHash, NTOBIT8(nI) , nI )
Next
Return oHash

Desempenho

Usando a função BitSToN(), 2.560.000 conversões levaram em média de 8,5 a 9 segundos para serem realizadas. Usando o objeto de Hash Map, o mesmo processamento levou em média 2 segundos. Isso significa quase 1280000 conversões por segundo, contra no máximo 320.000 conversões por segundo da BitSToN()

Observações

O tempo obtido foi realmente impressionante, porém para chegar neste desempenho, dada a quantidade de iterações, o objeto de cache foi montado e armazenado em variável local, e as chamadas para a busca do HashMap foram feitas com a função HMGet(), sem encapsulamento em Static Function. Após implementar o encapsulamento do HMGET() por Static Function, o tempo foi para perto de 4 segundos, o que ainda é a metade do tempo do algoritmo anterior. 😀

Conclusão

Sempre dá pra “apertar” mais um pouco o desempenho, usando as técnicas e abordagem adequadas.

Agradeço a todos a audiência, e lhes desejo TERABYTES DE SUCESSO !!! 

Referências

TDN – HashMap

 

 

MemCached Client em AdvPL – Parte 01

Introdução

O MemCached é um aplicativo que provê um cache de objetos em memória, do tipo chave/valor de alto desempenho. Ele possui APIs Client para várias linguagens de mercado, e agora também terá a sua API Client em AdvPL.

O MemCached

Open Source, Free , originalmente desenvolvido para Linux, ele também têm porte para Windows, sobe originalmente com um limite de 64 MB de memória para uso com elementos em cache, aceita conexões na porta 11211, em todas as interfaces de rede da máquina. Existe parametrização para permitir mudar a porta e colocar mais de uma instância de MemCached na mesma máquina, definir uso de mais memória, e inclusive permitir conexões apenas em uma interface de rede ou IP. Recomendo a leitura da documentação — pelo menos a abordagem inicial — disponível no site MemCached.ORG

Classe ZMEMCACHED

Como o mecanismo do MemCached trabalha com TCP/IP, a classe implementa a API de acesso para as funcionalidades do MemCached. Damos o nome de Chave a um identificador de conteúdo, uma string com até 150 bytes, e associamos a esta chave um conteúdo a ser colocado em cache. Este conteúdo pode ser colocado no cache com ou sem um tempo de vida (expires). As funcionalidades básicas incluem acrescentar, atualizar e remover uma tupla chave/valor do cache, incrementar ou decrementar um valor em cache — neste caso o conteúdo em cache deve ser um número — representado como string, obter status do cache, e limpar todo o cache. Vamos ao fonte: (fonte zMemCached,prw)

#include "protheus.ch"
#include "zLibStr2HexDmp.ch"

CLASS ZMEMCACHED FROM LONGNAMECLASS
 
   DATA cMemCacheIP     // IP da instancia Memcached
   DATA nMemCachePort   // Porta da instancia Memcached
   DATA nRvcTimeOut     // Timeout de recebimento em milissegundos ( default 1000 ) 
   DATA oTCPConn        // Objeto Socket Client
   DATA cError	        // Ultimo erro da API 
   DATA cResponse       // Response header da ultima requisicao
   DATA lVerbose        // Modo verbose de operação 
   
   METHOD New()         // Construtor da classe
   METHOD Connect()     // Estabelece conexão com o MemCached
   METHOD GetVersion()  // Recupera a versao do MemCached
   METHOD GetStats()    // Recupera estatisticas da intancia do memcached
   METHOD Disconnect()  // Desconecta do MemCAched

   METHOD Add()         // Acrescenta uma chave / valor ( apenas caso nao exista ) 
   METHOD Replace()     // Troca o valor de uma chave existente
   METHOD Set()         // Armazena uma chave / valor no MemCached 

   METHOD Get()         // Recupera o valor de uma chave armazeanda 
   METHOD Delete()      // Remove do cache um valor pela chave 
   METHOD Increment()   // Incrementa um contador pela chave -- valor em string numerica
   METHOD Decrement()   // Decrementa um contador pela chave -- valor em string numerica
   METHOD Flush()       // Limpa todas as variáveis do cache 

   // ********* METODOS DE USO INTERNO *********
   METHOD _Store( cMode, cKey , cValue, nOptFlag, nOptExpires )
   METHOD _GetTCPError()
  
ENDCLASS

 

Método NEW

Method NEW( cIp , nPorta ) CLASS ZMEMCACHED
::cMemCacheIP := cIp
::nMemCachePort := nPorta
::nRvcTimeOut := 1000
::oTCPConn := tSocketClient():New()
::cError := ''
::cResponse := ''
::lVerbose := .F.
Return self

O construtor da classe Client do MemCached recebe logo de cara o IP e Porta da instância do MemCached que ele deve utilizar. Ele já cria um objeto do tipo tSocketClient() para conversar com o MemCached, mas ainda não estabelele a conexão, apenas inicializa as propriedades de uso da classe.

Método CONNECT

METHOD Connect() CLASS ZMEMCACHED
Local iStat

::cError := ''
::cResponse := ''

IF ::lVerbose
	Conout("zMemCached:Connect() to "+::cMemCacheIP+" Port "+cValToChar(::nMemCachePort))
Endif

If ::oTCPConn:Isconnected()
	::cError := "Memcached client already connected."
	Return .F.
Endif

// Estabelece a conexao com o memcache DB
iStat := ::oTCPConn:Connect( ::nMemCachePort , ::cMemCacheIP, 100 )

If iStat < 0
	::cError := "Memcached connection Error ("+cValToChar(iStat)+")"
	::_GetTCPError()
	Return .F.
Endif

O método Connect apenas estabelece a conexão TCP no IP e Porta especificados no construtor. Nesta etapa, ele não faz nenhum tipo de HandShake — por hora — ele apenas abre a conexão TCP/IP. Em caso de erro, o método retorna .F., e a razão do erro pode ser recuperada na propriedade ::cError.

Método DISCONNECT

METHOD Disconnect() CLASS ZMEMCACHED
Local cSendCmd := 'quit'+CRLF
Local nSend

::cError := ''
::cResponse := ''

If ::oTCPConn == NIL
	::cError := "Memcached client already Done."
	Return .F.
Endif

if( ::oTCPConn:IsConnected() )
	// Se ainda está conectado, manda um "quit"
	// para fechar a conexao de modo elegante
	IF ::lVerbose
		Conout("zMemCached:DONE() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
		Conout(Str2HexDmp(cSendCmd))
	Endif
	nSend := ::oTCPConn:Send( cSendCmd )

	If nSend <= 0 
		::cError := "Memcached client SEND Error."
		::_GetTCPError()
		Return .F.
	Endif
	
Endif

::oTCPConn:CloseConnection()
::oTCPConn := NIL

Return .T.

O método Disconnect() desconecta do MemCached de forma “elegante”, enviando um aviso de desconexão (instrução quit). Simples assim. Após desconectar, a mesma instância pode ser aproveitada para uma nova conexão — por hora no mesmo IP e Porta — na mesma instância do MemCached.

Método GETVERSION

METHOD GetVersion( cVersion ) CLASS ZMEMCACHED
Local nRecv, cRecvBuff := ''
Local cSendCmd := "version" + CRLF
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetVersion() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Receive Error"
	::_GetTCPError()
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetVersion() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

If Left(cRecvBuff,8)!='VERSION '
	::cError := "Response Error : " + cRecvBuff
	Return .F.
Endif

// Recupera a versão por referencia 
cVersion := ::cResponse

Return .T.

O método GetVersion() deve passar a variável cVersion por referência — prefixada com “@” na chamada da função. Em caso de sucesso, a função retorna .T., e o valor da variável será atualizado. Caso contrário, a variável será NIL, e a mensagem de erro correspondente está na propriedade ::cError

Método GETSTATS

METHOD GetStats( aStats ) CLASS ZMEMCACHED
Local nRecv, cRecvBuff := ''
Local nI , nT , aTmp
Local cSendCmd := "stats" + CRLF
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetStats() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Receive stats error"
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Receive stats time-out
	::_GetTCPError()
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:GetStats() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Recupera estatisticas
aTmp := strtokarr2( strtran(cRecvBuff,CRLF,chr(10)) , chr(10) )

nT := Len(aTmp)
For nI := 1 to nT
	If Left(aTmp[nI],5)=='STAT '
		aadd(aStats , substr(aTmp[nI],6) )
	Endif
Next

// Limpa o array temporario
aSize(aTmp,0)

Return .T.

Cada instância do MemCached têm seus mecanismos internos de controle. Usando o método GetStats(), podemos perguntar ao MemCached as estatísticas de uso até o momento. As informações são retornadas por referência no Array aStats passado como parâmetro, onde cada linha é uma string contento um identificador e seu respectivo valor, veja o exemplo abaixo:

pid 11128
uptime 10
time 1547326165
version 1.4.5_4_gaa7839e
pointer_size 64
curr_connections 10
total_connections 11
connection_structures 11
cmd_get 0
cmd_set 0
cmd_flush 0
get_hits 0
get_misses 0
delete_misses 0
delete_hits 0
incr_misses 0
incr_hits 0
decr_misses 0
decr_hits 0
cas_misses 0
cas_hits 0
cas_badval 0
auth_cmds 0
auth_errors 0
bytes_read 16
bytes_written 26
limit_maxbytes 67108864
accepting_conns 1
listen_disabled_num 0
threads 4
conn_yields 0
bytes 0
curr_items 0
total_items 0
evictions 0
reclaimed 0

Método _STORE

// ===============================================================================
// Guarda um valor no memcache
// Mode = set, add, replace, append, prepend
// cas ainda nao implementado 

METHOD _Store( cMode, cKey , cValue, nOptFlag, nOptExpires ) CLASS ZMEMCACHED
Local cSendCmd := ''
Local nRecv
Local cRecvBuff := ''
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

If !( ('.'+cMode+'.') $ ('.set.add.replace.append.prepend.cas.') )
	::cError := "Invalid Store mode ["+cMode+"]"
	Return .F.
Endif

// <mode> <key> <flags> <exptime> <bytes>
// ------------------------------------------
cSendCmd += cMode + ' '
cSendCmd += cKey + ' '
If nOptFlag == NIL
	cSendCmd += '0 '
else
	cSendCmd += cValToChar(nOptFlag)+' '
Endif
If nOptExpires == NIL
	cSendCmd += '0 '
else
	cSendCmd += cValToChar(nOptExpires)+' '
Endif
cSendCmd += cValToChar(len(cValue))
cSendCmd += CRLF
// ------------------------------------------

IF ::lVerbose
	Conout("zMemCached:_Store() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Etapa 01 Envia o comando 
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif


// Etapa 02
// Envia o valor a ser armazenado 

nSend := ::oTCPConn:Send(cValue+CRLF)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Store("+cMode+") SEND VALUE ")
	Conout(Str2HexDmp(cValue+CRLF))
Endif

// Se tudo der certo, aqui eu devo receber um "stored"
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Store("+cMode+") failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Store("+cMode+") failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Store("+cMode+") RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If cRecvBuff != 'STORED'
	::cError := "Store ["+cMode+"] failed: "+cRecvBuff
	Return .F.
Endif

Return .T.

O método _STORE é de uso interno da classe. Ele é usado pelos métodos públicos ADD, REPLACE e SET. Internamente, a sintaxe dos comandos e o retorno é praticamente o mesmo para estas três ações de armazenamento. Logo, optei por criar um método interno capaz de realizar as três operações, e os três métodos públicos para consumir estas ações no fonte AdvPL.

Métodos ADD, REPLACE e SET

METHOD Add( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED
Return ::_Store("add", cKey , cValue, NIL, nOptExpires)

METHOD Replace( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED
Return ::_Store("replace", cKey , cValue, NIL, nOptExpires)

METHOD Set( cKey , cValue, nOptExpires ) CLASS ZMEMCACHED
Return ::_Store("set", cKey , cValue, NIL, nOptExpires)

Em cada um destes três métodos, informamos a chave de identificação do dado, o valor a ser gravado em cache, e opcionalmente podemos especificar um tempo de vida (expires) em segundos no cache. O default é 0 (zero=no expires). Todos os métodos acima retornam .F. quando a operação não pode ser realizada.

A diferença entre eles é que:

  1. ADD() somente var armazenar o valor caso a chave ainda não tenha sido gravada anteriormente. Ela não atualiza valor de chave existente.
  2. Replace() somente troca o valor de uma chave existente. Caso você tente trocar o valor de uma chave que não existe, ela retorna uma condição de erro.
  3. O método SET sempre atualiza o valor de uma chave, se ela ainda não existe no cache, ela é criada.

Método GET

METHOD Get( cKey , cValue ) CLASS ZMEMCACHED

Local cSendCmd := ''
Local nRecv
Local cRecvBuff := ''
Local nPos
Local cLine
Local aTmp
Local cTeco
Local nSize
Local nSend

::cError := ''
::cResponse := ''

// Limpa o retorno por referencia 
cValue := NIL

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	return -1
Endif

// Monta o comando de recuperacao 
cSendCmd += 'get '+cKey + CRLF

If ::lVerbose
	Conout("zMemCached:Get() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber os dados ...
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

If nRecv < 0
	::cError := "Get() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	return -1
Endif

If nRecv == 0
	::cError := "Get() failed - response time-out"
	::_GetTCPError()
	return -1
Endif

If ::lVerbose
	Conout("zMemCached:Get() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Parser do retorno

While !empty(cRecvBuff)
	
	// Primeiro pega a linha de status
	nPos := at(CRLF,cRecvBuff)
	If nPos < 1
		::cError := "Get() failed - missing CRLF"
		return -1
	Endif
	
	cLine := left(cRecvBuff,nPos-1)
	cRecvBuff := substr(cRecvBuff,nPos+2)
	
	If cLine == "END"
		// acabaram os dados
		// Sai do loop
		EXIT
	Endif
	
	If Left(cLine,6) == "VALUE "
		
		// Tem valor ... opa ... legal
		aTmp := strtokarr2(cLine,' ')
		
		// varinfo("aTmp",aTmp)
		// [1] "VALUE"
		// [2] <key>
		// [3] <flags>
		// [4] <size> 
		// [5] Optional [uniqueid]
		
		nSize := val(aTmp[4])
		
		While len(cRecvBuff) < nSize
			
			// Se ainda falta coisa pra receber, recebe mais um teco
			// e acrescenta no buffer
			cTeco := ''
			nRecv := ::oTCPConn:Receive(@cTeco,::nRvcTimeOut)
			
			If nRecv < 0
				::cError := "Get() failed - connection error" + cValTochar(nRecv)
				::_GetTCPError()
				return -1
			Endif
			
			If nRecv == 0
				::cError := "Get() failed - response time-out"
				::_GetTCPError()
				return -1
			Endif
			
			If ::lVerbose
				Conout("zMemCached:Get() RECV "+cValToChar(nRecv)+" Byte(s)")
				Conout(Str2HexDmp(cTeco))
			Endif
			
			// So acrescenta o que recebeu
			cRecvBuff += substr(cTeco,1,nRecv)
			
			If ::lVerbose
				Conout("zMemCached:Get() Total ReceivedBuffer ")
				Conout(Str2HexDmp(cRecvBuff))
			Endif
			
		Enddo
		
		// Valor ja foi recebido na integra
		// Coloca o valor recebido no retorno
		cValue := left(cRecvBuff,nSize) 
		
		// Arranca valor recebido do buffer
		// Ja desconsiderando o CRLF
		cRecvBuff := substr(cRecvBuff,nSize+3)
		
		// Limpa o array temporário
		aSize(aTmp,0)

		// Eu só espero recener um valor 		
		EXIT 
		
	Else
		
		// Se nao tem o valor, ou nao tem o "END", deu merda ?!
		::cError := "Get() failed - Unexpected ["+cLine+"]"
		return .F. 
		
	Endif
	
Enddo

If empty(cRecvBuff)
	// Se o buffer esta vazio, 	entao nao chegou nenhum valor 
	// A operação de GET foi feita com sucesso, 
	// naou houve erro, apenas o valor nao foi encontrado. 
	Return .T. 
Endif

If left(cRecvBuff,5) == "END" + CHR(13)+Chr(10)
	// Depois do valor, eu espero um END (CRLF) \
	// Se nao chegou um END, tem mais de um valor na chave ? ....
	Return .T. 
Endif

::cError := "Get() failed - Unexpected Multiple Value(s)"
return .F.

O método GET foi feito para recuperar o valor em cache associado a uma chave. A variável para receber o valor é informado por referência na chamada do método. O fonte é um pouco mais “rebuscado” pois precisa permanecer recebendo dados do MemCache enquanto ele não enviar o conteúdo inteiro armazenado.

Um detalhe importante: A função somente retorna .F. em caso de ERRO, por exemplo perda de conexão ou resposta inesperada ou não tratada do MemCached. Se o valor a ser recuperado na chave não existe no cache, isto não é considerado um erro, logo o método vai retornar .T. , e cabe ao desenvolvedor verificar se o dado retornado por referência não está NIL.

Método DELETE

Usamos o método DELETE para remover uma chave e seu valor associado do cache.

METHOD Delete( cKey ) CLASS ZMEMCACHED
Local cSendCmd 
Local nRecv
Local cRecvBuff := ''
Local nSend

::cError    := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

cSendCmd := 'delete ' + cKey + CRLF

// ------------------------------------------

If ::lVerbose
	Conout("zMemCached:Delete() SEND")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando 
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber DELETED 
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

// Pega apenas a primeira linha do resultado 
::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Delete() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Delete() failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Delete() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If cRecvBuff != 'DELETED'
	::cError := "Delete failed - Error: "+cRecvBuff
	Return .F.
Endif

Return .T.

Métodos INCREMENT e DECREMENT

Podemos armazenar no cache — usando Add ou Set — um vamor numérico representado em string em uma determinada chave. E, usando os métodos Increment() e Decrement(), podemos respectivamente aumentar ou diminuir o valor desta chave. Internamente o MemCached não vai deixar duas operações de incremento rodar ao mesmo tempo. Cada operação realizada retorna o novo valor da chave após a operação ser realizada. O valor é recuperado por reverência na chamada do método, no parâmetro nValue.

Method Increment( cKey , nValue , nStep ) CLASS ZMEMCACHED

Local cSendCmd := ''
Local nRecv
Local cRecvBuff := ''     
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

// Monta o comando de recuperacao
cSendCmd += 'incr '+cKey+' '
If nStep == NIL
	cSendCmd += '1'
Else
	cSendCmd += cValToChar(nStep)
Endif

cSendCmd += CRLF 

If ::lVerbose
	Conout("zMemCached:Increment() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber o valor apos o incremento
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Increment() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Increment() failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Increment() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Parser do retorno
cRecvBuff := strtran(cRecvBuff,CRLF,'')

If !(left(cRecvBuff,1)$'0123456789')
	::cError := "Increment() failed - Error "+cRecvBuff
	::_GetTCPError()
	Return .F.
Endif

// Pega e retorna o valor apos o incremento
nValue := val(cRecvBuff)

Return .T.

Method Decrement( cKey , nValue , nStep ) CLASS ZMEMCACHED

Local cSendCmd := ''
Local cRecvBuff := ''
Local nRecv
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

// Monta o comando de recuperacao
cSendCmd += 'decr '+cKey+' '
If nStep == NIL
	cSendCmd += '1'
Else
	cSendCmd += cValToChar(nStep)
Endif

cSendCmd += CRLF

If ::lVerbose
	Conout("zMemCached:Decrement() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

// Manda o comando
nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

// Se tudo der certo, aqui eu devo receber o valor apos o decremento
nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv < 0
	::cError := "Decrement() failed - connection error" + cValTochar(nRecv)
	::_GetTCPError()
	Return .F.
Endif

If nRecv == 0
	::cError := "Decrement() failed - response time-out"
	::_GetTCPError()
	Return .F.
Endif

If ::lVerbose
	Conout("zMemCached:Decrement() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

// Parser do retorno

cRecvBuff := strtran(cRecvBuff,CRLF,'')

If !(left(cRecvBuff,1)$'0123456789')
	::cError := "Decrement() failed - Error "+cRecvBuff
	Return .F.
Endif

// Pega e retorna o valor apos o decremento
nValue := val(cRecvBuff)

Return .T.

Método FLUSH

E, para finalizar, se eu quiser evaporar com todo o conteúdo em cache — todas as chaves e valores armazenadas — eu chamo o método Flush().

METHOD Flush() CLASS ZMEMCACHED
Local nRecv, cRecvBuff := ''
Local cSendCmd := "flush_all" + CRLF
Local nSend

::cError := ''
::cResponse := ''

If !::oTCPConn:Isconnected()
	::cError := "Memcached client not connected."
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:Flush() SEND "+cValToChar(len(cSendCmd))+" byte(s).")
	Conout(Str2HexDmp(cSendCmd))
Endif

nSend := ::oTCPConn:Send(cSendCmd)

If nSend <= 0 
	::cError := "Memcached client SEND Error."
	::_GetTCPError()
	Return .F.
Endif

nRecv := ::oTCPConn:Receive(@cRecvBuff,::nRvcTimeOut)

::cResponse := substr(cRecvBuff,1,at(CRLF,cRecvBuff)-1)

If nRecv == 0
	::cError := "Receive timed-out"
	::_GetTCPError()
	Return .F.
Endif

If nRecv < 0
	::cError := "Receive Error"
	::_GetTCPError()
	Return .F.
Endif

IF ::lVerbose
	Conout("zMemCached:Flush() RECV "+cValToChar(nRecv)+" Byte(s)")
	Conout(Str2HexDmp(cRecvBuff))
Endif

If Left(cRecvBuff,2)!='OK'
	::cError := "Response Error : " + cRecvBuff
	Return .F.
Endif

Return .T.

Programa de Testes

Para verificar as funcionalidades do Cache, criei um programa de testes da funcionalidade client da API, para confirmar os comportamentos, segue fonte abaixo:

#include 'Protheus.ch'
#include 'zLibStr2HexDmp.ch'

// ---------------------------------------------------------------------------------
// Utiliza direto uma inistancia da classe ZMEMCACHED 
// para ler e gravar valores, testa todas as operações, expires e contador/sequenciador

#define TEST_HOST		'localhost'
#define TEST_PORT		11211

User Function MemTst1()

Local oClient
Local cVersion := ""
Local aStats := {}
Local nI , nX
Local xValue 
Local nNewValue

oClient := ZMEMCACHED():New( TEST_HOST , TEST_PORT )

// Modo verbose apenas para depuração 
// oClient:lVerbose := .t.

IF !oClient:Connect()
   conout("Falha de conexão...")
   conout(oClient:cError)
   return
Endif

// Recupera a versao da instancia atual
If !oClient:GetVersion(@cVersion)
   conout("Falha ao recuperar versao...")
   conout(oClient:cError)
   return
endif

conout("Memcache Version: "+cVersion)

// Pega as estatisticas da instancia atual 
If !oClient:GetStats(@aStats)
   conout("Falha ao recuperar estatisticas...")
   conout(oClient:cError)
   return
endif

conout(padc(" STATISTICS ",79,"-"))
aEval(aStats , {|x| conout(x) })

// Apaga todas as chaves

If !oClient:Flush()
   conout("Falha na limpeza global...")
   conout(oClient:cError)
   return
endif

// Testando armazenamento de valores
cValue1 := RandomStr(64)
cValue2 := RandomStr(64)

// Acrescenta o valor 
// Fuciona apenas caso a chave nao exista 
If !oClient:Add( 'chave' , cValue1 )
	conout("Falha ao adicionar chave ...")
	conout(oClient:cError)
	Return 
Endif

// Agora tenta acrescentar na mesma chave
// isso nao deveria ser possivel 
If oClient:Add( 'chave' , cValue1 )
	UserException("Permitiu adicionar valor de chave ja existente")
Endif

// Troca valor - apenas se a chave existe 

If !oClient:Replace( 'chave' , cValue2 )
	conout("Falha ao trocar chave ...")
	conout(oClient:cError)
	Return
Endif

// Deleta a chave
If !oClient:Delete( 'chave')
	conout("Falha ao deletar chave ...")
	conout(oClient:cError)
	Return
Endif

// agora tenta trocar o valor. 
// deveria falhar, pois a chave nao existe 
If oClient:Replace( 'chave' , cValue1 )
	UserException("Permitiu troca de valor de chave que nao existe")
Endif

// Acrescenta o valor de novo 
// Deve funcionar, pois a chave tinha sido deletada
If !oClient:Add( 'chave' , cValue1 )
	conout("Falha ao adicionar chave ...")
	conout(oClient:cError)
	Return 
Endif

// Mostra no console o valor graavdo 
conout(padc(" STORED VALUE ",79,"-"))
Conout(Str2HexDmp(cValue1))

// Agora le o valor da chave
lOk := oClient:Get('chave' , @xValue )

If !lOk
	conout("Falha ao ler a chave ...")
	conout(oClient:cError)
	Return 
Endif

conout(padc(" READED VALUE ",79,"-"))
Conout(Str2HexDmp(xValue))

If ! (xValue == cValue1 ) 
	UserException("Divergencia de valor")
Endif

// busca uma chave que nao existe 
lOk := oClient:Get('naoexiste' , @xValue )

If !lOk
	conout("Retorno inesperado")
	conout(oClient:cError)
	Return 
Endif
             
// Cria um contador para incremento
// Ja inicializado com um valor

If !oClient:Add( 'contador' , '666' )
	conout("Falha ao adicionar contador ...")
	conout(oClient:cError)
	Return 
Endif

// Agora testa o incremento 
nNewValue := 0 

If !oClient:Increment( 'contador' , @nNewValue )
	conout("Falha ao incrementar contador ...")
	conout(oClient:cError)
	Return 
Endif

conout("nNewValue = "+cValToChaR(nNewValue))

If nNewValue != 667
	UserException("Incr Failed - Expected 667 Reveived "+cvaltochar(nNewValue))
Endif

If !oClient:Decrement( 'contador' , @nNewValue )
	conout("Falha ao incrementar contador ...")
	conout(oClient:cError)
	Return 
Endif

conout("nNewValue = "+cValToChaR(nNewValue))

If nNewValue != 666
	UserException("Decr Failed - Expected 667 Reveived "+cvaltochar(nNewValue))
Endif

// Agora incrementa um contador que nao existe 
If oClient:Increment( 'contador2' , @nNewValue )
	UserException("Nao deveria incrementar algo que nao existe")
Else
	Conout("-- Falha esperada -- contador realmente nao existe ")
Endif

// teste de valor com timeout                       
// expira em (aproximadamente) 2 segundos

If !oClient:Add( 'timer' , 'teste' , 2 )
	conout("Falha ao adicionar contador ...")
	conout(oClient:cError)
	Return 
Endif

// le o valor 4 vezes em intervalos de 1 segundo 
// A partir da terceira leitura o valor 
// já nao deveria existir 

For nX := 1 to 4
	// le direto o valor 
	lOk := oClient:Get('timer' , @xValue )
	conout(padc(" GET VALUE ",79,"-"))
	If xValue = NIL 
		conout("--- NIL --- ")	
	Else
		Conout(Str2HexDmp(xValue))
	Endif
	If !lOk
		conout("Falha ao ler a chave ...")
		conout(oClient:cError)
		Return 
	Endif
	Sleep(1000)
Next

// Pega as estatisticas no final do teste
If !oClient:GetStats(@aStats)
   conout("Falha ao recuperar estatisticas...")
   conout(oClient:cError)
   return
endif

conout(padc(" MEMCACHED STATSISTICS ",79,"-"))
aEval(aStats , {|x| conout(x) })

oClient:Disconnect()
FreeObj(oClient)

Return

// ---------------------------------------------------------------------------------
// Função RandomStr()
// Gera uma string com o tamanho espeficicado, sorteando caracteres 
// ASICI da faixa de 32 a 127, contemplando letras, números e simbolos 

STATIC Function RandomStr(nSize)
Local cRet := ''
While nSize>0
	cRet += chr(randomize(32,128))
	nSize--
enddo
Return cRet

O programa deve apenas emitir echo no log de console, ele apenas vai abortar a execução com um erro caso ele não consiga conexão, ou no caso de algum comportamento inesperado do MemCached. Os testes foram realizados com um MemCached 1.4.5 para Windows, mas a camada TCP é a mesma para Linux.

Cache Distribuído

Quem já usa APIs de MemCached deve estar se perguntando: — Onde eu acrescento os servidores do MemCached? Bem, esta é a primeira versão de client, então ela conecta com apenas uma instância de MemCached. O MemCached foi construído para ser um cache distribuído, onde a capacidade de montar um cluster e dividir os dados entre as instâncias online do MemCached é do Client.

Por hora, a implementação deste client em AdvPL conversa com apenas uma instância, que pode ser colocada por exemplo em uma máquina, onde existam mais de um serviço de Protheus Server para consumir o cache.

Cuidados no Uso

  • Recomendo a leitura das recomendações de uso do MemCached no site da ferramenta, tanto sobre dimensionamento como boas práticas de segurança.
  • Um serviço de MemCached normalmente pode ser acessado por mais de um programa Cliente consumidor do Cache, então não crie nomes “curtos demais” para identificar as suas chaves, inclusive preferencialmente crie um padrão de nomenclatura, como o environment + “_”  + Modulo ou programa + “_”  + identificador do cache.

Conclusão

Por hora a API armazena basicamente um buffer em formato Caractere no AdvPL. Inclusive, pode armazenar formato binário, como uma imagem. Caso seja necessário armazenar por exemplo um Array ou outros valores, é necessário convertê-los para Caractere, e depois convertê-los de volta. Estou estudando uma forma rápida de se fazer isso. Na continuação deste post, espero já ter finalizado esta questão de forma elegante.

Fontes da ZLIB

A partir de hoje —  13/01/2019 — os fontes da ZLIB estão em um repositório separado do GITHUB. Por hora só existe o branch “master”, e ele é o que sempre será atualizado. Os fontes existentes no repositorio do blog permanecem lá, mas sem receber atualização — apenas para não quebrar os links dos posts anteriores. Os fontes e includes da zLib estão na URL https://github.com/siga0984/zLIB

Agradeço a todos novamente pela audiência, e lhes desejo TERABYTES DE SUCESSO !!!

Referências

 

Acelerando o AdvPL – Importação de tabelas

Introdução

Existem muitas situações onde existe a necessidade de alimentar ou importar tabelas para uso do ERP Microsiga / Protheus. Quando esta necessidade envolve um grande número de registros, e um curto espaço de tempo, precisamos fazer esta operação ser o mais rápida possível. Nesse post vamos abordar algumas técnicas para realizar este tipo de operação.

  • Crie os índices da tabela apenas após a importação dos dados. 

Se a tabela está vazia, e os indices de trabalho desta tabela já existem, cada novo registro acrescentado faz o banco de dados atualizar todos os índices durante a inserção dos dados. Sem os índices, o banco apenas insere os dados. A inserção vai ser muito mais rápida, e os índices também são criados mais rápido no final do processo de importação, quando você já inseriu todos os registros na tabela. Se você precisa ter um índice na tabela, no processo de importação, por exemplo para fazer alguma validação ou query na tabela enquanto ela está sendo importada, crie apenas este índice.

  • Trabalhe com blocos de inserções transacionadas

Os bancos de dados relacionais garantem a integridade dos dados através do registro de um “LOG Transacional”. Cada operação de inserção primeiro é gravada no LOG do SGDB, e depois é “efetivada” — o Banco de Dados faz o COMMIT da informação. Quando não estamos usando uma transação explícita, cada instrução executada no Banco de Dados faz COMMIT automaticamente no final da instrução. É mais eficiente abrir uma transação — BEGIN TRANSACTION — pois enquanto a transação está aberta, cada instrução gera apenas o LOG Transacional, e depois de fazer por exemplo 1000 inserções na base de dados, você encerra a transação — END TRANSACTION no AdvPL — e neste momento o Banco de dados faz o COMMIT de todas as linhas do LOG de uma vez.

  • A nova tabela que vai receber os dados deve ser criada pelo DBAccess 

Mesmo que você saiba quais são os tipos dos campos da estrutura da tabela final, onde os dados serão gravados, quem sabe criar a tabela nos padrões e moldes necessários para o correto funcionamento da aplicação é o Protheus, que passa para o DBAccess a estrutura da tabela a ser criada. Internamente, o DBAccess cria constraints e outros elementos, além de alimentar algumas tabelas de controle interno. Deixe o DBAccess fazer a criação da tabela. Você pode até apagar os índices de dados para fazer a importação da tabela após eles terem sido criados, porém não apague o índice da primary key (R_E_C_N_O_), e se a tabela possui índice único (sufixo _UNQ), mantenha ele também, caso você queira que o próprio banco de dados aborte a operação no caso de haver uma chave única duplicada nos dados sendo importados.

  • Usando as funções de baixo nível de tabelas do AdvPL 

Caso a importação dos dados seja realizada por uma aplicação AdvPL, que foi criada para esta finalidade, que não vai concorrer com os processos do ERP, podemos usar diretamente as funções de baixo nível de tabelas do AdvPL. As funções de mais alto nível — como RECLOCK e MSUNLOCK devem ser usadas dentro dos programas do ERP, no ambiente do ERP, pois elas têm tratamentos adicionais ligados ao FrameWork do ERP. Se, ao invés disso, usarmos diretamente as funções diretas de manutenção de dados, em um cenário controlado, podemos obter mais ganhos de desempenho — DBAppend() para acrescentar um novo registro, DBRUnlock() — sem nenhum parâmetro — para soltar o bloqueio (lock) de todos os registros obtidos durante aquele processo (lembrando que eu não posso soltar o lock dentro de transação), abrir e fechar transação sem usar BEGIN e END TRANSACTION — Usando diretamente a função TCCommit().

IMPORTANTE

O Uso das funções básicas do AdvPL para a manutenção de tabelas NÃO DEVE SER MISTURADO com o uso das funções do Framework. Dentro de um processo preparado para o contexto de execução do ERP, BEGIN TRANSACTION trabalha junto com END TRANSACTION, Reclock() e MSUnlock() trabalham juntas, ambas possuem tratamentos diferenciados e automáticos quando você programa dentro do ambiente do ERP e/ou dentro de um bloco transacionado, e todas elas internamente fazem uso das funções de baixo nível do AdvPL. Se você vai fazer uma aplicação que não vai rodar dentro do contexto do ERP, como por exemplo as aplicações que eu publico no BLOG — CRUD em AdvPL por exemplo — você deve usar apenas as funções de baixo nível. Misturar chamadas diretas das funções de Framework com chamadas das funções de baixo nível NO MESMO PROCESSO pode causar efeitos imprevisíveis.

  • Abra a tabela de destino em modo exclusivo

Se a sua aplicação vai usar o ambiente do ERP — Prepare Environment, Reclock e afins, abra a tabela de destino em modo exclusivo — veja como na documentação da função ChkFile() do Framework AdvPL. Caso você vá utilizar as funções de baixo nível de acesso a tabelas, veja como abrir uma tabela em modo exclusivo na documentação da função DBUseArea() da TDN ou do comando USE da TDN. Caso a tabela esteja aberta em modo compartilhado, cada inserção de um novo registro faz o DBAccess registrar um bloqueio desse registro na inserção, e cada desbloqueio gera uma requisição a mais ao DBAccess para soltar o bloqueio. Quando usamos a tabela em modo EXCLUSIVO, somente o meu processo está usando a tabela, e não é feito nenhum lock no DBAccess.

  • Use um BULK INSERT ou ferramenta de apoio do próprio Banco de Dados

Não há forma mais rápida de inserir dados em uma tabela senão usando uma ferramenta do próprio Banco de Dados. Porém, nem sempre é possível fazer isso de forma automática, e alguns cuidados são necessários. Primeiro, a tabela já deve ter sido previamente criada pelo DBAccess. Depois, a gravação dos dados pelo Banco deve ser feita da mesma forma que o DBAccess faria — Qualquer campo do tipo caractere deve estar preenchido com espaços em branco até o final do tamanho do campo, nenhum campo pode ter valor NULL — exceto campo MEMO –, um campo “D” Data em AdvPL em uma tabela do DBAccess é gravado como “C” Caractere com 8 posições, no formato AAAAMMDD, uma data vazia são 8 espaços em branco, um campo “L” lógico do AdvPL é criado pelo DBAccess no SGDB como um campo de 1 Caractere, contento a letra “T” para valor verdadeiro e “F” para falso, um campo numérico têm o valor default = 0 (zero).

  • Diminua a distância entre as informações de origem e destino 

Normalmente uma ferramenta do próprio banco de dados que faz inserção deve ser executada na própria máquina onde está o SGDB. Isto a torna ainda mais rápida. Se você está fazendo uma importação de dados que estão sendo lidos pelo Protheus de alguma fonte de dados, e depois repassadas ao Banco de Dados através do DBAccess, e cada um destes componentes (Protheus, DBAccess e o SGDB) estão em máquinas distintas, onde existe uma interface de rede entre cada um destes componentes, fatalmente a rede vai influenciar no desempenho desta operação. Se for possível eliminar pelo menos uma camada de rede entre as aplicações, pelo menos na hora de fazer a importação, ela tende a ser mais rápida.

  • Se posssível, leia as informações da origem com Query

Se a origem dos dados for uma tabela da base de dados, ao invés de abrir a tabela no modo ISAM — com ChkFile() ou DBUseArea() — leia os dados da tabela com um SELECT — O DBAccess a partir de 2017 passou a trafegar blocos de linhas entre o DBAccess e o Protheus, o que é muito mais performático principalmente quando ambos estão em máquinas diferentes. No acesso ISAM emulado, cada DBSkip() na tabela traz apenas um registro. Quando usamos a Query, várias linhas são trafegadas em uma unica requisição, e cada DBSkip() na Query consome a próxima linha em cache sem usar a rede. Quando as linhas do cache terminarem, o DBAccess envia mais um bloco de linhas.

Inserção direta via TCSqlExec()

Da mesma forma que é possível usar uma aplicação externa para inserir dados direto no banco, é possível também realizar a inserção direta no banco em AdvPL usando TCSqlEXec() — MAS, POREM, TODAVIA, CONTUDO, ENTRETANTO, você precisa tomar os mesmos cuidados como se você estivesse alimentando estes dados por fora, E, você somente vai ter algum ganho de desempenho se você montar uma string de parâmetro com mais de uma inserção — por exemplo, inserindo múltiplas linhas na mesma instrução de insert — caso o banco de dados tenha suporte para isso — ou concatenando mais de uma instrução de inserção na mesma requisição, separando as instruções com “;” — ponto e vírgula.

cStmt := "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00001','Nome',1);"
cStmt += "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00002','Endereço',2);"
(...)
nRet := TCSqlExec(cStmt)

No exemplo acima, você pode por exemplo montar uma string de inserção com por exemplo 32 KB de dados, e enviar isso de uma vez para o banco de dados. Mas, inserir dados desta forma possui vantagens e desvantagens:

  • Uma requisição insere vários registros na mesma instrução. Usando DBAppend() / Reclock(), é inserido um registro por vez.
  • Uma requisição maior no lunar de várias requisições menores aproveita muito mais a banda de rede disponível.

Por outro lado…

  • Para montar a inserção dos valores de forma literal, você deve tratar manualmente a existência de caracteres especiais, aspas simples como conteúdo de campo, e para alguns bancos tratar “escape sequences” e caracteres especiais.
  • Campos MEMO (BLOB/CLOB/LONGVARBINARY) normalmente não são suportados com envio literal pela instrução de INSERT feita desta forma. Você acaba fazendo a inserção primeiro dos dados dos campos de tamanho fixo, e depois tem que usar o modo ISAM de acesso para alterar o registro em AdvPL, para então alimentar o conteúdo do campo.

IMPORTANTE

Nenhum dos métodos acima é recomendável de ser feito em processos concorrentes, em modo compartilhado, com outros programas inserindo informações na tabela em questão. As sugestões deste post mostram apenas jeitos diferentes e alternativas de importação de dados puros direto para tabelas de um banco de dados. Como nestes exemplos os dados são praticamente escritos “direto” nas tabelas, a aplicação que está importando os dados deve ser a responsável por criticar os dados de entrada, e garantir que a sua escrita esteja em conformidade com as regras do ERP. Mesmo uma operação de cadastro feita no ERP pode disparar integrações e processos auxiliares que alimentam outras tabelas. Mexer diretamente na base de dados sem respeitar as regras de negócio e/ou as validações do produto podem gerar comportamentos inesperados e indesejáveis no produto e nas rotinas que consomem estes dados. A forma mais segura de importar dados em tabelas do ERP é a utilização de rotinas automáticas, onde fatalmente vai existir um pênalti de desempenho para validar todos os valores de todos os campos informados, mas desta forma evita-se a alimentação incompleta ou inconsistente de dados que pode prejudicar o comportamento do produto.

Conclusão

Não existe “almoço grátis”, alguém está sempre pagando a conta. Uma inserção de dados é um processo deveras simples, você obtêm maior desempenho basicamente realizando requisições em blocos, e pode ganhar mais reduzindo as operações intermediárias intrínsecas ao processo — porém este ganho normalmente abre mão de validações que visam garantir a integridade das informações imputadas, ou acabam exigindo mais trabalho e etapas adicionais para a conclusão do processo.

Novamente agradeço pela audiência, curtidas e compartilhamentos, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências

 

Resolvendo o limite da função Randomize()

Introdução

No post Boas Práticas de Programação – Código Simples, Resultados Rápidos, eu mencionei um limite operacional da função Randomize() do AdvPL. Caso a diferença entre o maior e o menor número a ser sorteado for maior que 32767, a função vai sortear um número maior ou igual ao número inicial informado, e menor que o número inicial mais 32767.

Programa original

A chamada no fonte original era para sortear um número entre 100000 e 999999. Porém, devido a limitação operacional da função Randomize(), o maior resultado seria sempre menor que 132767.

nTmp := Randomize(100000,999999)

Para resolver esta limitação da função, podemos criar uma segunda função de sorteio, onde podemos tratar este limite de uma forma elegante e performática. Vamos criar a USER Function Randomic(). 

USER Function Randomic(nMin,nMax)
Local nDiff := nMax-nMin
Local nDec := 0
If nDiff < 32766
   Return Randomize(nMin,nMax)
Endif
While nDiff > 32765
   nDiff /= 10 
   nDec++
Enddo
nTmp := randomize(0,int(nDiff))
While nDec > 0 
   nTmp *= 10 
   nTmp += randomize(0,10) 
   nDec--
Enddo
Return nMin+nTmp

Como a U_Randomic() funciona

Inicialmente, determinamos usando a variável local nDiff qual é a diferença do maior para o menor número. Caso a diferença seja suportada pela função Randomize(), retornamos direto a chamada para a função Randomize(), passando os parâmetros originais.

Caso a diferença não seja contemplada pela função Randomize(), dividimos a diferença por 10 até que ela entre dentro do intervalo. Cada divisão realizada incrementa uma unidade na variável local nDec, que indica quantas casas da diferença original foram reduzidas. Uma vez que a diferença se enquadre nos valores suportados, chamamos a função Randomize(), para sortear um número entre 0 e o valor inteiro da diferença apurada após as divisões.

Sorteado este número, agora precisamos sortear mais alguns números para completar as casas decimais que foram “cortadas” da diferença original. A cada iteração, para quantas casas decimais foram cortadas — valor guardado na variável nDec — o valor anteriormente sorteado é multiplicado por 10, e um novo valor sorteado entre 0 e 9 é adicionado ao resultado, decrementando uma unidade em nDec. Terminado o processo, o número final sorteado será o número inicial somando com o número  final sorteado, armazenado em nTmp.

Exemplo

Vamos rodar o programa de testes abaixo, chamando uma vez a função U_RANDOMIC() usando o IDE/TDS em modo de depuração.

User Function TstRand()
Local nRand
nRand := U_Randomic(100000,1000000)
conout(nRand)
Return

Ao entrarmos na função U_Randomic(), a diferença entre o numero final e o inicial será de 900000 (novecentos mil). Ao passar pelo primeiro loop, a primeira divisão por 10 faz o número baixar para 90000 (noventa mil), e nDec é incrementado para uma unidade. Como o número ainda é maior que 32765, este loop é executado novamente, onde nDiff baixa agora para 9000 (nove mil) e nDec é incrementado para 2. Como agora nDiff é menor que 32765, o programa continua.

O sorteio do novo número, a ser armazenado na variável nTmp, será feito usando a função Randomize(), informando o valor mínimo 0 e o máximo 9000. Durante a depuração, o número sorteado por exemplo foi 1271.

Agora, como houve a redução de duas casas decimais, o próximo loop será executado duas vezes. Na primeira execução, o número nTmp é multiplicado por 10 — resultando em 12710 — e um novo dígito entre 0 e 9 será sorteado e acrescentado em nTmp. foi sorteado o número 7, e acrescentado em nTmp, fazendo seu valor atual ser 12717. Na segunda execução, este valor foi multiplicado novamente por 10 — resultando em 127170 — e um novo número foi sorteado e acrescentado — foi sorteado o número 4, e nTmp passou a conter o valor 127174.  Nas duas execuções, nDec foi decrementado duas vezes, voltando a ser 0 (zero). Pronto, na ultima linha, acrescentamos o número inicial (100000) ao número sorteado (127174), resultando no número 227174.

Da forma que a função foi escrita, ela torna muito rápido e seguro o processo de sorteio, não havendo chance do valor sorteado ser maior que o especificado como parâmetro para a função.

Operadores especiais utilizados

Quando queremos realizar uma operação aritmética com uma variável, e queremos atualizar esta mesma variável com o valor resultante da operação, normalmente utilizamos uma sintaxe como:

variavel := variavel <operador> <operando>

Por exemplo:

nVar := nVar * 10 
nVar := nVar + 10 
nVar := nVar + 1

Em AdvPL, existem operadores compostos  especiais, que ao mesmo tempo fazem a atribuição e a operação aritmética. Por exemplo, as operações acima podem ser escritas da seguinte forma:

nVar *= 10
nVar += 10 
nVar++

Os operadores +=-=*=  e  /= são binários — exigem dois argumentos, a variável do lado esquerdo que será usada como base para a operação e receberá o resultado, e a expressão do lado direito (variável ou constante), que será utilizada para realizar a operação. Respectivamente são os operadores de soma (+), subtração (-), multiplicação (*) e divisão (/). Já os operadores ++ e — são unários — têm apenas um argumento, que é a variável em questão, e respectivamente somam ou subtraem o valor 1 da variável informada.

Conclusão

Mesmo que alguma função básica da linguagem AdvPL possua alguma restrição operacional, o conhecimento das demais funcionalidades e capacidades da linguagem podem tornar fácil uma implementação de um novo recurso que atenda a sua necessidade.

Agradeço novamente a audiência, e desejo 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

Escalabilidade e Performance – Segredos

Introdução

Outo dia li um post muito interessante, onde o autor menciona alguns “segredos” para uma aplicação escalável, com algumas técnicas comuns utilizadas em aplicações WEB ( Vide post original nas referências no final do Post). Resolvi me basear nele para exemplificar o que cada um dos tópicos elencados poderia agregar ao sistema, e levantar algumas questões de aplicabilidade de cada um, dentro do ambiente AdvPL.

“STATELESS”

“Se você quer um sistema ou um serviço escalável, com certeza você quer que todas as requisições para este serviço sejam stateless.
Mas por que? Simplesmente por que caso em um futuro próximo você precise rodar a sua aplicação em um cluster, você não prende um cliente a um nó do cluster, cada requisição pode ir para o nó do cluster que estiver com a menor carga naquele momento, fazendo com que o tempo de resposta aquela requisição seja o menor possível, mantendo o nó do cluster que vai atender a esta requisição ocupado o menor tempo possível.” (Sobre Código: Os 5 segredos para um sistema altamente escalável, por Rodrigo Urubatan)

Esta afirmação casa com os princípios de escalabilidade e performance já mencionados em posts anteriores, mais especificamente sobre uma regra que diz “não estabeleça afinidade”. Basicamente, o “stateless” significa que uma requisição de processamento não deve depender de um estado específico de uma requisição anterior. Isto significa que o agente de processamento da requisição não deve reter nenhuma informação da requisição anterior, o que torna possível distribuir uma requisição de processamento para qualquer agente disponível no cluster.

Isto pode ser aplicado a diversos tipos de processamento, mas normalmente requer que a aplicação seja desenhada para trabalhar desta forma. Por exemplo, hoje quando desenvolvemos uma aplicação AdvPL para ser acessada através da interface do SmartClient, a aplicação trabalha com uma conexão TCP persistente, onde o programa AdvPL responsável pelas instruções de interface, também é responsável por montar o ambiente de execução e executar as aplicações de processamento e acesso a SGDB e meta-dados no mesmo processo.

Por exemplo, uma inclusão de um cliente através de um programa de interface SmartClient no AdvPL, é iniciada ao acionarmos um botão na janela, que executa uma função do código AdvPL pelo Application Server dentro do próprio processo, que acessa o SGDB e executa efetivamente a inserção e demais integrações e gatilhos sistêmicos relacionados (pontos de entrada do ERP), mantendo a interface em “espera” enquanto o processo é executado.

Para esta operação ser executada em um outro nó do cluster, a aplicação precisaria ser desenhada para, no momento de submeter a inclusão de um cliente, a função deveria gerar um request de inclusão de cliente, e despachá-la para um agente de distribuição de processos, que iria alocar um processo em espera no cluster de processamento, para este processo dedicado realizar o acesso ao banco e concluir a operação, liberando a interface para iniciar uma nova inclusão ou outra operação. A dificuldade de lidar com estes eventos é desenvolver um mecanismo resiliente, que permita gerar informações e um “BackLog” do que foi feito, pois procurar um erro de processamento ou comportamento em um ambiente assim, sem rastreabilidade das operações, não será uma tarefa fácil.

Um WebService por natureza é concebido como STATELESS. Existem algumas tecnologias que permitem criar afinidade e persistência em WebServices, porém isto foge ao objetivo principal da natureza das requisições e dos serviços que a implementação original se propõe a fazer. Se uma aplicação em WebServices precisa de afinidade e persistência, o uso de WebServices não parece uma boa escolha. Nem toda a operação pode ser totalmente assíncrona, mas muitas etapas de processo compensam o custo da transformação, revertida em escalabilidade.

“REQUEST RESPONSE TIME” (RRT)

Partindo de um ambiente de serviços distribuídos, onde vários processos podem realizar operações específicas, é fundamental que as requisições importantes ( ou prioritárias) sejam atendidas com o menor tempo de resposta possível. É o mesmo princípio dos caixas de um banco: Quando temos mais clientes do que caixas disponíveis, é formada uma fila, e o primeiro caixa que terminar o atendimento torna-se disponível e chama o primeiro da fila.

Se os caixas forem lentos, quando começar uma fila, as pessoas desta fila vão demorar para serem atendidas. Então, ou você coloca mais caixas, ou você melhora o desempenho dos caixas. Pensando por exemplo na priorização de atendimento a idosos, portadores de necessidades especiais, gestantes e mães com crianças de colo, são criadas duas filas de atendimento, vamos chamá-las de “normal” e “prioritária”. Todos os caixas podem atender a qualquer pessoa da fila, porém ocorre um escalonamento do atendimento.

Quanto mais rápido e eficiente for o atendimento de um caixa, mais clientes por hora ele será capaz de atender. A mesma regra vale para requisições de processamento (SOA/RPC/SOAP/REST). Se você tem um volume diário de X requisições, sujeitas a variações ou picos, e cada caixa aberto consegue atender em média N requisições por minuto, e você tem Y caixas abertos, se todas as X requisições chegarem ao mesmo tempo, as Y primeiras são atendidas imediatamente, e as demais entram na fila. Com todos os caixas atendendo, se ninguém mais entrar na fila, voce vai manter todos os caixas ocupados e trabalhando por M minutos até não ter mais ninguém na fila.

Pegamos o total de requisições (ou clientes) X , dividimos pelo numero de processos disponíveis (ou caixas abertos) Y, e temos uma média de quantas requisições devem ser processadas por cada processo (quantos atendimentos cada caixa deve fazer). Multiplicamos este número pelo tempo médio de processamento da requisição (tempo de atendimento), e teremos uma ideia de quanto tempo será necessário para eliminar a fila. Se chegam 20 requisições, e existem 4 processos disponíveis, cada um deles vai realizar 5 processamentos. Se cada processamento tem um tempo médio de 3 segundos, cada processo vai permanecer ocupado processando por 5×3 = 15 segundos. Se neste meio tempo não chegar mais ninguém na fila, em 15 segundos a fila acaba. Um caixa trabalhando sem parar em um minuto (60 segundos) , consegue atender (sem pausa ou intervalo) 60/3 = 20 requisições em um minuto. Como temos 4 processos, todos juntos conseguem atender 20 x 4 = 80 requisições por minuto.

Se dobrarmos o número de processos (8), conseguimos atender 160 requisições por minuto. Se o tempo de cada processamento (3 s.) diminuir pela metade (1,5 s.), conseguimos atender a 160 requisições por minuto com apenas 4 processos dedicados. Muitas vezes não temos como aumentar mais os processos, por limites de sistema ou esgotamento de recursos, então quanto mais leve e rápido for cada processo, melhor.

Usando AdvPL, existe flexibilidade em se criar pools de processos nativos, e dependendo de sua carga e consumo de memória, colocar múltiplos processos para atender requisições simultâneas. Porém, devem ser observados se os processos aderem aos princípios de paralelismo, senão a aplicação vai jogar memória fora e não vai escalar.

“CACHE”

Lembrando de uma das regras básicas de desempenho de tráfego de informações, buscar uma informação pela rede costuma ser mais lento que ler do disco, e pegar ela da memória é mais rápido do que ler do disco. Mas como não dá pra colocar tudo na memória, e estamos falando de paralelismo em sistemas distribuídos, usar um CACHE das informações mais repetidamente lidas na memória de cada máquina é uma forma bem eficiente de reduzir o RRT. Mas, lembre-se: Não é tudo que voce precisa colocar em um cache … apenas as informações mais acessadas, e com baixa volatilidade. Usar um cache local na memória do servidor, compartilhado pelos demais processos, pode economizar muitas idas e vindas de requisições pela rede.

Um exemplo disso se aplica mais facilmente a uma aplicação WEB dinâmica, onde uma página de promoções acessa o SGBD para mostrar os produtos em promoção. Se as promoções são alteradas diariamente, vale a pena fazer um cache dessa página, e removê-la do cache apenas quando houver alteração das promoções. Para um ERP, um cache interessante seria os meta-dados de algumas tabelas. Se boa parte dos usuários do sistema abre as mesmas tabelas e telas, a leitura destas definições poderiam ser colocadas em um cache na máquina dos slaves, alimentado sob demanda e com uma validade em tempo pré-determinada. O primeiro usuário que abre uma tela de cadastro de produtos ou pedidos alimenta o cache, e todos os demais usuários conectados em serviços naquela máquina pegam as definições do cache local, muito mais rápido, e sem consumir recurso de rede.

Existem meios de se criar caches em AdvPL, usando por exemplo variáveis globais, ou mesmo utilizar um cache de mercado (MemCacheDB, por exemplo), desde que seja escrito um Client em AdvPL que use a api client de Socket’s do Advpl (Classe tSocketClient) para fazer a comunicação. RPC e variáveis globais podem fazer uma bela dupla, mas vão precisar de um certo nível de controle e gerenciamento implementado manualmente, mas isto não inviabiliza esta alternativa.

“REMOTE DATA”

Quando falamos de aplicação Client-Server em Cluster, devemos ter em mente que as informações comuns a determinados processos precisam estar em um lugar onde todos os nós de processamento tenham acesso. Parte da premissa de não criar afinidade. Fica mais fácil citar um exemplo de servidor WEB com Upload de imagens, onde você grava a imagem em uma pasta do servidor: Ao invés de compartilhar esta pasta pelo sistema de arquivos, é possível criar um servidor dedicado a conteúdo estático, e fazer um mecanismo de cache ou réplica nos nós, se ou quando necessário.

A abordagem do AdvPL parte da premissa que cada serviço slave tenha acesso ao RootPath do ambiente, compartilhado pelo sistema operacional do equipamento. Definitivamente um excesso de acessos concorrentes pode prejudicar uma escalabilidade.

“REVERSE PROXY”

O exemplo de proxy reverso também se aplica mais em escalabilidade de ambientes WEB. Ele serve de “Front-End” da sua aplicação, permite a utilização de técnicas de Cache, mascaramento de IP, “esconde” a sua infra-estrutura interna, entre outas funcionalidades. O detalhe importante disso é que um Proxy Reverso, pelo fato de estar “na frente” de tudo, ele passa a ser um SPOF (Single Point of Failure). Se o Proxy Reverso “morrer”, sua aplicação morre junto.

Para aplicações SOA, uma alternativa interessante é usar o já mencionado “controlador” de requisições. Cada nó do cluster pergunta para um controlador de serviço onde está o nó mais próximo disponível, e cada serviço de controle fala com os demais. Assim, você pode deixar serviços ativos em vários nós, e caso um nó saia do ar, o controlador direciona a requisição para outro serviço disponível.

Resumo geral dos conceitos

Cada uma das técnicas ou “segredos” abordados exige que a aplicação seja desenvolvida para aderir a estes paradigmas. Uma aplicação Client-Server monolítica e de conexão persistente não se encaixaria neste modelo sem uma grande refatoração. É mais complicado gerenciar múltiplos recursos em múltiplos equipamentos, é mais complicado desenvolver pensando nestas características, a administração deste tipo de ambiente requer instrumentações, alertas e procedimentos da forma mais automatizada possível. Imagine você ter que conectar e abrir cada uma das máquinas para procurar por um LOG de erro específico, ou tentar descobrir por que ou como uma informação inconsistente foi parar na base de dados. Cada camada precisa estar “cercada” por mecanismos que visem minimizar inconsistências e evitar que elas se propaguem, além de permitir rastrear os caminhos da informação.

No AdvPL são fornecidos recursos como os WebServices (SOAP e REST), RPC nativo entre servidores, IPC entre processos da mesma instância de Application Server. A junção de todos eles pode tornar real e segura uma implementação desta natureza. Agrupando requisições por funcionalidade, e tendo um painel de gerenciamento e monitoramento dos recursos, você consegue medir quanto “custa” cada pedaço do sistema em consumo de recursos, e consegue lidar melhor com estimativas de crescimento, identificação de pontos críticos de melhoria, tornando assim possível a tão sonhada “escalabilidade horizontal” — em grande estilo.

Conclusão

A automatização de processos de Build e testes têm se mostrado muito efetiva em cenários de desenvolvimento e roll-out de produtos. Eu acredito que já esteja mais que na hora de criar mecanismos assistidos de deploy e mecanismos nativos de painéis de gestão de configuração e ambiente. Uma vez estabelecida a confiabilidade nos mecanismos de monitoramento, ações podem ser programadas e realizadas automaticamente, minimizando impactos e reduzindo os riscos de ter uma indisponibilidade total do ambiente computacional.

Criar uma aplicação que desde a sua concepção já considere estes pontos, será tão trabalhosa quando migrar uma aplicação que conta com comportamentos sistêmicos persistentes, mas ambos os esforços devem compensar o potencial de expansão que pode ser adquirido com esta abordagem.

Novamente, agradeço a audiência, e desejo a todos TERABYTES de sucesso 😀

Até o próximo post, pessoal 😉

Referências

Sobre Código: Os 5 segredos para um sistema altamente escalável, por Rodrigo Urubatan. Acessado em 06/12/2015 em <http://sobrecodigo.com/os-4-segredos-para-um-sistema-altamente-escalavel/>.

Stateless protocol. (2015, August 20). In Wikipedia, The Free Encyclopedia. Retrieved 01:33, October 25, 2015, from https://en.wikipedia.org/w/index.php?title=Stateless_protocol&oldid=677029705

Acelerando o AdvPL – Lendo arquivos TXT (ERRATA)

Pessoal,

No post “Acelerando o AdvPL – Lendo arquivos TXT”, foi corrigido um erro que causava um mau comportamento da rotina,  fazendo a leitura de linhas “inconsistentes”. Na chamada da função RAT(), o primeiro parâmetro deve ser a string a ser procurada, e o segundo parâmetro deve ser a string onde a primeira deve ser procurada. A passagem de parâmetros estava ao contrário, fazendo com que a quebra de linha no final de um bloco lido fosse identificada erroneamente, fazendo a rotina retornar linhas com quebras inexistentes. O código-fonte do post original já foi corrigido, segue abaixo o detalhe da correção.

Antes da correção (fonte incorreto)

// Determina a ultima quebra
nRPos := Rat(cBuffer,::cFSep)

Após a correção, o código deve ficar assim:

// Determina a ultima quebra
nRPos := Rat(::cFSep,cBuffer)

Estava trabalhando em um próximo post sobre escalabilidade e performance, e aproveitei a classe para ilustrar um processamento. Quando executei a rotina, percebi que as quebras de linha estavam inconsistentes, pois foram retornadas mais linhas do que eu havia inserido.