Abstração de Acesso a Dados e Orientação a Objetos – Parte 05

Introdução

No post anterior (Abstração de Acesso a Dados e Orientação a Objetos – Parte 04), foram implementadas algumas opções de exportação de tabelas, para os objetos ZDBFFILE e ZMEMFILE. Enquanto isso, foi implementado na classe ZISAMFILE a importação dos formatos SDF e CSV. Agora, para compatibilizar alguns recursos do AdvPL já existentes com estas classes, vamos partir para as novas classes ZTOPFILE e ZQUERYFILE.

Classe ZQUERYFILE

Quando queremos um result-set de uma Query em AdvPL, usamos a função TCGenQry() em conjunto com a função DBUseArea(), para abrir um cursor READ ONLY e FORWARD ONLY usando um ALIAS em AdvPL.

Embora todas as funções ISAM possam ser usadas em uma WorkArea aberta sob um ALIAS, para um cursor ou result-set de Query, aplicam-se apenas as seguintes funções:

  • DbSkip() — Avança um registro no cursor. Podemos especificar um número de registros positivo para avançar, mas nunca negativo. Lembre-se, cursor FORWARD ONLY.
  • DBGoTOP() — Se você ainda está no primeiro registro da Query, a função é ignorada. Se você já leu um ou mais registros, ou mesmo todos os registros da Query, como não tem como “voltar” registros, a função fecha e abre a query novamente no Banco de Dados, recuperando um novo result set. Logo, se no intervalo de tempo entre a primeira abertura do cursor e o DBGoTOP(), registros foram acrescentados ou tiveram algum campo alterado, a quantidade de registros retornada pela Query pode ser diferente.
  • TCSetField() — Se no banco de dados existem um campo “D” Data em AdvPL, ele é gravado e retornado pela Query como um campo “C” Caractere de 8 posições. Usando a função TCSetField() podemos informar ao Protheus que esta coluna deve ser retornada no campo do ALIAS j;a convertida para Data. O mesmo se aplica a precisão de campos numéricos, e a campos do tipo “L” lógico — que no Banco de Dados são gravados com um caractere “T” indicando verdadeiro ou “F” indicando falso.
  • DbCloseArea() — Fecha o alias da Query
  • FieldGet() — Recupera o valor de um campo da query pelo número do campo
  • DbSTruct() — Retorna o array com a definição dos campos retornados na Query.
  • FieldPos() — Retorna a posição de um campo na estrutura a partir do nome.

Quaisquer outras instruções são ignoradas, ou retornam algum tipo de erro. DBRLOCK() sempre retorna .F., pois um alias de Query é READ ONLY — Somente leitura. Nao é possível setar filtros, nem para registros deletados. O filtro de registros deletados deve ser uma condição escrita de forma literal na Query, usando o campo de controle D_E_L_E_T_ .

A ideia da classe ZQUERYFILE é encapsular o alias retornado da Query, e implementar as mesmas funcionalidades que um objeto de acesso a dados da ZLIB foram concebidos — como por exemplo o ZDBFFILE e o ZMEMFILE.

Desta forma, uma rotina escrita para trabalhar com um tipo de arquivo que herda desta classe ou que possui os métodos mínimos de navegação necessários possa utilizá-la também, onde colocamos um overhead mínimo apenas criando métodos para encapsular os acessos aos dados.

Classe ZTOPFILE

O DBAccess permite eu criar um arquivo em um banco relacional (SQL) homologado, e emular um acesso ISAM neste arquivo, como se ele fosse um arquivo DBF. Toda essa infra-estrutura já está pronta e operacional dentro do Protheus Server. O objetivo desta classe é fazer o mesmo encapsulamento de acesso feito pela ZQUERYFILE, porém implementando todas as funcionalidades disponíveis, como filtros e índices, além da navegação entre registros pela ordem de um índice.

Dentro da classe — que vai herdar a ZISAMFILE — implementamos os métodos para usar os recursos nativos, e alguns adicionais para permitir o uso de recursos implementados na ZLIB, como por exemplo o índice em memória do AdvPL — ZMEMINDEX.

Uso após a implementação

Alguns métodos das classes ZISAMFILE já foram concebidos para trabalhar nativamente com um objeto de dados, ou o ALIAS de uma WorkArea. A ideia de utilização do modelo de abstração é permitir flexibilizar a manutenção e persistência de dados — permanente ou temporária — usando instruções de alto nível, dentro de fronteiras e comportamentos pré-definidos para viabilizar escalabilidade, desempenho e resiliência para a aplicação.

Idéias e mais idéias

Quer ver uma coisa deveras interessante que pode ser feita com, esta infraestrutura ? Então, imagine um operador de sistema realizando por exemplo uma inclusão de dados com muitas informações, em um terminal com conexão síncrona, como o SmartClient. Em determinado momento, algo horrível aconteceu com a rede entre a estação (SmartClient) e o Servidor (Protheus Server) que ela estava conectada. Vamos ao pior cenário, houve um problema de Hardware (no servidor, na rede, na própria estação — acabou a luz. e o terminal em uso não têm No-Break. Adeus operação, dados já digitados, foi pro beleléu.

Bem, usando um cache — por exemplo o ZMEMCACHED — a aplicação poderia gravar no cache, em intervalos de tempo pré-definidos ou mesmo a cada informação significativa digitada, um registro com os dados já preenchidos até aquele momento, atrelado ao usuário que está realizando a operação ou a operação em si. No término da operação, esta informação é removida do cache. Caso o processo seja interrompido, no momento que o usuário fizer o login, ou quando ele entrar na rotina novamente na rotina, o programa verifica se tem alguma informação no cache daquele usuário, e em caso afirmativo, permita a informação ser recuperada, e o usuário conseguiria continuar a operação do ponto onde foi feito o último salvamento.

Para isso ser feito de forma simples, a operação deve ser capaz de serializar — representar em um formato armazenável e recuperável — o seu estado no momento, para tornar fácil a operação de salvamento e recuperação de estado. Para isso, poderemos usar — assim que estiver pronta — a classe ZSTREAM.

Com ela, a ideia é ser possível inclusive salvar não apenas valores, mas um objeto. Isso mesmo, pegar as propriedades de um objeto automaticamente e salvar o estado do objeto em um cache, no disco ou onde você quiser. Porém, para restaurar o estado do objeto, você deverá recria-lo, e fazer os métodos Save e Load nele, para ser possível ajustar as propriedades necessárias, lembrando que CodeBlock não dá para ser salvo e restaurado, o bloco de código pode depender de variáveis do ambiente e referências de onde ele foi criado.

Neste cenário, o mundo ideal seria criar uma classe apenas para ser um agrupador de propriedades e ser o container de contexto de dados de uma operação. Neste caso, poderíamos construir um método NEW() na estrutura, retornando self e populando as propriedades com seus valores default. e os métodos SAVE e LOAD para salvar ou recuperar o estado atual a partir de uma Binary String — ou Stream.

Conclusão

Quando começamos a encapsular recursos, realmente conseguimos reaproveitar muito código. Os exemplos dos posts anteriores de CRUD em AdvPL — usando SmartClient e AdvPL ASP — foram úteis para mostrar o que pode ser feito usando instruções básicas da linguagem AdvPL. Agora, com o agrupamento de algumas funcionalidades em um Framework (zLib), podemos reaproveitar código e implementar funcionalidades similares com menos esforço. As classes mencionadas acima ainda estão em desenvolvimento, e serão acompanhadas de novos posts assim que estiverem “prontas” para uso !!!

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

 

 

Abstração de Acesso a Dados e Orientação a Objetos – Parte 04

Introdução

Continuando a mesma linha dos posts anteriores, vamos ver agora como exportar um arquivo de dados — das classes ZMEMFILE e/ou ZDBFFILE — para os formatos SDF , CSV e JSON 😀

Formato SDF

O formato SDF é um arquivo texto com linhas de tamanho fixo (SDF = System Data Format, fixed length ASCII text). Cada linha do arquivo é composta pelo conteúdo de um registro da tabela, onde cada campo é gravado sem separador ou delimitador, na ordem de colunas da estrutura da tabela, onde o formato de gravação de cada campo depende do tipo.

SDF Text File Format Specifications
     ------------------------------------------------------------------------
     File Element        Format
     ------------------------------------------------------------------------
     Character fields    Padded with trailing blanks
     Date fields         yyyymmdd
     Logical fields      T or F
     Memo fields         Ignored
     Numeric fields      Padded with leading blanks for zeros
     Field separator     None
     Record separator    Carriage return/linefeed
     End of file marker  1A hex or CHR(26)
     ------------------------------------------------------------------------

Campos Caractere são gravados com espaços a direita para preencher o tamanho do campo da estrutura, campos do tipo “D” data são gravados no formato ANSI (AAAAMMDD), uma data vazia é gravada com 8 espaços em branco, campos “L” Lógicos são gravados como “T” ou “F”, campos numéricos usam o tamanho especificado na estrutura com espaços a esquerda, e “.” ponto como separador decimal. A quebra de linha é composta pela sequência de dois bytes CRLF — chr(13) + chr(10) —  e ainda têm um último byte indicando EOF (Final de Arquivo). Campos “M” memo não são exportados.

Este formato é muito rápido de ser importado, pois uma vez que eu trabalho com os tamanhos dos campos da estrutura, eu não preciso fazer um “parser” de cada linha para verificar onde começa e onde termina cada campo e informação. Porém, o formato SDF não leva junto a estrutura da tabela, portanto uma importação somente será efetuada com sucesso caso o arquivo de destino já exista, e tenha a mesma estrutura do arquivo de origem — estrutura idêntica, tipos e tamanhos de campos, na mesma ordem.

Formato CSV

Este formato é muito comum em integrações entre sistemas, definido pela RFC-4180 (CSV = Comma Separated Values). Os campos caractere são gravados desconsiderando espaços à direita, e delimitados por aspas duplas. Caso exista uma aspa dupla dentro do conteúdo do campo a aspas é duplicada. As colunas são separadas por vírgula, valores numéricos são gravados sem espaços e usando o separador decimal “.” (ponto), valores booleanos são gravados como “true” ou “false”, campos do tipo “D” Data são gravados entre aspas no formato AAAAMMDD, data vazia é uma string vazia. Campos “M” memo também não são exportados. A primeira linha do CSV possui a lista de campos da tabela — apenas o nome do campo.

Um arquivo CSV tende a ser menor que um SDF, quando os campos do tipo Caractere possuem espaços a direita — que são desconsiderados ao gerar o CSV. Porém, com campos de tamanho variável, é necessário tratar linha a linha para verificar onde começa e termina cada informação a ser recuperada.

FORMATO JSON

Existem várias formas de se especificar uma tabela neste formato ( JSON = JavaScript Object Notation). A forma mais econômica e segura de exportar uma tabela de dados neste formato é representá-la como um objeto de duas propriedades: “header”, composta de um array multidimensional de N linhas e 4 colunas, representando a estrutura da tabela (Campo, Tipo, Tamanho e Decimais), e a propriedade “data”, composta de um array multi-dimensional de X linhas por N colunas, onde cada linha representa um registro da tabela, na forma de um array com os dados das colunas, na ordem de campos especificada no header. O formato de representação dos dados é praticamente o mesmo do CSV, exceto o campo “C” Caractere, que caso tenha aspas duplas em seu conteúdo, esta aspa é representada usando a sequencia de escape  \” (barra inversa e aspa dupla). Por hora, campos “M” Memo não são exportados. A diferença de tamanho dos mesmos dados exportados para CSV ou JSON é mínima.

Então, como eu faço isso?

Até agora tudo é lindo, então vamos ver como fazer a exportação, usando o método — ainda em desenvolvimento — chamado Export(). Primeiramente, partimos de um exemplo onde eu abri uma Query e criei um arquivo em memória com os registros retomados da Query, usando a classe ZMEMFILE. Então, eu chamo o método EXPORT() deste objeto, informando o formato a ser gerado e o arquivo em disco com o resultado a ser criado.

User Function TstExport()
Local cQuery
Local nH

nH := tclink("MSSQL/DBLIGHT","localhost",7890)
IF nH < 0
	MsgStop("TCLINK ERROR - "+cValToChar(nH))
	Return
Endif

// Abre uma Query no AdvPL 
cQuery := "SELECT CPF , NOME, VALOR from DOADORES WHERE VALOR > 2000 order by 3 DESC"
USE (tcGenQry(,,cQuery)) ALIAS QRY SHARED NEW VIA "TOPCONN"
TCSetField("QRY","VALOR","N",12,2)

// Cria um arquivo em memoria com a Query usando o ALIAS / WORKAREA
oMemory := ZMEMFILE():New('qrymemory')
oMemory:CreateFrom("QRY",.T.)

// Fecha a Query 
USE

// Exporta os registros para SDF 
oMemory:Export("SDF",'\temp\tstexport.sdf' )

// Exporta os registros para CSV
oMemory:Export("CSV",'\temp\tstexport.csv' )

// Exporta para JSON
oMemory:Export("JSON",'\temp\tstexport.json' )

// Fecha a tabela temporária em memória 
oMemory:Close()
FreeObj(oMemory)

Return

Método EXPORT()

E, para apreciação, vamos ver por dentro o método de exportação de dados. Ele ainda está em desenvolvimento, estou estudando como parametrizar algumas características específicas dos formatos suportados. Por hora, ele está assim:

METHOD Export( cFormat, cFileOut ) CLASS ZISAMFILE
Local nHOut
Local nPos
Local cBuffer := ''
Local lFirst := .T. 

// Primeiro, a tabela tem qye estar aberta
IF !::lOpened
	UserException("ZISAMFILE:EXPORT() Failed - Table not opened")
	Return .F.
Endif

cFormat := alltrim(Upper(cFormat))

If cFormat == "SDF" 
	
	// Formato SDF
	// Texto sem delimitador , Campos colocados na ordem da estrutura
	// CRLF como separador de linhas 
	// Campo MEMO não é exportado
	
	nHOut := fCreate(cFileOut)
	If nHOut == -1
		::_SetError(-12,"Output SDF File Create Error - FERROR "+cValToChar(Ferror()))
		Return .F.
	Endif
	
	::GoTop()
	
	While !::Eof()
		
		// Monta uma linha de dados
		cRow := ""
		
		For nPos := 1 TO ::nFldCount
			cTipo := ::aStruct[nPos][2]
			nTam  := ::aStruct[nPos][3]
			nDec  := ::aStruct[nPos][4]
			If cTipo = 'C'
				cRow += ::FieldGet(nPos)
			ElseIf cTipo = 'N'
				cRow += Str(::FieldGet(nPos),nTam,nDec)
			ElseIf cTipo = 'D'
				cRow += DTOS(::FieldGet(nPos))
			ElseIf cTipo = 'L'
				cRow += IIF(::FieldGet(nPos),'T','F')
			Endif
		Next
		
		cRow += CRLF
		cBuffer += cRow
		
		If len(cBuffer) > 32000
			// A cada 32 mil bytes grava em disco
			fWrite(nHOut,cBuffer)
			cBuffer := ''
		Endif
		
		::Skip()
		
	Enddo

	// Grava flag de EOF
	cBuffer += Chr(26)

	// Grava resto do buffer que falta 
	fWrite(nHOut,cBuffer)
	cBuffer := ''
	
	fClose(nHOut)
	
ElseIf cFormat == "CSV" 
	
	// Formato CSV
	// Strings entre aspas duplas, campos colocados na ordem da estrutura
	// Virgula como separador de campos, CRLF separador de linhas 
	// Gera o CSV com Header
	// Campo MEMO não é exportado
	
	nHOut := fCreate(cFileOut)
	If nHOut == -1
		::_SetError(-12,"Output CSV File Create Error - FERROR "+cValToChar(Ferror()))
		Return .F.
	Endif
	
	// Primeira linha é o "header" com o nome dos campos 
	For nPos := 1 TO ::nFldCount
		If nPos > 1 
			cBuffer += ','
		Endif
		cBuffer += '"'+Alltrim(::aStruct[nPos][1])+'"'
	Next
	cBuffer += CRLF

	::GoTop()
	
	While !::Eof()
		
		// Monta uma linha de dados
		cRow := ""
		
		For nPos := 1 TO ::nFldCount
			cTipo := ::aStruct[nPos][2]
			nTam  := ::aStruct[nPos][3]
			nDec  := ::aStruct[nPos][4]
			If nPos > 1
				cRow += ","
			Endif
			If cTipo = 'C'
				// Dobra aspas duplas caso exista dentro do conteudo 
				cRow += '"' + StrTran(rTrim(::FieldGet(nPos)),'"','""') + '"'
			ElseIf cTipo = 'N'
				// Numero trimado 
				cRow += cValToChar(::FieldGet(nPos))
			ElseIf cTipo = 'D'
				// Data em formato AAAAMMDD entre aspas 
				cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'
			ElseIf cTipo = 'L'
				// Boooleano true ou false
				cRow += IIF(::FieldGet(nPos),'true','false')
			Endif
		Next
		
		cRow += CRLF
		cBuffer += cRow
		
		If len(cBuffer) > 32000
			// A cada 32 mil bytes grava em disco
			fWrite(nHOut,cBuffer)
			cBuffer := ''
		Endif
		
		::Skip()
		
	Enddo

	// Grava resto do buffer que falta 
	If len(cBuffer) > 0 
		fWrite(nHOut,cBuffer)
		cBuffer := ''
	Endif
	
	fClose(nHOut)
	
ElseIf cFormat == "JSON" 

	// Formato JSON - Exporta estrutura e dados   
	// Objeto com 2 propriedades 
	// header : Array de Arrays, 4 colunas, estrutura da tabela
	// data : Array de Arrays, cada linha é um registro da tabela, 
	// campos na ordem da estrutura
	// -- Campo Memo não é exportado 
	
	/*
	{
	"header": [
		["cCampo", "cTipo", nTam, nDec], ...
	],
	"data": [
	    ["José", 14, true], ...
	]
	}
	*/

	nHOut := fCreate(cFileOut)
	If nHOut == -1
		::_SetError(-12,"Output JSON File Create Error - FERROR "+cValToChar(Ferror()))
		Return .F.
	Endif


	cBuffer += '{' + CRLF
	cBuffer += '"header": [' + CRLF

	For nPos := 1 to len(::aStruct)
		If nPos = 1 
			cBuffer += "["
		Else
			cBuffer += '],'+CRLF+'['
		Endif
		cBuffer += '"'+Alltrim(::aStruct[nPos][1])+'","'+;
			::aStruct[nPos][2]+'",'+;
			cValToChar(::aStruct[nPos][3])+','+;
			cValToChar(::aStruct[nPos][4])
	Next

	cBuffer += ']'+CRLF
	cBuffer += ']' + CRLF
	cBuffer += ',' + CRLF
	cBuffer += '"data": [' + CRLF
		
	::GoTop()
	
	While !::Eof()
		
		// Monta uma linha de dados
		if lFirst
			cRow := "["
			lFirst := .F. 
		Else
			cRow := "],"+CRLF+"["
		Endif
				
		For nPos := 1 TO ::nFldCount
			cTipo := ::aStruct[nPos][2]
			nTam  := ::aStruct[nPos][3]
			nDec  := ::aStruct[nPos][4]
			If nPos > 1
				cRow += ","
			Endif
			If cTipo = 'C'
				// Usa Escape sequence de conteudo 
				// para astas duplas. -- 
				cRow += '"' + StrTran(rTrim(::FieldGet(nPos)),'"','\"') + '"'
			ElseIf cTipo = 'N'
				// Numero trimado 
				cRow += cValToChar(::FieldGet(nPos))
			ElseIf cTipo = 'D'
				// Data em formato AAAAMMDD como string
				cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'
			ElseIf cTipo = 'L'
				// Boooleano = true ou false
				cRow += IIF(::FieldGet(nPos),'true','false')
			Endif
		Next
		
		cBuffer += cRow
		
		If len(cBuffer) > 32000
			// A cada 32 mil bytes grava em disco
			fWrite(nHOut,cBuffer)
			cBuffer := ''
		Endif
		
		::Skip()
		
	Enddo

	// Termina o JSON
	cBuffer += ']' + CRLF
	cBuffer += ']' + CRLF
	cBuffer += '}' + CRLF

	// Grava o final do buffer
	fWrite(nHOut,cBuffer)
	cBuffer := ''
	
	// Fecha o Arquivo 
	fClose(nHOut)
	

Else

	UserException("Formato ["+cFormat+"] não suportado. ")

Endif

Return

Otimizações

Uma otimização interessante é usar a variável de memória cBuffer para armazenar os dados que devem ser gravados no arquivo, e apenas fazer a gravação caso ela tenha atingido ou ultrapassado 32000 (32 mil) bytes. É muito mais rápido fazer a variável de memória aumentar de tamanho, do que o arquivo no disco. Logo, é mais eficiente gravar um bloco de 32000 bytes no arquivo, do que gravar 32 blocos de 1000, pois a cada bloco gravado o sistema operacional aloca mais espaço para o arquivo, e é mais rápido alocar um espaço maior de uma vez do que várias chamadas de alocações menores.

Porém, não é por isso que eu vou deixar a minha String em AdvPL chegar a 1 MB de tamanho para fazer a gravação, pois com strings muito grandes na memória, as operações de alocar mais espaço em memória vão ficar mais pesadas. Para mim, algo perto de 32 KB é um “número mágico” bem eficiente e sem desperdício.

Conclusão

Bem, por hora foi feita a exportação… agora, vou queimar mais alguns neurônios pra fazer a importação destes dados nestes formatos 😀

Desejo a todos, como de costume, MAIS TERABYTES DE SUCESSO !!!

Referências

 

 

MemCached Client em AdvPL – Parte 02

Introdução

No post anterior (MemCached Client em AdvPL – Parte 01) vimos a implementação de uma classe nativa em AdvPL para fazer o papel de API Client do MemCached. Agora, vamos ver um pouco de como usar esta classe — agora na versão 1.01, suportando todos os tipos simples do AdvPL para armazenamento e recuperação do cache, inclusive Array.

Conceito e Boas Práticas

No post anterior, vimos um fonte de testes da classe zMemCached, apenas para teste de funcionalidade. Agora, vamos ver onde poderíamos usar esta classe em algumas situações.

Antes de mais nada, vamos conceituar o objetivo de um cache: Um cache normalmente é criado para tirar o peso de um processamento usado para obter um resultado, quando o mesmo resultado será muitas vezes , por um ou mais partes de um programa, por um ou múltiplos usuários, desde que a informação seja a mesma para todos.

O MemCached foi criado para ser um cache versátil — armazena qualquer coisa — e rápido. Normalmente é usado em aplicações WEB para reduzir a quantidade de requisições ao banco de dados, quando um mesmo resultado de uma Query ou de uma página renderizada será muito requisitada e possui um baixo índice de alteração — o que evita invalida e realimentar o cache constantemente.

Por ser um cache que pode atender múltiplas conexões de múltiplos usuários de um sistema, qualquer parte comum a todos eles, e constantemente requisitada, poderia ser colocado em cache. Porém, precisamos tomar cuidado com esta afirmação:  PODER, tudo pode, mas nem tudo DEVE ser feito. Senão, dobra a memória da máquina e coloca tudo em cache. Pronto, seu cache vai comer memória com farina, você vai ter um volume estrelar de dados da memória, aqueles dados que não são usados frequentemente somente ocupam espaço e oneram as demais operações que realmente ganhariam com o uso de um cache.

Logo, a premissa numero um é: Use um cache onde é necessário e adequado. Comece com aquilo que realmente “mata” o seu banco de dados, faça um Log Profiler primeiro, não saia usando o cache por que é “legal” 😉

Implementação

O segundo mandamento é: Salvo raras exceções, como o uso de uma chave com um valor para incrementou ou decremento, use o cache para colocar um agrupamento de dados com contexto. Por exemplo, se você têm uma Query que retorna os 10 produtos mais vendidos no mês, não armazene cada linha da query em uma chave, coloque os dados em um array e guarde o array. Quase todas as consultas feitas a esta chave vão querer, via de regra, os 10 produtos.

Quer fazer uma atualização automática dos resultados a cada 60 minutos? Uma forma elegante e sob demanda é armazenar o resultado em cache com expire time de 3600 segundos (ou 1 hora). Com isso, passou uma hora, o valor é apagado do cache, o próximo processo que pedir este valor vai ver que ele não está cacheado, roda a Query, e alimenta o cache novamente com o valor mais novo.

Vale lembrar que a responsabilidade de atualizar o cache é da aplicação que o consome, Ela sempre deve buscar primeiro a informação em Cache, e caso não a encontre, ela deve gerar a informação e alimentar o cache. Por isso, mesmo que o custo de processamento de consultar e armazenar no cache seja relativamente baixo, fazer isso para uma informação com baixa incidência de busca (low hit count) é desperdiçar recurso.

Colocando uma Query em Cache

Vamos partir de uma Query qualquer, onde desejamos guardar seu resultado em Cache. Por hora, partindo de um programa já existente, primeiro ele deve ser alterado para trabalhar com um Result Set em Array. Desse modo, podemos armazenar e recuperar o Array com o resultado da Query no Cache.

// Pega a Query do banco e coloca no array 
cQuery := "SELECT CAMPO1, CAMPO2, CAMPO3 FROM TABELA WHERE CAMPO1 = '001' ORDER BY 1,2"
USE (TcGenQry(,,cQuery)) ALIAS QRY EXCLUSIVE NEW VIA "TOPCONN"
While !eof()
	AADD(aQryData , { QRY->CAMPO1 , QRY->CAMPO2 , QRY->CAMPO3 } ) 
	QRY->(DbSkip())
Enddo
USE

Agora, vamos ao uso do cache. Primeiro, criamos o objeto client e a conexão.

// Cria instancia do Cache 
oMemCache := ZMEMCACHED():New("localhost",11211)

// Conecta no Cache 
If !oMemCache:Connect()
	conout("oMemCache:Connect() FAILED")	
	conout(oClient:GetErrorStr())
	return .F.
Endif

Antes de rodar a Query, verificamos se a informação está no cache. Vamos dar um nome para a chave, um identificador desta Query para o cache.

oMemCache:Get("TEST_QRYINCACHE",@aQryData)

Se o conteudo de aQryData estiver NIL após a chamada, este array não está no cache. Neste caso, rodamos a Query, e colocamos o array em cache, usando:

oMemCache:Set("TEST_QRYINCACHE",aQryData)

Logo, o fluxo da rotina passa a ser:

  • Cria conexão com o cache
  • Verifica se o array está lá, tentando recuperar o array
  • Se o array não foi recuperado, roda a Query no banco, cria o array e coloca ele em cache
  • Segue o fluxo normal da rotina usando o array

Usando o objeto client do cache

Normalmente criamos o objeto, conectamos, pedimos algo do cache, desconectamos e matamos o objeto — FreeObj() — após desconectar. Como a conexão com o cache é algo feito muito rapidamente, não faz muito sentido mantermos uma conexão permanente em uma variável STATIC

A exceção são os casos onde uma ou mais sub-rotinas durante o processamento também vão fazer requisições ao cache. Neste caso, podemos encapsular o objeto de cache usando uma classe de controle, armazenando o objeto client do cache em uma variável STATIC, e controlando o acesso a este objeto usando métodos GetCache() e ReleaseCache().

Neste caso, as rotinas e sub-rotinas que usam o cache poderiam buscar o objeto usando o GetCache(), que incrementaria um contador interno de uso, e ao terminar o processamento, chamar a ReleaseCache(), que decrementa a referência, anula a variável em uso, e caso a referência esta chegue em 0 (zero), desconecta e limpa o objeto armazenado, enquanto quem consome o cache apenas atribui NIL para a variável usada para armazenar o objeto de cache daquela rotina. Inclusive, podemos nos aproveitar de algumas características interessantes das classes em AdvPL. Vejamos:

#include "protheus.ch"

/* ==============================================================================
Classe      ZMEMCACHEDPOOL
Autor       Julio Wittwer
Data        01/2019
Descrição   Encapsula objeto client do MemCache, usando contador de referencias. 
Os programas que consomem o cache devem obter a instância usando :
   ZMEMCACHEDPOOL():GetCache( @oMemCache , @cError )
E após o uso, soltar a instancia usando : 
   ZMEMCACHEDPOOL():ReleaseCache( @oMemCache )

==============================================================================*/

STATIC _oMemCache        // Objeto do Cache em  "Cache"
STATIC _nRefCount := 0   // Contador de referencias 

CLASS ZMEMCACHEDPOOL FROM LONGNAMECLASS

   METHOD GetCache()
   METHOD ReleaseCache()
   METHOD RefCount() 
   
ENDCLASS

// ----------------------------------------------------
// Obtem por referencia uma instancia do cache, e em caso de
// falha, obtem o erro também por referência 

METHOD GetCache( oCache , cError ) CLASS ZMEMCACHEDPOOL

// Inicializa parâmetros passados por referência
oCache := NIL
cError := ""

IF _oMemCache != NIL
	// Já tenho um objeto de conexao 
	// Verifico se a conexão está OK
	IF _oMemCache:IsConnected()
		// Conexão OK, incremento contador de referencias
		// e retorno 
		_nRefCount++
		oCache := _oMemCache
	Else
		// A conexão não está OK
		// Limpa o objeto e passa para a próxima parte 
		FreeObj(_oMemCache)
		_oMemCache := NIL
	Endif
Endif

IF _oMemCache == NIL
	// Nao tenho o objeto de conexão
	// Crio o objeto e tento conectar 
	_oMemCache := ZMEMCACHED():New("localhost",11211)
	IF _oMemCache:Connect()
		// Conexão OK,incrementa contador e atribui o objeto 
		_nRefCount++
		oCache := _oMemCache
	Else
		// Nao conectou, recupera o erro, alimenta cError 
		// e mata este objeto
		cError := _oMemCache:GetErrorStr()
		FreeObj(_oMemCache)
		_oMemCache := NIL
	Endif
Endif
Return 

// ----------------------------------------------------
// Solta a referência do cache em uso, anula a variável 
// recebida por referencia, e caso o contador 
// seja menor que um, limpa o objeto da memória 
METHOD ReleaseCache( oCache ) CLASS ZMEMCACHEDPOOL

IF oCache != NIL 
	oCache := NIL
	_nRefCount--
	IF _nRefCount < 1 
		_oMemCache:Disconnect()
		FreeObj(_oMemCache)
		_oMemCache := NIL
	Endif
Endif

Return 

// ----------------------------------------------------
// Retorna o contador de referencias de uso 
// do objeto do Cache 

METHOD RefCount() CLASS ZMEMCACHEDPOOL
Return _nRefCount

Dessa forma, você centraliza a conexão com o MemCached, e no seu processo obtém uma instância única em uso. Basta tomar cuidado para não fazer mais Release()  do que Get(), senão você acaba matando a instância e todas as referências que ainda podem fazer uso dele.  Da mesma forma, não esqueça de fazer um Release() após usar, senão você larga o objeto na memória e deixa a conexão dele lá até que o processo termine. Vamos ver como ficariam os fontes que consomem o cache:

// --- Fonte Original ----

// **** Cria o objeto e conecta com o Cache ****
oMemCache := ZMEMCACHED():New("localhost",11211)
If !oMemCache:Connect()
	conout("oMemCache:Connect() FAILED")	
	conout(oClient:GetErrorStr())
	return .F.
Endif

// **** USA O CACHE ****

// **** Desconecta e limpa o objeto ****
oMemCache:Disconnect()
FreeObj(oMemCache)
oMemCache := NIL

// FONTE NOVO -- Usando o ZMEMCACHEDPOOL 

// Obtém o objeto do Cache 
oMemCache := NIL
cError := ''
ZMEMCACHEDPOOL():GetCache( @oMemCache , @cError )
IF oMemCache == NIL 
	conout("ZMEMCACHEDPOOL:GetCache() FAILED")	
	conout(cError)
	return .F.
Endif

// (...) 
// **** USA O CACHE ****
// (...) 
// **** Faz Release do objeto ****
ZMEMCACHEDPOOL():ReleaseCache( @oMemCache )

Conclusão

Valendo-se destas premissas, e usando elegantemente os recursos de cache, vários processos podem obter benefícios diretos e indiretos desta implementação.

Todos os fontes atualizados da ZLIB estão no GitHub https://github.com/siga0984/zLIB ! É baixar, compilar  e usar. Os exemplos de uso e teste do cache estão no https://github.com/siga0984/Blog

Novamente agradeço a audiência e desejo a todos TERABYTES DE SUCESSO !!! 

 

 

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

 

Abstração de Acesso a Dados e Orientação a Objetos – Parte 02

Introdução

No post anterior (Abstração de Acesso a Dados e Orientação a Objetos) vimos o conceito de abstração e uma ideia de implementar uma classe superior — ou superclasse — que eliminaria várias duplicidades entre as classes ZDBFFILE e ZMEMFILE. Bem, mãos a obra.

Classe ZISAMFILE

Tudo o que é comum e exatamente igual na implementação de ambas as classes de acesso a DBF em disco e em memória são parte de uma lógica de acesso e comportamento ISAM. Ao criar a classe ZISAMFILE, ela passa a ter as propriedades e métodos comuns a ambas implementações, que são removidas das respectivas implementações e colocadas nela.

A classe ZISAMFILE não tem construtor explícito, ela não têm um “New”. Mas não precisa, pois ela não foi feita para ser instanciada diretamente. Ela deve ser a classe superior a ser herdada pelas classes ZMEMFILE e ZDBFFILE, da seguinte forma:

// Ao invés de 
CLASS ZDBFFILE FROM LONGNAMECLASS
CLASS ZMEMFILE FROM LONGNAMECLASS

// Agora temos
CLASS ZISAMFILE FROM LONGNAMECLASS
CLASS ZDBFFILE FROM ZISAMFILE
CLASS ZMEMFILE FROM ZISAMFILE

Métodos reimplementados

Existem alguns métodos comuns implementados tanto na classe filha como na classe pai. Ao implementar na classe filha um método da classe pai, você pode ou não chamar o método da classe pai de dentro da classe filha, quando o objetivo do método não é substituir a implementação da classe pai, mas sim COMPLEMENTÁ-LA.

Por exemplo, cada uma das classes (ZDBFFILE e ZMEMFILE) possui propriedades específicas, declaradas em sua definição. E, a classe pai ( ZISAMFILE) também tem as suas propriedades, comuns a todas as heranças. Na implementação original, o método de uso interno da classe chamado _InitVars() foi feito para justamente inicializar estas propriedades, e ele agora também foi implementado na classe ZISAMFILE.

A forma correta e elegante de se fazer isso é: Cada método _InitVars() da sua classe inicializa as propriedades da sua classe. E, as classes que herdam a ZISAMFILE -- no caso ZMEMFILE e ZDBFFILE -- antes de mais nada chamam o método _InitVars() da classe superior (ZISAMFILE). Sendo assim, o método _InitVars da classe ZMEMFILE ficou assim:

METHOD _InitVars() CLASS ZMEMFILE 

// Inicialização das propriedades da classe pai
_Super:_InitVars()

// Inicializa demais propriedades da ZMEMFILE
::aFileData   := {}
::lOpened     := .F. 
::lExclusive  := .F. 
::lCanWrite   := .T. 
::dLastUpd    := ctod("")
::aGetRecord  := {}
::aPutRecord  := {}
::lUpdPend    := .F. 
::lSetDeleted := .F. 
::nRecno      := 0

Return

Como eu disse, ainda existem propriedades em duplicidade implementadas nas classes ZMEMFILE e ZDBFFILE, elas serão remanejadas em outro momento. Mas sabe o que é o mais lindo de tudo isso?

  • Os programas de teste que usavam as classes continuam funcionando perfeitamente, pois todos eles acessam as funcionalidades das classes através de métodos, até mesmo as propriedades são retornadas por métodos — recurso também chamado de “Getters and Setters” — torne as propriedades privadas da classe, e encapsule qualquer mudança de estado das propriedades em métodos Set<Propriedade>(), e as consultas por métodos Get<Propriedade>()
  • A classe ZISAMFILE ficou com 700 linhas. Isto significa que cada fonte das classes ZMEMFILE e ZDBFFILE agora tem cada um 700 linhas a menos, eliminando a duplicidade de código, e implementando as funcionalidades na classe pai. 
  • Até mesmo um método que era

Outras mudanças

Aproveitando o momento de refatoração, a classe de índices em memória deixou de se chamar ZDBFMEMINDEX e passou a ser ZMEMINDEX — afinal ela é usada pelos métodos e propriedades de controle da implementação da ZISAMFILE. Outra alteração interessante era o processamento de uma expressão AdvPL, onde era necessário trocar a ocorrência de campos na expressão pelo o:FieldGet() do campo. Isto era feito exatamente da mesma forma tanto na classe de índice quanto nas classes de ZDBFFILE e ZMEMFILE para aplicar filtros.

Agora, existe um método chamado _BuildFieldExpr(), que recebe uma string com uma expressão AdvPL qualquer que use campos da tabela — onde todos os campos na expressão devem ser colocados com letras maiúsculas — e retorna uma string com o texto do Codeblock com a expressão resultante. Agora, quem precisa desta funcionalidade chama o método  _BuildFieldExpr() da classe  ZISAMFILE, e com a expressão resultante, criar o Codeblock dinâmico com macro-execução e usar conforme a necessidade.

GITHUB

Conforme o projeto vai sendo alterado e os fontes refatorados, novos recursos arquivos vão sendo acrescentados no GITHUB, a versão mais atual de todos os fontes envolvidos está lá. Pode ser necessário remover alguns fontes do projeto e recompilar os programadas para dar tudo certo. Em breve os fontes das implementações de arquivo e implementações comuns vão fazer parte de um novo projeto — Chamado “ZLIB”.

Conclusão

Eu deixo a conclusão desse post e da implementação para vocês. Espero que este exemplo sirva não somente pela sua funcionalidade, mas como um modelo de boas práticas de desenvolvimento.

Desejo a todos novamente TERABYTES DE SUCESSO 😀

 

 

 

 

 

Arquivos em Memória – Classe ZMEMFILE

Introdução

Nos posts anteriores, acompanhamos a criação de uma classe de acesso a dados ISAM — chamada de ZDBFTABLE, renomeada para ZDBFFILE — , feita para leitura e manutenção de arquivos no formato DBF em AdvPL, sem dependência de nenhum Driver. Agora, tomando esta classe como base da implementação, nasceu a classe ZMEMFILE.

Classe ZMEMFILE

O lindo da orientação a objetos é o reaproveitamento de código. Como eu não comecei a implementação com uma classe abstrata, e não pretendia criar uma agora, a classe ZMEMFILE nasceu de um “Clone” da classe ZDBFFILE. A diferença é que, ao invés de eu endereçar um handler de arquivo em disco para ler e gravar dados, eu criei na classe uma propriedade chamada ::aFileData, que é um array multi-dimensional com as colunas da tabela, e uma coluna interna a mais — para indicar se o registro foi marcado para deleção ou não.

O meu “RECNO” passa a ser o próprio elemento do array. Cada inserção de novo registro é feita no final do array, e a deleção apenas habilita um flag na ultima coluna do array. Os demais mecanismos são os mesmos, o registro atual lido em memória é uma cópia do original em um array separado, a atualização de valores idem, e os dados da tabela estão associados ao objeto da tabela, visível apenas pelo processo atual, enquanto o objeto não foi destruído ou a tabela for fechada.

Todos os demais métodos da classe que não acessavam fisicamente o arquivo, simplesmente não foram alterados. Estes métodos são os candidatos para uma futura refatoração, criando uma classe superior com estes métodos, fazendo as classes ZDBFFILE e ZMEMFILE herdarem esta classe base, e removendo as duplicidades desnecessárias da implementação.

Aproveitamento da classe ZMEMINDEX

Como a classe que cria o índice em memória não acessa diretamente os dados de nenhum arquivo, mas faz as leituras, criação de índice e demais operações usando os métodos da classe ZDBFFILE, eu praticamente não precisei mexer em nenhuma linha da ZMEMINDEX para usá-la com a ZMEMFILE. 

Isso me deixou simplesmente radiante. A implementação de filtro foi clonada, a implementação de bisca indexada e manutenção de índices também clonada, uma vez que eu reimplementei os métodos que efetivamente acessavam o disco para acessar um array da própria classe, o fonte já funcionava.

Fonte de Testes

Vamos ver o fonte abaixo, chamado de CriaMem.PRW

#include "protheus.ch"

USER Function CriaMEM()
Local cFile := 'memfile.dbf'
Local oDbf
Local aStru := {}

SET DATE BRITISH
SET CENTURY ON 
SET EPOCH TO 1950

// Define a estrutura 
aadd(aStru,{"CPOC","C",10,0})
aadd(aStru,{"CPOM","M",10,0})

// Cria o objeto da tabela 
oDbf := ZMEMFILE():New(cFile)

// Cria a tabela em si 
oDbf:Create(aStru)

// Abre em modo de escrita 
If !oDbf:Open(.T.,.T.)
	UserException( oDBF:GetErrorStr() )
Endif

// Insere um registro
oDBF:Insert()
oDBF:Fieldput(1,'Laranja')
oDBF:Fieldput(2,'0000000001')
oDBF:Update()

// Insere mais um registro 
oDBF:Insert()
oDBF:Fieldput(1,'Banana')
oDBF:Fieldput(2,'0000000002')
oDBF:Update()

// Insere um terceiro registro 
oDBF:Insert()
oDBF:Fieldput(1,'Abacate')
oDBF:Fieldput(2,'0000000003')
oDBF:Update()

conout("Mostrando 3 registros")
oDBF:GoTop()
While !oDBF:Eof()
	// Mostra o registro atual
	ShowRecord(oDBF)
	oDBF:Skip()
Enddo

// Agora cria um indice
conout("Criando indice por CPOC")
oDBF:CreateIndex("CPOC")

// Mostra os dados da tabela novamente, agora ordenados
// A criacao de um indice já o torna ativo, e reposiciona 
// a tabela no primeiro registro 
While !oDBF:Eof()
	// Mostra o registro atual
	ShowRecord(oDBF)
	oDBF:Skip()
Enddo

// Fecha a tabela
// -- Os dados sao eliminados da memoria 
oDBF:Close()

// Limpa / Libera o Objeto
FreeObj(oDBF)

Return

// Função feita para mostrar o conteudo do registro atual 
STATIC Function ShowRecord(oDBF)
Local nI
Local aStru := oDBF:GetStruct()
conout(replicate('-' ,79))
conout("RECNO() ...... " + cValToChar(oDBF:Recno()) +" | " + "DELETED() .... "+cValToChar(oDBF:Deleted()) )
conout("BOF() ........ " + cValToChar(oDBF:Bof())   +" | " + "EOF() ........ "+cValToChar(oDBF:Eof()) )
conout("Index ........ " + "("+cValToChar(oDBF:IndexOrd())+") "+oDBF:IndexKey())
conout("")
For nI := 1 to len(aStru)
	conout("Fld #"+padr(cValToChar(nI),3)+" | "+ ;
	  aStru[nI][1]+" ("+aStru[nI][2]+") => ["+cValToChar(oDBF:Fieldget(nI))+"]" )
Next
conout("")
Return

Em poucas palavras, o fonte de exemplo cria a tabela, insere três registros, mostra no console, cria um índice pelo campo “CPOC”, e mostra novamente os dados ordenados.

Sabe o que você precisa fazer para isso rodar com um DBF ? Apenas troque a seguinte linha:

// Troque a linha abaixo 
oDbf := ZMEMFILE():New(cFile)

// Para esta aqui:
oDbf := ZDBFFILE():New(cFile)

E está feito. Salve o fonte, compile novamente e teste. O arquivo chamado “memfile.dfb” será criado no disco. Recomendo baixar a pasta inteira do GirHub (Blog), e criar um projeto com os seguintes fontes:

zDBFFile.prw
zDbfMemIndex.prw
zDBTFile.prw
zFPTFile.prw
zLibDateTime.prw
zLibDec2Hex.prw
zLibNBin.prw
zMEMFile.prw

Na próxima refatoração, a classe ZDBFMEMINDEX será renomeada para ZMEMINDEX — afinal ela não serve apenas para DBF 😀

Por que não usar um Array “direto” ?

A-há !! A pergunta de meio milhão de dinheiros …risos… Sim, armazenar um resultado temporário de qualquer coisa em um array em AdvPL pode ser feito de forma direta, sem dúvida. Se o seu uso de dados em Array é simples, o array é pequeno e as buscas são ridículas, e a manutenção de elementos praticamente inexistente, então você não precisa usar uma classe para emular um arquivo em memória. Escreva seu fonte criando um array direto e seja feliz.

Agora, se você vai ter um número maior de elementos, precisa de uma busca ou navegação ordenada — HASH faz busca, mas não ordena — e já tem um código que usa o arquivo em disco, como um container temporário, mas ele não vai acabar com a memória do Protheus Server se ele for usado em memória … pronto, você tem uma implementação virtual de arquivo para pintar e bordar.

Evolução natural de funcionalidades

Conforme a implementação vai sendo utilizada, naturalmente sentimos a necessidade de algumas melhorias, alguns métodos ou mesmo novas classes facilitadoras. Por exemplo, copiar dados de um arquivo em disco para a memória. É simples montar, basta instanciar o arquivo em disco em um objeto, pegar a estrutura, criar o objeto em memória, fazer um loop lendo  o primeiro arquivo e inserindo os dados no segundo arquivo.

Porém, se cada vez que você precisar fazer isso, você replicar a implementação, dá-lhe código duplicado. Nada como implementar um método CreateFrom(). Você cria o objeto do arquivo no disco, onde estão os dados, cria o objeto do arquivo em memória, então chama o método CreateFrom() do arquivo em memória, passando como parâmetro o objeto do arquivo em disco. O método CreateFrom() ainda não existe, mas é o próximo da fila… risos…

Internamente este método já vai fazer o que tem que ser feito. Quer algo mais elegante que isso? Seu arquivo temporário em memória não precisa de todos os dados do arquivo em disco, porém apenas os registros que atendam uma determinada condição. Basta setar um filtro no arquivo de origem, o método CreateFrom() vai copiar apenas os registros logicamente visíveis, que atendam a condição de filtro.

Conclusão

Por hora, a conclusão óbvia é que, embora inicialmente pareça um pouco mais difícil mudar seu mind-set para pensar Orientado a Objetos, adotar este paradigma da forma consciente e adequada só têm benefícios.

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

Referências

PROGRAMAÇÃO ORIENTADA A OBJETOS. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2018. Disponível em: <https://pt.wikipedia.org/w/index.php?title=Programa%C3%A7%C3%A3o_orientada_a_objetos&oldid=53496356>. Acesso em: 2 nov. 2018.

Classe ZDBFTABLE – Índice em Memória

Introdução

Nos posts anteriores, começamos a ver a implementação da classe ZDBFTABLE, uma forma de leitura de arquivos no formato DBF, sem o uso de Driver ou RDD, lendo o arquivo direto no disco usando as funções de baixo nível de arquivo do AdvPL. Agora, vamos ver como criar um índice eficiente em memória

Classe ZDBFMEMINDEX

Criar um índice para um arquivo DBF é uma forma não apenas de permitir uma navegação ordenada nos registros, mas também uma forma de realizar uma busca muito rápida pelo índice. Como a ideia original da classe ZDBFTABLE é realizar leitura de dados de um DBF, seria deveras interessante ter uma forma de criar um ou mais índices para a tabela.

Devido a natureza das tabelas DBF, um índice com chave simples ou composta (um ou mais campos) pode usar qualquer expressão ou função do AdvPL, onde o resultado da expressão é gravado em um índice no disco. A implementação da classe de índice em memória implementa esta funcionalidade, mas sem persistir o índice em disco. Ele é criado e mantido na memória por um array ordenado.

CLASS ZDBFMEMINDEX

   DATA oDBF		// Objeto ZDBFTABLE relacionado ao índice 
   DATA cIndexExpr      // Expressão AdvPL original do índice
   DATA bIndexBlock     // CodeBlock para montar uma linha de dados do índice
   DATA aIndexData      // Array com os dados do índice ordenado pela chave 
   DATA aRecnoData      // Array com os dados do índice ordenado pelo RECNO 
   DATA nCurrentRow     // Numero da linha atual do índice 
   DATA lSetResync      // Flag de resincronismo pendente da posição do índice
   DATA lVerbose

   METHOD NEW(oDBF)     // Cria o objeto do índice
   METHOD CREATEINDEX(cIndexExpr) // Cria o índice baseado na chave fornecida 
   METHOD CLOSE()       // Fecha o índice e limpa os dados da memória 

   METHOD GetFirstRec() // Retorna o RECNO do primeiro registro do índice
   METHOD GetPrevRec()  // Retorna o RECNO do Registro anterior do índice
   METHOD GetNextRec()  // Retorna o RECNO do próximo registro do índice
   METHOD GetLastRec()  // Retorna o RECNO do último registro do índice 
   
   METHOD GetIndexExpr()  // Rertorna a expressão de indexação 
   METHOD GetIndexValue() // Retorna o valor da chave de indice do registro atual 
   METHOD GetIndexRecno() // REtorna o numero do RECNO da posição do índice atual 
   METHOD IndexSeek()     // Realiza uma busca ordenada por um valor informado 
   METHOD RecordSeek()    // REaliza uma busca no indice pelo RECNO 
   METHOD UpdateKey()     // Atualiza uma chave de indice ( em implementação ) 
   
   METHOD _BuildIndexBlock(cIndexExpr) // Cria o codeblock da chave de indice 
   METHOD _CheckSync()    // Verifica a necessidade de sincronizar o indice 
   METHOD SetResync()     // Seta flag de resincronismo pendente
   METHOD SetVerbose()    // Seta modo verbose com echo em console ( em implementação 
   
ENDCLASS

Trocando em miúdos

Quando navegamos pela ordem física de registros de uma tabela DBF — ou seja, pelo número do RECNO — basicamente somamos uma unidade no marcador de registro para acessar o próximo, e subtraímos uma unidade para acessar o registro anterior, o primeiro registro da tabela é o número um, e o último registro é o maior registro da tabela (LastRec).

Quando optamos por realizar a navegação por um índice, basicamente criamos uma lista ordenada do tipo chave-valor, onde cada registro gera uma chave baseado na expressão de indexação, e o índice é responsável por guardar a chave e o número do RECNO correspondente desta chave. Uma vez que a lista seja ordenada, cada chave aponta para um registro físico (RECNO).

Logo, ao navegar usando um índice, eu preciso ter um marcador de controle (::nCurrentRow) para informar em qual linha do índice eu estou posicionado, e quando a tabela receber uma instrução de SKIP(), para ir para o próximo registro, se eu estou usando um índice, eu pergunto para o índice qual é o número do próximo registro que eu preciso posicionar.

Método CreateIndex()

METHOD CREATEINDEX( cIndexExpr ) CLASS ZDBFMEMINDEX

// Guarda a expressão original do indice
::cIndexExpr := cIndexExpr

// Monta o CodeBlock para a montagem da linha de dados
// com a chave de indice
::_BuildIndexBlock( cIndexExpr )

// Agora varre a tabela montando o o set de dados para criar o índice
::aIndexData := {}
::aRecnoData := {}

// Coloca a tabela em ordem de regisrtros para a criação do indice
::oDBF:DbSetORder(0)
::oDBF:DBClearFilter()
::oDBF:DbGoTop()

While !::oDBF:Eof()
	// Array de dados 
	// [1] Chave do indice
	// [2] RECNO
	// [3] Numero do elemento do array aIndexData que contém este RECNO
	aadd( ::aIndexData , { Eval( ::bIndexBlock , ::oDBF ) , ::oDBF:Recno() , NIL } )
	::oDBF:DbSkip()
Enddo

// Sorteia pela chave de indice, usando o RECNO como criterio de desempate
// Duas chaves iguais, prevalesce a ordem fisica ( o menor recno vem primeiro )
aSort( ::aIndexData ,,, { |x,y| ( x[1] < y[1] ) .OR. ( x[1] == y[1] .AND. x[2] < y[2] ) } )

// Guardando a posicao do array ordenado pelos dados na terceira coluna do array 
aEval( ::aIndexData , {| x,y| x[3] := y })

// Agora, eu preciso tambem de um indice ordenado por RECNO 
// Porem fazendo referencia a todos os elementos do array, mudandi apenas a ordenação 

// Para fazer esta magica, cria um novo array, referenciando 
// todos os elementos do array principal , então ordena
// este array pelo RECNO
::aRecnoData := Array(len(::aIndexData))
aEval(::aIndexData , {|x,y| ::aRecnoData[y] := x })

// Ordena o array aRecnoData em ordem crescente de RECNO
aSort( ::aRecnoData ,,, { |x,y| x[2] < y[2] } )

Return .T.

Aqui é que a mágica acontece. Neste fonte eu uso e abuso de Arrays, aEval e ASort. Primeiro, eu preciso criar um array que tenha pelo menos o valor da expressão de ordenação de cada registro, e o número do registro correspondente. Estes dados serão armazenados no array ::aIndexData. Por razões que eu vou explicar mais abaixo, este array possui uma coluna adicional, que contém qual é o número deste elemento no próprio array — parece não fazer sentido, mas vai fazer.

Primeiro, a expressão AdvPL recebida para criar o índice pode retornar uma String, um número ou uma data. Podemos criar uma chave composta usando dois campos por exemplo, mas como a expressão final do valor chave a ser ordenado é única, a expressão de índice normalmente concatena estes campos — sendo eles do tipo  “C” Caractere, por exemplo. Se existem campos de tipos diferentes a serem usados para gerar um índice, normalmente convertemos tudo para string: Campos numéricos são convertidos para Caractere usando a função STR(), passando como parâmetro o valor do campo, o tamanho do campo e a quantidade de decimais. Para campos data, convertemos para string usando DTOS() — que converte a data no formado AAAAMMDD (ou YYYYMMDD), própria para ordenação em string.

::_BuildIndexBlock( cIndexExpr )

Uma vez montado o CodeBlock para gerar um valor de chave de ordenação por registro, usamos o objeto  oDBF para limpar qualquer filtro da tabela, usar a ordem física dos registros, posicionar no primeiro, e criar o array por enquanto apenas com o valor de cada chave, e o RECNO correspondente — primeira e segunda colunas do Array — varrendo a tabela do primeiro ao último registro.

While !::oDBF:Eof()
	// Array de dados 
	// [1] Chave do indice
	// [2] RECNO
	// [3] Numero do elemento do array aIndexData que contém este RECNO
	aadd( ::aIndexData , { Eval( ::bIndexBlock , ::oDBF ) , ::oDBF:Recno() , NIL } )
	::oDBF:DbSkip()
Enddo

Feito isso, a ordenação deste array deve levar em conta que, caso hajam expressões de índice de mesmo valor, o campo RECNO passa a ser um fator de ordenação. Chaves repetidas do índice são ordenadas pelo valor do RECNO, no menor para o maior.

// Sorteia pela chave de indice, usando o RECNO como criterio de desempate
// Duas chaves iguais, prevalesce a ordem fisica ( o menor recno vem primeiro )
aSort( ::aIndexData ,,, { |x,y| ( x[1] < y[1] ) .OR. ( x[1] == y[1] .AND. x[2] < y[2] ) } )

Agora que o array ::aIndexData está ordenado, vamos preencher a terceira coluna com a posição de cada elemento no array ordenado. Para isso,  preenchemos a terceira coluna deste Array usando:

// Guardando a posicao do array ordenado pelos dados na terceira coluna do array 
aEval( ::aIndexData , {| x,y| x[3] := y })

A função AEVAL() passa dois parâmetros para o Codeblock, o primeiro é o elemento do Array em processamento, e o segundo parâmetro é o número da posição deste elemento no Array. Deste modo, eu uso esta AEval() para atribuir o número da própria posição neste array.

O pulo do gato vêm agora. Embora eu possa estar usando um índice para navegar na tabela,  uma vez que faça um “Goto” em um determinado registro, eu preciso posicionar nele. E ao fazer isso, eu preciso pesquisar no array de índice qual é a posição atual do índice para o registro que eu posicionei. Afinal, uma vez posicionado em um registro pelo seu número, ao perguntar qual é o próximo registro desta ordem para o índice, como ele vai saber onde ele está ?!

::aRecnoData := Array(len(::aIndexData))
aEval( ::aIndexData , {|x,y| ::aRecnoData[y] := x })
aSort( ::aRecnoData ,,, { |x,y| x[2] < y[2] } )

Para isso, o array chamado de ::aRecnoData, foi criado. Ele serve para referenciar todos os elementos do primeiro array, porém ordenando os elementos pelo RECNO. Deste modo eu não duplico os dados na memória, apenas faço referência aos elementos do primeiro array, em outra ordenação.

Assim, quando eu faço um posicionamento direto na tabela usando Goto() — sem usar o índice — eu ligo um flag de re-sincronismo pendente, e quando eu voltar a navegar pelo índice, caso exista um re-sincronismo pendente, eu uso o array ordenado pelo RECNO para realizar uma busca binária, para localizar qual é a linha do array ordenado pela chave de índice (::aIndexData) que corresponde ao RECNO atualmente posicionado na tabela — e justamente esta informação está na terceira coluna dos elementos de ambos os arrays.  Ao recuperar a posição do índice atual, eu consigo ir para o anterior ou próximo elementos na ordem do índice.

Método interno _BuildIndexBlock

De modo similar — praticamente uma cópia — ao método de filtro de tabela, o método que cria o Codeblock para a geração do valor chave de ordenação a partir da expressão AdvPL informada, espera que qualquer campo da tabela referenciado no índice esteja em letras maiúsculas. Desse modo ele cria um Codeblock de ordenação que recebe o objeto da tabela em si, e retorna o valor da chave de ordenação baseado na expressão informada usando o conteúdo dos campos do registro atualmente posicionado.

METHOD _BuildIndexBlock(cIndexExpr) CLASS ZDBFMEMINDEX
Local aCampos := {}
Local cTemp
Local nI, nPos

// Cria lista de campos
aEval( ::oDBF:aStruct , {|x| aadd(aCampos , x[1]) } )

// Ordena pelos maiores campos primeiro
aSort( aCampos ,,, {|x,y| alltrim(len(x)) > alltrim(len(y)) } )

// Copia a expressao de índice
cTemp := cIndexExpr

// Troca os campos por o:Fieldget(nCpo)
// Exemplo : CAMPO1 + CAMPO2 será trocado para o:FieldGet(1) + o:FieldGet(2)

For nI := 1 to len(aCampos)
	cCampo := alltrim(aCampos[nI])
	nPos   := ::oDBF:Fieldpos(cCampo)
	cTemp  := StrTran( cTemp , cCampo,"o:FieldGet(" +cValToChar(nPos)+ ")")
Next

// Monta a string com o codeblock para indice
cTemp := "{|o| "+cTemp+"}"

// Monta efetivamente o codeblock de indice
::bIndexBlock := &(cTemp)

Return

Busca Binária – Métodos RecordSeek() e IndexSeek()

Uma busca binária somente pode ser realizada em cima de um array ordenado. A ideia e a implementação são simples: Você define um limite superior e inferior de busca, começando no primeiro e terminando no último elemento da lista, tira a média deste valor — calcula o “meio” entre estes dois pontos — e verifica se o valor procurado é igual, menor ou maior.

Se for igual, achou. Se for menor, redefine que o limite inferior para a busca é a posição do meio menos um , recalcula o meio novamente considerando os novos valores de topo e fim, e repete a busca. Caso o valor buscador for maior que o valor do meio da lista, redefine que o limite superior é o valor atual de meio mais um, recalcula o meio novamente considerando os novos valores de topo e fim, e repete a busca.

O desempenho desse tupo de busca é espantoso, pois a cada operação de comparação, metade da lista é desconsiderada. Deste modo, num pior cenário, em uma lista com 4 bilhões de registros, foram necessárias — no máximo — 32 comparações para encontrar um valor ou saber que ele não está lá. Vamos ver por exemplo o método para a busca no array ordenado pelo RECNO:

METHOD RecordSeek(nRecno) CLASS ZDBFMEMINDEX
Local lFound := .F. 
Local nTop := 1 
Local nBottom := Len(::aRecnoData)
Local nMiddle 

If nBottom > 0

	If nRecno < ::aRecnoData[nTop][2]
		// Chave de busca é menor que a primeira chave do indice
		Return 0
	Endif

	If nRecno > ::aRecnoData[nBottom][2]
		// Chave de busca é maior que a última chave
		Return 0
	Endif

	While nBottom >= nTop

		// Procura o meio dos dados ordenados
		nMiddle := Int( ( nTop + nBottom ) / 2 )

		If ::aIndexData[nMiddle][2] == nRecno
			// Achou 
			lFound := .T. 
			EXIT
		ElseIf nRecno < ::aRecnoData[nMiddle][2]
			// RECNO menor, desconsidera daqui pra baixo 
			nBottom := nMiddle-1
		ElseIf nRecno > ::aRecnoData[nMiddle][2]
			// RECNO maior, desconsidera daqui pra cima
			nTop := nMiddle+1
		Endif
	
	Enddo

	If lFound
		// Retorna a posição do array de dados 
		// ordenados (aIndexData) que contem este RECNO 
		Return ::aRecnoData[nMiddle][3]
	Endif
	
Endif

Return 0

O método IndexSeek() também usa o mesmo princípio, porém é feito em cima do array ordenado pela chave de índice (::aIndexData, coluna 1). Normalmente as buscas demoram menos de dois milésimos de segundo, em testes realizados com uma lista de 109 mil elementos. Também pudera, no máximo em 16 comparações este algoritmo localiza a informação. Se a informação procurada estiver fora do range de dados, em apenas uma ou duas comparações o algoritmo já determina que a informação com aquela chave não existe e retorna .F. para a operação de busca.

Observações

A classe de índice em memória não é usada diretamente, na verdade ela é usada internamente pelos métodos de criação de índice, ordenação e busca ordenada publicados na classe ZDBFTABLE. Através dela criamos, usamos e fechamos um ou mais índices.

Conclusão

Era uma prova de conceito de leitura, está virando quase um Driver completo. Já estou trabalhando na criação de tabelas e inclusão/alteração de dados, mas isso exige outros truques com os índices já criados, para permitir manutenção dos índices nestas operações. Assim que tiver mais novidades, sai mais um post.

Novamente agradeço a todos pela audiência, e lhes desejo TERABYTES de SUCESSO 😀