Object Concurrency Options
It is important to specify concurrency appropriately when you open or delete persistent objects.
Why Specify Concurrency?
The following scenario demonstrates why it is important to control concurrency appropriately when you read or write objects. Consider the following scenario:
-
Process A opens an object without specifying the concurrency.
SAMPLES>set person = ##class(Sample.Person).%OpenId(5) SAMPLES>write person 1@Sample.Person
-
Process B opens the same object with the concurrency value of 4.
SAMPLES>set person = ##class(Sample.Person).%OpenId(5, 4) SAMPLES>write person 1@Sample.Person
-
Process A modifies a property of the object and attempts to save it using %Save() and receives an error status.
SAMPLES>do person.FavoriteColors.Insert("Green") SAMPLES>set status = person.%Save() SAMPLES>do $system.Status.DisplayError(status) ERROR #5803: Failed to acquire exclusive lock on instance of 'Sample.Person'
This is an example of concurrent operations without adequate concurrency control. For example, if process A could possibly save the object back to the disk, it must open the object with concurrency 4 to ensure it can save the object without conflict with other processes. (These values are discussed next.) In this case, Process B would then be denied access (failed with a concurrency violation) or would have to wait until Process A releases the object.
Options
You can specify concurrency at several different levels:
-
You can specify the concurrency argument for the method that you are using.
Many of the methods of the %PersistentOpens in a new tab class allow you to specify this argument, an integer. This argument determines how locks are used for concurrency control. There are multiple allowed concurrency values.
If you do not specify the concurrency argument in a call to the method, the method sets the argument to -1, which means that InterSystems IRIS uses the value of the DEFAULTCONCURRENCY class parameter for the class you are working with; see the next item.
-
You can specify the DEFAULTCONCURRENCY class parameter for the associated class. All persistent classes inherit this parameter from %PersistentOpens in a new tab, which defines the parameter as an expression that obtains the default concurrency for the process; see the next item.
You could override this parameter in your class and specify a hardcoded value or an expression that determines the concurrency via your own rules. In either case, the value of the parameter must be one of the allowed concurrency values.
-
You can set the default concurrency mode for a process. To do so, use the $system.OBJ.SetConcurrencyMode() method (which is the SetConcurrencyMode()Opens in a new tab method of the %SYSTEM.OBJOpens in a new tab class).
As in the other cases, you must use one of the allowed concurrency values. If you do not set the concurrency mode for a process explicitly, the default value is 1.
The $system.OBJ.SetConcurrencyMode() method has no effect on any classes that specify an explicit value for the DEFAULTCONCURRENCY class parameter.
To take a simple example, if you open a persistent object using its %OpenId() method without specifying a concurrency value (or you specify a value of -1 explicitly), the class does not specify the DEFAULTCONCURRENCY class parameter, and you do not set the concurrency mode in code, the default concurrency value for the object is set to 1.
Some system classes have the DEFAULTCONCURRENCY class parameter set to a value of 0. Sharded classes always use a concurrency value of 0. To check the concurrency value of an object, examine its %Concurrency property.
Concurrency Values
This section describes the possible concurrency values. First, note the following details:
-
Atomic writes are guaranteed when concurrency is greater than 0.
-
InterSystems IRIS acquires and releases locks during operations such as saving and deleting objects; the details depend upon the concurrency value, what constraints are present, lock escalation status, and the storage structure.
-
In all cases, when an object is removed from memory, any locks for it are removed.
The possible concurrency values are as follows; each value has a name, also shown in the list.
No locks are used.
Locks are acquired and released as needed to guarantee that an object read will be executed as an atomic operation.
InterSystems IRIS does not acquire any lock when creating a new object.
While opening an object, InterSystems IRIS acquires a shared lock for the object, if that is necessary to guarantee an atomic read. InterSystems IRIS releases the lock after completing the read operation.
The following table lists the locks that are present in each scenario:
When object is created | While object is being opened | After object has been opened | After save operation is complete | |
---|---|---|---|---|
New object | no lock | N/A | N/A | no lock |
Existing object | N/A | shared lock, if that is necessary to guarantee an atomic read | no lock | no lock |
The same as 1 (atomic read) except that opening an object always acquires a shared lock (even if the lock is not needed to guarantee an atomic read). The following table lists the locks that are present in each scenario:
When object is created | While object is being opened | After object has been opened | After save operation is complete | |
---|---|---|---|---|
New object | no lock | N/A | N/A | no lock |
Existing object | N/A | shared lock | no lock | no lock |
InterSystems IRIS does not acquire any lock when creating a new object.
While opening an existing object, InterSystems IRIS acquires a shared lock for the object.
After saving a new object, InterSystems IRIS has a shared lock for the object.
The following table lists the scenarios:
When object is created | While object is being opened | After object has been opened | After save operation is complete | |
---|---|---|---|---|
New object | no lock | N/A | N/A | shared lock |
Existing object | N/A | shared lock | shared lock | shared lock |
When an existing object is opened or when a new object is first saved, InterSystems IRIS acquires an exclusive lock.
The following table lists the scenarios:
When object is created | While object is being opened | After object has been opened | After save operation is complete | |
---|---|---|---|---|
New object | no lock | N/A | N/A | exclusive lock |
Existing object | N/A | exclusive lock | exclusive lock | exclusive lock |
If you do not specify a concurrency value explicitly, the value is set to -1, meaning that the default concurrency value is used. See Object Concurrency Options for information on how the default concurrency value is determined.
Concurrency and Swizzled Objects
An object referenced by a property is swizzled on access using the default concurrency defined by the swizzled object’s class. If the default is not defined for the class, the object is swizzled using the default concurrency mode of the process. (See Object Concurrency Options for details.) The swizzled object does not use the concurrency value of the object that swizzles it.
If the object being swizzled is already in memory, then swizzling does not actually open the object — it simply references the existing in-memory object; in that case, the current state of the object is maintained and the concurrency is unchanged.
There are two ways to override this default behavior:
-
Upgrade the concurrency on the swizzled object with a call to the %OpenId() method that specifies the new concurrency. For example:
Do person.Spouse.%OpenId(person.Spouse.%Id(),4,.status)
where the first argument to %OpenId()Opens in a new tab specifies the ID, the second specifies the new concurrency, and the third (passed by reference) receives the status of the method.
-
Set the default concurrency mode for the process before swizzling the object. For example:
Set olddefault = $system.OBJ.SetConcurrencyMode(4)
This method takes the new concurrency mode as its argument and returns the previous concurrency mode.
When you no longer need a different concurrency mode, reset the default concurrency mode as follows:
Do $system.OBJ.SetConcurrencyMode(olddefault)
Version Checking (Alternative to Concurrency Argument)
Rather than specifying the concurrency argument when you open or delete an object, you can implement version checking. To do so, you specify a class parameter called VERSIONPROPERTY. All persistent classes have this parameter. When defining a persistent class, the procedure for enabling version checking is:
-
Create a property of type %IntegerOpens in a new tab that holds the updateable version number for each instance of the class.
-
For that property, set the value of the InitialExpression keyword to 0.
-
For the class, set the value of the VERSIONPROPERTY class parameter equal to the name of that property. The value of VERSIONPROPERTY cannot be changed to a different property by a subclass.
This incorporates version checking into updates to instances of the class.
When version checking is implemented, the property specified by VERSIONPROPERTY is automatically incremented each time an instance of the class is updated (either by objects or SQL). Prior to incrementing the property, InterSystems IRIS compares its in-memory value to its stored value. If they are different, then a concurrency conflict is indicated and an error is returned; if they are the same, then the property is incremented and saved.
You can use this set of features to implement optimistic concurrency.
To implement a concurrency check in an SQL update statement for a class where VERSIONPROPERTY refers to a property called InstanceVersion, the code would be something like:
SELECT InstanceVersion,Name,SpecialRelevantField,%ID
FROM my_table
WHERE %ID = :myid
// Application performs operations on the selected row
UPDATE my_table
SET SpecialRelevantField = :newMoreSpecialValue
WHERE %ID = :myid AND InstanceVersion = :myversion
where myversion is the value of the version property selected with the original data.