Written by

Senior Cloud Architect at InterSystems
MOD
Article Eduard Lebedyuk · 1 hr ago 4m read

Locks in InterSystems IRIS

Despite the fact that LOCK (docs) is a foundational part of InterSystems IRIS, responsible for concurrency, there is not a lot of discussion on the Developer Community about it. Which is understandable, considering it's stable and fairly low-level command. In this article, I will show a simple example of how to use locks with interoperability. In our example, we'll have a local table with reference data used by two distinct processes:

  • A Utility Function that reads from the table (used by various DTL/Rules in Production)
  • A dedicated Business Operation that updates the table

The problem here is that when Business Operation updates the table (in the worst case, doing a full rebuild), the custom function would be unable to get the data from the table, which would cause issues in DTL/Rule processing.

Locks would help us with that. Here's how: 

  • Utility function would take a shared lock before getting the data. Any number of processes can hold a shared lock, so there are no concurrency issues. Once the data is retrieved, we release the shared lock.
  • Updater Business Operation would take a shared lock first and then take out an exclusive lock. Once a process has an exclusive lock, IRIS guarantees that no other process can obtain a lock on the same resource. This way, the utility function won't be able to take a shared lock while an exclusive lock is held. Once our Business Operation finishes updating the table, it releases an exclusive lock, allowing utility functions to access the table.

    Let's do it!

Local table

A bit simplistic (and would probably be better as a LUT in a real project), but our goal here is to show how locks work, rather than build a complicated table:

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

Note that the table itself is not modified in any way.

Utility function

Here's our utility function:

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
}

}

Note that we acquire a shared lock on ^Lock.RefData("ref") before running SQL and release the lock only after getting a value into a local variable.

Updater Operation

Next, let's implement the Business Operation. It can run on schedule or manually. In our case it will accept Ens.StringContainer with an integer specifying how many seconds to simulate work.

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
}

}

Production

To show how it all works together, I also added a Router with a Business Rule which calls a custom function and a Business Service which calls the Router every second. Here's a sample of the Production Event Log:

At 14:26:39.780 updater gets a shared lock and immediately upgrades it to an exclusive lock. At 14:26:40.360 a shared lock is requested by a custom function, however it's granted only 14:26:44.785, immediately after the exclusive lock is released by the updater Business Operation.

Conclusion

Locks provide a low-level tooling to manage concurrency in a multiprocess system.

Links