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