From 3fb180a393860fee60bc8135e8c7773f077698e4 Mon Sep 17 00:00:00 2001 From: Steven Niu Date: Tue, 27 Oct 2026 05:59:11 +0000 Subject: [PATCH] add doc for dbms_lock package --- .../dbms_lock_impl.adoc | 177 +++++++++++ .../compat_dbms_lock.adoc | 298 ++++++++++++++++++ .../dbms_lock_impl.adoc | 177 +++++++++++ .../compat_dbms_lock.adoc | 298 ++++++++++++++++++ 4 files changed, 950 insertions(+) create mode 100644 CN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc create mode 100644 CN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc create mode 100644 EN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc diff --git a/CN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc b/CN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc new file mode 100644 index 00000000..9aab8bf9 --- /dev/null +++ b/CN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc @@ -0,0 +1,177 @@ +:sectnums: +:sectnumlevels: 5 + + += **功能概述** + +IvorySQL在 `ivorysql_ora` 扩展中提供兼容Oracle的内置包 `DBMS_LOCK`,用于创建应用锁、申请和释放共享锁或排他锁,以及暂停当前会话。当前实现提供 `ALLOCATE_UNIQUE`、`REQUEST`、`RELEASE` 和 `SLEEP` 四个子程序。 + +基本用法如下: + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('order_batch', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 10); + + IF v_result = dbms_lock.success THEN + -- 执行需要串行化的业务操作 + v_result := dbms_lock.release(v_handle); + END IF; +END; +/ +``` + +== 实现原理 + +=== 总体结构 + +`DBMS_LOCK`由C函数和PL/iSQL包包装器两层组成。 + +C实现位于: + +``` +contrib/ivorysql_ora/src/builtin_packages/dbms_lock/dbms_lock.c +``` + +SQL定义位于: + +``` +contrib/ivorysql_ora/src/builtin_packages/dbms_lock/dbms_lock--1.0.sql +``` + +SQL文件首先在 `sys` 模式注册四个 `STRICT` C函数: + +```sql +sys.dbms_lock_allocate_unique(text) RETURNS text +sys.dbms_lock_request(text, int, int) RETURNS int +sys.dbms_lock_release(text) RETURNS int +sys.dbms_lock_sleep(float8) RETURNS void +``` + +随后创建 `DBMS_LOCK` 包。包体中的子程序只负责调用上述C函数,使用户可以按Oracle包的形式访问功能。构建系统在 `contrib/ivorysql_ora/Makefile` 和 `contrib/ivorysql_ora/meson.build` 中编译 `dbms_lock.c`,扩展安装脚本 `ivorysql_ora--1.0.sql` 中也包含相同的包定义。 + +=== 锁模式与返回码 + +C代码定义并实现两种锁模式: + +[cols="2,1,4"] +|==== +|*包常量*|*值*|*内部实现* +|`S_MODE`|4|PostgreSQL会话级共享advisory lock +|`X_MODE`|6|PostgreSQL会话级排他advisory lock +|==== + +包声明公开以下返回码常量: + +[cols="2,1,4"] +|==== +|*包常量*|*值*|*含义* +|`SUCCESS`|0|操作成功 +|`TIMEOUT`|1|在指定时间内没有获得锁 +|`DEADLOCK`|2|死锁 +|`PARAMETER_ERROR`|3|参数错误 +|`ALREADY_OWNED`|4|当前会话已持有该锁 +|`ILLEGAL_HANDLE`|5|锁句柄无效 +|==== + +当前C实现会返回 `SUCCESS`、`TIMEOUT` 和 `PARAMETER_ERROR`。`DEADLOCK`、`ALREADY_OWNED` 和 `ILLEGAL_HANDLE` 已作为兼容常量公开,但当前代码没有返回这些值。传入除4和6以外的锁模式时,`is_exclusive_mode` 直接报告 `invalid DBMS_LOCK mode` 错误,而不是返回状态码。 + +=== ALLOCATE_UNIQUE句柄生成 + +`ivorysql_dbms_lock_allocate_unique` 将锁名称转换为C字符串,使用种子为0的 `hash_any_extended` 计算64位哈希值,再把无符号哈希值格式化为十进制文本。缓冲区最大长度为64字节,因此足以容纳生成的十进制句柄。 + +```text +lock name -> hash_any_extended(name, seed 0) -> uint64 -> decimal text handle +``` + +该实现不创建系统表记录,也不在数据库目录中持久化“名称到句柄”的映射。相同输入由同一算法生成相同句柄;调用方应把返回的句柄传给 `REQUEST` 和 `RELEASE`,不应依赖句柄的具体文本形式。 + +=== 锁句柄到advisory lock键的映射 + +`REQUEST` 和 `RELEASE` 通过 `lockname_to_key` 把句柄文本转换为PostgreSQL advisory lock使用的64位有符号键。函数分别计算两个32位哈希值: + +* 高32位使用 `hash_any`; +* 低32位使用种子为 `0x9e3779b9` 的 `hash_any_extended`; +* 将两部分拼接为一个 `int64` 锁键。 + +因此,`ALLOCATE_UNIQUE` 返回的是公开句柄,advisory lock键则是该句柄再次哈希后的内部值。哈希映射理论上存在碰撞可能,当前实现没有额外的名称目录用于检测或消除碰撞。 + +=== 后端私有锁记录表 + +每个数据库后端按需创建一个名为 `pg_dbms_lock table` 的私有哈希表,初始容量为128。哈希表以内部64位锁键为键,记录最近一次成功申请的锁模式: + +```c +typedef struct +{ + int64 key; + int8 mode; +} lock_entry; +``` + +`dbms_lock_record_acquire` 在申请成功后写入记录,`dbms_lock_check` 在释放前检查当前后端记录的模式,`dbms_lock_record_release` 在释放成功后删除记录。该表只存在于当前后端内存中,不在会话之间共享;实际的跨会话互斥由PostgreSQL锁管理器中的advisory lock完成。 + +=== REQUEST锁申请流程 + +`ivorysql_dbms_lock_request` 接受句柄、模式和以秒为单位的整数超时时间,其执行流程如下: + +1. 将句柄映射为64位advisory lock键; +2. 将 `X_MODE` 映射到 `pg_try_advisory_lock_int8`,将 `S_MODE` 映射到 `pg_try_advisory_lock_shared_int8`; +3. 尝试获取会话级advisory lock; +4. 成功时把键和模式写入后端私有哈希表并返回0; +5. 失败且 `timeout = 0` 时立即返回1; +6. 失败且尚未超时时,每100毫秒重试一次,并调用 `CHECK_FOR_INTERRUPTS()` 响应取消、中断等事件。 + +`timeout < 0` 时返回3。超时计算使用 `GetCurrentTimestamp()`,将已经过的微秒换算成整数秒后与传入值比较。 + +这里调用的是会话级advisory lock接口,所以锁不会随当前事务的提交或回滚自动释放。正常使用时必须调用 `DBMS_LOCK.RELEASE`,或者通过结束数据库会话让PostgreSQL清理该会话持有的锁。 + +=== RELEASE锁释放流程 + +`ivorysql_dbms_lock_release` 首先把句柄转换为内部锁键,然后查询当前后端的私有哈希表: + +* 记录为 `S_MODE` 时,调用 `pg_advisory_unlock_shared_int8`; +* 记录为 `X_MODE` 时,调用 `pg_advisory_unlock_int8`; +* PostgreSQL确认释放成功后,从私有哈希表删除记录并返回0; +* 找不到本后端的记录或底层释放失败时返回3。 + +私有哈希表使 `RELEASE` 能选择正确的共享或排他解锁函数,但目前每个锁键只保存一个模式和一条记录。PostgreSQL会话级advisory lock允许同一会话重复申请并维护引用计数,而该私有表不维护申请次数。因此,同一会话不应在未释放前对同一句柄重复申请,也不应依赖锁模式转换行为。 + +=== SLEEP等待实现 + +`ivorysql_dbms_lock_sleep` 接受双精度秒数,将其换算为微秒,并按每段最多一秒调用 `pg_usleep`。每段等待之后执行 `CHECK_FOR_INTERRUPTS()`,因此长时间等待仍可响应查询取消和服务器中断。 + +负数会报告 `DBMS_LOCK.SLEEP: seconds must be non-negative`;大于100000000秒会报告参数过大。参数是 `float8`,因此支持小数秒。C函数声明为 `STRICT`,传入NULL时不会执行函数体。 + +=== 构建与回归测试 + +Oracle兼容回归测试输入和预期输出分别位于: + +``` +contrib/ivorysql_ora/sql/dbms_lock.sql +contrib/ivorysql_ora/expected/dbms_lock.out +``` + +测试由 `contrib/ivorysql_ora/Makefile` 注册,覆盖以下行为: + +* 调用 `DBMS_LOCK.SLEEP(1)`; +* 分配句柄并成功申请 `S_MODE`,在 `pg_locks` 中确认模式为 `ShareLock`; +* 释放共享锁,并确认 `pg_locks` 中不再存在该锁; +* 分配句柄并成功申请 `X_MODE`,在 `pg_locks` 中确认模式为 `ExclusiveLock`; +* 释放排他锁,并确认 `pg_locks` 中不再存在该锁。 + +可在已构建并安装的环境中通过 `make oracle-check` 执行Oracle兼容回归测试。 + +=== 当前实现边界 + +当前实现与完整Oracle `DBMS_LOCK` 接口相比存在以下边界: + +* 只支持 `S_MODE` 和 `X_MODE`,不支持其他Oracle锁模式; +* `REQUEST` 只提供句柄、模式和超时三个参数,没有 `release_on_commit` 参数; +* 使用会话级锁,事务结束不会自动释放; +* 没有持久化锁名称,也不校验任意文本是否为 `ALLOCATE_UNIQUE` 生成的合法句柄; +* `DEADLOCK`、`ALREADY_OWNED` 和 `ILLEGAL_HANDLE` 仅定义为包常量,当前路径不会返回; +* 同一会话重复申请同一个锁可能导致PostgreSQL锁引用计数与私有哈希表记录不一致; +* 当前回归测试验证单会话申请、锁模式和释放,尚未覆盖多会话竞争、超时及重复申请。 diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc new file mode 100644 index 00000000..7a8641d1 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc @@ -0,0 +1,298 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += DBMS_LOCK + +== 目的 + +IvorySQL提供兼容Oracle的内置包 `DBMS_LOCK`。应用可以使用命名句柄协调多个数据库会话,以共享方式或排他方式保护业务资源;包还提供可响应中断的会话等待功能。 + +本文档介绍IvorySQL当前支持的 `DBMS_LOCK` 常量、子程序、返回值和使用限制。 + +== 功能说明 + +当前包提供以下接口: + +[cols="2,4"] +|==== +|*接口*|*用途* +|`ALLOCATE_UNIQUE`|根据锁名称生成锁句柄 +|`REQUEST`|申请共享锁或排他锁 +|`RELEASE`|释放当前会话通过本包申请的锁 +|`SLEEP`|暂停当前会话指定的秒数 +|==== + +`DBMS_LOCK` 随 `ivorysql_ora` 扩展安装,以下示例应在Oracle兼容模式以及已经安装该扩展的数据库中执行。 + +=== 锁模式常量 + +IvorySQL当前支持两种锁模式: + +[cols="2,1,4"] +|==== +|*常量*|*值*|*说明* +|`dbms_lock.s_mode`|4|共享锁;多个会话可以同时持有同一资源的共享锁,但与排他锁冲突 +|`dbms_lock.x_mode`|6|排他锁;同一时刻只允许一个会话以排他方式持有资源,并与共享锁冲突 +|==== + +其他数值不是当前支持的锁模式,传入后会报告 `invalid DBMS_LOCK mode` 错误。 + +=== 返回码常量 + +[cols="2,1,4"] +|==== +|*常量*|*值*|*说明* +|`dbms_lock.success`|0|操作成功 +|`dbms_lock.timeout`|1|等待超时 +|`dbms_lock.deadlock`|2|死锁;当前实现不会返回此值 +|`dbms_lock.parameter_error`|3|参数错误,或者无法按当前会话记录释放锁 +|`dbms_lock.already_owned`|4|已经持有;当前实现不会返回此值 +|`dbms_lock.illegal_handle`|5|非法句柄;当前实现不会返回此值 +|==== + +调用程序应始终检查 `REQUEST` 和 `RELEASE` 的返回值,不应把所有非0值都解释成同一种错误。 + +=== ALLOCATE_UNIQUE + +`ALLOCATE_UNIQUE` 根据名称生成一个文本锁句柄。 + +语法如下: + +```sql +dbms_lock.allocate_unique( + lockname IN text, + lockname_handle OUT text +); +``` + +参数说明: + +* `lockname`:应用定义的锁名称; +* `lockname_handle`:输出的文本句柄,后续传给 `REQUEST` 和 `RELEASE`。 + +示例: + +```sql +DECLARE + v_handle text; +BEGIN + dbms_lock.allocate_unique('monthly_report', v_handle); + RAISE NOTICE 'handle=%', v_handle; +END; +/ +``` + +当前实现通过哈希算法直接生成句柄,不在数据库中保存名称。相同名称会生成相同句柄。应用应把锁名称设计为稳定且能够表示业务资源的字符串,例如 `invoice:2026-10`,并避免依赖句柄的具体数字内容。 + +=== REQUEST + +`REQUEST` 尝试为句柄申请锁。 + +语法如下: + +```sql +dbms_lock.request( + lockname_handle IN text, + mode IN integer, + timeout IN integer +) RETURN integer; +``` + +参数说明: + +* `lockname_handle`:由 `ALLOCATE_UNIQUE` 返回的句柄; +* `mode`:`dbms_lock.s_mode` 或 `dbms_lock.x_mode`; +* `timeout`:等待的最长秒数。0表示只尝试一次,正整数表示在超时前循环尝试,负数返回 `dbms_lock.parameter_error`。 + +成功时返回 `dbms_lock.success`,指定时间内未获得锁时返回 `dbms_lock.timeout`。 + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('monthly_report', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 5); + + CASE v_result + WHEN dbms_lock.success THEN + RAISE NOTICE 'lock acquired'; + WHEN dbms_lock.timeout THEN + RAISE NOTICE 'lock request timed out'; + ELSE + RAISE EXCEPTION 'DBMS_LOCK.REQUEST failed: %', v_result; + END CASE; +END; +/ +``` + +申请得到的是会话级锁,不会在 `COMMIT` 或 `ROLLBACK` 时自动释放。应用必须保存句柄并显式调用 `RELEASE`,通常应在正常路径和异常处理路径中都执行释放。 + +=== RELEASE + +`RELEASE` 释放当前会话使用本包申请的锁。 + +语法如下: + +```sql +dbms_lock.release(lockname_handle IN text) RETURN integer; +``` + +释放成功返回 `dbms_lock.success`。当前会话没有通过本包记录该句柄、或者底层锁没有成功释放时,返回 `dbms_lock.parameter_error`。 + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('monthly_report', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 5); + + IF v_result = dbms_lock.success THEN + -- 受锁保护的操作 + v_result := dbms_lock.release(v_handle); + IF v_result <> dbms_lock.success THEN + RAISE EXCEPTION 'DBMS_LOCK.RELEASE failed: %', v_result; + END IF; + END IF; +END; +/ +``` + +不要在同一会话中对同一句柄连续多次成功调用 `REQUEST` 后只调用一次 `RELEASE`。当前包只保存一条本地模式记录,而PostgreSQL底层会对重复申请进行计数,这种用法可能留下仍被会话持有的锁。 + +=== SLEEP + +`SLEEP` 暂停当前会话,参数单位为秒并支持小数。 + +语法如下: + +```sql +dbms_lock.sleep(seconds IN double precision); +``` + +示例: + +```sql +CALL dbms_lock.sleep(0.5); +``` + +等待期间可以响应查询取消。秒数不能为负数,也不能大于100000000;超出范围会报告错误。 + +=== 共享锁示例 + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('configuration_snapshot', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.s_mode, 0); + IF v_result = dbms_lock.success THEN + RAISE NOTICE 'shared lock acquired'; + ELSE + RAISE NOTICE 'shared lock not acquired, rc=%', v_result; + END IF; +END; +/ +``` + +成功申请后,可以在当前会话查询 `pg_locks`,对应模式为 `ShareLock`: + +```sql +SELECT mode +FROM pg_locks +WHERE pid = pg_backend_pid() + AND locktype = 'advisory'; +``` + +=== 排他锁与异常清理示例 + +以下示例在异常处理器中尝试释放锁,避免业务语句失败后把会话级锁保留到后续操作中: + +```sql +DECLARE + v_handle text; + v_result integer; + v_acquired boolean := false; +BEGIN + dbms_lock.allocate_unique('settlement_job', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 10); + + IF v_result <> dbms_lock.success THEN + RAISE EXCEPTION 'cannot acquire settlement lock, rc=%', v_result; + END IF; + + v_acquired := true; + -- 执行需要排他保护的业务操作 + + v_result := dbms_lock.release(v_handle); + v_acquired := false; + IF v_result <> dbms_lock.success THEN + RAISE EXCEPTION 'cannot release settlement lock, rc=%', v_result; + END IF; +EXCEPTION + WHEN OTHERS THEN + IF v_acquired THEN + v_result := dbms_lock.release(v_handle); + END IF; + RAISE; +END; +/ +``` + +排他锁在 `pg_locks` 中显示为 `ExclusiveLock`。 + +=== 使用注意事项 + +* 当前只支持共享模式和排他模式; +* 锁是会话级的,事务提交或回滚不会自动释放; +* 当前 `REQUEST` 没有 `release_on_commit` 参数; +* 任意文本都可以作为 `REQUEST` 的句柄参数,当前实现不校验句柄是否来自 `ALLOCATE_UNIQUE`; +* 名称和句柄最终通过哈希映射到64位锁键,理论上存在哈希碰撞可能; +* 同一会话不要重复申请同一句柄,也不要尝试通过重复 `REQUEST` 转换锁模式; +* 会话异常退出时,PostgreSQL会清理该会话持有的advisory lock;正常长连接中仍应显式释放; +* 包声明中的部分Oracle兼容返回码目前只是预留常量,实际实现不会返回这些状态。 + +== 测试用例 + +IvorySQL回归测试分别验证共享锁和排他锁的申请、`pg_locks` 中的模式以及释放结果。可使用下面的精简用例验证排他锁: + +```sql +CREATE TABLE dbms_lock_handle(handle text); + +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('LOCKX', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 0); + ASSERT v_result = dbms_lock.success, + format('DBMS_LOCK.REQUEST failed: rc=%s', v_result); + INSERT INTO dbms_lock_handle VALUES (v_handle); +END; +/ + +SELECT mode +FROM pg_locks +WHERE pid = pg_backend_pid() + AND locktype = 'advisory' + AND mode = 'ExclusiveLock'; + +DECLARE + v_handle text; + v_result integer; +BEGIN + SELECT handle INTO v_handle FROM dbms_lock_handle; + v_result := dbms_lock.release(v_handle); + ASSERT v_result = dbms_lock.success, + format('DBMS_LOCK.RELEASE failed: rc=%s', v_result); +END; +/ + +DROP TABLE dbms_lock_handle; +``` + +申请成功后查询应返回 `ExclusiveLock`;调用 `RELEASE` 后,当前会话中对应的advisory lock应消失。 diff --git a/EN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc new file mode 100644 index 00000000..8d0c6469 --- /dev/null +++ b/EN/modules/ROOT/pages/master/compatibility_features_design/dbms_lock_impl.adoc @@ -0,0 +1,177 @@ +:sectnums: +:sectnumlevels: 5 + + += **Feature Overview** + +IvorySQL provides the Oracle-compatible built-in package `DBMS_LOCK` in the `ivorysql_ora` extension. It creates application locks, acquires and releases shared or exclusive locks, and pauses the current session. The current implementation provides four subprograms: `ALLOCATE_UNIQUE`, `REQUEST`, `RELEASE`, and `SLEEP`. + +Basic usage: + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('order_batch', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 10); + + IF v_result = dbms_lock.success THEN + -- Perform business operations that must be serialized + v_result := dbms_lock.release(v_handle); + END IF; +END; +/ +``` + +== Implementation + +=== Overall Structure + +`DBMS_LOCK` consists of two layers: C functions and a PL/iSQL package wrapper. + +The C implementation is located at: + +``` +contrib/ivorysql_ora/src/builtin_packages/dbms_lock/dbms_lock.c +``` + +The SQL definitions are located at: + +``` +contrib/ivorysql_ora/src/builtin_packages/dbms_lock/dbms_lock--1.0.sql +``` + +The SQL file first registers four `STRICT` C functions in the `sys` schema: + +```sql +sys.dbms_lock_allocate_unique(text) RETURNS text +sys.dbms_lock_request(text, int, int) RETURNS int +sys.dbms_lock_release(text) RETURNS int +sys.dbms_lock_sleep(float8) RETURNS void +``` + +It then creates the `DBMS_LOCK` package. The subprograms in the package body simply call these C functions, allowing users to access the functionality in Oracle package form. The build system compiles `dbms_lock.c` through `contrib/ivorysql_ora/Makefile` and `contrib/ivorysql_ora/meson.build`. The extension installation script `ivorysql_ora--1.0.sql` also contains the same package definition. + +=== Lock Modes and Return Codes + +The C code defines and implements two lock modes: + +[cols="2,1,4"] +|==== +|*Package Constant*|*Value*|*Internal Implementation* +|`S_MODE`|4|PostgreSQL session-level shared advisory lock +|`X_MODE`|6|PostgreSQL session-level exclusive advisory lock +|==== + +The package declaration exposes the following return-code constants: + +[cols="2,1,4"] +|==== +|*Package Constant*|*Value*|*Meaning* +|`SUCCESS`|0|The operation succeeded +|`TIMEOUT`|1|The lock was not acquired within the specified time +|`DEADLOCK`|2|Deadlock +|`PARAMETER_ERROR`|3|Invalid parameter +|`ALREADY_OWNED`|4|The current session already owns the lock +|`ILLEGAL_HANDLE`|5|The lock handle is invalid +|==== + +The current C implementation returns `SUCCESS`, `TIMEOUT`, and `PARAMETER_ERROR`. `DEADLOCK`, `ALREADY_OWNED`, and `ILLEGAL_HANDLE` are exposed as compatibility constants, but the current code does not return these values. When a lock mode other than 4 or 6 is passed, `is_exclusive_mode` raises an `invalid DBMS_LOCK mode` error directly instead of returning a status code. + +=== ALLOCATE_UNIQUE Handle Generation + +`ivorysql_dbms_lock_allocate_unique` converts the lock name to a C string, computes a 64-bit hash using `hash_any_extended` with a seed of 0, and formats the unsigned hash value as decimal text. The buffer has a maximum length of 64 bytes, which is sufficient for the generated decimal handle. + +```text +lock name -> hash_any_extended(name, seed 0) -> uint64 -> decimal text handle +``` + +The implementation neither creates a system-table record nor persists the name-to-handle mapping in the database catalog. The same input generates the same handle through the same algorithm. Callers should pass the returned handle to `REQUEST` and `RELEASE` and should not rely on its specific textual representation. + +=== Mapping a Lock Handle to an Advisory Lock Key + +`REQUEST` and `RELEASE` use `lockname_to_key` to convert the handle text to the 64-bit signed key used by PostgreSQL advisory locks. The function calculates two 32-bit hash values: + +* The high-order 32 bits use `hash_any`; +* The low-order 32 bits use `hash_any_extended` with a seed of `0x9e3779b9`; +* The two parts are combined into one `int64` lock key. + +Therefore, the value returned by `ALLOCATE_UNIQUE` is the public handle, while the advisory lock key is an internal value obtained by hashing that handle again. Hash collisions are theoretically possible, and the current implementation has no additional name directory to detect or eliminate them. + +=== Backend-Local Lock Record Table + +Each database backend creates a private hash table named `pg_dbms_lock table` on demand, with an initial capacity of 128. The table is keyed by the internal 64-bit lock key and records the mode of the most recently acquired lock: + +```c +typedef struct +{ + int64 key; + int8 mode; +} lock_entry; +``` + +`dbms_lock_record_acquire` writes a record after a successful acquisition, `dbms_lock_check` checks the current backend's recorded mode before release, and `dbms_lock_record_release` removes the record after a successful release. The table exists only in the current backend's memory and is not shared between sessions. Actual cross-session mutual exclusion is provided by advisory locks in the PostgreSQL lock manager. + +=== REQUEST Lock Acquisition Flow + +`ivorysql_dbms_lock_request` accepts a handle, a mode, and an integer timeout in seconds. It performs the following steps: + +1. Maps the handle to a 64-bit advisory lock key; +2. Maps `X_MODE` to `pg_try_advisory_lock_int8` and `S_MODE` to `pg_try_advisory_lock_shared_int8`; +3. Attempts to acquire the session-level advisory lock; +4. On success, writes the key and mode to the backend-local hash table and returns 0; +5. On failure with `timeout = 0`, immediately returns 1; +6. On failure before the timeout expires, retries every 100 milliseconds and calls `CHECK_FOR_INTERRUPTS()` to respond to cancellation, interrupts, and similar events. + +When `timeout < 0`, the function returns 3. Timeout calculation uses `GetCurrentTimestamp()`, converts the elapsed microseconds to whole seconds, and compares the result with the supplied value. + +The implementation calls session-level advisory lock interfaces, so a lock is not released automatically when the current transaction commits or rolls back. During normal use, the caller must invoke `DBMS_LOCK.RELEASE`, or PostgreSQL must clean up the lock when the database session ends. + +=== RELEASE Lock Release Flow + +`ivorysql_dbms_lock_release` first converts the handle to an internal lock key and then queries the current backend's private hash table: + +* If the recorded mode is `S_MODE`, it calls `pg_advisory_unlock_shared_int8`; +* If the recorded mode is `X_MODE`, it calls `pg_advisory_unlock_int8`; +* After PostgreSQL confirms successful release, it removes the record from the private hash table and returns 0; +* If no record is found for the current backend or the underlying release fails, it returns 3. + +The private hash table allows `RELEASE` to select the correct shared or exclusive unlock function, but it currently stores only one mode and one record per lock key. PostgreSQL session-level advisory locks allow the same session to acquire a lock repeatedly and maintain a reference count, while this private table does not track the number of acquisitions. A session should therefore not acquire the same handle again before releasing it and should not rely on lock-mode conversion behavior. + +=== SLEEP Implementation + +`ivorysql_dbms_lock_sleep` accepts a double-precision number of seconds, converts it to microseconds, and calls `pg_usleep` in intervals of no more than one second. It executes `CHECK_FOR_INTERRUPTS()` after every interval, so a long sleep can still respond to query cancellation and server interrupts. + +A negative value raises `DBMS_LOCK.SLEEP: seconds must be non-negative`; a value greater than 100000000 seconds raises an argument-too-large error. The argument is a `float8`, so fractional seconds are supported. The C function is declared `STRICT`, so its body is not executed when NULL is passed. + +=== Build and Regression Tests + +The Oracle-compatible regression test input and expected output are located at: + +``` +contrib/ivorysql_ora/sql/dbms_lock.sql +contrib/ivorysql_ora/expected/dbms_lock.out +``` + +The tests are registered by `contrib/ivorysql_ora/Makefile` and cover the following behavior: + +* Calling `DBMS_LOCK.SLEEP(1)`; +* Allocating a handle, successfully acquiring `S_MODE`, and confirming that the mode in `pg_locks` is `ShareLock`; +* Releasing the shared lock and confirming that it no longer exists in `pg_locks`; +* Allocating a handle, successfully acquiring `X_MODE`, and confirming that the mode in `pg_locks` is `ExclusiveLock`; +* Releasing the exclusive lock and confirming that it no longer exists in `pg_locks`. + +The Oracle-compatible regression tests can be run with `make oracle-check` in a built and installed environment. + +=== Current Implementation Limitations + +Compared with the complete Oracle `DBMS_LOCK` interface, the current implementation has the following limitations: + +* Only `S_MODE` and `X_MODE` are supported; other Oracle lock modes are not supported; +* `REQUEST` accepts only a handle, a mode, and a timeout; it has no `release_on_commit` parameter; +* The implementation uses session-level locks, which are not released automatically when a transaction ends; +* Lock names are not persisted, and arbitrary text is not checked to determine whether it is a valid handle generated by `ALLOCATE_UNIQUE`; +* `DEADLOCK`, `ALREADY_OWNED`, and `ILLEGAL_HANDLE` are defined only as package constants and are not returned by the current code paths; +* Repeatedly acquiring the same lock in one session can cause the PostgreSQL lock reference count to become inconsistent with the private hash-table record; +* The current regression tests verify single-session acquisition, lock modes, and release, but do not yet cover multi-session contention, timeouts, or repeated acquisition. diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc new file mode 100644 index 00000000..b2962eac --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbms_lock.adoc @@ -0,0 +1,298 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += DBMS_LOCK + +== Purpose + +IvorySQL provides the Oracle-compatible built-in package `DBMS_LOCK`. Applications can use named handles to coordinate multiple database sessions and protect business resources in shared or exclusive mode. The package also provides an interruptible session sleep function. + +This document describes the `DBMS_LOCK` constants, subprograms, return values, and usage restrictions currently supported by IvorySQL. + +== Features + +The package currently provides the following interfaces: + +[cols="2,4"] +|==== +|*Interface*|*Purpose* +|`ALLOCATE_UNIQUE`|Generates a lock handle from a lock name +|`REQUEST`|Requests a shared or exclusive lock +|`RELEASE`|Releases a lock acquired by the current session through this package +|`SLEEP`|Pauses the current session for the specified number of seconds +|==== + +`DBMS_LOCK` is installed with the `ivorysql_ora` extension. The following examples must be run in Oracle-compatible mode in a database where this extension has been installed. + +=== Lock Mode Constants + +IvorySQL currently supports two lock modes: + +[cols="2,1,4"] +|==== +|*Constant*|*Value*|*Description* +|`dbms_lock.s_mode`|4|Shared lock; multiple sessions can hold a shared lock on the same resource concurrently, but it conflicts with an exclusive lock +|`dbms_lock.x_mode`|6|Exclusive lock; only one session can hold the resource exclusively at a time, and it conflicts with shared locks +|==== + +Other values are not currently supported lock modes and result in an `invalid DBMS_LOCK mode` error. + +=== Return Code Constants + +[cols="2,1,4"] +|==== +|*Constant*|*Value*|*Description* +|`dbms_lock.success`|0|The operation succeeded +|`dbms_lock.timeout`|1|The wait timed out +|`dbms_lock.deadlock`|2|Deadlock; the current implementation does not return this value +|`dbms_lock.parameter_error`|3|Invalid parameter, or the lock cannot be released according to the current session's records +|`dbms_lock.already_owned`|4|Already owned; the current implementation does not return this value +|`dbms_lock.illegal_handle`|5|Invalid handle; the current implementation does not return this value +|==== + +Calling programs should always check the return values of `REQUEST` and `RELEASE` and should not interpret all nonzero values as the same error. + +=== ALLOCATE_UNIQUE + +`ALLOCATE_UNIQUE` generates a text lock handle from a name. + +Syntax: + +```sql +dbms_lock.allocate_unique( + lockname IN text, + lockname_handle OUT text +); +``` + +Parameters: + +* `lockname`: An application-defined lock name; +* `lockname_handle`: The output text handle, which is subsequently passed to `REQUEST` and `RELEASE`. + +Example: + +```sql +DECLARE + v_handle text; +BEGIN + dbms_lock.allocate_unique('monthly_report', v_handle); + RAISE NOTICE 'handle=%', v_handle; +END; +/ +``` + +The current implementation generates the handle directly with a hash algorithm and does not store the name in the database. The same name generates the same handle. Applications should use stable lock names that identify business resources, such as `invoice:2026-10`, and should not rely on the specific numeric contents of a handle. + +=== REQUEST + +`REQUEST` attempts to acquire a lock for a handle. + +Syntax: + +```sql +dbms_lock.request( + lockname_handle IN text, + mode IN integer, + timeout IN integer +) RETURN integer; +``` + +Parameters: + +* `lockname_handle`: A handle returned by `ALLOCATE_UNIQUE`; +* `mode`: `dbms_lock.s_mode` or `dbms_lock.x_mode`; +* `timeout`: The maximum wait time in seconds. A value of 0 means to try only once, a positive integer means to retry until the timeout expires, and a negative value returns `dbms_lock.parameter_error`. + +The function returns `dbms_lock.success` on success and `dbms_lock.timeout` if the lock is not acquired within the specified time. + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('monthly_report', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 5); + + CASE v_result + WHEN dbms_lock.success THEN + RAISE NOTICE 'lock acquired'; + WHEN dbms_lock.timeout THEN + RAISE NOTICE 'lock request timed out'; + ELSE + RAISE EXCEPTION 'DBMS_LOCK.REQUEST failed: %', v_result; + END CASE; +END; +/ +``` + +The acquired lock is session-level and is not released automatically by `COMMIT` or `ROLLBACK`. The application must retain the handle and explicitly call `RELEASE`, normally on both the success and exception-handling paths. + +=== RELEASE + +`RELEASE` releases a lock acquired by the current session through this package. + +Syntax: + +```sql +dbms_lock.release(lockname_handle IN text) RETURN integer; +``` + +The function returns `dbms_lock.success` when the lock is released successfully. It returns `dbms_lock.parameter_error` if the current session has no package record for the handle or if the underlying lock cannot be released. + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('monthly_report', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 5); + + IF v_result = dbms_lock.success THEN + -- Perform operations protected by the lock + v_result := dbms_lock.release(v_handle); + IF v_result <> dbms_lock.success THEN + RAISE EXCEPTION 'DBMS_LOCK.RELEASE failed: %', v_result; + END IF; + END IF; +END; +/ +``` + +Do not successfully call `REQUEST` multiple times in succession for the same handle in one session and then call `RELEASE` only once. The current package stores only one local mode record, while the underlying PostgreSQL implementation counts repeated acquisitions. This usage can leave a lock held by the session. + +=== SLEEP + +`SLEEP` pauses the current session. Its argument is specified in seconds and can include a fractional part. + +Syntax: + +```sql +dbms_lock.sleep(seconds IN double precision); +``` + +Example: + +```sql +CALL dbms_lock.sleep(0.5); +``` + +The sleep can be interrupted by query cancellation. The number of seconds cannot be negative or greater than 100000000; a value outside this range causes an error. + +=== Shared Lock Example + +```sql +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('configuration_snapshot', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.s_mode, 0); + IF v_result = dbms_lock.success THEN + RAISE NOTICE 'shared lock acquired'; + ELSE + RAISE NOTICE 'shared lock not acquired, rc=%', v_result; + END IF; +END; +/ +``` + +After the lock is acquired successfully, query `pg_locks` in the current session. The corresponding mode is `ShareLock`: + +```sql +SELECT mode +FROM pg_locks +WHERE pid = pg_backend_pid() + AND locktype = 'advisory'; +``` + +=== Exclusive Lock and Exception Cleanup Example + +The following example attempts to release the lock in an exception handler, preventing a session-level lock from remaining in the session after a business statement fails: + +```sql +DECLARE + v_handle text; + v_result integer; + v_acquired boolean := false; +BEGIN + dbms_lock.allocate_unique('settlement_job', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 10); + + IF v_result <> dbms_lock.success THEN + RAISE EXCEPTION 'cannot acquire settlement lock, rc=%', v_result; + END IF; + + v_acquired := true; + -- Perform business operations that require exclusive protection + + v_result := dbms_lock.release(v_handle); + v_acquired := false; + IF v_result <> dbms_lock.success THEN + RAISE EXCEPTION 'cannot release settlement lock, rc=%', v_result; + END IF; +EXCEPTION + WHEN OTHERS THEN + IF v_acquired THEN + v_result := dbms_lock.release(v_handle); + END IF; + RAISE; +END; +/ +``` + +An exclusive lock appears as `ExclusiveLock` in `pg_locks`. + +=== Usage Notes + +* Only shared and exclusive modes are currently supported; +* Locks are session-level and are not released automatically when a transaction commits or rolls back; +* `REQUEST` currently has no `release_on_commit` parameter; +* Any text can be passed to `REQUEST` as a handle. The current implementation does not verify that the handle was generated by `ALLOCATE_UNIQUE`; +* Names and handles are ultimately mapped to 64-bit lock keys by hashing, so hash collisions are theoretically possible; +* Do not acquire the same handle repeatedly in one session or attempt to convert the lock mode through repeated calls to `REQUEST`; +* PostgreSQL cleans up advisory locks held by a session if the session terminates abnormally. A normal long-lived connection should still release them explicitly; +* Some Oracle-compatible return codes in the package declaration are currently reserved constants and are not returned by the implementation. + +== Test Cases + +IvorySQL regression tests verify shared- and exclusive-lock acquisition, the modes reported in `pg_locks`, and the release results. The following abbreviated test case can be used to verify an exclusive lock: + +```sql +CREATE TABLE dbms_lock_handle(handle text); + +DECLARE + v_handle text; + v_result integer; +BEGIN + dbms_lock.allocate_unique('LOCKX', v_handle); + v_result := dbms_lock.request(v_handle, dbms_lock.x_mode, 0); + ASSERT v_result = dbms_lock.success, + format('DBMS_LOCK.REQUEST failed: rc=%s', v_result); + INSERT INTO dbms_lock_handle VALUES (v_handle); +END; +/ + +SELECT mode +FROM pg_locks +WHERE pid = pg_backend_pid() + AND locktype = 'advisory' + AND mode = 'ExclusiveLock'; + +DECLARE + v_handle text; + v_result integer; +BEGIN + SELECT handle INTO v_handle FROM dbms_lock_handle; + v_result := dbms_lock.release(v_handle); + ASSERT v_result = dbms_lock.success, + format('DBMS_LOCK.RELEASE failed: rc=%s', v_result); +END; +/ + +DROP TABLE dbms_lock_handle; +``` + +After the lock is acquired successfully, the query should return `ExclusiveLock`. After `RELEASE` is called, the corresponding advisory lock should disappear from the current session.