CRUD em AdvPL – Parte 04

Introdução

No post anterior, vimos alguns detalhes e possibilidades de melhoria no código do programa Agenda. Agora, vamos implementar algumas delas, e avaliar as implementações realizadas, e ver outras possibilidades.

Validação no GET

Existem várias formas de consistir ou validar se as informações necessárias para inserir ou alterar um registro da agenda estão sendo fornecidas, e dependendo da informação, podemos inclusive verificar se elas estão corretas.

Uma delas é realizar a consistência sob demanda, para cada campo informado no formulário. Conseguimos fazer isso através da cláusula “VALID” no comando @ ..  GET

Por exemlo, vamos alterar o GET do campo “UF” para o fonte abaixo:

@ 95,60 GET oGet7 VAR cUF PICTURE "!!" SIZE CALCSIZEGET(2) ,12 VALID VldUf(cUF) OF oPanelCrud PIXEL

Agora, vamos inserir no final do fonte a função de validação que acabamos de inserir a chamada para verificar o valor informado no GET:

// Release 1.1 
// Exemplo de validação do campo cUF ( Estado ou Unidade da Federação )

STATIC Function VldUf(cUF)
If Empty(cUF)
  // Estado nao informado / vazio / em branco 
  Return .T.
Endif
If cUF $ "AC,AL,AP,AM,BA,CE,DF,ES,GO,MA,MT,MS,MG,PA,PB,PR,PE,PI,RJ,RN,RS,RO,RR,SC,SP,SE,TO"
  // Estado digitado está na lista ? Beleza
  Return .T.
Endif

// Chegou até aqui ? O estado informado não é válido. 
// Mostra a mensagem e retorna .F., nao permitindo 
// que o foco seja removido do campo 
MsgSTop("Unidade da Federação inválida ou esconhecida : ["+cUF+"] - "+;
       "Informe um valor válido ou deixe este campo em branco.",;
       "Erro na Validação")

Return .F.

Quando você abrir o formulário de dados para incluir ou alterar um registro, uma vez que você dê foco no campo UF, somente será possível remover o foco do campo e executar qualquer outra ação neste formulário quando, ou o campo UF estiver vazio, ou preenchido com a sigla correta de um dos 27 estados do Brasil, contando com o Distrito Federal.

Criação de Novo ID na Inclusão

Como eu havia comentado no post anterior, se dois usuários abem ao mesmo tempo a tela de inclusão de novo registro na Agenda, ambas iriam pegar o último número da base + 1, e mostrá-lo na tela, sem permitir edição, e este valor fatalmente ficaria duplicado na base de dados se ambos os usuários confirmassem a inclusão.

Uma das formas mais “primitivas” de se evitar isso é criar uma função para retornar um novo ID para ser utilizado, e apenas fazer isso no momento efetivo de gravar um novo registro na agenda. Vamos ao fonte:

STATIC Function GetNewID()
Local cNewId
DBSelectArea("AGENDA")
DbsetOrder(1)
DBGobottom()
cNewId := StrZero( val(AGENDA->ID) + 1 , 6 )
Return cNewId

No exemplo acima, ainda temos um problema de concorrência: Se duas instâncias do programa confirmam uma inclusão de registro na agenda no mesmo instante, pode acontecer de ambos os processos pegarem o mesmo ID.

Para resolvermos esta questão, sem usar outro mecanismo para gerar o novo ID, a forma mais simples disso ser feito é utilizando um recurso de seção crítica ou MUTEX (Exclusão Mútua), para garantir  que nunca mais de um processo tempo realize as etapas de obter um novo número e incluir um novo registro ao mesmo tempo.

Assim, caso dois programas executassem a etapa de confirmação da Inclusão re registro, apenas um inciaria o processo, pegando o último número da base, somando uma unidade, utilizando este número na inclusão do novo registro, e somente quando a inclusão estivesse completa, o processo que ficou aguardando a finalização do primeiro pegaria o último número atualizado, para gerar um novo identificador.

O problema óbvio desse tipo de implementação é justamente o fato de você não mais permitir que duas inclusões ocorram em paralelo, apenas uma inclusão por vez pode ser feita. Quando falamos de uma agenda de contatos pessoal, onde praticamente não há concorrência, a geração de dois IDs iguais pode simplesmente não ocorrer, ou acontecer muito esporadicamente. Agora, quando falamos de sistemas transacionais com grandes volumes de dados, precisamos tomar cuidado quanto optamos por tornar um acesso a uma operação sequencial. Sem possibilidade de paralelismo, fatalmente ele vai atingir um limite de operações por segundo usando um processo único.

MUTEX em AdvPL

Existem algumas formas de implementar ou emular um semáforo ou um MUTEX em AdvPL. Algumas considerações básicas sobre cada implementação é o quando “custa” de recursos para a implementação, e qual é o escopo ou abrangência dela.

Semáforo em Disco

Uma forma simples de implementar um semáforo de escopo global, que pode ser visto e compartilhado entre vários servidores de aplicação em um ambiente, é usar as funções de baixo nível de arquivos do AdvPL (FCreate, FOpen, FClose), e criar um arquivo no disco em uma pasta a partir do ROOTPATH do ambiente.

Ao criar um arquivo com a função FCreate(), mesmo que nada seja escrito nele, em caso de sucesso na operação, o arquivo criado permanece aberto em modo exclusivo pelo processo que o criou, podendo ser fechado explicitamente pela função FClose() ou implicitamente no final do processo — inclusive caso o processo seja finalizado de forma elegante (fim da rotina) ou em caso de erro.  Vamos a um exemplo:

User Function Mutex1()
Local nHnd 

nHnd := fCreate('\semaforo\mutex1.lck')

If nHnd >= 0 
   // ------------------------------
   // Arquivo criado com sucesso e aberto em modo exclusivo. 
   // Mesmo que o arquivo já exista no disco, se ele não estiver
   // em uso ou aberto por nenhuma rotina, a função FCreate() 
   // consegue recriar o arquivo. 
   // Desta forma, o que você rodar dentro desse bloco de fonte
   // não será executado por mais de um usuário ao mesmo tempo 

   MsgInfo("Bloqueio obtido. Continue o programa para soltar o bloqueio")

   // Aqui deve rodar o código que não pode rodar 
   // ao mesmo tempo por mais de um usuário 

   FClose(nHnd)
   MsgInfo("Bloqueio liberado.")

Else

   MsgStop("Nao é possível criar/abrir o arquivo de bloqueio.")

Endif

Return

Comportamento esperado

Ao subir um SmartClient rodando o programa U_MUTEX1, ele deve conseguir fazer o bloqueio. Ao aparecer a janela com a mensagem  “Arquivo Bloqueado”, inicie uma segunda instância do SmartClient executando o mesmo programa. Você deve receber a mensagem “Não é possível criar/abrir o arquivo de bloqueio.” Somente será possível um outro SmartClient obter o bloqueio após o primeiro programa ter fechado o arquivo com FClose() ou ter terminado.

Detalhes da implementação

O diretório usado como “Raiz” ou RootPath do ambiente deve ser visível e o mesmo para um determinado ambiente ou Environment em execução no Protheus Server. Inclusive, quando usados mais de um serviço de Protheus Server, o RootPath do ambiente deve ser compartilhado com os demais Slaves do Protheus usando o sistema de compartilhamento de arquivos do sistema operacional em uso. Logo, o escopo dessa implementação global entre ambientes que acessam o mesmo RootPath. 

O peso dessa implementação é um ponto importante. O Excesso de acesso ao sistema de arquivos em disco do servidor pode gerar fila de acesso a disco, caso este recurso seja usado muitas vezes por segundo, para realizar bloqueios curtos de operações concorrentes. Recomenda-se evitar o uso deste tipo de semáforo.

Outro ponto é que este semáforo é do tipo “espera ocupada”, isto é, se você não conseguiu criar o arquivo, pois outro processo está acessando o mesmo em modo exclusivo, você vai ter que repetir a operação quantas vezes for necessário até que você tenha o bloqueio. Isso aumenta mais ainda o custo da implementação, pois o seu processo poderia estar fazendo outra coisa ao invés de tentar até conseguir o bloqueio do arquivo.

Lock Virtual nomeado no DBAccess

Caso sua aplicação use o DBAccess, existe uma forma de criar um bloqueio nomeado virtual, usando as funções TCVLock() e TCVunlok(). O bloqueio é compartilhado entre todas as conexões com o DBAccess feitas para o mesmo ambiente / DSN. Da mesma forma que usamos o sistema de arquivos, podemos usar um lock nomeado no DBAccess.

User Function Mutex2()
Local nHTop

nHTop := tcLink()
IF nHTop < 0 
   MsgStop("Falha de conexão com DBAccess -- Erro "+cValToChar(nHTop))
   return
Endif

If TCVLock('MUTEX2')
  // ------------------------------
  // Bloqueio nomeado criado no DBaccess em memória

  MsgInfo("Bloqueio obtido. Continue o programa para soltar o bloqueio")

  // Aqui deve rodar o código que não pode rodar 
  // ao mesmo tempo por mais de um usuário 

  TCVUnlock('MUTEX2')

  MsgInfo("Bloqueio liberado.")

Else

  MsgStop("Nao foi possível adquirir o bloqueio.")

Endif

TCUnlink(nHTop)
Return

Comportamento esperado

De forma similar ao outro bloqueio, ambos são modelos de espera ocupada ou bloqueio ativo. Não há tempo de espera para obter o bloqueio, caso o identificador nomeado de bloqueio esteja em uso por outro processo, a função TCVLock() retorna .F. imediatamente.

Este bloqueio é um pouco mais leve do que o bloqueio em Disco, pois usa a conexão de rede entre o Protheus Server e o DBAccess. Essa solução é mais interessante do que usar o bloqueio com o arquivo em disco, e atende com louvor a necessidade da Agenda.

Bloqueio usando Framework do ERP

O Framework do ERP Microsiga possui uma função de lock nomeado genérica para ser usada para bloqueios de escopo global, onde parametrizamos inclusive se o escopo do bloqueio deve ser restrito a empresa e filial do usuário atual do sistema. As funções se chamam LockByName() e UnlockByName(), estão documentadas na TDN, e internamente elas utilizam funções internas do servidor de licenças do ERP Microsiga para gerenciar a lista de bloqueios nomeados na memória do license server. Porém, este tipo de bloqueio exige que a aplicação AdvPL em uso esteja utilizando as funções do Framework do ERP Microsiga relacionadas a criação de um contexto de execução do ERP, como a função RpcSetEnv() ou o comando PREPARE ENVIRONMENT, ou que você execute a sua aplicação a parir do Menu do ERP — onde o processo já têm um contexto de execução preparado para você usar as funções do Framework.

Outros MUTEX em AdvPL

Existe um bloqueio global, em memória, com escopo apenas do servidor de aplicação, usando as funções AdvPL GlbLock() e GlbUnlock() — porém este bloqueio não é nomeado. Desse modo, ele somente pode ser usado por uma rotina. Após 2016 foram criadas novas funções de Bloqueio com o mesmo escopo, porém são bloqueios nomeados, usando as funções GlbNmLock() e GlbNmUnlock(). Estas funções estão documentadas na TDN, em uma seção dedicada a funções de sincronismo, vide link nas referências no final do post.

Conclusão

Um processo de melhoria contínua de um sistema normalmente é baseado no crescimento deste sistema, e das adequações que devem ser feitas nos programas para eles suportarem este crescimento. Como a melhor solução é aquela que atende a sua necessidade, com o crescimento do sistema, as necessidades podem mudar, ou surgirem novas. Quanto mais conhecimento da linguagem e da plataforma você tiver, mais fácil será avaliar entre as possibilidades de implementação, qual delas que melhor lhe atende.

No próximo post desta sequência, vamos ver como usar os índices criados no programa Agenda para realizar buscas de dados usando o índice, com exemplos e as respectivas explicações 🙂

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

Referências

 

 

 

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s