Customizing Start and Stop Behavior with ^%ZSTART and ^%ZSTOP Routines
Customizing Start and Stop Behavior with ^%ZSTART and ^%ZSTOP Routines
Caché can execute your custom code when certain events occur. Two steps are required:
-
Define the ^%ZSTART routine, the ^%ZSTOP routine, or both.
In these routines, you can define subroutines to execute when the certain activities start or stop.
^%ZSTART and ^%ZSTOP must be defined in the %SYS namespace, although they can be mapped to a non-default database.
-
Use the Management Portal to configure Caché to invoke the desired subroutines.
Specifically, if you define the routine ^%ZSTART and ^%ZSTOP and you include subroutines with specific names, the system automatically calls these subroutines when the activity is beginning or ending. The subroutine names are as follows:
-
SYSTEM — Executed when Caché as a system starts or stops
-
LOGIN — Executed when a user performs a login or logout using the %Service_Console or Service_Telnet services.
-
JOB — Executed when a JOB begins or ends
-
CALLIN: — Executed when an external program begins or completes a CALLIN
For example, when a user logs in, the system automatically invokes LOGIN^%ZSTART, if that is defined and if you have used the Management Portal to enable this subroutine.
These subroutines are not intended to do complex calculations or run for long periods of time. Long calculations or potentially long operations like network accesses will delay the completion of the activity until the called routine returns. In this case, users may take a long (elapsed) time to login, or JOB throughput may be curtailed because they take too long to start.
These subroutines are called as part of normal Caché operation. This means that an external event which terminates Caché abnormally, such as a power failure, will not generate a call to ^%ZSTOP.
If a system implements ^%ZSTOP, and an application implements one or more $HALT routines, the ^%ZSTOP code is not executed until the last $HALT terminates with a HALT command. The failure of a $HALT routine to issue its own HALT command can prevent the ^%ZSTOP code from running.
Design Considerations
Because ^%ZSTART and ^%ZSTOP run in a somewhat restricted environment, the designer must keep several things in mind, namely:
-
The routines must be written in ObjectScript.
-
Since ^%ZSTART is essentially run as though it is started with an argumentless new command, it can not be used to perform tasks such as initializing local variables for users.
-
There are no values passed as arguments when any of the routine entry points are called. If different algorithms are applicable in various circumstances, the called entry point must determine what to do by examining data external to the routine: global, system variables, and so on.
-
Make sure that the routines are well-behaved under all possible conditions. They should be written defensively. That is, they should check to make sure that all the resources needed to complete their task are at hand and, if possible, reserved to them before computation starts. Errors which occur are reported as failures of that system function so it is important to think about the design from the viewpoint of error containment and handling. Failure to properly account for recovery in the face of missing resources or the presence of errors has varied consequences: Caché may fail to start; major functions such as Studio may act strangely; or more subtle and insidious consequences may occur which are not immediately detected. It is strongly recommended that these routines be carefully written and debugged under simulated conditions, and then tested under simulated environment conditions before being put into production systems.
-
No assumption should be made that conditions found during a previous invocation or a different entry point are still valid. Between successive calls to JOB^%ZSTART, for example, a file used by the prior call could have been deleted before this call occurred.
-
Each entry point should perform its task efficiently. If part of the task is potentially long-running, it is recommended that you queue enough information to complete the task for later completion by another part of your application.
-
If an entry point wishes to have data around persistently for, say, statistical purposes, it must use something like a global or an external file to hold the data.
-
The routines should make minimal assumptions about the environment they are running in. A developer of one of these routines cannot, for example, assume that the program will always be executed under a specific job number. The designer cannot assume that the various entry point will be called in a specific order. The sequence of bringing up the multiple processes that implement Caché is rarely deterministic.
-
The routine cannot assume that it is being called at a specific point during the system startup. The sequence of events during startup may change from release to release, or even from restart to restart.
-
With a few exceptions, the routine must leave things as it found them. As an illustration of this principle, reassigning the value of $IO in the subroutine without saving and restoring it upon entry and exit is an almost certain source of error. The calling routine has no way of knowing that such things are changed, and it is very difficult for the caller to defend against any possible change to the execution environment. Therefore, the burden of not disturbing the system processing context lies on the subroutine being called.
The general exceptions to the no-changes rule are that changing process-local values specific to an application or installation are allowed. For example, the SYSTEM^%ZSTART entry point may set system-wide defaults. Similarly, for application testing, it could set the date to a specific value to validate end-of-month processing.
-
^%ZSTOP cannot contain references to globals in remote databases. At the time it is called, some of these may no longer be accessible.
-
Once SYSTEM^%ZSTOP is invoked during the shutdown process, a user is no longer able to use the JOB command to start new processes. Currently existing processes are allowed to finish.
-
If these routines are mapped to a database different from CACHESYS, then Caché will attempt to execute them from that database, not from CACHESYS. Caché will, of course, make certain that the calling routine has the appropriate access to that database beforehand. It is the responsibility of the administrator to ensure that the routine has access to any application globals and mappings that it requires from that namespace.
-
SYSTEM^%ZSTART and SYSTEM^%ZSTOP are run with $USERNAME set to %SYSTEM and $ROLES set to %All. To run your code with a different username, use $SYSTEM.Security.Login() to set the desired name and then continue with your custom code. If you use JOB in your SYSTEM^%ZSTART code to launch any additional processes, those processes will inherit the same username (and roles) as the initiating process.
Caution:All the entry points in ^%ZSTART and ^%ZSTOP are invoked at critical points in system operation and can have widespread effects on the operation of a system or even on its data. The specified purpose of these routines makes this high level of privilege necessary. Because of this, you must make sure that all code that can be invoked by these entry points has been thoroughly tested. Further, do not allow any user-specified code to be run via XECUTE or indirection.
-
The exiting (that is, halting) process may get a <FUNCTION> error on any reference that requires an answer from an ECP data server.
On upgrades, Caché preserves only the %Z* routines that are mapped to the CACHESYS database, and if the .INT or .MAC code is available, recompiles them. Preservation of routines in other databases are the responsibility of the site administrator.
Enabling %ZSTART and %ZSTOP
Once the routines have been designed, developed, compiled, and are ready to be tested, individual entry points may be enabled through the Management Portal. Navigate to the Startup Settings page by selecting System Administration, then Configuration, then Additional Settings, then Startup Settings, and edit the appropriate individual settings:
-
SystemStart, SystemHalt
-
ProcessStart, ProcessHalt
-
JobStart, JobHalt
-
CallinStart, CallinHalt
To deactivate one or more of the entry points, use the same procedure but change the value to false.
Debugging ^%ZSTART and ^%ZSTOP
The opportunities for debugging ^%ZSTART and ^%ZSTOP in their final environment are very limited. If an error occurs, errors are written to the operator console log, which is the current device while these routines are running. This file is cconsole.log and is found in the Manager’s directory.
The message indicates the reason for failure and location where the error was detected. This may be different from the place where the error in the program logic or flow actually occurred. The developer is expected to deduce the nature and location of the error from the information provided, or modify the routine so that future tests provide more evidence as to the nature of the error.
Removing %ZSTART and ^%ZSTOP
It is strongly recommended that you disable the entry point options via the Management Portal before deleting the routines. If the portal warns that a restart of Caché is needed for them to take effect, do this as well before proceeding. This guarantees that none of the entry points are being executed while they are being deleted.
Remember that ^%ZSTART and ^%ZSTOP (as well as any supporting routines) are stored persistently. To remove all traces of them, delete them through the Management Portal.
Example
The following example demonstrates a simple log for tracking system activity. It shows examples for ^%ZSTART and ^%ZSTOP, both of which use subroutines of a third example routine, ^%ZSSUtil, for convenience.
^%ZSSUtil Example
This routine has two public entry points. One writes a single line to the operator console log file. The other writes a list of name-value pairs to a local log file. Both files reside in the Manager’s directory, which is returned by the ManagerDirectory() method of the %Library.FileOpens in a new tab class.
%ZSSUtil ;
; this routine packages a set of subroutines
; used by the %ZSTART and %ZSTOP entry points
;
; does not do anything if invoked directly
quit
#define Empty ""
#define OprLog 1
WriteConsole(LineText) PUBLIC ;
; write the line to the console log
; by default the file cconsole.log in the MGR directory
new SaveIO
; save the current device and open the operator console
; set up error handling to cope with errors
; there is little to do if an error happens
set SaveIO = $IO
set $ZTRAP = "WriteConsoleExit"
open $$$OprLog
use $$$OprLog
; we do not need an "!" for line termination
; because each WRITE statement becomes its
; own console record (implicit end of line)
write LineText
; restore the previous io device
close $$$OprLog
; pick up here in case of an error
WriteConsoleExit ;
set $ZTRAP = ""
use SaveIO
quit
WriteLog(rtnname, entryname, items) PUBLIC ;
; write entries into the log file
; the log is presumed to be open as
; the default output device
;
; rtnname: distinguishes between ZSTART & ZSTOP
; entryname: the name of the entry point we came from
; items: a $LIST of name-value pairs
new ThisIO, ThisLog
new i, DataString
; preserve the existing $IO device reference
; set up error handling to cope with errors
; there is little to do if an error happens
set ThisIO = $IO
set $ZTRAP = "WriteLogExit"
; construct the name of the file
; use the month and day as part of the name so that
; it will create a separate log file each day
set ThisLog = "ZSS"
_ "-"
_ $EXTRACT($ZDATE($HOROLOG, 3), 6, 10)
_".log"
; and change $IO to point to our file
open ThisLog:"AWS":0
use ThisLog
; now loop over the items writing one line per item pair
for i = 1 : 2 : $LISTLENGTH(items)
{
set DataString = $LISTGET(items, i, "*MISSING*")
if ($LISTGET(items, (i + 1), $$$Empty) '= $$$Empty)
{
set DataString = DataString
_ ": "
_ $LISTGET(items, (i + 1))
}
write $ZDATETIME($HOROLOG, 3, 1),
?21, rtnname,
?28, entryname,
?35, DataString, !
}
; stop using the log file and switch $IO back
; to the value saved on entry
close $IO
; pick up here in case of an error
WriteLogExit ;
set $ZTRAP = ""
use ThisIO
quit
Here is an description for each label:
This routine (as well as the others) begins with a QUIT command so that it is benign if invoked via
do ^%ZSSUtil
The #DEFINE sequence cosmetically provides named constants in the body of the program. In this instance, it names the empty string and the device number of the operator console log.
The entry point is very simple. It is designed for low volume output, and as a minimally intrusive routine to use for debugging output.
It takes a single string as its argument and writes it to the operator console log. However, it must take care to preserve and restore the current $IO attachment across its call.
Each item sent to the device results in a separate record being written to the console log. Thus, the following results in four records being written.
WRITE 1, 2, 3, !
The first three consist of a single digit and the fourth is a blank line. If multiple items are desired on a line, it is the responsibility of the caller to concatenate them into a string.
This subroutine can be called by any entry point within ^%ZSTART or ^%ZSTOP. The first two arguments supply the information needed to report how the subroutine was launched. The third argument is a $LIST of name-value pairs to be written to the log.
This entry point first builds the name of the file it will use. To make log management easier, the name contains the month and day when the routine is entered. Therefore, calls to this subroutine create a new file whenever the local time crosses midnight. because the name is determined only at the time of the call. All the name-value pairs passed as the argument will be displayed in the same file.
Once the name has been constructed, the current value of $IO is saved for later use and the output device is switched to the named log file. The parameters used for the OPEN command ensure that the file will be created if it is not there. The timeout of zero indicates that Caché will try a single time to open the file and fail if it cannot.
Once the file has been opened, the code loops over the name value pairs. For each pair, the caller routine name and the entry point name are written followed in the line by the name-value pair. (If the value part is the empty string, only the name is written.) Each pair occupies one line in the log file. The first three values on each line are aligned so they appear in columns for easier scanning.
When all the pairs have been written, the log file is closed, the previous value $IO is restored and control returns to the caller.
^%ZSTART
This routine contains the entry point actually invoked by Caché. It uses the services of ^%ZSSUtil just described. All the entry points act more or less the same, they place some information in the log. The SYSTEM entry point has been made slightly more elaborate than the others. It places information in the Operator console log as well.
%ZSTART ; User startup routine.
#define ME "ZSTART"
#define BgnSet "Start"
#define Empty ""
; cannot be invoked directly
quit
SYSTEM ;
; Cache starting
new EntryPoint, Items
set EntryPoint = "SYSTEM"
; record the fact we got started in the console log
do WriteConsole^%ZSSUtil((EntryPoint
_ "^%"
_ $$$ME
_ " called @ "
_ $ZDATETIME($HOROLOG, 3)))
; log the data accumulate results
set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3),
"Job", $JOB,
"Computer", $ZUTIL(110),
"Version", $ZVERSION,
"StdIO", $PRINCIPAL,
"Namespace", $ZUTIL(5),
"CurDirPath", $ZUTIL(12),
"CurNSPath", $ZUTIL(12, ""),
"CurDevName", $ZUTIL(67, 7, $JOB),
"JobType", $ZUTIL(67, 10, $JOB),
"JobStatus", $ZHEX($ZJOB),
"StackFrames", $STACK,
"AvailStorage", $STORAGE,
"UserName", $ZUTIL(67, 11, $JOB))
do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
quit
LOGIN ;
; a user logs into Cache
new EntryPoint, Items
set EntryPoint = "LOGIN"
set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
quit
JOB ;
; JOB'd process begins
new EntryPoint, Items
set EntryPoint = "JOB"
set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
quit
CALLIN ;
; a process enters via CALLIN interface
new EntryPoint, Items
set EntryPoint = "CALLIN"
set Items = $LISTBUILD($$$BgnSet, $ZDATETIME($HOROLOG, 3))
do WriteLog^%ZSSUtil($$$ME, EntryPoint, Items)
quit
Here is an description for each label:
This routine begins with a QUIT command so that it is benign if invoked as a routine rather than beginning its execution properly at one of its entry points.
This routine also defines named constants (as macros) for its own name, a starting string and the empty string.
This subroutine constructs a string consisting of the calling routine name, entry point, and the date and time it was invoked. Then it calls WriteConsole^%ZSSUtil to place it in the operator console log.
Afterward, it constructs a list of name-value pairs that it wishes to be displayed. It passes this to WriteLog^%ZSSUtil to place into the local log file. Then it returns to its caller.
These subroutines do not place any information in the operator console log. Instead, they construct a short list of items, enough to identify that they were invoked, and then use WriteLog^%ZSSUtil to record it.
^%ZSTOP
This routine contains the entry points actually invoked by Caché and it uses subroutines in ^%ZSSUtil. This example is similar to the example for ^%ZSTART. See the previous section for details.
%ZSTOP ; User shutdown routine.
#define ME "ZSTOP"
#define EndSet "End"
#define Empty ""
; cannot be invoked directly
quit
SYSTEM ; Cache stopping
new EntryPoint
set EntryPoint = "SYSTEM"
; record termination in the console log
do WriteConsole^%ZSSUtil((EntryPoint
_ "^%"
_ $$$ME
_ " called @ "
_ $ZDATETIME($HOROLOG, 3)))
; write the standard log information
do Logit(EntryPoint, $$$ME)
quit
LOGIN ; a user logs out of Cache
new EntryPoint
set EntryPoint = "LOGIN"
do Logit(EntryPoint, $$$ME)
quit
JOB ; JOB'd process exits.
new EntryPoint
set EntryPoint = "JOB"
do Logit(EntryPoint, $$$ME)
quit
CALLIN ; process exits via CALLIN interface.
new EntryPoint
set EntryPoint = "CALLIN"
do Logit(EntryPoint, $$$ME)
quit
Logit(entrypoint, caller) PRIVATE ;
; common logging for exits
new items
set items = $LISTBUILD($$$EndSet, $ZDATETIME($HOROLOG, 3))
do WriteLog^%ZSSUtil(caller, entrypoint, items)
quit