JOB
的正确打开方式
JOB
的正确打开方式
- 最近有一些小伙伴在使用
JOB
时,由于使用不当,引起一些问题。例如把license
占满,JOB
运行的方法有一些没有执行等。
通常我们这样使用Job
命令:j ##class(className).methodName(args)
,实际上这种方式会引起一些未知问题。正确方式见下文。
- 笔者通过这篇文章把
JOB
的正确使用方式介绍一下,避免在日后使用JOB
时出现如上的一些问题。
简介
JOB
命令用于在操作系统级别启动一个新的进程。该进程可以执行特定的方法。
工作原理
当使用
JOB
命令时,会在当前进程的上下文中启动一个新的进程。新的进程会从特定的代码开始执行,并在该代码或命令返回后退出。
注:因为JOB
每次启动一个进程。由于IRIS
的特性,单用户启用超过20个进程时,这20
个进程将转化为占用对应进程数量的license
。
注:JOB
命令功能类似于JAVA
开启一个线程,在单独的环境中运行程序。但是线程是轻量级的,一个进程可以包含多个线程。进程是重量级的,开销要比线程大。在IRIS
、Caché
中,没有线程的概念。如果想使用异步功能,只能由JOB
命令,开启一个进程。
使用场景
- 多进程:开发人员可以使用
JOB
命令来创建新的进程,从而提高应用程序的并发性能和响应能力。例如,可以在后台启动一个长时间运行的任务(如数据导入)而不会阻塞前台服务。 - 异步:执行第三方应用程序接口:
JOB
命令可以在新的进程中直接执行不需要判断返回值且耗时比较长的第三方应用程序接口。
使用方式
JOB
的简写方式为J
,可以使用以下参数方式启动:
JOB:pc routine(routine-params):(process-params):timeout
JOB:pc routine(routine-params)[joblocation]:(process-params):timeout
J:pc ##class(className).methodName(args):(process-params):timeout
J:pc ..methodName(args):(process-params):timeout
J:pc $CLASSMETHOD(className,methodName,args):(process-params):timeout
- 简单启用
JOB
。
编写一个需要运行的方法,该方法挂起10秒。
ClassMethod Process()
{
h 10
w "执行Process",!
s ^yx("PID") = $job
}
ClassMethod Job()
{
w "do执行",!
d ..Process()
w "do结束",!
w "job执行",!
j ..Process()
w "job结束"
}
可以看到通过DO
运行时,当前进程挂起10
秒后继续运行,JOB
运行时,该方法为瞬间启动,没有阻塞当前进程。
- 启动
JOB
进程后,会为其分配单独的内存分区,并为其分配唯一的JOB
编号(也称为进程ID
或PID
)。JOB
进程号存储在$JOB
特殊变量中。
注:当JOB
启动的方法运行完成时,创建的进程也随之销毁。
- 使用
JOB
调用其他命名空间方法。
示例如下,在其他命名空间创建如下方法:
Class M.Job Extends %RegisteredObject
{
ClassMethod JobOtherNamespace()
{
s ^yx("JobOtherNamespace") = $i(^yx("JobOtherNamespace"))
}
}
- 使用
JOB
命令时,要指定:(namespace)
参数。
ClassMethod JobNameSpace()
{
j ##class(M.Job).JobOtherNamespace():("yx")
}
如下可以看到,每次启动JOB
时,yx
命名空间Global
自增1
。
USER>d ##class(M.Job).JobNameSpace()
USER>zw ^|"yx"|yx("JobOtherNamespace")
^|"yx"|yx("JobOtherNamespace")=1
USER>d ##class(M.Job).JobNameSpace()
USER>zw ^|"yx"|yx("JobOtherNamespace")
^|"yx"|yx("JobOtherNamespace")=2
USER>d ##class(M.Job).JobNameSpace()
USER>zw ^|"yx"|yx("JobOtherNamespace")
^|"yx"|yx("JobOtherNamespace")=3
注意事项
JOB
只能按值传递参数,不能传递引用参数与多维数组传。如果使用JOB使用引用传递参数,Studio将提示如下错误。
JOB
不能使用可变参数。
- 不能将对象引用传递给
JOB
进程。 这是因为对调用上下文中存在的对象的引用不能在新的进程JOB
上下文中引用。传递OREF
会将空字符串(""
)传递到JOB
进程。
ClassMethod JobObj()
{
s m = ..%New()
w "当前对象", m,!
j ..Obj(m)
}
ClassMethod Obj(m)
{
s ^yx("Obj") = m
}
USER>d ##class(M.Job).JobObj()
当前对象1@M.Job
USER>zw ^yx("Obj")
^yx("Obj")=""
- 启动
Job
时需要指定参数timeout
- 在超时并中止JOB
之前等待JOB
进程启动的秒数。前面的冒号是必需的。必须将超时指定为整数值或表达式。如果JOB
进程超时,则将停止该进程,并将$test
特殊变量设置为0
。
ClassMethod JobTimeOut()
{
for i = 1 : 1 : 500 {
j ..Process()::3
w "i:",i," $test:",$test,!
}
}
注:如果启动JOB没有指定超时参数,那么该进程将无限期等待,导致死进程。
启动JOB
失败的情况
- 内存不足。
- 超过许可的进程数量。
- 其他等。
JOB
正确打开方式
错误方式
在介绍如何正确使用JOB
之前,首先看一下错误的使用方式造成的后果。运行JobTimeOut()
方法。
可以看到当license
被当前用户占满,这种情况下,其他正常的请求将都会等待,导致业务卡顿。
正确方式
首先需要了解2
个系统变量$ZPARENT
、$ZCHILD
。
$ZPARENT
包含JOB
处理当前进程的进程的PID
,如果当前进程不是通过JOB
命令创建的,则为0
。
在Process()
方法里增加s ^yx("PARENT") = $zparent
ClassMethod JobParent()
{
w "$job前:",$job,!
w "$zparent前:",$zparent,!
j ..Process()::3
w "$job后:",$job,!
w "$zparent后:",$zparent,!
}
USER>d ##class(M.Job).JobParent()
$job前:20568
$zparent前:0
$job后:20568
$zparent后:0
USER>zw ^yx("PARENT")
^yx("PARENT")=20568
$ZCHILD
包含JOB
命令创建的最后一个进程的PID
,无论是否成功。
ClassMethod JobChild()
{
w "$job前:",$job,!
w "$zchild前:",$zchild,!
j ..Process()::3
w "$job后:",$job,!
w "$zchild后:",$zchild,!
}
USER>d ##class(M.Job).JobChild()
$job前:20568
$zchild前:23940
$job后:20568
$zchild后:25880
USER>d ##class(M.Job).JobChild()
$job前:20568
$zchild前:25880
$job后:20568
$zchild后:32308
- 确认
JOB
是否启用成功,必须要判断$zchild
前后的值与超时。
通过使用$ZCHILD
,可以通过比较运行JOB
命令前后的$ZCHILD
值来确定远程JOB
的执行状态。如果BEFORE
和AFTER
值不同,并且AFTER
值为非零,则AFTER``$ZCHILD
值是新创建的JOB
的PID
,表示进程已成功创建。如果AFTER
值为零,或者AFTER
值与BEFORE
值相同,则不会创建JOB
。
正确使用JOB
的方式如下:
- 设置
JOB
超时参数。 - 判断是否超时。
- 判断
$ZCHILD
系统变量前后是否不一致并且不为0
。 - 根据返回值判断
JOB
是否启动成功。如果不判断JOB
是否启动成功,会发生JOB
运行的方法有没有执行等问题。
ClassMethod JobCorrect()
{
s curChildID = $zchild
j ..Process()::3
s nowChildID = $zchild
if '$test {
q "JOB启动超时" _ " "_ $zchild
}
if (nowChildID '= 0)&&(curChildID '= nowChildID){
q "JOB启动成功" _ " "_ $zchild
}
q "JOB启动失败" _ " "_ $zchild
}
注:$ZCHILD
只能告诉JOB
已创建;不会告诉JOB
是否成功运行程序。要确定JOB
进程运行的程序是否无错误并运行到完成,最好的做法是在正在运行的代码中提供某种类型的日志记录和错误捕获。JOB
机制不会以任何方式通知父进程进程错误或进程终止,无论成功与否。
注:JOB
进程之间的通信。首选方案是使用父子进程都知晓的Global
。使用Global
目的是允许进程之间交换信息。
进阶方式
前面"正确的方式",仅能判断JOB
是否正确启动。但无法判断由JOB
引起的死进程。
- 首先创建一个 通用的
JOB
程序。
- 由于
JOB
不能使用可变参数,所以需要将参数做一次处理转化。
ClassMethod Run(className, methodName, params)
{
s $zt = "Error"
if (params '= "") {
s arg = $ll(params)
for i = 1 : 1 : arg {
s arg(i) = $lg(params, i)
}
} else {
s arg = 0
}
d $classmethod(className , methodName, arg...)
q $$$OK
Error
s $zt = ""
q $ze
}
- 该方法为
JOB
启动通用方法,该方法为布尔类型,返回$$$YES
代表JOB
启动成功,返回$$$NO
代表启动失败。
ClassMethod IsRunJob(classname, methodname, arg...) As %Boolean
{
s $zt = "Error"
s params = ""
if ($d(arg)) {
for i = 1 : 1 : arg {
s params = params _ $lb(arg(i))
}
}
s curChildID = $zchild
j ..Run(classname, methodname, params)::3
s nowChildID = $zchild
if '$test {
q $$$NO
}
if (nowChildID '= 0)&&(curChildID '= nowChildID){
q $$$YES
}
q $$$NO
Error
s $zt = ""
q $$$NO
}
- 在循环中启动
350
次JOB
,将JOB
开启的进程全部结束掉防止有出现死进程。
- 这里将
JOB
进程进行缓存,用于统一结束。
ClassMethod JobFinishProcess(num)
{
s array = ""
for i = 1 : 1 : num {
s bool = ..IsRunJob("M.Job","Process")
if (bool) {
s array(i) = $zchild
w "进程开启成功:" _ i _ " JOB进程ID:" _ $zchild,!
} else {
w "进程开启失败:" _ i _ " JOB进程ID:" _ $zchild,!
}
}
d ..TerminateProcess(.array, 0)
q $$$OK
}
TerminateProcess
方法,增加了程序挂起时间,可以设定一个阈值,当程序在这个阈值内没有结束,认为为死进程,将其结束掉。
ClassMethod TerminateProcess(ByRef array, time)
{
h time
s i = ""
for {
s i = $o(array(i))
q:(i = "")
s ret = $system.Process.Terminate(array(i))
s desc = $s(ret = "1" : "成功", ret = "0" : "失败 - 进程无法结束", ret = "-1" : "失败 - 进程没有响应", ret = "-2" : "失败 - 进程处于非活动状态死进程", ret = "-3" : "失败 - 没有此pid进程", ret = "-4" : "失败 - 进程包含开放的事务,该事务当前处于停止或回滚状态。")
w "结束进程状态:" _ ret _ " " _ desc,!
}
q $$$OK
}
- 如下图可以观察到,成功将没有结束的进程终止掉。
注:实际上结束进程状态:-3 失败 - 没有此pid进程
,是缓存的JOB进程ID已经运行完成进程已结束,认为成功。
代码d ..TerminateProcess(.array, 0)
如果设置了阈值时间,例如设置10分钟,会导致生产程序返回结果耗时过久,这里可以将DO
调用改为JOB
调用。
- 设置临时
Global
缓存Job
进程ID
,通过s bool = ..IsRunJob("M.Job", "TerminateProcessJob", 0)
调用后台结束进程程序。
ClassMethod JobFinishProcess1(num)
{
k ^yx("JobPid")
s array = ""
for i = 1 : 1 : num {
s bool = ..IsRunJob("M.Job","Process")
if (bool) {
s ^yx("JobPid", i) = $zchild
w "进程开启成功:" _ i _ " JOB进程ID:" _ $zchild,!
} else {
w "进程开启失败:" _ i _ " JOB进程ID:" _ $zchild,!
}
}
s bool = ..IsRunJob("M.Job", "TerminateProcessJob", 0)
if ('bool) {
w "终结程序启动失败",!
}else {
w "终结程序启动成功",!
}
q $$$OK
}
ClassMethod TerminateProcessJob(time)
{
s ^yx("TerminateProcessJob") = time
h time
s i = ""
for {
s i = $o(^yx("JobPid", i))
q:(i = "")
s ret = $system.Process.Terminate(^yx("JobPid", i))
s desc = $s(ret = "1" : "成功", ret = "0" : "失败 - 进程无法结束", ret = "-1" : "失败 - 进程没有响应", ret = "-2" : "失败 - 进程处于非活动状态死进程", ret = "-3" : "失败 - 没有此pid进程", ret = "-4" : "失败 - 进程包含开放的事务,该事务当前处于停止或回滚状态。")
w "结束进程状态:" _ ret _ " " _ desc,!
}
q $$$OK
}
终极方式
细心的朋友应该发现进阶方式存在了如下问题:
- 终止进程方法也使用
JOB
启用,如果JOB
启用失败了,那么死进程还是结束不了。 - 终止进程方法结束的
PID
也有问题,如果JOB
的PID
已经完成,其他并发的用户请求在此时间段内又生成了相同的PID
进程号,会把正常的业务进程结束掉。 - 用户启用的
JOB
数量没有限制,单个用户就能把license
占满,导致正常的业务请求失败,应该限制单用户开启的JOB
数量,结合当前的license
数量,应设置一个阈值,超过此阈值的JOB
不允许开启。 - 如果不判断返回值,如何保证调用的
JOB
程序全部启用成功,应设置缓存失败的JOB
方法与参数,设置后台重试。
这里我已经将代码封装到拓展系统命令ZSAFEJOB
中,使用拓展系统命令ZSAFEJOB
启用需要后台调用的程序即可。
ZSAFEJOB
命令包含功能:
下面演示ZSAFEJOB
命令使用方式:
ZSAFEJOB "classname_methodname(params)"
ZSJ "classname_methodname(params)"
classname
- 类名。methodname
- 查询名。params
- 使用单引号包裹参数,逗号分割的参数列表。- 需要返回值时,可使用
%zjob
变量判断Job
是否启用成功。
- 有参数时:需要将参数使用单引号包裹,多个参数使用逗号分割。如下使用
ZSJ
调用如下Sum
方法。
ClassMethod Sum(a, b, c)
{
s sum = a + b + c
s ^yx("Sum") = sum
q sum
}
USER>zsj "M.Job_Sum('1','2','3')"
- 无参数时,可省略括号。如下使用
ZSJ
调用如下NoParams
方法。
ClassMethod Process()
{
s ^yx("NoParams",$i(^yx("NoParams"))) = ""
h 20
w "执行Process",!
}
示例Process()
方法挂起进程20
秒,zsj
命令为异步,并没有挂起等待,输出记录的Global
验证调用成功。
USER>zsj "M.Job_Process"
使用其他拓展系统命令可参考如下连接:https://yaoxin.blog.csdn.net/article/details/130157870
- 验证超过
license
阈值,使用job
失败,防止占用过多license
,导致正常业务失败。示例中的阈值为当前license
最大数量300*0.8 = 240
。
ClassMethod SafeJob(num)
{
k ^yx("JobPid")
s array = ""
for i = 1 : 1 : num {
zsj "M.Job_Process"
w i," ",%zjob,!
}
q $$$OK
}
- 验证将启动失败的
Job
方法,进行重新调用。如下图示例:238
,239
,启用的JOB
失败,然后使用方法JobTask
进行重试,观察可发现重试2
次成功。
JobTask
方法需要挂任务进行重试,包含以下功能:
- 删除由
Job
开启的死进程,判断规则为进程持续时间超过10分钟,则把该进程进行结束。 - 重试
Job
启动失败的方法。
总结
本篇由浅入深讲解了Job
命令的使用方式,了解Job
命令的原理,可防止一些意外的情况,并推荐使用ZSAFEJOB
命令使用JOB
命令功能。
以上是个人对JOB
命令的一些理解,由于个人能力有限,欢迎大家提出意见,共同交流。