I've worked around this by creating a comprehensive work-space with Atelier projects for all the databases I need to search. Then I can just drop into shell and use grep/ack. Definitely not as good as Find-in-Files but good enough until Atelier 1.1 and definitely better than firing up a windows VM.

Warning - there is a large storage overhead - mostly in the Atelier/Eclipse .metadata cache especially for large codebases. However operations such as synchronizing and autocomplete perform well.

Most of my methods look like this:

Method MyMethod() As %Status {
  // Initialization - note locking and transactions only when needed
  Set sc = $$$OK
  Lock +^SomeGlobal:5 If '$TEST Quit $$$ERROR($$$GeneralError,"Failed to obtain lock")

  Try {
    TSTART

    // When error is significant
    $$$ThrowOnError(..SomeMethod())

    // When error can be ignored
    Do ..SomeMethod()

    // When only certain errors apply
    Set sc = ..SomeMethod()
    If $$$ISERR(sc),$SYSTEM.Status.GetErrorText(sc)["Something" $$$ThrowStatus(sc)
  
    TCOMMIT
  }
  Catch e {
    TROLLBACK
    Set sc = e.AsStatus()
  }
  Lock -^SomeGlobal
  Quit sc
}

I agree with both Nic and Rich.

The issue with e.Log() is that it clutters up the error log with repetitive entries, each subsequent one with less detail than the prior. The same thing happens in Ensemble when errors bubble through the host jobs.

The trick here is knowing when to log an error verses when to just bubble it up. Using Nic's method we lose the context of the stack since the log isn't written until the entry method with the Try/Catch. Using your method we get noise in the logs, but at least the first one has the detail we'll need.

I believe the root problem here is re-throwing the status. An exception should represent something fatal and usually out of the applications control (e.g. FILEFULL) while a %Status is a call success indicator. To that end your code could be refactored to just Quit on an error instead of throwing it. That way a single log is generated in the method that triggered the exception and the stack is accurate.

However this doesn't work well in nested loops. In that case a Return would work unless there is a cleanup chore (e.g. releasing locks, closing transactions, etc). I haven't come up with a pattern for nested loops that doesn't clutter up the source with a bunch of extra $$$ISERR checks that are easily missed and lead to subtle bugs.

Personally I use your style without logging because:

  • Every method uses the same control structure
  • Works with nested loops without extra thought
  • Can ignore errors by simply invoking with Do instead of $$$TOE/$$$ThrowOnError
  • Cleanup code is easy to find or insert
  • Using ByRef/Output parameters makes it trivial to refactor to return more than one value

I do lose the ability to see an accurate stack trace but most of the time the line reference in the error is enough for me to quickly debug an issue so it is an acceptable trade-off. Only in the most trivial methods is the Try/Catch skipped.

All that said Nic's style is a solid approach too. By removing a bunch of boilerplate Try/Catch code it lets the programmer focus on the logic and makes the codebase much easier on the eyes.

Here you go - this will compile in USER namespace

Class User.SQLForSome Extends %Persistent {

Property Name As %String;

Property Domains As list Of %String;

Index Domains On Domains(ELEMENTS) [ Type = bitmap ];

Query TestQuery(Favorites As %List) As %SQLQuery(ROWSPEC = "Name:%String") {
    SELECT Name
    FROM   SQLForSome
    WHERE  FOR SOME %ELEMENT(Domains)(%VALUE %INLIST :Favorites)
}

// Broken when table alias used. Still will save/compile though
Query TestQueryBroken(Favorites As %List) As %SQLQuery(ROWSPEC = "Name:%String") {
    SELECT foo.Name
    FROM   SQLForSome foo
    WHERE  FOR SOME %ELEMENT(foo.Domains)(%VALUE %INLIST :Favorites)
}

Storage Default
{
<Data name="SQLForSomeDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Domains</Value>
</Value>
<Value name="3">
<Value>Name</Value>
</Value>
</Data>
<DataLocation>^User.SQLForSomeD</DataLocation>
<DefaultData>SQLForSomeDefaultData</DefaultData>
<IdLocation>^User.SQLForSomeD</IdLocation>
<IndexLocation>^User.SQLForSomeI</IndexLocation>
<StreamLocation>^User.SQLForSomeS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}

Two issues:

First in 2016.2 (Cache for UNIX (Apple Mac OS X for x86-64) 2016.2 (Build 618U) Wed Mar 16 2016 19:23:28 EDT) two processes are created and both call the Server method. Looks like CSP is invoking %CSP.WebSocket:Page multiple times during the initial connection. The following seems to fix the issue:

Method Server() As %Status
{
    Quit:$D(^CacheTemp.Chat.WebSockets(..WebSocketID,"$J"))
    Set ^CacheTemp.Chat.WebSockets(..WebSocketID,"$J")=$J
    // ...
 

 

Second in WebKit browsers (Safari, Chrome) if the page is refreshed the WebSocket will open but no ..StatusUpdate is received and at least one process starts spinning in an infinite loop. Sometimes it may take more than one refresh, other times multiple processes start spinning.  This may be due to a long-standing bug in chromium where WebSocket close is not called on refresh/close like Firefox does (IE not tested). Adding an addition listener to init() solved the issue and seems to work cross browser:

window.addEventListener("beforeunload", function (event) {
  if (ws) {
    ws.close();
    ws = null
  }
});