Artigo
· Set. 10 8min de leitura

Um guia para iniciantes para dados Órfãos - como limpamos mais 200+Gb com confiança

Propósito deste artigo

Tem dois ótimos artigos WRC de melhores práticas Mensagens Órfãs do Ensemble | Comunidade de Desenvolvedores InterSystems | Melhores
e a postagem sobre a ajuda de eliminação DeleteHelper - Uma classe para ajudar a deletar Classes Persistentes Referenciadas que tratam de registros órfãos e como lidar com eles. Esse artigo não tem a intenção de substituir esses escritos por profissionais da InterSystems, mas construir em cima deles e como podemos usar ambos com confiança e ajudar a manter nossa base de dados num tamanho mais compacto usando essa informação e outras discussões, incluindo nossos métodos de limpar esses dados.

O Cenário

Nossos backups estão ficando cada vez maiores. Tivemos um caso de um servidor forçado a falhar no começo do ano e foi necessário restaurar. Ter uma base de dados mais larga e até copiar essa base de dados leva um longo tempo além de restaurar e reconstruir o servidor de sombra. Então decidimos atacar esse crescimento. Já havia sido identificada uma causa inicial.

  1. A tarefa criada a parte assumidamente rodava, mas não tinha os corpos de mensagens marcados. Isso é porque ao fazer a consulta em um corpo de mensagem, conseguimos o ID um de mais de dez anos atrás. Essa task era a default Ens.Util.Tasks.Purge como referenciado nas melhores práticas. Isso nos leva a dica 1 no processo.

Entenda seus dados

Que dados você está guardando na sua base? Você tem dados que ficam em tabelas de registros? Por seus dados transacionais, eu quero dizer as mensagens ligadas às headers que você pode consultar usando

SELECT Distinct(MessageBodyClassName) from Ens.MessageHeader

Disso, inicialmente eu olharia nas classes de mensagens e as abriria para entender o que essas globais estão guardando.

Isso te dá algum nível de entendimento de onde seus dados são guardados.

Nota: se você está salvando diretamente em streams como %LIBRARY.GLOBALBINARYSTREAM, essa é uma indicação de órfãos, já uqe essas deveriam ser salvadas em containers de streams. Vamos cobrir isso posteriormente.
Rodando o relatório Global size
Uma ferramenta para ver os tamanhos das suas bases de dados num relance é rodar GSize. Isso deve te dar alguma indicação de onde os dados estão sendo guardados na sua base de dados.. No terminal, rode os passos a seguir

do ^%GSIZE
Directory name: NAMESAPCE/ =>
All Globals? No => YES
Show details => NO

Isso pode ser uma indicação de onde os dados estão sendo usados para te ajudar.

Passo 2 - impedindo futuros órfãos 

Isso está coberto muito bem no guia de melhores práticas, mas para quebrar em passos, nós também cobrimos.

  1. Ache classes diretamente enviando Cache streams e migrando o código para usar um container Stream.
  2. Ache classes embutidas que não são manejadas - adicionando exclusão manual e na ferramenta delete helper
  3. Veja alguns %OnSaves para mensagens hl7
Passo 3 - limpando órfãos
  1. Rodar uma tarefa de limpeza diariamente
  2. Limpar streams e outros órfãos de miscelânea

 

Classes usando classes diretas de stream

Foi notado por Suriya Narayanan que qualquer classe %library não deve ser usada diretamente. Streams terminam em ^Ens.Streams se não estiverem contidas.  Esses dados não são guardados com boas referências, então você deve procurar pela global para descobrir qual é o último dado que deseja guardar.

Em nosso cenário, foi um BP enviando a mensagem para uma operation. Ao invés de mandar sua stream, adiciona em um container.

 

set HTMLDocument=##class(%Library.GlobalBinaryStream).%New()
set tSC=..SendRequestSync(..DocmanRouterName,HTMLDocument, .aResponse,20,"")
//needed sent instead as container
set requestContainer = ##class(Ens.StreamContainer).%New()
set tSc=requestContainer.StreamSet(HTMLDocument)
set tSC=..SendRequestSync(..DocmanRouterName,requestContainer, .aResponse,20,"")
%Saves

Nem todos os %Saves de classes antes de enviar causam esse problema, somente se não forem enviados. Isso pode acontecer juntando uma cópia de um hl7 frequentemente salvo e talvez salvando um item provisório que não é enviado. Abaixo é um exemplo de stream após manipulação por hl7 que não foi salvo, então criou órfãos.  Após algumas limpezas nos órfãos, você pode monitorar e ver que estarão mais limpos. O objeto %Save abaixo nunca foi usado, então seria um órfão já que somente o hl7 foi enviado. Tem um objeto %New anterior a isso

 Objetos embutidos

Isso é bastante referenciado no documento do ajudante de exclusão. Abaixo, um exemplo. Mensagens XML são notórias por isso.

Class Messages.XML.GenericWif.fileParameters Extends (%Persistent, %XML.Adaptor)
{
Property revisionNumber As %String;
Property primaryLink As Messages.XML.GenericWif.primaryLink;
Property additionalIndexes As Messages.XML.GenericWif.additionalIndexes;

Quando esse objeto é definido e limpado, somente Messages.XML.GenericWif.fileParameters é deletado. Você pode ter duas abordagens

1) Você vai em cada objeto e adiciona o método de classe OnDelete. O SQL chama ele, que checa por "subobjetos" existentes e os deleta. Você pode verificar usando object script ou sql.

ClassMethod %OnDelete(oid As %ObjectIdentity) As %Status [ Private ]
{
      // Delete the property object references.
      Set tSC = $$$OK, tThis = ##class(Messages.XML.GenericWif.fileParameters).%Open(oid)
      If $ISOBJECT(tThis.primaryLink) Set tSC = ##class(Messages.XML.GenericWif.primaryLink).%DeleteId(tThis.primaryLink.%Id())
      If $ISOBJECT(tThis.additionalIndexes) Set tSC = ##class(Messages.XML.GenericWif.additionalIndexes).%DeleteId(tThis.additionalIndexes.%Id())
      Quit tSC
}

/// Callback/Trigger for SQL delete
Trigger OnDelete [ Event = DELETE ]
{
      // Delete the property object references. {%%ID} holds the id of the record being deleted.
      Set tID={%%ID}
      Set tThis = ##class(Messages.XML.GenericWif.fileParameters).%OpenId(tID)
      If $ISOBJECT(tThis.primaryLink) Do ##class(Messages.XML.GenericWif.primaryLink).%DeleteId(tThis.primaryLink.%Id())
      If $ISOBJECT(tThis.additionalIndexes) Do ##class(Messages.XML.GenericWif.additionalIndexes).%DeleteId(tThis.additionalIndexes.%Id())
      Quit
}

Com ObjectScript, você abre um dos seus ids. Nós copiamos me um sistema de produção ou não-produtivo ao vivo para consultar por tipo de instância um registro mais velho e então consultar as outras tabelas nesse exemplo Messages_XML_GenericWif.primaryLink em SQL. Então você pode ver se consegue abrir os subids dele, i.e., você adicionou o código que deleta o id 1 de fileParameters que tem mensagens embutidas.

set a = ##class(Messages.XML.GenericWif.primaryLink).%OpenId(2)
zw a 
//this output the info. Now delete parent
set tSC=##class(Messages.XML.GenericWif.fileParameters).%DeleteId(1)
set a = ##class(Messages.XML.GenericWif.primaryLink).%OpenId(2)
zw a 
//a should be ""

A opção 2 é o deleteHelper. O que ele faz é adicionar o Classmethod %onDelete e não o SQL no .int do código. Você pode fazer o mesmo teste acima basicamente, é útil. Tudo o que você precisa é adicionar a deleteSuper classe no extends da sua classe de mensagens, i.e.

Class Messages.BoltonRenal.Pathology.Outbound.PathologyResult Extends (Ens.Request, SRFT.Utility.DeleteHelper.OnDeleteSuper)
{
Property requestingClinician As %String;
Property department As Messages.BoltonRenal.Pathology.Outbound.Department;
// the on deletesuper is a class like this in the link
include Ensemble
/// A class to help assist in "deep" deleting of an instance, including references to other persistent classes.
/// <br><br>
/// To use simply add as a Super Class in your persistent class<br>
/// The class defines a Generator %OnDelete method that will generate code for your class, 
/// deleting, if needed, references (inclduing collections) to other persistent classes<br>
ClassMethod %OnDelete(oid As %ObjectIdentity) As %Status [ CodeMode = objectgenerator, Private, ServerOnly = 1 ]
{

	// a list ($ListBuild format) of "simple" (non-collection) property names we'll want to delete the references of (because they're Persistent)
	Set delPropNames = ""
                           

Se você for em rotinas, a mensagem e o .int, você deve começar a ver objetos embutidos sendo deletados, i.e.

 

Limpando mensagens

Depois de tudo isso nós escrevemos nossa própria classe para excluir mensagens além de 150 dias. A ideia é a seguinte

  • Defina seus tipos de mensagens customizadas para deletar em uma lookup table de dados (uma melhoria seria guardar isso numa tabela para deixar o código mais limpo)
  • Manualmente defina o corpo de Ens.Message mais velho para deletar. O resto pode ser descoberto pelo SQL.
  • Dados globais criados para guardar resultados.
  • Manualmente limpe streams e outros remanescentes.

Novamente, delete sob seu risco

A ideia é que você tenha a lista de tipos de corpo de mensagem (sua base é provavelmente HL7 e detalhes de Message body)

O código faz um loop nessa tabela e pega o ID menor da header (para o corpo de mensagem você precisa procurar a olho sua mensagem customizada mais velha e salvar em Ens.MessageBody) e deletar.

if msgBodyName="Ens.MessageBody"{
            set tMinMsgId=..MinBodyID
}else
{
    set tMinMsgId=..GetMinimumIDForMessage(rs.MessageBodyClassName)
}

// Min ID is just basically this query   set minIDQuery="SELECT  TOP(1) MessageBodyID  FROM Ens.MessageHeader where MessageBodyClassName=?"
And deletes it. Has added code around it to log 
  SET tSC1=$CLASSMETHOD(className,"%DeleteId",tResult.ID)
 

Você pode testar quantos de cada tipo deletar. Escolhemos 500.000 para quando rodasse HL7 e corpos de mensagem e 7.200.000 por rodada ao usar só corpos de mensagem para rodar após o processo normal de limpeza

Novamente, teste isso em uma cópia da sua basse de dados ao vivo primeiro (não produtivo ou sistema de desenvolvimento)

Você não vai querer deletar de mais para fazer seus journals grandes demais, então é um processo de tentativa e erro

 

classe usada e anexada - Eu não me responsabilizo se for usado e perder dados necessários, teste antes de usar

Limpando Streams

Nós temo suma pequena tarefa que pode ser usada para matar streams de globais. Atualize os números e rode. Deixe bem claro qual é a stream mais recente que você deve manter, e só funciona se as streams que você deixar forem sequenciais.

ClassMethod StreamPurge() As %Status
{
	set i=1
	while (i<200000){
		k ^CacheStream(i)
		s i=i+1
	}
	q $$$OK
}
Discussão (0)1
Entre ou crie uma conta para continuar