注意

本文档适用于 Ceph 的开发版本。

错误处理

在 Seastar 中,future 表示一个尚未可用但稍后可能可用的值。future 可以具有以下状态之一:

  • 不可用:值尚不可用,

  • 值,

  • 失败:计算值时抛出了异常。该异常已被捕获并通过 std::exception_ptr 存储在 future 实例中。

在最后一种情况下,可以使用 future::handle_exception()future::handle_exception_type() 来处理异常。Seastar 甚至提供了 future::or_terminate(),以便在 future 失败时终止程序。

但在 Crimson 中,许多错误并没有严重到足以使程序完全失败。例如,如果我们尝试通过对象 ID 查找一个对象,并且该操作可能因为对象不存在或已损坏而失败,我们需要恢复该对象以满足请求,而不是终止进程。

换句话说,这些错误是预期中的。此外,非正常路径的性能也应与正常路径的性能相当。我们还希望有一种方法来确保所有预期错误都得到处理。它应该类似于编译器执行的静态分析,如果在 switch-case 语句中未处理任何枚举值时发出警告。

不幸的是,seastar::future 无法满足这两个要求。

  • Seastar 要求重新抛出异常以在不同类型的异常之间进行分派。这性能不高,甚至没有可扩展性,因为可能会发生语言运行时中的锁定。

  • Seastar 不会在返回的 seastar::future 类型中编码预期的异常类型。只编码了值的类型。这给程序员带来了巨大的心智负担,因为确保所有预期错误都得到处理需要手动代码审计。

因此,创建了“errorator”。它是对普通 seastar::future 的封装。它解决了性能和可扩展性问题,同时将所有预期错误类型的信息嵌入到 future 的类型中。

using ertr = crimson::errorator<crimson::ct_error::enoent,
                                crimson::ct_error::einval>;

在上面的示例中,我们定义了一个允许两种错误类型的 errorator

  • crimson::ct_error::enoent

  • crimson::ct_error::einval.

这些(以及 crimson::ct_error 命名空间中的其他错误)基本上是 std::error_code 的不可抛出封装,以排除意外抛出并确保以启用编译时检查的方式发出错误信号。

errorator 中最基本的是 seastar::future 的后代,可以用作例如函数的返回类型

static ertr::future<int> foo(int bar) {
  if (bar == 42) {
    return crimson::ct_error::einval::make();
  } else {
    return ertr::make_ready_future(bar);
  }
}

值得注意的是,返回不属于 errorator 错误集的错误会导致编译时错误

static ertr::future<int> foo(int bar) {
  // Oops, input_output_error is not allowed in `ertr`.  static_assert() will
  // terminate the compilation. This behaviour is absolutely fundamental for
  // callers -- to figure out about all possible errors they need to worry
  // about is enough to just take a look on the function's signature; reading
  // through its implementation is not necessary anymore!
  return crimson::ct_error::input_output_error::make();
}

errorator 概念更进一步。它不仅向调用者提供嵌入在函数类型中的所有潜在错误信息;它还在调用者站点确保所有这些错误都得到处理。正如读者可能知道的,seastar::future 中的主要方法是 then()。在 errorated future 上,它是可用的,但仅当 errorator 的错误集为空时(字面意思是:errorator<>::future);否则,调用者必须改用 safe_then()

seastar::future<> baz() {
  return foo(42).safe_then(
    [] (const int bar) {
      std::cout << "the optimistic path! got bar=" << bar << std::endl
      return ertr::now();
    },
    ertr::all_same_way(const std::error_code& err) {
      // handling errors removes them from errorator's error set
      std::cout << "the error path! got err=" << err << std::endl;
      return ertr::now();
    }).then([] {
      // as all errors have been handled, errorator's error set became
      // empty and the future instance returned from `safe_then()` has
      // `then()` available!
      return seastar::now();
    });
}

在上面的示例中,ertr::all_same_way 已用于以相同方式处理所有错误。这不是强制性的——调用者可以分别处理每个错误。此外,它只能为错误的子集提供处理程序。代价是 then() 的可用性

using einval_ertr = crimson::errorator<crimson::ct_error::einval>;

// we can't return seastar::future<> (aka errorator<>::future<>) as handling
// as this level deals only with enoent leaving einval without a handler.
// handling it becomes a responsibility of a caller of `baz()`.
einval_ertr::future<> baz() {
  return foo(42).safe_then(
    [] (const int bar) {
      std::cout << "the optimistic path! got bar=" << bar << std::endl
      return ertr::now();
    },
    // provide a handler only for crimson::ct_error::enoent.
    // crimson::ct_error::einval stays unhandled!
    crimson::ct_error::enoent::handle([] {
      std::cout << "the enoent error path!" << std::endl;
      return ertr::now();
    }));
  // .safe_then() above returned `errorator<crimson::ct_error::einval>::future<>`
  // which lacks `then()`.
}

也就是说,处理错误会从 errorated future 的错误集中移除它们。这也适用于相反的方向——在 safe_then() 中返回新错误会将它们附加到错误集中。当然,这个集合必须与 baz() 签名中的错误集兼容

using broader_ertr = crimson::errorator<crimson::ct_error::enoent,
                                        crimson::ct_error::einval,
                                        crimson::ct_error::input_output_error>;

broader_ertr::future<> baz() {
  return foo(42).safe_then(
    [] (const int bar) {
      std::cout << "oops, the optimistic path generates a new error!";
      return crimson::ct_error::input_output_error::make();
    },
    // we have a special handler to delegate the handling up. For convenience,
    // the same behaviour is available as single argument-taking variant of
    // `safe_then()`.
    ertr::pass_further{});
}

可以看出,在 safe_then() 中处理和发出错误信号基本上是对在编译时检查的错误集的操作。

更多详细信息可以在 Seastar Summit 2019 上展示的ceph::errorator<> throw/catch-free, compile time-checked exceptions for seastar::future<> 的幻灯片中找到。

由 Ceph 基金会为您呈现

Ceph 文档是由非营利性 Ceph 基金会 资助和托管的社区资源。如果您希望支持这项工作和我们的其他努力,请考虑 立即加入