Hi

Business Services support an OnInit() method and in that method you could write some code like this:

In the Business service add the following Parameters:

Parameter StartPort = 51000

Parameter LastPort = 51100

Parameter SETTINGS = "StartPort:Basic,LastPort:Basic"

/// You will need a mapping table to global in the form ^BusinessServiceProductionNames({Bussiness_Service_ClassName})={Production_Item_Name}

method OnInit() as %Status

{

    set tSC=$$$OK

    try {

        set  tPort=..#StartPort,found=0

        while 'found {

              if $l($g(^BusinessServicePorts(tPort))) {

                     set tPort=tPort=tPort+1

                     if tPort>..#LastPort {set tSC=$$$ERROR(5001,"Cannot find available Port") $$$ThrowStatus(tSC)}

                     else {continue}

             Else {

                     set ^BusinessServicePorts(tPort)=$classname(),^BusinessServicePortsIndex($classname())=tPort

                     set ..Adapter.Port=tPort,found=1 quit

              }

         }

    }

    catch ex {set tSC=ex.AsStatus()}

    if 'tSC!('found) set ..Adapter.Port=""

    quit tSC

}

method OnTearDown () as %Status

{

    set tSC=$$$OK

    try {

         if ..Adapter.Port'="" {

              kill ^BusinessServicePorts(..Adapter.Port),^BusinessServicePortsIndex($classname())

       }

    }

    catch ex {set tSC=ex.AsStatus()}

    quit tSC

}

The only thing you would need to do now is somehow notify the Client appliction would need to be notified which Port they should use to access a particular Business Service. I would suggest a WebService that accepts a request consisting of a property "BusinessServiceName" and will return ^BuinessServiceNames($classname()) and a second property "BusinessServicePort" which will return ^BusinessServicePortsIndex($classname())

You would need to supply the client with a list of available Business Service Names and what the Business Service Does. Again you could do this with a WebService which the client invokes and the WebService would return a List of Business Services and Function Description

This solution does make certain assumptions such as the client being willing/able to introduce the code to invoke the WebServices to get the list of Available Services and the Port number for a specific Business Service

The reason you need a 'LastPort" parameter is to ensure that the Business Service will not get into a loop testing every port from 51000 through to whatever the last Possible port number is in TCP/Port architecture

Nigel

Hi

It has been a generally understood convention for many many years that software houses and developers should not write classes or routines in the MGR (%SYS) database/namespace. The reason for this is that %SYS contains all of the Cache/Ensemble/IRIS system management classes and routines. By default any class or routine that is prefixed with the '%' character are mapped to all other namespaces in the Cache/Ensemble/IRIS Instance. In earlier versions of Cache many of the system configuration, security, operation utilities were written as Cache Objectscript routines. Over the years almost all of those routines and classes have been grouped into packages such as %SYSTEM and are accessed via the Cache Management Portal or through $system. $system is a shorthand way of referring to ##class(%System.{Classname}).{MethodName}(). Any classes or routines that are not prefixed with a '%' character are not mapped to other namespaces and can only be run in %SYS. 

The InterSystems developers used class or routine names that typically reflected the functionality of the class/routine, for example ^LOCKTAB, ^DATABASE, ^JOURNAL are routines that allow you to manage the Lock Table, Database utilities and Journalling utilities. Therefore it was always considered to be unwise to write classes or routines in %SYS due to the possibility that InterSystems might introduce a new class or routine that coincidentally has the same name as the routine or class your developers have written.

The general advise given to developers was as follows:

1)  If you need a routine or class to be accessible across many or all namespaces then create a database and namespace named "{Application}System" i.e. "MyCompanySystem" and then use the Namespace Routine and Package mapping feature of Namespace definitions to map those routines and/or classes to the desired namespaces where access to those routines/classes is required

2) If you absolutely have to write the classes and/or routines in %SYS then prefix the routine/class name with a 'z' (specifically lowercase 'z') as in ^%zMyRoutine or ^zMyOtherRotuine. InterSystems developers in turn would never write system classes/routines with a 'z' prefix

3) All InterSystem routines and classes will contain comments right at the start of the routine/class specifically saying that the code has been written by InterSystems and typically reads as follows:

LOCKTAB  ; LOCKTAB Utility  ; JO2010, 2/19/06
         /*
         +--------------------------------------------------------+
         | Copyright 1986-2015 by InterSystems Corporation,       |
         | Cambridge, Massachusetts, U.S.A.                       |
         | All rights reserved.                                   |
         |                                                        |
         | Confidential, unpublished property of InterSystems.    |
         |                                                        |
         | This media contains an authorized copy or copies       |
         | of material copyrighted by InterSystems and is the     |
         | confidential, unpublished property of InterSystems.    |
         | This copyright notice and any other copyright notices  |
         | included in machine readable copies must be reproduced |
         | on all authorized copies.                              |
         +--------------------------------------------------------+
         */

4) Use the %Dictionary.ClassDefinition class queries and methods to identify all classes in the %SYS namespace and when you open a class definition of a system class you will see that there is a property 'System' which if 'True' indicates that it is an InterSystems system class.

5) You can run the utility ^%ROU which will create a list of all routines in a namespace in a temporary global ^CacheTempUtil("ROU",{Name}) .  The entry point is the label DIR as in do DIR^%ROU({directory_name})

6) The databases 'CACHELIB", which  contains all of the Cache class Utility classes and 'ENSLIB' which contains the Ensemble utility classes are READ ONLY databases and therefore you cannot create routines or classes in them. %SYS on the otherhand has READ/WRITE permissions and so it was not untypical for developers to write routines in %SYS particularly in very early versions of cache that did not support Namespace Package Mapping.

I trust this helps

Nigel

The way that I have resolved this in the past is to have two properties in my Production Item.

The first is called PropertyDescription and the second is called PropertyID . PropertyDescription is referenced in the SETTINGS parameter and the query returns the Display Value of my property, the second Property is flagged as CALCULATED and when referenced it invokes the PropertyIDGet() method which says

method PropertyIDGet() as %String {

'select ID into :tId from {table} where {table}.Description=:..PropertyDescription'

quit tId

}

In your case you would use the syntax  :

..PropertyLogicalToDisplay(..Property)

Nigel

/// This is a nice little debugging class<BR><BR>
/// All of my classes have an Include statement in them<BR><BR>
/// 
/// Include ExampleInc<BR><BR>
/// 
/// Then the Include Routine Example.inc has the following #define in it<BR><BR>
/// 
/// #define DebugLog(%s1,%s2,%s3) do ##class(Example.Debug.Logging).CreateDebugLog($classname(),%s1,%s2,%s3)<BR><BR>
/// 
/// Then in your code you can add calls to the Debug Logger as follows:<BR><BR>
/// 
/// $$$DebugLog("MyKey","This is my Debug Message",.dSC)<BR><BR>
/// 
/// To enable Debug Logging execute the following code in the namespace where your production is running<BR><BR>
/// 
/// do ##class(Example.Debug.Logging).DebuggingOnOff(1)<BR>
Class Example.Debug.Logging Extends %Persistent
{
Property CreateTS As %TimeStamp [ InitialExpression = {$zdt($now(),3,1,6)} ];
Property ClassName As %String(MAXLEN = 150);
Property Username As %String [ Required ];
Property Key As %String(MAXLEN = 100) [ Required ];
Property Message As %String(MAXLEN = 3641144, TRUNCATE = 1);
Index CDT On CreateTS;
Index CN On ClassName;
Index UN On Username;
Index K1 On Key;
ClassMethod CreateDebugLog(pClassName As %String = "", pKey As %String = "", pMessage As %String(MAXLEN=3641144,TRUNCATE=1), ByRef pStatus As %Status)
{
               set pStatus=$$$OK
               try {
                              // You might want to put a check in here to test whether you want to create a debug log
                              // So if you port the code to Production you can leave the debug calls in your code but
                              // turn off debugging
                              if '(+$get(^Example.Debugging)) quit
                              set obj=##class(EMCI.Debug.Logging).%New()
               set obj.ClassName=pClassName,obj.Key=pKey,obj.Message=pMessage,obj.Username=$username
                              set pStatus=obj.%Save() if 'pStatus quit
               }
               catch ex {
                              set pStatus=ex.AsStatus()
               }
               quit
}
ClassMethod DebuggingOnOff(pOnOff As %Boolean)
{
               set ^Example.Debugging=pOnOff
}
ClassMethod PurgeDebugLog(pNumberOfDays As %Integer = 30, ByRef pRowCount As %Integer) As %Status
{
               set tSC=$$$OK,pRowCount=0
               try {
                              set date=$zdt($h-pNumberOfDays,3),id=""
                              for {
                                             set date=$o(^Example.Debug.LoggingI("CDT",date),-1) quit:date=""
                                             for {
                                                            set id=$o(^Example.Debug.LoggingI("CDT",date,id)) quit:id=""
                                                            set tSC=##class(Example.Degug.Logging).%DeleteId(id)
                                                            if 'tSC {!,"Unable to delete Debug Log with ID: "_id set tSC=$$$OK Continue}
                                                            else {set pRowCount=pRowCount+1}
                                             }             
                              }
               }
               catch ex {
                              set tSC=ex.AsStatus()
               }
               quit tSC
}

}

Hi

I have long ago resolved this issue. The issue can be flagged as answered

Nigel

Hi

I have contacted he developer bu to no avail. it is a pity as we are looking to inmcorporate similar functionality into our application and would be looking at  a deployment pricing model but I can't do anything unless i can test the solution,

Yours

Nigel

Hi

So you can use global indirection here:

you can set a variable to the name of a global

set gbl="^MyGlobalName"

you can then do either of the following:

if $(@gbl)#10 {write !,"Global: ",gbl," has a value of ",@gbl}
else {write !,"The global: ",gbl," is not defined"}

or you can use it to reference nodes within the global root:

for gbl="^A","^B","^C","^D","^E" {write !,"Global: ",gbl," for the node: ",gbl,"(""SYSTEM"") has a value: ",@gbl@("SYSTEM")

you can also do this:

set gbl="^NigelGlobal(""Subscript1"""_","_"""Subscript2"")"
set y=$o(@gbl@(y)

which effectively reads

set y=$o(^NigelGlobal("Subscript1","Subscript2",y) )

so before this I executed the following line of code:

set ^NigelGlobal("Subscript1","Subscript2","This is a Test")=""
set y=""
set y=$o(@gbl@(y))
if y'=""  write !,y
This is a Test

if you want to use this across namespaces then you need to do the following:

set ns="My other Namespace Name" e.g. set ns="DEV", my current Namespace is "QC"
set gbl1="^MyGlobalA",gbl2="^["""_ns_"""]MyGlobalB"
merge @gbl1@("Node A")=@gbl2("Node B")

this then reads as:

merge ^MyGlobalA("NodeA")=^["DEV"]MyGlobalB("NodeB")

Yours

Nigel

Hi

Is this running on a UNIX/Linux system?

If so I have noticed that if the production timeout is set too low Ensemble will spawn off a lot of jobs to try and clear the queues before it shuts down and this can consume lots of CPU and Memory and the system becomes unresponsive. Try setting the Shut down wait time to 60 seconds

Yours

Nigel

Hi Evgeny

The link still doesn't work. I will try and contact the developers

Nigel

Hi

I have an example of exactly what you are trying to do. It makes use of the IRIS 2019.1 9or later) FHIR Bundle resource, it handles stateid, paging and so on. I have a meeting I have to attend online very shortly and then I will put the code  together and update this thread.

Nigel