传播错误并回滚跨过程调用的事务?

问题描述 投票:0回答:1

我的问题涉及当事务跨越几个级别的过程调用时捕获错误并回滚数据库事务。

我编写代码,首先只是为了让它工作,并对所有数据库代码使用了很多

if [catch {db eval {...}} result catchDict]
语句;然后尝试将其包装在交易中。

有必要重新抛出捕获的错误,我尝试了类似的代码

if [catch {db eval {...}} result catchDict] {
  dict incr catchDict -level 2
  return -options $catchDict $result
}

这似乎在将错误传播到调用过程方面起作用。然而,它不适用于

db transaction {...}
,但当然我可能做错了什么。我试过了

if [catch {db transaction immediate {::DOCS::AppendToUndoChain $request}} result catchDict] {
puts "rolling back"
puts $result
puts $catchDict
}

在发生错误时停止了

AppendToUndoChain
中的执行,但没有回滚由
AppendToUndoChain
调用/调用的过程或该过程下的过程中执行的数据库更改。

以下内容似乎工作正常并回滚所有内容,无论执行查询的过程调用级别如何。

db eval {begin immediate;}
if [catch {::DOCS::AppendToUndoChain $request} result catchDict] {
 db eval {rollback;}
 puts "Rolled back."
 puts $result
} else {
  db eval {commit;}
}

问题 1: 我是否在

db transaction {...}
上做错了什么,或者重新抛出错误,以致它不会在所有过程中回滚?

问题 2: 是否真的需要在每个

catch
上添加一个
db eval {...}
,或者调用
::DOCS::AppendToUndoChain
的人是否可以处理这一切,以便该过程中以及该过程“下面”的任何过程中的所有错误都将得到解决。 “向上”传播到此
catch
并处理错误并回滚事务,以便所有查询都可以编码为
set result [db eval {...}]
而不是
if [catch {db eval {...}} result]

我尝试在相对于这个单一/外部

catch
的不同级别的程序中引入不同类型的错误,它似乎捕获了所有错误并回滚了整个事务;但我想知道这是否是一种合理的方法以及我可能会忽略什么。

感谢您考虑我的问题。

sqlite tcl
1个回答
0
投票

透明地捕获和重新抛出异常比您预期的(或文档所明确的)要微妙一些:案例

break
/
continue
error
return
ok
在透明地重新抛出它们所需的小提琴方面都有点不同:

proc my_transaction script {
    set rollback 0
    # start transaction
    try {
        uplevel 1 $script
    } on break {r o} - on continue {r o} {
        dict incr o -level 1
        return -options $o $r
    } on return {r o} {
        dict incr o -level 1
        dict set o -code return
        return -options $o $r
    } on error {r o} {
        set rollback 1
        return -options $o $r
    } on ok r {
        return $r
    } finally {
        if {$rollback} {
            # do rollback
        } else {
            # do commit
        }
    }
}

这就是(实际上)sqlite 在 c 级别上使用其

db transaction {...}
命令所做的事情,还添加了跟踪事务嵌套并调用
ROLLBACK TO _tcl_transaction ; RELEASE _tcl_transaction
RELEASE _tcl_transaction
(如果嵌套而不是
ROLLBACK
)和分别是
COMMIT
。但如果你想做类似的事情(即:在一个部分中运行一些代码,并根据其异常状态执行一些非数据库提交/回滚操作,然后重新抛出异常,以便外部
db transaction
做正确的事情,那么您需要使用类似的结构。

它的外观取决于您的用例(您的问题不太清楚)。如果您正在做一堆必须在数据库级别以原子方式发生的事情,并且在失败时只报告错误,那么习惯用法将类似于:

try {
    db transaction {
        foreach client $clients {
            set name [dict get $client name]
            db eval {insert into clients (name) values (:name)}
            foreach addr [dict get $client addresses] {
                db eval {insert into addresses (client, addr) values (:client, :addr)}
            }
        }
    }
    puts "will only be reached if no exception was thrown"
} on error {errmsg options} {
    puts stderr "Error doing things: [dict get $options -errorinfo]"
} on ok {} {
    puts "Things went fine"
}

又好又简单:如果加载它们的代码抛出错误,则要么加载所有客户端地址,要么没有加载任何客户端地址(确切的代码

TCL_ERROR
- 其他任何内容:
TCL_BREAK
TCL_CONTINUE
等都被视为成功) sqlite 的
db transaction
并将触发提交)。

但是,如果代码within中的

db transaction
需要通过重新抛出异常来进行自己的异常处理,那么您将需要类似的东西:

proc with_logo {logovar client script} {
    upvar 1 $logovar logo
    set old [pwd]
    set tmpdir [file tempdir]
    try {
        cd $tmpdir
        set logo client_logo[expr {int(rand()*1000)}].png
        exec curl -o $logo [dict get $client logo_url]
        uplevel 1 $script
    } on break {r o} - on continue {r o} {
        dict incr o -level 1
        return -options $o $r
    } on return {r o} {
        dict incr o -level 1
        dict set o -code return
        return -options $o $r
    } finally {
        cd $old
        file delete -force $tmpdir
    }
}

try {
    db transaction {
        foreach client $clients {
            with_logo fn $client {
                set logo_md5 [exec md5sum $fn]
                set logo_size [file size $fn]
                if {$logo_size == 0} {
                    puts "client [dict get $client name]'s logo is empty"
                    continue    ;# skip clients with broken logos
                }
            }
            set name [dict get $client name]
            db eval {insert into clients (name, logo_md5, logo_size) values (:name, :logo_md5, :logo_size)}
            foreach addr [dict get $client addresses] {
                db eval {insert into addresses (client, addr) values (:client, :addr)}
            }
        }
    }
    puts "will only be reached if no exception was thrown"
} on error {errmsg options} {
    puts stderr "Error doing things: [dict get $options -errorinfo]"
} on ok {} {
    puts "Things went fine"
}

顺便说一句:请注意以下代码:

if [catch {foo}] {...}
if
的第一个参数是一个表达式。像这样写,它将获取
catch
命令的结果,并 reparse 将其作为 Tcl 表达式(一种不同的语言)并进行另一轮替换,这很难推理,并且存在丰富的远程代码执行安全漏洞来源。因此,我强烈建议始终使用大括号引用表达式(例如
expr {$a + $b}
而不是
expr $a + $b
,或者像本例一样,
if
while
for
等的表达式参数)。在这种情况下它是安全的,但出于脆弱的原因:
catch
的返回值将始终是一个整数,其字符串表示形式永远不会被解释为在 expr 解析轮中触发额外危险替换的东西。

© www.soinside.com 2019 - 2024. All rights reserved.