Escrito por

Artigo Evandro Wendt · 4 hr atrás 4m read

Locks no InterSystems IRIS

Apesar de o comando LOCK (docs) ser uma parte fundamental do InterSystems IRIS, responsável pela concorrência, não há muita discussão sobre ele na Developer Community. O que é compreensível, considerando que é um comando estável e de nível relativamente baixo. Neste artigo, vou mostrar um exemplo simples de como usar locks com interoperabilidade. No nosso exemplo, teremos uma tabela local com dados de referência utilizada por dois processos distintos:

  • Uma função utilitária que lê da tabela (usada por vários DTL/Regras em Produção)
  • Uma Business Operation dedicada que atualiza a tabela

O problema aqui é que, quando a Business Operation atualiza a tabela (no pior caso, fazendo uma reconstrução completa), a função personalizada não conseguiria obter dados da tabela, o que causaria problemas no processamento de DTL/Regras.

Os locks nos ajudam com isso. Veja como:

  • A função utilitária obtém um lock compartilhado antes de acessar os dados. Qualquer número de processos pode manter um lock compartilhado, então não há problemas de concorrência. Após recuperar os dados, o lock compartilhado é liberado.
  • O Business Operation de atualização primeiro obtém um lock compartilhado e depois um lock exclusivo. Quando um processo possui um lock exclusivo, o IRIS garante que nenhum outro processo possa obter um lock sobre o mesmo recurso. Dessa forma, a função utilitária não poderá obter um lock compartilhado enquanto o lock exclusivo estiver ativo. Quando o business operation termina de atualizar a tabela, ela libera o lock exclusivo, permitindo que as funções utilitárias voltem a acessar a tabela.

Vamos lá!

Tabela local

Um pouco simplista (e provavelmente seria melhor como uma LUT em um projeto real), mas nosso objetivo aqui é demonstrar como os locks funcionam, e não construir uma tabela complexa:

Class Lock.RefData Extends %Persistent
{
Property Value;
}

Observe que a própria tabela não é modificada de forma alguma.

Função utilitária

Aqui está nossa função utilitária:

Class Lock.CF Extends Ens.Rule.FunctionSet
{

ClassMethod GetRefData() As %String
{
 
	$$$TRACE("Request Shared lock")
	LOCK +^Lock.RefData("ref")#"S"
	$$$TRACE("Gained Shared lock")
	Try {
		Set rs = ##class(%SQL.Statement).%ExecDirect(,"SELECT Value FROM Lock.RefData")
	} Catch ex {
		LOCK -^Lock.RefData("ref")#"S"
		Return "EX - NO DATA"
	}
	
	if 'rs.%Next()
	{
		$$$TRACE("NO DATA, Releasing lock")
		LOCK -^Lock.RefData("ref")#"S"
		Quit "SQL - NO DATA"
	}
	Set val = rs.Value
	$$$TRACE("Val: " _ val)
	$$$TRACE("Releasing lock")
	LOCK -^Lock.RefData("ref")#"S"
	Quit val
}

}

Observe que adquirimos um lock compartilhado em ^Lock.RefData("ref") antes de executar o SQL e liberamos o lock somente após obter um valor em uma variável local.

Operação de Atualização

Em seguida, vamos implementar a Business Operation. Ela pode ser executada de forma agendada ou manualmente. No nosso caso, ela receberá um Ens.StringContainer com um inteiro indicando quantos segundos simular o trabalho.

Class Lock.BO Extends Ens.BusinessOperation
{

Method OnMessage(pRequest As Ens.StringContainer, Output pResponse As Ens.Response) As %Status
{
    Set sc = $$$OK
	Try {
		$$$TRACE("Request Shared lock")
		LOCK +^Lock.RefData("ref")#"S"
		$$$TRACE("Gained Shared lock")
		Try {
			$$$TRACE("Request Exclusive lock")
			LOCK +^Lock.RefData("ref")
			$$$TRACE("Gained Exclusive lock.")
			Try {
				$$$TRACE("Simulate the purge work")
				&sql(DELETE FROM Lock.RefData)
                
                Hang +pRequest.StringValue
                
				Set obj = ##class(Lock.RefData).%New()
				Set obj.Value  = $zdt($h, 3, 1, 3)
				Do obj.%Save()             
                
                $$$TRACE("Release exclusive lock on succesfull completion")
                LOCK -^Lock.RefData("ref")
			} Catch ex {
				$$$TRACE("Release exclusive lock on error")
                LOCK -^Lock.RefData("ref")
				THROW ex // re-throw the exception after releasing lock
            }
            $$$TRACE("Release shared lock on successfull completion")
            LOCK -^Lock.RefData("ref")#"S"
		} Catch ex {
			$$$TRACE("Release shared lock on error")
			LOCK -^Lock.RefData("ref")#"S"
			THROW ex // re-throw the exception after releasing lock
		}
	} Catch ex {
		Set sc = ex.AsStatus()
	}
	Set pResponse = ##class(Ens.Response).%New()
	$$$TRACE("Done.")
	Quit sc
}

}

Produção

Para mostrar como tudo funciona junto, também adicionei um Router com uma business rule que chama uma função personalizada e um Business Service que chama o Router a cada segundo. Aqui está um exemplo do log de eventos da produção:

Às 14:26:39.780, o updater obtém um lock compartilhado e o atualiza imediatamente para um lock exclusivo. Às 14:26:40.360, um lock compartilhado é solicitado por uma função personalizada, mas só é concedido às 14:26:44.785, imediatamente após o lock exclusivo ser liberado pelo Business Operation de atualização.

Conclusão

Os locks fornecem uma ferramenta de baixo nível para gerenciar concorrência em um sistema multiprocessos.

Links