Warning: Stale Persistent Objects created by $THIS

tl;dr I have discovered that using $THIS in a very specific way will make persistent objects go stale.

I found that it only happens when using $get on an array that contains the OID reference of $THIS. I assume that this is an unwanted feature and have raised a WRC. The problem can be recreated following the steps below, I have also posted the workaround that I am currently testing and looks to be working fine.

1. Create a persistent class...

Class Test.Store Extends %Persistent
{

Property SomeValue As %String;

Method SomeMethod()
{
    //this will cause $THIS to go stale in this process
    if $get(seen($THIS)) quit
}

ClassMethod Initialise() As %Status
{
    set store=..%New()
    set store.SomeValue=$zh
    quit store.%Save()
}

ClassMethod Update() As %Status
{
    set store=..%OpenId(1)
    set store.SomeValue=$zh
    quit store.%Save()
}

}

2. Open up a terminal and... 

do ##class(Test.Store).Initialise()

3. Open a second terminal and...

set obj=##class(Test.Store).%OpenId(1)
zw obj
do obj.SomeMethod()
kill obj


Note, it's when calling SomeMethod() that this object will become stale.

4. In the first terminal...

do ##class(Test.Store).Update()
set obj=##class(Test.Store).%OpenId(1)
zw obj

5. In the second terminal...  

set obj=##class(Test.Store).%OpenId(1)
zw obj

You will see that the second terminal still has a stale version of the persistent object.

These stale objects are also apparent in CSP sessions.

Workaround

Thanks to Timothy for a simpler workaround...

if $get(seen(""_$THIS))

Appending an empty string prevents the side effect.

Instead of using the OID value of $THIS directly, create a separate variable and create a stringy value of the OID...

set oid=$lg($THIS."%%OID",1)_"@"_$lg($THIS."%%OID",2)

Using the oid variable instead of $THIS makes the problem go away.

Sean.

Comments

What Caché version are you seeing this on Sean?

A simpler workaround seems to be:

if $get(seen(""_$THIS)) quit

And here's another way to demonstrate that something weird is going on under the hood:

SAMPLES>k
 
SAMPLES>d $system.OBJ.ShowObjects()
No registered objects
 
SAMPLES>s obj = ##class(Test.Store).%OpenId(1)
 
SAMPLES>d $system.OBJ.ShowObjects()
Oref      Class Name                    Ref Count
----      ----------                    ---------
1         Test.Store                    1
 
SAMPLES>d obj.SomeMethod()
 
SAMPLES>d $system.OBJ.ShowObjects()
Oref      Class Name                    Ref Count
----      ----------                    ---------
1         Test.Store                    2
 
SAMPLES>k obj
 
SAMPLES>d $system.OBJ.ShowObjects()
Oref      Class Name                    Ref Count
----      ----------                    ---------
1         Test.Store                    1

Interestingly, $data doesn't seem to have the same issue.

$THIS is object reference (OREF) -- unique identifier of object in memory. Different objects might have the same OREF during process lifetime

And subscript of local/global can be only numeric or a string -- not an object reference.

So, while indeed $GET(a($THIS)) triggers something wrong, the construction itself is not correct.

Timothy's suggestion converts OREF to string:

if $GET(seen(""_$THIS)) quit

making command correct.

Notice, that in workaround you proposed -- you are using OID, that is unique identifier of object on disk. Different objects cannot have the same OID.

Out of context of the original code, I agree.

The actual implementation is there to stop an infinite loop on objects that reference each other as part of a JSON serialiser, see line 9...

https://github.com/SeanConnelly/Cogs/blob/master/src/Cogs/Lib/Json/Cogs....

I'm not sure the construction of seen($THIS) is so much incorrect, just problematic. Both seen($THIS) and seen(""_$THIS) will produce an array item with a stringy representation, and works perfectly fine with exception of the unwanted side effect.

My assumption was that $THIS used inside the $get was to be avoided for persistent classes, whilst I could continue to use the OREF for non persistent classes, hence finding that the persistent objects OID was a perfectly good workaround.

However, as it turns out seen(""_$THIS) prevents the unwanted behaviour and makes the code much simpler to read, so thanks to Timothy for testing a different idea out.

Out of interest, I have since discovered that the pattern of seen(+$THIS) is used extensively in Caché / Ensemble library code, where the + symbol will coerce the string to the ordinal integer value from the OREF. I was tempted to use this approach, but one thing I am not sure of is if ordinal values are unique across a mixed collection of objects...

Tim's comment demonstrates that $get leaks an OREF when $this is used as a subscript of an array that doesn't exist. That's a bug.

An OREF as a number is unique within a process, so +$this is more compact, and potentially faster than ""_$this. You should never see, e.g., 1@foo and 1@bar at the same time.