锈病时误差 - 深度潜水

2021-06-06 06:42:45

本文是零中的零生产中的一个样本,一本锈病的后端开发书。您可以在Zero2Prod.com上获取这本书的副本。订阅新闻稿发布时要通知的时事通知。

要发送确认电子邮件,您必须拼接多个操作:用户输入验证,电子邮件调度,各种数据库查询。他们都有一个共同点:他们可能会失败。

在第6章中,我们讨论了锈病中误差处理的构建块 - 结果?操作员。我们留下了许多问题:错误如何适应我们申请的更广泛的架构中?良好的错误是什么样的?谁是错误的?我们应该使用图书馆吗?哪一个?

对Rust中的错误处理模式的深入分析将是本章的唯一重点。

// src /路由/订阅.rs // [...] pub async fn store_token(事务:& mut事务< postgres>,subscriber_id:uuid,subscription_token:& str,) - >结果<(),sqlx ::错误> {sqlx ::查询!(r#"插入subscription_tokens(subscription_token,subscriber_id)值($ 1,$ 2)"#,subscription_token,subscriber_id)。执行(事务).Await。 map_err(| e | {追踪::错误!("无法执行查询:{:?}" e); e})?好的(())}

我们正试图将行插入Subscription_Tokens表中,以便对Subscriber_ID存储新生成的令牌。 Execute是一个难以犯的操作:我们可能会在与数据库交谈时有一个网络问题,我们尝试插入的行可能会违反一些表约束(例如,主键的唯一性)。

如果发生故障,则执行的呼叫者最有可能被告知 - 他们需要相应地反应,例如,重试查询或传播上游的故障?,如在我们的示例中。

Rust利用类型系统进行通信,操作可能不会成功:返回类型的执行是结果,枚举。

然后,编译器强制调用者以表达他们计划如何处理这两种情况 - 成功和失败。

如果我们唯一的目标是与呼叫者沟通发生错误发生的错误,我们可以使用更简单的定义:

不需要通用错误类型 - 我们可以检查执行返回的ERR VAR,例如,

让结果= sqlx ::查询!(/ * ... * /)。执行(事务).await;如果Outcome == CapeignAlal :: Err {//如果失败}

这是有效的,如果只有一个失败模式.TRUTH是,操作可以以多种方式失败,我们可能希望根据发生的方式反应不同的反应。让'查看SQLX ::错误的骨架,Execute的错误类型:

// sqlx-core / src / error.rs pub枚举错误{configuration(/ * * /),数据库(/ * /),IO(/ * * /),TLS(/ * * /),协议(/ * * /),townotfound,typenotfound {/ * * /},columentexoutofbounds {/ * * /},columnnotfound(/ * * /),columndecode {/ * * /),解码(/ * * /),pooltimedout,poollosed,workercrashed ,迁移(/ * * /),}

相当列表,AIN' t呢? SQLX ::错误实现为枚举,以允许用户在返回的错误上匹配,并根据底层的故障模式表现不同。例如,当您可能放弃ColumnNotFound时,您可能希望重试PoolTimedOut。

如果操作具有单个故障模式,那么什么 - 如果我们只是使用()作为错误类型?

err(())可能足以让呼叫者确定该做什么 - 例如。向用户返回500个内部服务器错误。

但是控制流不是应用程序中错误的唯一目的。我们希望错误对未能为运营商(例如开发人员)生成报告来带有足够的上下文,其中包含足够的细节以进行解决和排除问题。

报告我们的意思是什么?在像我们这样的后端API中,它通常是日志事件。在CLI中,当使用verbose标志时,它可能是终端中显示的错误消息。

实施细节可能有所不同,目的保持不变:帮助人类了解出现问题。那个'究竟在初始代码段中我们正在做什么:

// src /路线/订阅.rs // [...] pub async fn store_token(/ * * /) - >结果<(),sqlx ::错误> {sqlx ::查询!(/ * * /)。执行(事务).Await。 map_err(| e | {追踪::错误!("无法执行查询:{:?}" e); e})? // [...]}

如果查询失败,我们抓取错误并发出日志事件。然后,我们可以在调查数据库问题时转到并检查错误日志。

到目前为止,我们专注于我们的API函数的内部,调用其他函数和运营商试图在发生之后感受到混乱。用户怎么样?

就像运营商一样,用户期望API在遇到故障模式时发出信号。

我们的API用户是什么时候看到Store_Token失败的?我们可以通过查看请求处理程序来了解:

// src /路线/订阅.rs // [...] pub异步fn订阅(/ * * /) - >结果< httpresponse,httpresponse> {// [...] Store_Token(& mut事务,subscriber_id,& subscription_token).await。 map_err(| _ | httpresponse :: internalservererror()。完成())? // [...]}

它们收到了HTTP响应,没有身体和500个内部服务器错误状态代码。

状态代码符合Store_Token中错误类型的相同目的:它是调用者(例如浏览器)可以用于确定下一步的机器可解释的信息(例如,假设&#39重试请求' sa瞬态失败)。

浏览器背后的人呢?我们在告诉他们什么?不多,响应体是空的。这实际上是一个良好的实现:用户不应该小心他们所呼叫的API的内部 - 它们没有它的心理模型,无法确定它失败的原因。那个'是操作员的领域。我们通过设计省略这些细节。

相反,在其他情况下,我们需要向人类用户传达其他信息。让'查看我们同一端点的输入验证:

// src /路由/订阅.rs#[派生(serde :: deserialize)] pub struct formdata {email:string,name:string,} islich tryinto< needubscriber> for formdata {类型错误=字符串; fn try_into(self) - >结果< needubscriber,self ::错误> {让name = subscribername :: parse(self .name)?让电子邮件= subscriberemail :: parse(self .email)? OK(Newsubscriber {eament,name})}}}

我们收到了电子邮件地址和名称,作为附加到用户提交的表单的数据。两个字段都经历了一轮验证 - Subscribername :: Parse和SubscriberEmail :: Parse。这两种方法是缺乏缺乏的 - 它们将一个字符串作为错误类型返回,以解释出了什么问题:

// src / domain / subscriber_email.rs // [...] icluberifer email {pub fn parse(s:string) - >结果< subscriberemail,string> {如果validate_email(& s){好(self(s))} else {err(格式!(" {}不是有效的订阅者电子邮件。",s))}}}}}}}}

它是,我必须承认,不是最有用的错误消息:我们正在告诉用户他们输入的电子邮件地址是错误的,但我们没有帮助他们确定原因。最终,它不得不重要:我们没有将任何信息作为API的响应的一部分发送给用户 - 它们获得了一个400个不良请求,没有身体。

// SRC /路由/订阅.s // [...] PUB Async Fn订阅(/ * * /) - >结果< httpresponse,httpresponse> {让rem_subscriber =表单。 0。 try_into()。 map_err(| _ | httpresponse :: badrequest()。完成())? // [...]

这是一个错误的错误:用户留在黑暗中,无法根据需要调整其行为。

控制流程是脚本:在机器中可以访问接下来的要做的决定所需的所有信息。我们使用类型(例如枚举变体),用于内部错误的方法和字段。我们依赖于边缘处错的状态代码。

相反,错误报告主要由人类消耗。必须根据受众调整内容。操作员可以访问系统的内部结构 - 它们应在故障模式下尽可能多地提供。用户坐在应用程序2的边界之外:如果需要,它们只能给出调整其行为所需的信息量(例如,修复格式错误的输入)。

我们可以使用2x2表可视化此心理模型,其中包含列和目的作为行:

我们将花费剩下的章节提高表中每一个单元格的错误处理策略。

让' s从错误报告开始运算符。在出现错误时,我们现在正在做好伐木工作吗?

//测试/ API /订阅.rs // [...]#[ACTIX_RT :: TEST] ASYNC FN SUBSCRIBE_FAILS_IF_THERE_IS_A_FATAL_DATABASE_ERROR(){//安排留言= spawn_app().await;让身体=" name = le%20guin&电子邮件= ursula_le_guin%40gmail.com&#34 ;; // sabotage数据库sqlx ::查询!(" alter表subscription_tokens drop列subscription_token;&#34 ;,)。执行(& app.db_pool).aiawait。 unwrap(); // act让response = app。 post_subscriptions(身体。进入())。等待; // assert assert_eq!(响应。状态()。as_u16(),500);}

测试通过直接传递 - 让'查看应用程序3发出的日志。

#sqlx日志有点斑点,将它们切割出来减少噪声出口rust_log =" sqlx =错误,信息" Export Test_log =已启用的Cargo T subscribut_fails_if_there_is_a_fatal_database_error |布尼安

info:[http请求 - 开始]信息:[添加新的订阅者 - start]信息:[在数据库中保存新的订户详细信息 - start]信息:[在数据库中保存新订户详细信息 - END] INFO:[商店订阅令牌在数据库 - 启动]错误:[数据库中的存储订阅令牌 - 事件 - 事件]无法执行查询:数据库(PGDatabaseError {severity:错误,代码:" 42703",消息:"列& #39; subscription_token'关系' subscription_tokens'不存在" ...})target = zero2prod ::路由::订阅信息:[在数据库中存储订阅令牌 - END]信息:[添加新的订户 - end]错误:[HTTP请求 - 事件]内部服务器错误:"" log.target = Actix_http :: responseError:[HTTP请求 - 事件]在处理传入的HTTP请求时遇到的错误:""例外.details ="&#34 ;,例外.."""",tarting = tracing_actix_web :: middleware信息:[http请求 - 结束]例外.details ="&# 34;,例外..Message ="",target = tracing_actix_web :: root_span_builder,http.status_code = 500

你如何阅读这样的东西?理想情况下,您从结果开始:在请求处理结束时发出的日志记录。在我们的情况下,即:

这告诉我们什么?请求返回了500个状态代码 - 它失败了。我们不仅仅是学习了很多:异常和异常.Message是空的。

如果我们查看接下来的两个错误日志,那么情况不会更好,由Actix_Web本身(通过ACTIX_HTTP)和其他来自Tracing_Actix_Web的另一个)发出:

错误:[http请求 - 事件]内部服务器错误:"" log.target = Actix_http :: responseError:[HTTP请求 - 事件]在处理传入的HTTP请求时遇到的错误:""例外.details ="&#34 ;,例外.."",target = tracing_actix_web :: middleware

没有任何可操作的信息。记录"哎呀!出了问题!"本来就像有用。

错误:[数据库中存储订阅令牌 - 事件]无法执行查询:数据库(pgdatabaseError {severity:错误,代码:" 42703",消息:"列' subscription_token&#39 ;关系' subscription_tokens'不存在" ...})target = Zero2Prod ::路由::订阅

当我们尝试与数据库交谈时出现了出现问题 - 我们期待在订阅_Tokens表中看到一个订阅列,但由于某种原因,它不是在那里。这实际上是有用的!

这是500的原因吗?只需查看日志很难说 - 开发人员将不得不克隆代码库,检查日志线来自的地方,并确保它确实是问题的原因。它可以完成,但需要时间:如果[HTTP请求 - 结束]日志记录报告了关于底层根本原因的内容以及异常和异常的内容,则会更容易。

要了解为什么出来的日志记录出来的tracing_actix_web是如此糟糕,我们需要检查(再次)我们的请求处理程序和Store_Token:

// src /路线/订阅.rs // [...] pub异步fn订阅(/ * * /) - >结果< httpresponse,httpresponse> {// [...] Store_Token(/ * * /).await。 map_err(| _ | httpresponse :: internalservererror()。完成())? // [...]} PUB Async Fn Store_Token(/ * * /) - >结果<(),sqlx ::错误> {sqlx ::查询!(/ * * /)。执行(事务).Await。 map_err(| e | {追踪::错误!("无法执行查询:{:?}" e); e})? // [...]}

我们找到的有用错误日志确实是该跟踪::错误调用发出的错误 - 错误消息包括返回的SQLX ::错误。我们使用该错误向上传播错误?操作员,但是链条中的休息 - 我们丢弃了从Store_Token(.map_err(| _ | / * * /)收到的错误,并建立一个裸500响应,以便从订阅框架中返回Err Variant。

那个err(httpresponse :: internalservererror()。finish())是Actix_Web和Tracing_Actix_Web :: TracingLogger当他们即将发出各自的日志记录时访问的唯一方法。错误不包含关于底层根本原因的任何上下文,因此日志记录同样无用。

我们需要开始利用由Actix_Web曝光的错误处理机械 - 特别是Actix_Web :: Error.According到文档:

它听起来与我们正在寻找的东西。我们是否建立了Actix_Web ::错误的实例?文件指出

有点间接,但我们可以弄清楚4.我们可以使用的唯一/进入实施,浏览文档中列出的实施似乎是这个:

///从任何实现`responseError`的错误构建`contix_web :: serror`:responseError +'静态>来自< t>对于错误{fn(err:t) - >错误{错误{原因:box :: new(err),}}}

///可以转换为`响应的错误。 Pub Trait ResponseError:FMT :: Debug + FMT ::显示{///响应' s状态代码。 /// ///默认实现返回内部服务器错误。 fn status_code(& self) - > statuscode; ///从错误中创建响应。 /// ///默认实现返回内部服务器错误。 fn error_response(& self) - >回复;}

我们只需要为我们的错误实施它! Actix_Web为两种方法提供默认实现,返回500个内部服务器错误 - 完全是我们所需要的。因此它足够写的'

错误[e0117]:只有当前箱子中定义的特征只能为任意类型实现 - > SRC /路由/订阅.RS:162:1 | 162 | icharm responseerror for sqlx ::错误{} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ---------- | | | | | `sqlx ::错误`未在当前的箱子中定义| ichildn' T仅使用当前箱内内部的类型| =注意:定义和实现特征或新类型

我们刚刚碰到了生锈'孤儿规则:禁止实施外国类型的外国特质,外国代表"来自另一个箱子"这种限制旨在保留一致性:如果您添加了为SQLX ::错误的responseerror的responseerror实现了依赖关系,则应在调用特性方法时使用编译器使用哪一个?

孤儿规则一边,我们仍然是一个错误的SQLX ::错误来实现responseerror。当我们在尝试持有订户令牌时遇到SQLX ::错误时,我们希望返回500个内部服务器错误。在另一种情况下,我们可能希望以不同的方式处理SQLX ::错误。

// SRC /路由/订阅.rs // [...] //使用新的错误类型! PUB Async Fn Store_Token(/ * * /) - >结果<(),storetokenerror> {sqlx ::查询!(/ * * /)。执行(事务).Await。 map_err(| e | {// [...] //包装底层错误StoreTokenError(e)})? // [...]} //一个新的错误类型,包装SQLX ::错误PUB struct StoreTokeError(SQLX ::错误); ichar responseerror for storeTokenError {}

错误[e0277]:`storetokenerror`' t实现`std :: fmt ::显示' - > SRC /路线/订阅.RS:164:6 | 164 | icharmonatheryror for storeTokenError {} | ^^^^^^^^^^^^^^^菜单不能用默认格式化器格式化| | 59 | PUB TRAIT ReScumentError:FMT :: Debug + FMT ::显示{| | ------------- |在“响应形象”中的绑定需要= help:trait` std :: fmt ::展示:storetokenerror`error [e0277]:`storetokenerror`并不实现`std :: fmt :: debug` - > SRC /路由/订阅.RS:164:6 | 164 | icharmonatheryror for storeTokenError {} | ^^^^^^^^^^^^^^^^`StoreTokenError`无法使用`{:? “| | | 59 | PUB TRAIT ReScumentError:FMT :: Debug + FMT ::显示{| | ---------在“响应形象”中的界限要求= help:trait` std :: fmt :: debug`未为`storeTokenerror` =注意:添加`#[派生(debug)]`或手动实现`std :: fmt :: debug`

我们在StoreTokenError上缺少两个特质实施:调试和显示。这两个特质都涉及格式化,但它们提供了不同的目的。调试应返回将一个面向程序员的表示,作为潜在类型结构的忠实,以帮助调试(顾名思义)。几乎所有公共类型都应实施调试。相反,显示应返回基础类型的面向用户的表示。大多数类型都不实现显示,无法自动使用#[派生(显示)]属性。

使用错误时,我们可以推理如下的两个特征:调试返回尽可能多的信息,同时显示我们遇到的故障,具有必要的上下文。

// src /路由/订阅.rs // [...] //我们派生`调试,容易和无痛。 #[派生(debug)] pub struct storetokenerror(sqlx ::ser错); iclich std :: fmt :: for storetokenerror {fn fmt(& self,f:& mut std :: fmt :: formatter<>) - > std :: fmt ::结果{写!(f,"遇到数据库错误,而尝试存储订阅令牌。")}}

// src /路线/订阅.rs // [...] pub异步fn订阅(/ * * /) - >结果< httpresponse,actix_web ::错误> {// [...] //“?`operator透明地调用我们代表我们的`to` trait // - 我们不再需要一个明确的`map_err`。 Store_Token(/ * * /).await ?; // [...]}

#sqlx日志有点斑点,将它们切割出来减少噪声出口rust_log =" sqlx =错误,信息" Export Test_log =已启用的Cargo T subscribut_fails_if_there_is_a_fatal_database_error |布尼安

... info:[http请求 - 结束]例外.details = storetokenerror(pgdatabaseerror {severity:错误,代码:" 42703"消息:"列' subscription_token&#39 ;关系' subscription_tokens'不存在",...}))异常.Message ="尝试时遇到了数据库故障

......