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:
- ADD() somente var armazenar o valor caso a chave ainda não tenha sido gravada anteriormente. Ela não atualiza valor de chave existente.
- 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.
- 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