Hi @Davi Massaru Teixeira Muta ,

As far as i know, there is no lock log history.

This is understandable, as there would be a lot of them on heavily used systems.

If you can modify the application code, may be you can add log trace when this error happens.

With ^$LOCK you can retrieve the owner of a lock.

Example:

Lock +^my.global(1)
Write "Owner is : ", ^$LOCK($Name(^my.global(1)),"OWNER")
;Owner is : 5216

Even if you recover the PID, analysis can be difficult if the process no longer exists at the time you perform the analysis.  Maybe add something like this for logging (executed with a job):

/// Start this method in job ex:
/// Job ##class(pkg.ClassName).TraceLock($Name(^my.global))
ClassMethod TraceLock(lockname As %String) As %Status
{
    Set sc = $$$OK
    
    Set pid = ^$LOCK(lockname, "OWNER")
    If pid = "", $QLength(lockname)>0 {
        ; check if the node is fully locked
        Set pid = ^$LOCK($QSubscript(lockname,0), "OWNER")
    }
    
    If pid = "" {
        ; no data ...
        Return sc	
    }
    
    Set key = $Increment(^debug.locktrace)
    
    Set ^debug.locktrace(key, "from pid") = $ZParent	; Just to keep a trace of the pid started this job
    Set ^debug.locktrace(key, "info") = $ZDateTime($Horolog, 3, 1) _ " The lock " _ lockname _ " owner is "_pid
    
    Set process = ##CLASS(%SYS.ProcessQuery).Open(pid)
    If $IsObject(process) {
        Set ^debug.locktrace(key, "owner-UserName") = process.UserName
        Set ^debug.locktrace(key, "owner-StartupClientIPAddress") = process.StartupClientIPAddress
        Set ^debug.locktrace(key, "owner-StartupClientNodeName") = process.StartupClientNodeName
        Set ^debug.locktrace(key, "owner-CurrentLineAndRoutine") = process.CurrentLineAndRoutine
        Set ^debug.locktrace(key, "owner-Routine") = process.Routine
    }
    
    Return $$$OK
    
    ; Exemple of result:
    ;^debug.locktrace=1
    ;^debug.locktrace(1,"from pid")=12964
    ;^debug.locktrace(1,"info")="2024-11-22 12:25:25 The lock ^my.global owner is 5216"
    ;^debug.locktrace(1,"owner-CurrentLineAndRoutine")=""
    ;^debug.locktrace(1,"owner-Routine")="shell"
    ;^debug.locktrace(1,"owner-StartupClientIPAddress")="127.0.0.1"
    ;^debug.locktrace(1,"owner-StartupClientNodeName")="TRM:"
    ;^debug.locktrace(1,"owner-UserName")="_SYSTEM"
}

Hope this help.

Lorenzo.
 

Hi @Dmitry Maslennikov ,

Yesterday I experienced this kind of problem with web socket.
To limit the license usage, I did this : 

Class dc.journalindexer.WebSocket Extends %CSP.WebSocket
{

Method OnPreServer() As %Status
{
    Do %session.Login("wsuser",,1)
    Set ..SharedConnection = 1
    ; ... some lines of code
    Quit sc
}

Method OnClientMessage(
	data As %String = "",
	close As %Integer) As %Status
{
    ; force logout and EndSession on disconnect from client
    If $Get(close) = 1 {
        Do %session.Logout()        
        Set %session.EndSession = 1
        Do ..EndServer()
    }

	Quit 0
}
}

Hello @Michael Davidovich ,

Did you try to override OnPreDispatch ?

Also you can configure your web application to use a subclass of %CSP.SessionEvents and override OnStartRequest, OnEndRequest .

Set Application = "/csp/YourWebApp"
Do ##class(Security.Applications).Get(Application, .p)
Set p("EventClass") = "YourCSPEventsClassName"
Set sc = ##class(Security.Applications).Modify(Application, .p)

Hi @Scott Roth ,

Ten years ago, I implement %ZSTOP routine to execute code when a job process exits or when the instance shutdown.  I removed my code, but the routine looks like this: 

%ZSTOP ; User shutdown routine. 
    Quit
SYSTEM ; Cache stopping
	
	Set oldNs = $Namespace
	
	Try {
		Set $Namespace = "MYAPPNAMESPACE"
		; Do something when stopping the instance.
	}Catch(ex) {}
	
	Set $Namespace = oldNS
    Quit
LOGIN ; a user logs out of Cache (user account or telnet)
    Quit
JOB ; JOB'd process exits. 
	If $Namespace = "MYAPPNAMESPACE" {
		; do something when job process exits
	}
    Quit
CALLIN ; process exits via CALLIN interface. 
    Quit
Logit(entrypoint, caller) PRIVATE ;
    Quit

Hello @Pietro Montorfano 

You can try to write your own export method.
Using %Library.RoutineIndex looks good.  Example to export all ".MAC" :

set tRes = ##class(%SQL.Statement).%ExecDirect(.tStmt,"select name||'.'||type as itemName  from %Library.RoutineIndex where type = ? and $Extract(name,1) <> '%'","MAC")
While tRes.%Next() {
	Do $SYSTEM.OBJ.ExportUDL(tRes.%Get("itemName"),"<dir>/"_tRes.%Get("itemName"))
}

For adding a member, this is the IP of the first member (primary).  It's correct.
No matter the virtual IP, it's the role of the arbiter\agent to communicate who is primary in case of a switch.  

However, a virtual IP is convenient for access to your applications.

For example, with web applications: If the system switches from node A to node B, it is more convenient to use a virtual IP address.  You can use this in your web server configuration so that it always points to the primary node.

Hi,

Two years ago, I analyzed the behaviours using stream and %Persistent class.  

Class Test.Stream1 Extends %Persistent
{

Property st As %GlobalBinaryStream;

ClassMethod add1() As %Status
{
	; write stream in ^Test.Stream1S
	Set o = ..%New()
	Do o.st.Write("azeruiop")
	Quit o.%Save()
}

ClassMethod add2() As %Status
{
	; write stream in ^CacheStream
	Set o = ..%New()
	Set st = ##class(%GlobalBinaryStream).%New()
	Do st.Write("azeruiop")
	Set o.st = st
	Quit o.%Save()
}

Storage Default
{
<Data name="Stream1DefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>st</Value>
</Value>
</Data>
<DataLocation>^Test.Stream1D</DataLocation>
<DefaultData>Stream1DefaultData</DefaultData>
<IdLocation>^Test.Stream1D</IdLocation>
<IndexLocation>^Test.Stream1I</IndexLocation>
<StreamLocation>^Test.Stream1S</StreamLocation>
<Type>%Storage.Persistent</Type>
}

}

The method add2 use ^CacheStream due to the default storage usage (as described by @Robert Cemper ).  

However, We can force the storage in ^Test.Stream1S with :
Do st.%LocationSet("^Test.Stream1S")

Hi @Robert Cemper, @Vitaliy.Serdtsev 

Thank you for your replies!
I found a solution to do this without any change to an existing code, not simple but It works and could be useful in a critical situation.

I read the code of ^%ETN and see these lines :

UserError() s $zt="UserErrorE"
 i $d(^rOBJ("%ZERROR")) d 
 . n %00000 d ^%ZERROR

So, If we create a "%ZERROR" routine, we have an entry point :

ROUTINE %ZERROR

%ZERROR
    If $Data(^zForceCommit($Job)) { ; to avoid do this for all processes...
        While $TLEVEL {
            TCOMMIT
        }
    }

    Quit

And then, we must terminate the process like that:

Set pid = "1234" ; pid to terminate
Set ^zForceCommit(pid)=""
Zn "%SYS"
Set process = ##class(SYS.Process).%OpenId(pid)
Set sc = process.Terminate(1)

It's important to use the SYS.Process class and the Terminate method with argument 1 to use ^%ETN.

Code snipet to expose the problem.

Class ZUser.NewClass1 Extends %Persistent [ Not ProcedureBlock ]
{

ClassMethod Demo()
{

    Do ..TestLock()
    ; This class is Not ProcedureBlock the record still locked

    ; If the class is ProcedureBlock the record is released.
    ; try by yourself :-)
}

ClassMethod TestLock() As %Status
{
    Set id = $Order(^ZUser.NewClass1D(""))
    If id = "" {
        Set obj = ##class(ZUser.NewClass1).%New()
        Do obj.%Save()
        Kill obj
    }

    Set id = $Order(^ZUser.NewClass1D(""))

    Set obj = ##class(ZUser.NewClass1).%OpenId(id, 4)

    ; in case of usage Not ProcedureBlock you should 
    ; kill obj or set obj="" (and all others variables with this object reference) to release the lock


    Return $$$OK


}
}