Programming in Ensemble
This chapter discusses common programming tasks and topics in Ensemble. It includes the following sections:
Introduction
When you create business host classes or adapter classes, you typically implement callback methods, define additional methods as needed, and add or remove settings.
Within a business host or adapter, your methods typically do some or all of the following tasks:
-
Get or set values of properties of the business host or adapter.
-
Define callback methods. A callback method is an inherited method that does nothing by default; your task would be to override and implement this method.
-
Execute inherited helper methods of the business host or adapter. Helper method is an informal term for a method meant for use by other methods.
In business operations and business processes, your methods typically invoke inherited methods to send messages to other business hosts within the production.
-
Generate log, alert, or trace notifications in response to various conditions that may arise. Ensemble uses the following terminology:
-
Log entries are intended to note items such as external, physical problems (for example, a bad network connection).
Ensemble automatically writes these to the Event Log.
-
Alerts are intended to alert users, via an alert processor that you define and add to the production.
Ensemble also automatically writes these to the Event Log.
-
Trace items are meant for debugging and diagnostic purposes. You can use these, for example, to locate program errors before deploying the production. Ensemble can write these to the Event Log, to the Terminal, or both.
Not all types of error or activity should necessarily generate these notifications. It is up to the developer to choose which occurrences to record, and how to record them. Note that the Event Log should not register program errors; these should be resolved before the production is released.
-
Key Principles
It is important to understand the programming practices that are best suited within Ensemble productions. Ensemble business hosts execute in separate processes, which means that you should make sure that:
-
If a business host allocates any system resources (such as taking out locks or opening devices), the same business host should release them.
-
If a business host starts a transaction, the same business host should complete it or roll it back.
-
Any information that is to be shared between business hosts should be carried within a message sent between them (rather than via public variables).
Similar considerations apply to business rules and data transformations.
Also, you must often handle error codes received from Ensemble methods. The Ensemble development framework is designed to allow your custom code to be simple and linear as possible with regard to error codes. For example:
-
The OnProcessInput() method of business services and the OnMessage() and other user-written MessageMap methods of business operations are wrapped by the Ensemble framework, so that you do not need to include any additional error trapping in your code.
-
The adapter methods available for use by custom business operation code are guaranteed never to trap out, as are the framework methods available to custom code, such as SendAlert() and SendRequestSync(). Furthermore, the adapter methods automatically set suitable values for Retry and Suspend when error conditions do occur.
Given these precautions built into the Ensemble framework, InterSystems recommends that for normal circumstances your custom code should simply check the error code of each call, and if it is an error value, quit with that value. The following is an example of this coding style. Note the absence of traps or Do {} While 0 blocks:
Class Test.FTP.FileSaveOperation Extends Ens.BusinessOperation
{
Parameter ADAPTER = "EnsLib.File.OutboundAdapter";
Method OnMessage(pRequest As Test.FTP.TransferRequest,
Output pResponse As Ens.Response) As %Status
{
Set pResponse=$$$NULLOREF
Set tFilename=..Adapter.CreateTimestamp(pRequest.Filename,"%f_%Q")
; file with timestamp should not already exist
$$$ASSERT('..Adapter.Exists(tFilename))
Set tSC=..Adapter.PutStream(tFilename,pRequest.StreamIn) Quit:$$$ISERR(tSC) tSC
Quit $$$OK
}
}
More complicated scenarios are sometimes useful, such as for instance executing a number of SQL statements in a business operation using the SQL adapter and then calling a rollback before returning if any of them fail. However, the coding style in the previous example is the best practice in simple circumstances.
Later chapters in this book provide additional principles for specific kinds of business hosts.
Passing Values by Reference or as Output
If you are not familiar with passing values by reference or output, this section is intended to orient you to this practice.
Many Ensemble methods return at least two values: a status (an instance of %StatusOpens in a new tab) and a response message or other returned value. Typically the response message is returned by reference or as output. If a value is returned by reference or as output, that means:
-
When you define the method, the method must set the corresponding variable.
-
When you invoke the method, you must include a period before the corresponding argument.
The following examples demonstrate these points.
Typical Callback Method
The following shows the signature of a typical callback method:
method OnRequest(request As %Library.Persistent, Output response As %Library.Persistent) as %Status
The keyword Output indicates that the second argument is meant to be returned as output. In your implementation of this method, you would need to do the following tasks, in order to satisfy the method signature:
-
Set a variable named response equal to an appropriate value. This variable must have a value when the method completes execution.
-
End with the Quit command, followed by the name of a variable that refers to an instance of %StatusOpens in a new tab.
For example:
Method OnRequest(request As %Library.Persistent, Output response As %Library.Persistent) as %Status
{
//other stuff
set response=myObject
set pSC=..MyMethod() ; returns a status code
quit pSC
}
This example discusses a value returned as output, but the details are the same for a value passed by reference.
Typical Helper Method
The following shows the signature of a typical inherited helper method:
method SendRequestSync(pTargetDispatchName As %String,
pRequest As %Library.Persistent,
ByRef pResponse As %Library.Persistent,
pTimeout As %Numeric = -1,
pDescription As %String = "") as %Status
The keyword ByRef indicates that the third argument is meant to be returned by reference. To invoke this method, you would use the following:
set sc=##class(pkg.class).SendRequestSync(target,request,.response,timeout,description).
Notice the period before the third argument.
This example discusses a value passed by reference, but the details are the same for a value returned as output.
Adding and Removing Settings
To provide new settings for a production, a business host, or an adapter, modify its class definition as follows:
-
Add a property for each configuration setting you wish to define.
-
Add a class parameter called SETTINGS to the class.
-
Set the value of SETTINGS to be a comma-delimited list of the names of the properties you have just defined. For example:
Property foo As %String; Property bar As %String; Parameter SETTINGS = "foo,bar";
See the following section for additional details for SETTINGS.
The foo and bar settings now automatically appear in the configuration display on the Production Configuration page whenever an item of that class is selected for configuration.
To remove an inherited configuration setting, list the property in the SETTINGS class parameter, preceded by a hyphen (-). For example:
Parameter SETTINGS = "-foo";
Specifying Categories and Controls for Settings
By default, a setting is displayed in the Additional Settings category on the Details tab when you configure a production. By default, the setting provides one of the following input mechanisms, depending on the property on which the setting is based:
-
In most cases, the setting is displayed with a plain input field.
-
If the property specifies the VALUELIST parameter, the setting is displayed with a drop-down list.
-
If the property is of type %BooleanOpens in a new tab, the setting is displayed as a check box.
In all cases, the InitialExpression of the property (if specified) controls the initial state of the input field.
You can override both the location and control type. To do so, include the setting name in SETTINGS as follows:
Parameter SETTINGS = "somesetting:category:control";
Or:
Parameter SETTINGS = "somesetting:category";
Parameter SETTINGS = "somesetting::control";
Where category and control indicate the category and control to use, respectively. The following subsections provide details.
You can include multiple settings, as follows:
Parameter SETTINGS = "setting1:category:control1,setting2:control2:editor,setting3:category:control3";
For example (with disallowed line breaks included):
Parameter SETTINGS = "HTTPServer:Basic,HTTPPort:Basic,SSLConfig:Connection:sslConfigSelector,
ProxyServer:Connection,ProxyPort:Connection,ProxyHTTPS:Connection,
URL:Basic,Credentials:Basic:credentialsSelector,UseCookies,ResponseTimeout:Connection"
Category for a Setting
category is one of the following case-sensitive, literal values:
-
Info — Places the setting into the Information Settings category.
-
Basic — Places the setting into the Basic Settings category.
-
Connection — Places the setting into the Connection Settings category.
-
Additional — Places the setting into the Additional Settings category (the default).
-
Alerting — Places the setting into the Alerting Control category.
-
Dev — Places the setting into the Development and Debugging category.
Or use your own category name.
Control for a Setting
control specifies the name of a specific Zen control to use when viewing and modifying the setting in the Production Configuration page. Use selector or use the name of a class in the package EnsPortal.Component. See “Examples for Settings Controls.”
To supply extra values to your component in order to display a suitable set of options, append a set of name-value pairs as follows:
myControl?Prop1=Value1&Prop2=Value2&Prop3=Value3
Where:
-
Prop1, Prop2, Prop3, and so on are names of properties of the control. For example: multiSelect
-
Value1, Value2, Value3, and so on are the corresponding values.
Use the syntax @propertyname to refer to a property of the Javascript zenPage object. The supported variables are these:
-
currHostId refers to the ID of the current Ens.Config.ItemOpens in a new tab.
-
productionId refers to the ID of the Ens.Config.ProductionOpens in a new tab in use.
Use \@ to indicate the character @ as is.
To pass values to the context property of a control, enclose the value in curly braces. See the subsection.
-
Passing Values to the context Property of a Control
Ensemble uses the general-purpose selector component, which you can also use; to do so, specify control as selector and then append property/value pairs.
The selector component allows almost any list of data or options to be displayed to the user and which allows the user to type data if needed. To configure the component in this way, use the following syntax to set the context property of the control:
context={ContextClass/ContextMethod?Arg1=Value1&Arg2=Value2&Arg3=Value3}
Where:
-
ContextClass is the name of the context class. This must be a subclass of %ZEN.Portal.ContextSearchOpens in a new tab or Ens.ContextSearchOpens in a new tab. See “Examples for Settings Controls.”
-
ContextMethod is a method in that class. This method provides the data from which the user chooses values.
-
Arg1, Arg2, Arg3, and so on are the names of multidimensional arguments of that method.
-
Value1, Value2, Value3, and so on are values for those arguments. The rules for values are the same as given previously (for example, you can use @propertyname).
The selector component also has a property named multiSelect. By default, the user can select only one item. To enable the user to select multiple items, include a property/value pair as follows: multiSelect=1.
The argument names and values must be URL encoded. For example, replace % with %25.
Examples for Settings Controls
The following list gives examples for control, organized by the kind of data that they enable the user to select.
These examples use Ensemble classes. Many of these examples use the Ens.ContextSearchOpens in a new tab class, which provides a large number of useful methods. If this list does not cover your scenario, see the class documentation for Ens.ContextSearchOpens in a new tab to determine whether any of the existing methods supply the data you need. If that class does not cover your scenario, you can create your own subclass of Ens.ContextSearchOpens in a new tab.
selector?multiSelect=1&context={Ens.ContextSearch/ProductionItems?targets=1&productionName=@productionId}
Or, if the user should choose only one business host:
selector?context={Ens.ContextSearch/ProductionItems?targets=1&productionName=@productionId}
partnerSelector
ruleSelector
selector?context={Ens.ContextSearch/CharacterSets}
credentialsSelector
selector?context={Ens.ContextSearch/getDisplayList?host=@currHostId&prop=Framing}
selector?context={Ens.ContextSearch/TCPLocalInterfaces}
selector?context={Ens.ContextSearch/SchemaCategories?host=classname}
Where classname is the class name of the business host. For example:
selector?context={Ens.ContextSearch/SearchTableClasses?host=EnsLib.MsgRouter.RoutingEngineST}
selector?context={Ens.ContextSearch/SearchTableClasses?host=classname}
Where classname is the class name of the business host. For example:
selector?context={Ens.ContextSearch/SearchTableClasses?host=EnsLib.EDI.ASTM.Service.Standard}
sslConfigSelector
Specifying Default Values for Settings
As you define business host classes (and possibly adapter classes), you should consider how to control the default values for any settings of those items. Ensemble can take the default value for a setting from one of three sources:
-
The production definition.
-
The values defined for the Ensemble instance, but stored outside the production. For information, see “Defining Production Defaults” in Configuring Ensemble Productions.
-
The default value for the property as defined in the host class. In this case, the default value is determined by the InitialExpression property keyword.
Some settings are dependent on the environment, such as TCP/IP addresses or file paths; typically you configure these settings to have their source outside the production, while others, such as ReplyCodeActions are design decisions and most likely you develop your application to retrieve these from the production definition.
You can develop your production to have configuration settings come from different sources. The primary purpose is to make it easier to move productions from one Ensemble instance to another, such as from test to live.
Once you define a production, you can change the source of both production and business host settings on the Production Configuration page of the Management Portal. See Configuring Ensemble Productions for details.
The use of default settings allows you to define production and business host settings outside the production definition where you can preserve them during a production upgrade. To facilitate updating productions or moving productions from one system to another, you can omit settings and take their values from a structure that is installed on the system. When a setting is missing, Ensemble retrieves the default setting from outside the production definition if one exists.
See the descriptions for the following methods in the Ens.DirectorOpens in a new tab class entry of the Class Reference for programming details:
-
GetProductionSettingValue()
-
GetProductionSettings()
Accessing Properties and Methods from a Business Host
When you define a method in a business host class, you might need to access properties or methods of that class or of the associated adapter. This section briefly describes how to do these things.
Within an instance method in a business host, you can use the following syntaxes:
-
..bushostproperty
Accesses a setting or any other property of the business host. (Remember that all settings are properties of their respective classes.)
-
..bushostmethod()
Accesses an instance method of the business host.
-
..Adapter.adapterproperty
Accesses a setting or any other property of the adapter. (Note that every business host has the property Adapter. Use that property to access the adapter and then use dot syntax to access properties of the adapter.)
-
..Adapter.adaptermethod()
Accesses an instance method of the adapter.
Accessing Production Settings
You might need to access a setting of the production. To do so, use the macro $$$ConfigProdSetting. For example, $$$ConfigProdSetting("mySetting") retrieves the value of the production setting called mySetting. InterSystems suggests you wrap this macro in a $GET call for safety; for example:
set myvalue=$GET($$$ConfigProdSetting("mySetting"))
For details about $GET, see the Caché ObjectScript Reference.
Also see “Using Ens.Director to Access Settings” in the chapter “Advanced Topics.”
Choosing How to Send Messages
In business operations and business processes, your methods typically invoke inherited methods to send messages to other business hosts within the production. This section discusses the options.
Synchronous and Asynchronous Sending
When you define business service, business process, and business operation classes, you specify how to send a request message from that business host. There are two primary options:
-
Synchronously — The caller stops all processing to wait for the response.
-
Asynchronously — The caller does not wait; immediately after sending the request the caller resumes other processing. When sending a request asynchronously, the caller specifies one of two options regarding the response to this request:
-
Ask to receive the response when it arrives.
-
Ignore the possibility of a response.
-
The choice of how to send a message is not recorded in the message itself and is not part of the definition of the message. Instead, this is determined by the business host class that sends the message.
Deferred Sending
In addition to the straightforward alternatives of synchronous (wait) and asynchronous (do not wait), it is possible to send messages outside Ensemble using a mechanism called deferred response.
Suppose a business process wishes to invoke an action outside Ensemble. It sends a request to a business operation, which performs the invocation and returns the response. The business process is the intended recipient of any response; the business operation is simply the means by which the request goes out and the response comes in. The business operation will relay a response back if the business process made the request synchronously, or if it made the request asynchronously with asynchronous response requested. The following diagram summarizes this mechanism.
![generated description: deferred step1](images/egdv_deferred_step1.png)
Now suppose the business operation that receives a request from a business process has been written to use the deferred response feature. The original sender is unaware of the fact that the response is going to be deferred by the business operation. Deferring the response is a design decision made by the developer of the business operation. If the business operation does in fact defer the response, when the original sender receives the response at the end of the deferral period, it is unaware that the response was ever deferred.
A business operation defers a response by calling its DeferResponse() method to generate a token that represents the original sender and the original request. The business operation must also find a way to communicate this token to the external entity, which is then responsible for including this token in any later responses to Ensemble. For example, if the external destination is email, a business operation can include the token string in the subject line of the outgoing email. The entity receiving this email can extract this token from the request subject line and use it in the response subject line. In the following diagram, the item “t” represents this token.
Between the time when the business operation defers the request, and when the response is finally received by the original sender, the request message has a status of Deferred. After the original sender receives the corresponding response, the request message status changes from Deferred to Completed.
An incoming event in response to the request can be picked up and returned to the original sender by any business host in the production. Exactly where the event arrives in Ensemble depends on the design of the production; typically, it is the task of a business service to receive incoming events from outside Ensemble. The business host that receives the incoming event must also receive the deferred response token with the event. The business host then calls its SendDeferredResponse() method to create the appropriate response message from the incoming event data and direct this response to the original sender. The original sender receives the response without any knowledge of how it was returned. The following figure shows a request and its deferred response.
![generated description: deferred step2](images/egdv_deferred_step2.png)
Generating Event Log Entries
The Event Log is a table that records events that have occurred in the production running in a given namespace. The Management Portal provides a page that displays this log, which is intended primarily for system administrators, but which is also useful during development. (For details on this page, see Monitoring Ensemble Productions.)
The primary purpose of the Event Log is to provide diagnostic information that would be useful to a system administrator in case of a problem while the production is running.
Ensemble automatically generates Event Log entries, and you can add your own entries. Any given event is one of the following types: Assert, Info, Warning, Error, and Status. (The Event Log can also include alert messages and trace items, discussed in the next sections.)
To generate Event Log entries:
-
Identify the events to log.
Not all types of error or activity should necessarily generate Event Log entries. You must choose the occurrences to note, the type to use, and the information to record. For example, Event Log entries should appear in case of an external, physical problem, such as a bad network connection.
The Event Log should not register program errors; these should be resolved before the production is released.
-
Modify the applicable parts of the production (typically business host classes) to generate Event Log entries in ObjectScript or in Basic, as described in the following subsections.
If you need to notify users actively about certain conditions or events, use alerts, which are discussed in the next section and in “Defining Alert Processors,” later in this book.
Generating Event Log Entries in ObjectScript
Within business host classes or other code used by a production, you can generate Event Log entries in ObjectScript. To do so, use any of the following macros. These macros are defined in the Ensemble.inc include file, which is automatically included in Ensemble system classes:
Macro | Details |
---|---|
$$$LOGINFO(message) | Writes an entry of type Info. Here and later in this table, message is a string literal or an ObjectScript expression that evaluates to a string. |
$$$LOGERROR(message) | Writes an entry of type Error. |
$$$LOGWARNING(message) | Writes an entry of type Warning. |
$$$LOGSTATUS(status_code) | Writes an entry of type Error or Info, depending on the value of the given status_code, which must be an instance of %StatusOpens in a new tab. |
$$$ASSERT(condition) | Writes an entry of type Assert, if the argument is false. condition is an ObjectScript expression that evaluates to true or false. |
$$$LOGASSERT(condition) | Writes an entry of type Assert, for any value of the argument. condition is an ObjectScript expression that evaluates to true or false. |
The following shows an example with an expression that combines static text with the values of class properties:
$$$LOGERROR("Awaiting connect on port "_..Port_" with timeout "_..CallInterval)
The following example uses an ObjectScript function:
$$$LOGINFO("Got data chunk, size="_$length(data)_"/"_tChunkSize)
Generating Event Log Entries in Basic
To generate Event Log entries in Caché Basic, use the following syntax:
"Ens.Util.Log".LogMethodName(Me.%ClassName(1),CurrentMethod,arg)
Where LogMethodName is one of the method names in the following table, arg is the argument for that method, and CurrentMethod is the name of the current method.
Method | Details |
---|---|
LogInfo | Writes an entry of type Info. The argument is a string literal or an ObjectScript expression that evaluates to a string. |
LogError | Writes an entry of type Error. The argument is a string literal or an ObjectScript expression that evaluates to a string. |
LogWarning | Writes an entry of type Warning. The argument is a string literal or an ObjectScript expression that evaluates to a string. |
LogStatus | Writes an entry of type Error or Info, depending on the value of argument. |
LogAssert | Writes an entry of type Assert, if the argument is false. The argument is an ObjectScript expression that evaluates to true or false. |
For example:
"Ens.Util.Log".LogInfo(Me.%ClassName(1),MyMethod,"log this")
Generating Alerts
An alert sends notifications to applicable users while an Ensemble production is running, in the event that an alert event occurs. The intention is to alert a system administrator or service technician to the presence of a problem. Alerts may be delivered via email, text pager, or another mechanism. All alerts also write messages to the Ensemble Event Log, with the type Alert.
The Ensemble alert mechanism works as follows:
-
When you create business host classes for the production, include code that:
-
Detects undesirable conditions or other circumstances that a user must address.
-
Generates alerts on those occasions.
-
-
You define and configure an alert processor, which is a business host, named Ens.Alert. The alert processor can optionally manage the alert to track the process of resolving the event. For details on defining an alert processor, see Defining an Alert Processor. Any production can include no more than one alert processor.
In a business host class (other than a BPL process class), do the following to generate an alert:
-
Create an instance of Ens.AlertRequestOpens in a new tab.
-
Set the AlertText property of this instance. Specify it as a string that provides enough information so that the technician has a good idea of how to address the problem.
-
Invoke the SendAlert() method of the business host class. This method runs asynchronously and thus does not delay the normal activities of the business host.
For example, in ObjectScript:
set alert=##class(Ens.AlertRequest).%New()
set msg="Suspended HL7 Message "_pRequest.%Id()
_" because ACK.MSA.Acknowledgment Code = '"_tAcknowledgmentCode_"'"
set alert.AlertText=msg
do ..SendAlert(alert)
Adding Trace Elements
Tracing is a tool for use primarily during development. You add trace elements so that you can see the behavior of various elements in a production, for the purpose of debugging or diagnosis. To add trace elements to a production, you identify the areas in your code (typically business host classes) where you would like to see runtime information. In those areas, you add lines of code that (potentially) write trace messages. Note that these are messages only in a general sense; trace messages are simply strings and are unrelated to Ens.Message and its subclasses.
In most cases, you can define two kinds of trace elements: user elements and system elements. In most cases, it is more appropriate to define user trace elements.
For information on writing trace elements in BPL, DTL, or business rules, see Developing BPL Processes, Developing DTL Transformations, and Developing Business Rules.
Also, for information on enabling tracing, see the chapter “Enabling Tracing” in Monitoring Ensemble.
Writing Trace Messages in ObjectScript
To write trace messages in ObjectScript, use the following lines of code:
-
To write a user trace message:
$$$TRACE(trace_message)
Where trace_message is a string containing useful information about the context in which you add this line of code.
-
To write a system trace message (less common):
$$$sysTRACE(trace_message)
You might see $$$sysTRACE in Ensemble system code, but the appropriate choice for your own business host classes is generally $$$TRACE.
For example:
$$$TRACE("received application for "_request.CustomerName)
Writing Trace Messages in Basic
To write trace messages in Caché Basic, use the following lines of code:
-
To write a user trace message:
"Ens.Util.Trace".WriteTrace("user",Me.%ClassName(1),CurrentMethod,arg)
Where CurrentMethod is the name of the current method and arg is the string that you want written to the console.
-
To write a system trace message (less common):
"Ens.Util.Trace".WriteTrace("system",Me.%ClassName(1),CurrentMethod,arg)
Writing Trace Messages in BPL or DTL
To write user trace messages in a BPL business process or in a DTL data transformation, use the <trace> element. See the Ensemble Business Process Language Reference or the Ensemble Data Transformation Language Reference.