[Bro-Dev] Rethinking Broker's blocking API

Matthias Vallentin vallentin at icir.org
Fri Jan 6 10:32:50 PST 2017


> I think the name "error" is not just misleading but would also turn
> out tricky to use correctly. 

Agreed.

>     auto msg = ep.receive();
> 
>     if (msg)
>         return f(*msg); // unbox contained message
> 
>     if (msg.failed())
>         cout << "Trouble: " << to_string(msg.status()) << endl;
>     else
>         cout << "Status change: " << to_string(msg.status()) << endl;
> 
> In either case one could then also use a switch to differentiate the
> status() further.

I think this is semantically what we want. Because broker::error is just
a type alias for caf::error (and broker::expected just caf::expected),
it's currently not possible to change the API of those classes. I see
two solutions, one based on the existing vehicles and one that
introduces a new structure with three states for T, error, and status.

Here's the first. A caf::error class has three interesting member
functions:

  class error {
    uint8_t code();
    atom_value category();
    const message& context();
  };

The member category() returns the type of error. In Broker, this is
always "broker". CAF errors have "caf" as category. We could simply
split the "broker" error category into "broker-status" and
"broker-error" to distinguish the error class. We would keep the two
different enums and provide free functions for a user-friendly API.
Example:

  // -- Usage -------------------------------------------

  auto msg = ep.receive(); // expected<message>

  if (msg) {
    f(*msg); // unbox message
  } else if (is_error(msg)) {
    cout << "error: " << to_string(msg.error()) << endl;
    if (msg == error::type_clash)
      // dispatch concrete error
  } else if (is_status(msg)) {
    cout << "status: " << to_string(msg.error()) << endl;
    if (msg == status::peer_added)
      // dispatch concrete status
  } else {
    // CAF error
  }

  // -- Broker implementation ---------------------------

  using failure = caf::error;

  enum status : uint8_t { /* define status codes * /};

  enum error : uint8_t { /* define error codes */ };

  bool is_error(const failure& f) { 
    return f.category() == atom("broker-error");
  }

  bool is_status(const failure& f) { 
    return f.category() == atom("broker-status");
  }

  template <class T>
  bool failed(const expected<T>& x) { 
    return !x && failed(x.error());
  }

  template <class T>
  bool operator==(const expected<T>& x, status s) {
    return !x && x.error() == s;
  }

The only downside here is that we're still calling msg.error() in the
case of a status. That's where the second option comes in. Let's call it
result<T> for now (it won't matter much, because most people will use it
with "auto"). A result<T> wraps an expected<T> and provides a nicer API
to distinguish errors and status more cleanly. Example:

  // -- Usage -------------------------------------------

  auto msg = ep.receive(); // result<message>

  if (msg) {
    f(*msg); // unbox T
  } else if (auto e = msg.error()) {
    cout << "error: " << to_string(*e) << endl;
    if (*e == error::type_clash)
      // dispatch concrete error
  } else if (auto s = msg.status()) {
    cout << "status: " << to_string(*s) << endl;
    if (*s == status::peer_added)
      // dispatch concrete status
  } else {
    assert(!"not possible");
  }

  // -- Broker implementation ---------------------------

  enum status : uint8_t { /* define status codes * /};

  enum error : uint8_t { /* define error codes */ };

  template <class T>
  class result {
  public:
    optional<caf::error> status() const;
    optional<caf::error> error() const;

    // Box semantics
    explicit operator bool() const;
    T& operator*();
    const T& operator*() const;
    T* operator->();
    const T* operator->() const;

  private:
    expected<T> x_;
  };

Thoughts?

    Matthias


More information about the bro-dev mailing list