Data Juliana em AdvPL

Introdução

Neste final de semana um amigo me perguntou se tinha alguma função AdvPL que fosse capaz de converter uma data do nosso calendário (gregoriano) representada por dia, mês e ano, em uma data Juliana.

Problemática e Pesquisa

Na verdade nem eu sabia o que era uma “data Juliana”, até realizar uma pesquisa. Trocando em miúdos, uma data Juliana é representada em um número de dias decorridos a partir do “dia zero”, ou data de início do calendário. No caso, a “data zero” do calendário Juliano no calendário Gregoriano é  24/11/4714 ac. Logo, a data Juliana 2458588 no nosso calendário refere-se ao dia 14 de Abril de 2019, pois este foi o número de dias decorridos desde 24/11/4714 ac até 14/04/2019 (dc).

Nas pesquisas, encontrei e cheguei até a implementar uma função matemática que realizaria este cálculo, a partir de uma data qualquer, encontrar a data Juliana correspondente. Porém, estava um pouco difícil fazer o contrário — a partir de uma data Juliana, determinar sua representação no calendário gregoriano.

Solução

Por incrível que pareça, a solução para isso em AdvPL, ou qualquer outra linguagem que seja capaz de calcular um intervalo em dias a partir de uma data qualquer é muito simples. Se a linguagem permite calcular uma diferença entre datas, e permite somar ou subtrair um número de dias a partir de uma data qualquer, basta fazer o seguinte:

  1. Estipular uma “data zero” como base de cálculo. Por exemplo , 01/01/1980.
  2. Encontrar a data Juliana correspondente — no caso, 2444240.
  3. Para encontrar a data Juliana a partir de uma data qualquer, basta usar as funções de data disponíveis na sua linguagem, para determinar o número de dias decorridos desde 01/01/1980 até a data em questão, e somar 2444240.
  4. Para encontrar a data Gregoriana correspondente a uma data Juliana, basta subtrair 2444240 unidades da data Juliana para obter o número de dias decorridos desde 01/01/1980 — que pode ser um valor positivo, negativo ou mesmo zero — e depois partir da data 01/01/1980 e adicionar esta diferença, obtendo a data.

Em AdvPL

// ----------------------------------------
// Converte Data Juliana em Data AdvPL 
STATIC Function Date2DJ(dDate)
Return (dDate - ctod("01/01/1980")) + 2444240 

// ----------------------------------------
// Converte Data Juliana em Data AdvPL 
STATIC Function DJ2Date(nDJ)
Return ctod("01/01/1980") + ( nDJ - 2444240 )

Conclusão

Sim, pode ser feito, e de forma simples. Agradecimentos ao meu amigo Marcos Gomes, cuja dúvida me ajudou a conhecer mais um pouco sobre Calendários 😉

Referências

 

 

ZLIB Framework – Parte 01

Introdução

Vamos ver um pouco sobre Bibliotecas de Funções e Framework, com destaque para as funcionalidades em implementação no projeto ZLIB.

Bibliotecas e Frameworks

Com as funções básicas da linguagem, conseguimos criar qualquer programa. Alguns programas podem dar mais trabalho que outros, tudo depende de quantas funcionalidades serão implementadas. Porém, quando você precisa implementar muitas funcionalidades parecidas, é mais eficiente isolar o código comum em classes ou funções parametrizáveis, para não ter que escrever tudo de novo ou copiar-e-colar, replicando código desnecessariamente. Neste ponto, começa o nascimento de uma Biblioteca de funções.

Na ciência da computaçãobiblioteca é uma coleção de subprogramas utilizados no desenvolvimento de software. Bibliotecas contém código e dados auxiliares, que provém serviços a programas independentes, o que permite o compartilhamento e a alteração de código e dados de forma modular. Alguns executáveis são tanto programas independentes quanto bibliotecas, mas a maioria das bibliotecas não são executáveis.

Quando falamos em Framework, não apenas estamos usando funções genéricas de uma biblioteca, mas sim uma abstração de nível mais alto, que impõe um fluxo de controle na aplicação.

Um framework em desenvolvimento de software, é uma abstração que une códigos comuns entre vários projetos de software provendo uma funcionalidade genérica. Um framework pode atingir uma funcionalidade específica, por configuração, durante a programação de uma aplicação. Ao contrário das bibliotecas, é o framework quem dita o fluxo de controle da aplicação, chamado de Inversão de Controle.[1]

Projeto ZLIB

A ideia — necessidade — de uma LIB (Biblioteca) de componentes surgiu com os posts da série do CRUD em AdvPL, que acabou virando uma Agenda de Contatos, feita originalmente atrelada a interface do SmartClient, e depois implementada em uma interface WEB/HTTP.

Muito daquele código é comum a aplicações de mesma funcionalidade — cadastro simples. Inclusão, Alteração, Exclusão, Consulta ordenada, consulta por filtro. Outras funcionalidades, como exibição e cadastro de imagem, envio de email e mapa do endereço não necessariamente são usadas em todos os cadastros, mas podem ser colocadas em componentes de uma biblioteca para reaproveitamento.

A ideia da ZLIB é ser uma Biblioteca de Funções, que vai servir de base para construir um Framework. Ela já está versionada no GITHUB, mas ainda em desenvolvimento e com pouca (nenhuma) documentação, e como os componentes ainda estão nascendo, muitas alterações drásticas estão sendo feitas a cada atualização.

Orientação a Objetos e Abstração

Estas são duas chaves importantes no reaproveitamento de código e desenvolvimento modular. A orientação a objetos nos permite criar classes com uma finalidade (abstração) e implementar para múltiplos cenários ou recursos.

Por exemplo, as classes implementadas para acesso a arquivos DBF e arquivos em memória. Ambas possuem a mesma declaração de métodos para implementar as suas funcionalidades. Logo, o mesmo programa que insere um registro em uma tabela da classe ZDBFFILE pode realizar a mesma operação usando um objeto da ZMEMFILE.

Uma classe de geração de LOG de operação ou execução não precisa saber onde o log será gravado, ou mesmo conhecer a interface de gravação. Ela pode receber como parâmetro um objeto de uma classe de gravação de LOG. Ele pode ser de uma classe que grave os registros emitidos de log em um arquivo TXT, ou em um banco de dados, ou ainda seja um encapsulamento de uma interface “client” de log, que envia os dados gerados para serem gravados remotamente por um Log Server.

Criação de Componentes

Um dos primeiros mandamentos da criação de componentes é : A CRIAÇÃO DE QUALQUER COMPONENTE DEVE SER MOTIVADA PELA NECESSIDADE. Criar componentes adicionais ou agregar funcionalidades demais a um componente só por que vai ser “legal” só engorda código. Limite-se a uma funcionalidade por classe, e coloque nela o que realmente é comum a todos. Exceções são tratadas na implementação, a abstração é genérica.

Quando aos níveis de implementação — ou camadas — normalmente os componentes de alto nível são construídos para usar os de mais baixo nível. Na prática eles são construídos para usar todas as implementações feitas sobre uma abstração. Por exemplo, um componente de CRUD feito para usar a abstração ZISAMFILE pode usar qualquer implementação feita sobre ela, como a ZDBFFILE, ZTOPFILE, ZMEMFILE…

Como a implementação está por baixo da abstração, eu posso por exemplo criar uma abstração de exportação de arquivo, e implementar uma exportação para cada formato, a mesma coisa para importação.

Objetivo Final

Criar um conjunto de funções e funcionalidades que, permitam escrever programas, funções e rotinas, separando totalmente o processamento da interface, focando em SOA utilizando micro-serviços, filas e controladores, com foco em desempenho, escalabilidade, resiliência e alta disponibilidade.

Conclusão

Por hora, a primeira missão das funções em desenvolvimento é permitir a reescrita do programa de Agenda para SmartClient, usando componentes destacados, que permitam um elevado índice de reaproveitamento de código, e uma forma de declarar e executar as validações e procedimentos de cada operação que torne a codificação mais fácil e rápida, usando uma abordagem que permita aproveitar o CORE de cada componente em integrações encapsuladas por APIs (RPC Advpl, REST, SOAP) para serem consumidas por interfaces criadas em AdvPL ou qualquer outra linguagem ou plataforma.

Referências

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 😀