aboutsummaryrefslogblamecommitdiff
path: root/external/glim/hget.hpp
blob: cebbe416f069792f8cb307aed1208e8cfb624ecc (plain) (tree)






























































































































































































































































                                                                                                                                         
// Simple header-only wrapper around libevent's evhttp client.
// See also: https://github.com/cpp-netlib/cpp-netlib/issues/160

#ifndef _GLIM_HGET_INCLUDED
#define _GLIM_HGET_INCLUDED

#include <event2/event.h>
#include <event2/dns.h>
#include <evhttp.h> // http://stackoverflow.com/a/5237994; http://archives.seul.org/libevent/users/Sep-2010/msg00050.html

#include <memory>
#include <functional>
#include <stdexcept>
#include <iostream>
#include <vector>

#include <stdint.h>
#include <string.h>
#include <errno.h>

#include "exception.hpp"
#include "gstring.hpp"

namespace glim {

/// HTTP results
struct hgot {
  int32_t status = 0;
  /// Uses errno codes.
  int32_t error = 0;
  struct evbuffer* body = 0;
  struct evhttp_request* req = 0;
  size_t bodyLength() const {return body ? evbuffer_get_length (body) : 0;}
  /// Warning: the string is NOT zero-terminated.
  const char* bodyData() {return body ? (const char*) evbuffer_pullup (body, -1) : "";}
  /// Returns a zero-terminated string. Warning: modifies the `body` every time in order to add the terminator.
  const char* cbody() {if (!body) return ""; evbuffer_add (body, "", 1); return (const char*) evbuffer_pullup (body, -1);}
  /// A gstring *view* into the `body`.
  glim::gstring gbody() {
    if (!body) return glim::gstring();
    return glim::gstring (glim::gstring::ReferenceConstructor(), (const char*) evbuffer_pullup (body, -1), evbuffer_get_length (body));}
};

/// Used internally to pass both connection and handler into callback.
struct hgetContext {
  struct evhttp_connection* conn;
  std::function<void(hgot&)> handler;
  hgetContext (struct evhttp_connection* conn, std::function<void(hgot&)> handler): conn (conn), handler (handler) {}
};

/// Invoked when evhttp finishes a request.
inline void hgetCB (struct evhttp_request* req, void* ctx_){
  hgetContext* ctx = (hgetContext*) ctx_;

  hgot gt;
  if (req == NULL) gt.error = ETIMEDOUT;
  else if (req->response_code == 0) gt.error = ECONNREFUSED;
  else {
    gt.status = req->response_code;
    gt.body = req->input_buffer;
    gt.req = req;
  }

  try {
    ctx->handler (gt);
  } catch (const std::runtime_error& ex) { // Shouldn't normally happen:
    std::cerr << "glim::hget, handler exception: " << ex.what() << std::endl;
  }

  evhttp_connection_free ((struct evhttp_connection*) ctx->conn);
  //freed by libevent//if (req != NULL) evhttp_request_free (req);
  delete ctx;
}

/**
  C++ wrapper around libevent's http client.
  Example: \code
  hget (evbase, dnsbase) .setRequestBuilder ([](struct evhttp_request* req){
    evbuffer_add (req->output_buffer, "foo", 3);
    evhttp_add_header (req->output_headers, "Content-Length", "3");
  }) .go ("http://127.0.0.1:8080/test", [](hgot& got){
    if (got.error) log_warn ("127.0.0.1:8080 " << strerror (got.error));
    else if (got.status != 200) log_warn ("127.0.0.1:8080 != 200");
    else log_info ("got " << evbuffer_get_length (got.body) << " bytes from /test: " << evbuffer_pullup (got.body, -1));
  }); \endcode
 */
class hget {
 public:
  std::shared_ptr<struct event_base> _evbase;
  std::shared_ptr<struct evdns_base> _dnsbase;
  std::function<void(struct evhttp_request*)> _requestBuilder;
  enum evhttp_cmd_type _method;
 public:
  typedef std::shared_ptr<struct evhttp_uri> uri_t;
  /// The third parameter is the request number, starting from 1.
  typedef std::function<float(hgot&,uri_t,int32_t)> until_handler_t;
 public:
  hget (std::shared_ptr<struct event_base> evbase, std::shared_ptr<struct evdns_base> dnsbase):
    _evbase (evbase), _dnsbase (dnsbase), _method (EVHTTP_REQ_GET) {}

  /// Modifies the request before its execution.
  hget& setRequestBuilder (std::function<void(struct evhttp_request*)> rb) {
    _requestBuilder = rb;
    return *this;
  }

  /** Uses a simple request builder to send the `str`.
   * `str` is a `char` string class with methods `data` and `size`. */
  template<typename STR> hget& payload (STR str, const char* contentType = nullptr, enum evhttp_cmd_type method = EVHTTP_REQ_POST) {
    _method = method;
    return setRequestBuilder ([str,contentType](struct evhttp_request* req) {
      if (contentType) evhttp_add_header (req->output_headers, "Content-Type", contentType);
      char buf[64];
      *glim::itoa (buf, (int) str.size()) = 0;
      evhttp_add_header (req->output_headers, "Content-Length", buf);
      evbuffer_add (req->output_buffer, (const void*) str.data(), (size_t) str.size());
    });
  }

  struct evhttp_request* go (uri_t uri, int32_t timeoutSec, std::function<void(hgot&)> handler) {
    int port = evhttp_uri_get_port (uri.get());
    if (port == -1) port = 80;
    struct evhttp_connection* conn = evhttp_connection_base_new (_evbase.get(), _dnsbase.get(),
      evhttp_uri_get_host (uri.get()), port);
    evhttp_connection_set_timeout (conn, timeoutSec);
    struct evhttp_request *req = evhttp_request_new (hgetCB, new hgetContext(conn, handler));
    int ret = evhttp_add_header (req->output_headers, "Host", evhttp_uri_get_host (uri.get()));
    if (ret) throw std::runtime_error ("hget: evhttp_add_header(Host) != 0");
    if (_requestBuilder) _requestBuilder (req);
    const char* get = evhttp_uri_get_path (uri.get());
    const char* qs = evhttp_uri_get_query (uri.get());
    if (qs == NULL) {
      ret = evhttp_make_request (conn, req, _method, get);
    } else {
      size_t getLen = strlen (get);
      size_t qsLen = strlen (qs);
      char buf[getLen + 1 + qsLen + 1];
      char* caret = stpcpy (buf, get);
      *caret++ = '?';
      caret = stpcpy (caret, qs);
      assert (caret - buf < sizeof (buf));
      ret = evhttp_make_request (conn, req, _method, buf);
    }
    if (ret) throw std::runtime_error ("hget: evhttp_make_request != 0");
    return req;
  }
  struct evhttp_request* go (const char* url, int32_t timeoutSec, std::function<void(hgot&)> handler) {
    return go (std::shared_ptr<struct evhttp_uri> (evhttp_uri_parse (url), evhttp_uri_free), timeoutSec, handler);
  }

  void goUntil (std::vector<uri_t> urls, until_handler_t handler, int32_t timeoutSec = 20);
  /**
     Parse urls and call `goUntil`.
     Example (trying ten times to reach the servers): \code
       std::string path ("/path");
       hget.goUntilS (boost::assign::list_of ("http://server1" + path) ("http://server2" + path),
        [](hgot& got, hget::uri_t uri, int32_t num)->float {
         std::cout << "server: " << evhttp_uri_get_host (uri.get()) << "; request number: " << num << std::endl;
         if (got.status != 200 && num < 10) return 1.f; // Retry in a second.
         return -1.f; // No need to retry the request.
       });
     \endcode
     @param urls is a for-compatible container of strings (where string has methods `data` and `size`).
   */
  template<typename URLS> void goUntilS (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) {
    std::vector<uri_t> parsedUrls;
    for (auto&& url: urls) {
      // Copying to stack might be cheaper than malloc in c_str.
      int len = url.size(); char buf[len + 1]; memcpy (buf, url.data(), len); buf[len] = 0;
      struct evhttp_uri* uri = evhttp_uri_parse (buf);
      if (!uri) GTHROW (std::string ("!evhttp_uri_parse: ") + buf);
      parsedUrls.push_back (uri_t (uri, evhttp_uri_free));
    }
    goUntil (parsedUrls, handler, timeoutSec);
  }
  /**
     Parse urls and call `goUntil`.
     Example (trying ten times to reach the servers): \code
       hget.goUntilC (boost::assign::list_of ("http://server1/") ("http://server2/"),
        [](hgot& got, hget::uri_t uri, int32_t num)->float {
         std::cout << "server: " << evhttp_uri_get_host (uri.get()) << "; request number: " << num << std::endl;
         if (got.status != 200 && num < 10) return 1.f; // Retry in a second.
         return -1.f; // No need to retry the request.
       });
     \endcode
     Or with `std::array` instead of `boost::assign::list_of`: \code
       std::array<const char*, 2> urls {{"http://server1/", "http://server2/"}};
       hget.goUntilC (urls, [](hgot& got, hget::uri_t uri, int32_t num)->float {
         return got.status != 200 && num < 10 ? 0.f : -1.f;});
     \endcode
     @param urls is a for-compatible container of C strings (const char*).
   */
  template<typename URLS> void goUntilC (URLS&& urls, until_handler_t handler, int32_t timeoutSec = 20) {
    std::vector<uri_t> parsedUrls;
    for (auto url: urls) {
      struct evhttp_uri* uri = evhttp_uri_parse (url);
      if (!uri) GTHROW (std::string ("Can't parse url: ") + url);
      parsedUrls.push_back (uri_t (uri, evhttp_uri_free));
    }
    goUntil (parsedUrls, handler, timeoutSec);
  }
};

inline void hgetUntilRetryCB (evutil_socket_t, short, void* utilHandlerPtr); // event_callback_fn

/** `hget::goUntil` implementation.
 * This function object is passed to `hget::go` as a handler and calls `hget::go` again if necessary. */
struct HgetUntilHandler {
  hget _hget;
  hget::until_handler_t _handler;
  std::vector<hget::uri_t> _urls;
  int32_t _timeoutSec;
  int32_t _requestNum;
  uint8_t _nextUrl; ///< A round-robin pointer to the next url in `_urls`.
  HgetUntilHandler (hget& hg, hget::until_handler_t handler, std::vector<hget::uri_t> urls, int32_t timeoutSec):
    _hget (hg), _handler (handler), _urls (urls), _timeoutSec (timeoutSec), _requestNum (0), _nextUrl (0) {}
  void operator() (hgot& got) {
    uint8_t urlNum = _nextUrl ? _nextUrl - 1 : _urls.size() - 1;
    float retryAfterSec = _handler (got, _urls[urlNum], _requestNum);
    if (retryAfterSec == 0.f) retry();
    else if (retryAfterSec > 0.f) {
      struct timeval wait;
      wait.tv_sec = (int) retryAfterSec;
      retryAfterSec -= wait.tv_sec;
      wait.tv_usec = (int) (retryAfterSec * 1000000.f);
      int rc = event_base_once (_hget._evbase.get(), -1, EV_TIMEOUT, hgetUntilRetryCB, new HgetUntilHandler (*this), &wait);
      if (rc) throw std::runtime_error ("HgetUntilHandler: event_base_once != 0");
    }
  }
  void start() {retry();}
  void retry() {
    uint8_t nextUrl = _nextUrl++;
    if (_nextUrl >= _urls.size()) _nextUrl = 0;
    ++_requestNum;
    _hget.go (_urls[nextUrl], _timeoutSec, *this);
  }
};

/// Used in `hget::goUntil` to wait in `evtimer_new` before repeating the request.
inline void hgetUntilRetryCB (evutil_socket_t, short, void* utilHandlerPtr) { // event_callback_fn
  std::unique_ptr<HgetUntilHandler> untilHandler ((HgetUntilHandler*) utilHandlerPtr);
  untilHandler->retry();
}

/**
 * Allows to retry the request using multiple URLs in a round-robin fashion.
 * The `handler` returns the number of seconds to wait before retrying the request or -1 if no retry is necessary.
 */
inline void hget::goUntil (std::vector<uri_t> urls, until_handler_t handler, int32_t timeoutSec) {
  HgetUntilHandler (*this, handler, urls, timeoutSec) .start();
}

}

#endif // _GLIM_HGET_INCLUDED