需求

LotusScript 最近的更新是版本 7/8 时增加的 WebService 调用者、提供者,这已经是快十年前的事情了。现在 LotusScript 已经明显落后,欠缺的大量基础能力:JSON 解析器、跨代理的数据缓存、HTTP Client、完善的面向对象机制、将函数作为参数传递……这严重制约了应用层面的开发。

IBM 对此问题的应对是推出 XPages,一种新的开发模式。XPages 确实是一个不错的东西,展现和数据分离、內置 Ajax 框架……但是,作为应用开发商我们无法抛弃历史资产一切推倒重来。所以 XPages 我们只能用来做一些新的业务、功能点,对于原有的业务还是要继续在 LotusScript 上发展。

所以我们必须自己来扩展 LotusScript 的能力。

方案

LotusScript 类、函数库

首先想到的就是直接用 LotusScript 来实现,但这只能解决一部分问题:比如 JSON 解析器。而类似于 HTTP Client 这种,LotusScript 就无能为力了。

我们也确实做了一版 LotusScript 的 JSON 解析器,但是性能实在是太差了:由于需要一个字符一个字符的解析,最终导致一个 15K 的 JSON 串解析出来要花 0.5 秒。结果我们又废弃了它,重新回到手工拆解字符串的老路上。

此路不通。

LSX(LotusScript eXtension)

这是最正统的扩展 LotusScript 的方式(看看名字就知道),实际上我们使用的 NotesDocument 等 Domino 后台对象都是用这种方式扩展出来的。实际的 C 代码编译成共享库,位于 nlsxbe.dll(n=windows平台前缀,lsx,be=Back-end)中,LotusScript 引擎会自动加载它,所以我们不用再通过 USELSX lsxbe 来手工加载。

LSX 的优点有很多:跨平台、与 LotusScript 无缝集成……

但是,LSX 现在已经处于一个半废弃的状态。为什么这么说呢,LSX 最新版 8.0 于 2009 年发布,再往前一版就是 1999 年了。所以即使是最新版也缺少了很多内容:xlinux 64 位的支持等。

此路不通。

LS2C(LotusScript to C API)

这是一个比较成熟、使用较多的方案。一般都用于在 LotusScript 中调用底层的 C API(例如隐藏设计、重新编译所有代码),因为这些 API 没有暴露到 LotusScript 这一层。

LS2C 与 LSX 相比会多一些限制,比如:

  • 只能传递数值、字符串等基础类型,无法传递 NotesDocuemnt
  • 不支持 LotusScript 扩展语法(doc.itemName 这种)

但这个方案至少在持续的更新,而且 C 这个层面不需要使用 Lotus 的开发工具包,用任何编译器生成标准的共享库即可。

我们最终采用了此方案。

经验总结

变长字符串

C 语言中不支持变长字符串,所以我们在返回变长字符串时,实际返回的是一个 handle 值;这个 handle 值再通过另一个 API 获取实际的字符串(类似于 NSFDbOpen 返回的 handle)。

handle 用完之后要注意释放(类似于 NSFDbClose)。

中文的处理

LS2C 的字符串参数传递有三种声明方式:

  1. arg As String
    采用操作系统本地的编码方式,windows 一般为 GBK,linux 一般为 UTF-8。不同系统编码不同,不利于统一处理
  2. arg As LMBCS String
    不管操作系统本地的编码方式,统一采用 LMBCS。这种编码方式适用范围太小,仅 Lotus 自己的产品线
  3. arg As Unicode String
    不管操作系统本地的编码方式,统一采用 UTF-16。推荐使用此方式,对环境的适应性更好。
    但要注意 UTF-16 英文也用双字节存储,C 语言中会把其中的 0x00 识别为字符串结束。所以进入到 C 代码内部后,可以转为 UTF-8 存储。
    还要注意不同硬件下字节序的问题,x64 和 Power 架构下刚好是相反的。

Double 类型的传递

LS2C 在 64 位环境下的参数传递有 bug,浮点数无法正常处理。

bug 已经提交给 IBM 处理(更新:已确认不修复),在解决前可以先把参数改为按引用传递(即使是只读的参数,也不按值传递)。

资源的释放

有一部分内存在 C 代码中分配后,后续会被 LotusScript 端继续使用。所以需要在 LotusScript 端不再使用的时候,手工释放这部分资源。

推荐的做法是把这类资源包装成 LotusScript Class,在 Delete 析构函数中释放资源。这样相当于 LotusScript 引擎会帮忙自动跟踪变量的引用,比较方便。

调试共享库加载失败

Domino 给出的错误信息很模糊:“dll 加载失败”,没有太多有用的信息。而错误背后可能的原因有很多:文件找不到、32/64 位不匹配、缺少依赖的库……

可以先通过其他语言(比如 LuaJIT)调用共享库,会得到更详细的错误信息。成功之后再放到 Domino 环境下调试。

共享库的权限

Linux、Aix 环境下要注意上传的 so 库必须要有运行权限。

共享库的更新

Linux、Aix 环境下 so 库更新的行为有些奇怪,有时候覆盖 so 文件后会导致 Domino 崩溃。看上去像是内存中保留了一部分旧版的代码,与新版搭配在一起就不行了。

所以更新共享库时要停掉Domino。

总结

虽然 LotusScript 已经步入晚年,但我们希望通过 LS2C 这种方式给它注入新的活力,在既有业务资产中继续发光发热。

11月3日,IBM 发布了一个重要的信息:Notes/Domino 的发展路线图。原文件请点此查看,以下是我个人的解读:

Domino 又一次要死掉了?

前些时间由于 902 版本的取消,Domino 社区里引起了很多讨论,很多人对 Domino 后续的发展持悲观态度(例如:There is no 9.0.2. Dead. Canceled. Killed. )。

IBM 官方的回应是 901 版本的支持会延长到 2021 年,而且期间不仅是 bug fix,也包含新功能的增强,实际上的 902 版会以 901 feature pack 的形式发布。

产品更新策略

一个重要的变化,901 FP7 已经从 fix pack 转变为 feature pack,后续的所有新功能也都是以 FP 形式发布,所以才会有上文提及的取消 902 版本。

IBM 官方的说法是为了加快版本迭代速度,才引入了这个变化。其实这个行为也很好理解,类似于 Windows 10 的策略,似乎大家都不准备让产品的大版本号往前滚动了:毕竟升一个大版本要做很艰难的决定,小版本就简单多了不是么。

但是对于用户来讲,以前 FP 包发布后是可以无脑更新的,毕竟都是在改 bug;现在加入了新功能,是否要更新肯定就要打一个大大的问号了。

支持的操作系统

新的路线图中,将现有的操作系统分为两类:

  • Win/RHEL:支持后续的 feature pack,有功能的持续改进
  • 其他操作系统(AIX/IBM i):仅有安全性更新、bug修复

Verse/iNotes

Verse 的私有环境版(非云环境,Verse On-premises)将于 12 月发布。VOP 将替代现有的 iNotes,作为今后的新邮件客户端。

Notes/Domino 后续新功能

  • Java 1.8 + OSGi/Eclipse 4.x

祖传的 1.6 终于更新了,感动啊

  • 视图索引可移至 nsf 外部

这个还是不错的,nsf 就应该存持久化的数据;索引这种缓存类数据分离出来后,更方便独立优化

  • XPages

持续更新,说起来 XPages 早就已经开始版本内的功能迭代了(853 Upgrade Pack 1)

总结

总体来说,Domino 肯定会继续发展和更新的。但是主要集中在 Java 端:VOP 基于 Connections、XPages。

啥时候把祖传的 NSF 存储也更新一下啊,万恶的 16K、32K 限制……

在分析 Lotusscript 性能时,我们经常需要找到耗时最长的瓶颈点,然后再进行针对性的优化。这时最常用的工具是代理性能简要表,但它有个限制是只能记录 Domino 相关对象方法的运行时间,比如 db.getView、view.getAllDocumentsByKey 等,对于原生 Lotusscript 的语句无法记录。

这时我们就需要将代码打成各个片段,记录每个片段的运行时间以缩小范围、进而定位到性能瓶颈的几条语句。但是 Lotusscript 中的 Now 只能精确到秒,显然是不够用的。还好通过 getThreadInfo 我们可以获取到毫秒级的时间值,具体代码如下:

Dim startTic As Long
Dim seconds As Double

startTic = Getthreadinfo(6)
seconds = (Getthreadinfo(6) – startTic) / Getthreadinfo(7)

msgbox “Finished. The code ran for ” & seconds & ” seconds.”

Domino 环境中各种 ID 的介绍这篇文章中我介绍过,NSF 的副本 ID 实际上就是库的创建时间,文档 UNID 的后半部分也是文档的创建时间。我们来看看样例:

副本标识符:48257BA6:033771DF

文档UNID:9CC4EF2FC8A1639748257CB40036EBE6

看上去粗体的部分是很类似,只不过和时间看不出太大联系,应该是编码过了。

再看两个 Domino 自带数据库的文档样例。第一个是管理请求文档:

domino-hex-time-1第二个是 Catalog 文档:

domino-hex-time-2这两个文档存储的都是数据库副本 ID,但从右边的结果上看实际存储的都是时间值。看上去日期/时间和副本 ID 的 16 进制数应该是可以互相转换的。通过查看相关视图设计,我们发现一个文档中没有的公式函数:

@Text(“ProxyReplicaID”; “*”)

通过 @Text 的”*“参数,可以将一个时间值转换为对应的字符串。对应的,通过以下 Lotusscript 代码可以反过来转换:

Dim replicaTD as New NotesDateTime(dbTarget.ReplicaID)

参考文档:
Creating a replica ID item for adminp requestsHow to interpret the Hexadecimal values in a Time/Date value

几年前,我写过一篇关于复制冲突的文章,今天来聊聊它的小兄弟:保存冲突。

从名字上就能看出来,复制冲突是在两台服务器上修改同一个文档后产生的,而保存冲突则是在同一台服务器上修改同一个文档后产生的。

在 web 应用中修改一个文档,主要有两种方式:

  1. 以 ?EditDocument URL 命令打开文档,然后调用 form.submit() 保存当前文档
  2. 通过代理等后台代码的方式修改并保存文档

以下图的时间线场景为例:

由于代理的运行时间一般很短, 3、4 两个代理在时间线上可以考虑为点,所以理论上他们之间的时间是不会有交叉的,也就是两个代理保存同一个文档一般不会出现保存冲突

通过 ?EditDocument 方式一般持续时间较长,从打开文档到点击保存按钮这一段时间都是其处理时间,这期间出现保存冲突的可能性很大。例如:在用户A编辑期间,代理C保存了同一条文档,那么用户A点击保存按钮的时候,就会产生冲突文档。同样的,用户B编辑期间,用户A通过用户A点击保存按钮修改了同一条文档,用户B点击保存按钮的时候,就会产生冲突文档。

那么如何避免保存冲突的产生呢?NotesDocument.lock 方法就是针对这个问题的,但是很遗憾此方法在 web 端无法使用。所以我们需要自己实现一个文档锁的逻辑:每次进入编辑状态时加锁,离开编辑状态后解锁;未取得的锁的情况下不允许进入编辑状态。这样就能保证同时只有一人编辑文档,避免了保存冲突的产生。这里有一个思路供参考:Distributed Document Locking Web Style

再回到上面的一个问题,代理的运行时间短,一般不会导致冲突文档。如果代理的运行时间很长呢,两个代理保存同一个文档会不会产生冲突文档?为此我写了如下代码做测试:

Sub Initialize
Dim s As New NotesSession
Dim db As NotesDatabase
Dim doc As NotesDocument
MsgBox “agent start”
Set db = s.Currentdatabase
Set doc = db.Getdocumentbyunid(“FC71D0667744C41D48257D6C0011AB7F”)
Dim count As Integer
count  = doc.count(0)
MsgBox count
Sleep(5)
doc.count = count + 1
doc.agentname = “1”
MsgBox doc.save(true,true)
MsgBox “agent end”
End Sub

为了拉长运行时间,我在中间加了 Sleep(5)。连续运行两次代理,测试结果如下:

  • doc.save(false,false):后一次保存失败,文档未保存,返回值为 false
  • doc.save(false,true):后一次保存为前一次的答复文档(注意不是冲突文档),返回值为 true
  • doc.save(true,true):后一次保存覆盖前一次保存的内容,前一次的内容丢失,返回值为 true(第二个参数随便传true/false,没有影响)

总结一下:web 端通过 ?EditDocument 方式编辑文档时,很容易出现保存冲突,注意用文档那个锁的方式来避免;代理保存文档时,一般不会出现冲突,但如果 getDocument 至 doc.save 之间的代码逻辑很复杂、运行时间长,也要注意处理数据覆盖的问题。

今天是 2014 年的最后一天了,祝大家新年快乐!保存冲突系列还有一篇没写完,明年继续。

Edit:经测试发现,如果用户A和代理C是以同一个用户身份保存的,那么不会有冲突文档,结果是后保存的覆盖之前的。(这样也合理,同一个用户间的数据覆盖没问题,不同用户间就要保留两份文档,供管理员后续合并冲突文档时参考)

最近在项目上遇到了一个奇怪的问题:某个 XPages 页面偶发性报错,对数据库进行签名后问题消失,但过些时间就又会出现。

由于签名后问题消失,最开始排查时就认为是代码签名的问题,可能是服务器上有对应的模板,设计不定时的被 Design 任务刷新回来导致问题。但是通过各种方法找了很多遍,都确认库的设计没有被刷新过。

最终通过检查代码的错误行发现,是一处 session.getDatabase 调用时数据库路径的大小写有问题,修改成与文件系统一致后问题消失。问题虽然解决了,但为什么同样的代码大部分情况下没问题,只有偶发性的报错呢?同样的代码在同为 Linux 平台的产品测试环境上,为什么就一切正常呢?

带着这些疑问继续探索,终于找到了一些端倪。在这个技术文档中提到如下内容:

DB on server names AgentRunner.nsf. Notes the upper/lowercase.

Steps to Reproduce:

After a server restart or a “dbcache flush”:

> lo fixup agentrunner
> 25.06.2002 09:58:00   Database fixup process started
25.06.2002 09:58:00   Unable to fixup database agentrunner: File
does not exist
25.06.2002 09:58:00   Database fixup process shutdown
–> does not work, because db is not in server cache

lo fixup AgentRunner
> 25.06.2002 09:58:07   Database fixup process started
25.06.2002 09:58:07   Performing consistency check on
AgentRunner.nsf…
25.06.2002 09:58:08   Completed consistency check on
AgentRunner.nsf
25.06.2002 09:58:08   Database fixup process shutdown
–> works – it`s exact-case

> lo fixup agentrunner
> 25.06.2002 09:58:12   Database fixup process started
25.06.2002 09:58:12   Database fixup process shutdown
–> works now with lowercase, cause db is in cache

dbcache flush
> lo fixup agentrunner
> 25.06.2002 09:58:21   Database fixup process started
25.06.2002 09:58:21   Unable to fixup database agentrunner: File
does not exist
25.06.2002 09:58:21   Database fixup process shutdown
–> db removed from cache –> doesn`t work

整个过程总结如下:

  • 首先用大小写错误的文件名访问,报错
  • 再用大小写正确的文件名访问,正常
  • 再次用大小写错误的文件名访问,正常(因为上一条命令已经将数据库加入了 dbcache)
  • 清空 dbcache 后,再次用大小写错误的文件名访问,报错

导致这个现象的根本原因就是:

  • 当数据库不在 dbcache 中时,Domino 使用数据库文件名通过文件系统获取数据库对象,此时是区分大小写的
  • 当数据库在 dbcache 中时,Domino 使用数据库文件名在 dbcache 中查找,找到后直接返回数据库对象,此过程无需访问文件系统,所以就不区分大小写

分析到这里,最初的所有现象就能说通了:

  • 错误偶发,是因为大部分情况下此库都在 dbcache 中,可能只有服务器刚刚启动/缓存超限将此库移除时,才会报错
  • 签名后错误消失,是因为签名的过程肯定会把此库加入到 dbcache

吃一堑长一智,既然这个问题让我纠结了这么久,那么如何才能避免此类问题发生呢?建议如下:

  • 数据库名始终使用全小写,代码中获取数据库时也用全小写
  • *nux 测试环境下建议通过 NSF_DbCache_Disable=1 禁用缓存。虽然性能会差一些,但是能尽早的发现问题(千万别在生产服务器上这样做)

在使用 Lotusscript 开发 webservice 的过程中,有时会提示以下错误信息:

Web Service 已经保存,但是无效。: Please specify which class exposes your web service interface(s), using the ‘PortType class’ field of the Web Service properties panel.

按照提示排查,发现在属性框中’PortType class’已经指定了对应的类名。既然不是这里的问题,那只能回头看代码了:

Class Test
Sub printDoc(doc As NotesDocument)

End Sub
End Class

代码足够简单:仅有一个空白方法的 Class。

这个方法没有声明权限,按帮助中说的默认是 Public。那么此方法就会暴露到 WSDL 中,可以通过 webservice 调用。

问题出来了:通过 webservice 调用时如何能传进来一个 NotesDocument 对象呢?这显然是不可能的。赶紧把方法改成 Private 或者把 doc 参数改为 String 类型,问题就解决了。

总结:暴露给 webservice 的方法/函数,不能以 Domino 对象作为参数。

在对时间、数值域进行全文搜索的时候,如果使用了>、<等关系运算符,有时会报如下错误:

文本域不支持关系运算符

去文档域中查看一下,明明是日期域,怎么提示的却是文本域呢?

问题是这样的:在 nsf 中,有一个叫 UNK 表(可以用 notespeek 查看,在 Item Def Table 中)的东西,保存了数据库中所有字段的类型。当包含某个字段的第一个文档添加到库时,会写入 UNK 表。所以如果之前在数据库中曾经出现过同名的文本字段,那么这个 UNK 表中记录的就是文本型,所以会报前面说的错误。

原因搞清楚了,想要解决就简单了:

  1. 清除所有非日期型的同名字段
  2. 删除全文索引
  3. 压缩数据库,此时会重建 UNK 表(我测试时需要 compact -c 才行)
  4. 重建全文索引

参考资料:Searching on a field in Notes fails when indexer does not recognize field data type

通过 web 方式访问某个数据库时,Domino 报如下错误:

Error 500: Unknown OS error.

解决方法如下:

  • 集群环境:在 admin 端找到问题数据库,右键>集群…>集群可用性状态 选择 “使用中”>确定
  • 非集群环境:将问题库新建拷贝/新建副本 即可

IBM的技术支持文档,描述了此问题的解决方案,但并未说明是如何导致的。