使用线程宏的惯用 error/exception 处理
Idiomatic error/exception handling with threading macros
我使用 http 请求一次从 API 中获取数千个实体。作为管道的下一步,我想将它们全部铲入数据库。
(->> ids
(pmap fetch-entity)
(pmap store-entity)
(doall))
fetch-entity
需要一个 String
id 并尝试使用 http 请求和 returns 一个 Map
或抛出异常(例如,因为超时)。
store-entity
期望 Map
并尝试将其存储在数据库中。它可能会抛出异常(例如,如果 Map
与数据库模式不匹配,或者它根本没有收到 Map
)。
不优雅的错误处理
我的第一个 "solution" 是编写包装函数 fetch-entity'
和 store-entity'
来捕获它们各自原始函数的异常。
fetch-entity'
returns 它在失败时的输入,如果 http 请求失败,基本上会传递一个 String
id。这确保了整个管道继续进行卡车运输。
store-entity'
检查其参数的类型。如果参数是 Map
(获取实体成功并返回 Map
),它会尝试将其存储在数据库中。
如果存储到数据库的尝试抛出异常,或者如果 store-entity'
传递的是 String
(id) 而不是 Map
,它将 conj
传递给error_ids
.
的外部 Vector
这样我以后可以使用 error_ids
来计算失败的频率以及哪些 ID 受到影响。
感觉上面的方法不是实现我想要做的事情的明智方法。例如,我写 store-entity'
的方式使用前面的管道步骤 (fetch-entity'
) 来完成函数,因为它的行为根据前面的管道步骤是否成功而有所不同。
同时让 store-entity'
意识到一个叫做 error_ids
的外部 Vector
感觉一点也不对。
有没有一种惯用的方法来处理这些情况,你有多个管道步骤,其中一些步骤可能会抛出异常(例如,因为它们是 I/O),而我不能轻易地使用谓词来进行确定函数的行为是可预测的,我不想打扰管道,只是稍后检查它在哪些情况下出错了?
我的第一个想法是将 fetch-entity
和 store-entity
合并为一个操作:
(defn fetch-and-store [id]
(try
(store-entity (fetch-entity id))
(catch ... <log error msg> )))
(doall (pmap fetch-and-store ids))
这样的东西行得通吗?
可以使用 Try
monad 类型,例如 cats
library:
It represents a computation that may either result in an exception or return a successfully computed value. Is very similar to the Either monad, but is semantically different.
It consists of two types: Success and Failure. The Success type is a simple wrapper, like Right of the Either monad. But the Failure type is slightly different from Left, because it always wraps an instance of Throwable (or any value in cljs since you can throw arbitrary values in the JavaScript host).
(...)
It is an analogue of the try-catch block: it replaces try-catch’s stack-based error handling with heap-based error handling. Instead of having an exception thrown and having to deal with it immediately in the same thread, it disconnects the error handling and recovery.
基于堆的错误处理正是您想要的。
下面我以fetch-entity
和store-entity
为例。我让 fetch-entity
在第一个 ID (1) 上抛出一个 ExceptionInfo
,store-entity
在第二个 ID (0) 上抛出一个 DivideByZeroException
。
(ns your-project.core
(:require [cats.core :as cats]
[cats.monad.exception :as exc]))
(def ids [1 0 2]) ;; `fetch-entity` throws on 1, `store-entity` on 0, 2 works
(defn fetch-entity
"Throws an exception when the id is 1..."
[id]
(if (= id 1)
(throw (ex-info "id is 1, help!" {:id id}))
id))
(defn store-entity
"Unfortunately this function still needs to be aware that it receives a Try.
It throws a `DivideByZeroException` when the id is 0"
[id-try]
(if (exc/success? id-try) ; was the previous step a success?
(exc/try-on (/ 1 (exc/extract id-try))) ; if so: extract, apply fn, and rewrap
id-try)) ; else return original for later processing
(def results
(->> ids
(pmap #(exc/try-on (fetch-entity %)))
(pmap store-entity)))
现在您可以分别使用 success?
或 failure?
过滤 results
成功或失败,并通过 cats-extract
检索值
(def successful-results
(->> results
(filter exc/success?)
(mapv cats/extract)))
successful-results ;; => [1/2]
(def error-messages
(->> results
(filter exc/failure?)
(mapv cats/extract) ; gets exceptions without raising them
(mapv #(.getMessage %))))
error-messages ;; => ["id is 1, help!" "Divide by zero"]
请注意,如果您只想在 errors
或 successful-results
上循环一次,您可以按如下方式使用换能器:
(transduce (comp
(filter exc/success?)
(map cats/extract))
conj
results))
;; => [1/2]
我使用 http 请求一次从 API 中获取数千个实体。作为管道的下一步,我想将它们全部铲入数据库。
(->> ids
(pmap fetch-entity)
(pmap store-entity)
(doall))
fetch-entity
需要一个 String
id 并尝试使用 http 请求和 returns 一个 Map
或抛出异常(例如,因为超时)。
store-entity
期望 Map
并尝试将其存储在数据库中。它可能会抛出异常(例如,如果 Map
与数据库模式不匹配,或者它根本没有收到 Map
)。
不优雅的错误处理
我的第一个 "solution" 是编写包装函数 fetch-entity'
和 store-entity'
来捕获它们各自原始函数的异常。
fetch-entity'
returns 它在失败时的输入,如果 http 请求失败,基本上会传递一个 String
id。这确保了整个管道继续进行卡车运输。
store-entity'
检查其参数的类型。如果参数是 Map
(获取实体成功并返回 Map
),它会尝试将其存储在数据库中。
如果存储到数据库的尝试抛出异常,或者如果 store-entity'
传递的是 String
(id) 而不是 Map
,它将 conj
传递给error_ids
.
Vector
这样我以后可以使用 error_ids
来计算失败的频率以及哪些 ID 受到影响。
感觉上面的方法不是实现我想要做的事情的明智方法。例如,我写 store-entity'
的方式使用前面的管道步骤 (fetch-entity'
) 来完成函数,因为它的行为根据前面的管道步骤是否成功而有所不同。
同时让 store-entity'
意识到一个叫做 error_ids
的外部 Vector
感觉一点也不对。
有没有一种惯用的方法来处理这些情况,你有多个管道步骤,其中一些步骤可能会抛出异常(例如,因为它们是 I/O),而我不能轻易地使用谓词来进行确定函数的行为是可预测的,我不想打扰管道,只是稍后检查它在哪些情况下出错了?
我的第一个想法是将 fetch-entity
和 store-entity
合并为一个操作:
(defn fetch-and-store [id]
(try
(store-entity (fetch-entity id))
(catch ... <log error msg> )))
(doall (pmap fetch-and-store ids))
这样的东西行得通吗?
可以使用 Try
monad 类型,例如 cats
library:
It represents a computation that may either result in an exception or return a successfully computed value. Is very similar to the Either monad, but is semantically different.
It consists of two types: Success and Failure. The Success type is a simple wrapper, like Right of the Either monad. But the Failure type is slightly different from Left, because it always wraps an instance of Throwable (or any value in cljs since you can throw arbitrary values in the JavaScript host).
(...)
It is an analogue of the try-catch block: it replaces try-catch’s stack-based error handling with heap-based error handling. Instead of having an exception thrown and having to deal with it immediately in the same thread, it disconnects the error handling and recovery.
基于堆的错误处理正是您想要的。
下面我以fetch-entity
和store-entity
为例。我让 fetch-entity
在第一个 ID (1) 上抛出一个 ExceptionInfo
,store-entity
在第二个 ID (0) 上抛出一个 DivideByZeroException
。
(ns your-project.core
(:require [cats.core :as cats]
[cats.monad.exception :as exc]))
(def ids [1 0 2]) ;; `fetch-entity` throws on 1, `store-entity` on 0, 2 works
(defn fetch-entity
"Throws an exception when the id is 1..."
[id]
(if (= id 1)
(throw (ex-info "id is 1, help!" {:id id}))
id))
(defn store-entity
"Unfortunately this function still needs to be aware that it receives a Try.
It throws a `DivideByZeroException` when the id is 0"
[id-try]
(if (exc/success? id-try) ; was the previous step a success?
(exc/try-on (/ 1 (exc/extract id-try))) ; if so: extract, apply fn, and rewrap
id-try)) ; else return original for later processing
(def results
(->> ids
(pmap #(exc/try-on (fetch-entity %)))
(pmap store-entity)))
现在您可以分别使用 success?
或 failure?
过滤 results
成功或失败,并通过 cats-extract
(def successful-results
(->> results
(filter exc/success?)
(mapv cats/extract)))
successful-results ;; => [1/2]
(def error-messages
(->> results
(filter exc/failure?)
(mapv cats/extract) ; gets exceptions without raising them
(mapv #(.getMessage %))))
error-messages ;; => ["id is 1, help!" "Divide by zero"]
请注意,如果您只想在 errors
或 successful-results
上循环一次,您可以按如下方式使用换能器:
(transduce (comp
(filter exc/success?)
(map cats/extract))
conj
results))
;; => [1/2]