buffet: Support retries for 5xx errors.

5xx should be generally retried a couple of times
as they usaully mean some issues on server-side.

BUG=chromium:420580
TEST=cros_workon_make buffet --test && manual testing.

Change-Id: Iada5961a77acd203ce55ba5ec914e382f2681df0
Reviewed-on: https://chromium-review.googlesource.com/223310
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Tested-by: Anton Muhin <antonm@chromium.org>
Commit-Queue: Anton Muhin <antonm@chromium.org>
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc
index c67dce0..bb7c29d 100644
--- a/buffet/device_registration_info.cc
+++ b/buffet/device_registration_info.cc
@@ -465,27 +465,102 @@
   return device_id_;
 }
 
+namespace {
+
+template <class T>
+void PostToCallback(base::Callback<void(const T&)> callback,
+                    std::unique_ptr<T> value) {
+  auto cb = [callback] (T* result) {
+    callback.Run(*result);
+  };
+  base::MessageLoop::current()->PostTask(
+      FROM_HERE, base::Bind(cb, base::Owned(value.release())));
+}
+
+// TODO(antonm): May belong to chromeos/http.
+
+void SendRequestAsync(
+    const std::string& method,
+    const std::string& url,
+    const std::string& data,
+    const std::string& mime_type,
+    const chromeos::http::HeaderList& headers,
+    std::shared_ptr<chromeos::http::Transport> transport,
+    int num_retries,
+    base::Callback<void(const chromeos::http::Response&)> callback,
+    base::Callback<void(const chromeos::Error&)> errorback) {
+  chromeos::ErrorPtr error;
+
+  auto on_retriable_failure = [&error, method, url, data, mime_type,
+       headers, transport, num_retries, callback, errorback] () {
+    if (num_retries > 0) {
+      auto c = [method, url, data, mime_type, headers, transport,
+            num_retries, callback, errorback] () {
+        SendRequestAsync(method, url,
+                         data, mime_type,
+                         headers,
+                         transport,
+                         num_retries - 1,
+                         callback, errorback);
+      };
+      base::MessageLoop::current()->PostTask(
+          FROM_HERE, base::Bind(c));
+    } else {
+      PostToCallback(errorback, std::move(error));
+    }
+  };
+
+  chromeos::http::Request request(url, method.c_str(), transport);
+  request.AddHeaders(headers);
+  if (!data.empty()) {
+    request.SetContentType(mime_type.c_str());
+    if (!request.AddRequestBody(data.c_str(), data.size(), &error)) {
+      on_retriable_failure();
+      return;
+    }
+  }
+
+  std::unique_ptr<chromeos::http::Response> response{
+    request.GetResponse(&error)};
+  if (!response) {
+    on_retriable_failure();
+    return;
+  }
+
+  int status_code{response->GetStatusCode()};
+  if (status_code >= chromeos::http::status_code::Continue &&
+      status_code < chromeos::http::status_code::BadRequest) {
+    PostToCallback(callback, std::move(response));
+    return;
+  }
+
+  // TODO(antonm): Should add some useful information to error.
+  LOG(WARNING) << "Request failed. Response code = " << status_code;
+
+  if (status_code >= 500 && status_code < 600) {
+    // Request was valid, but server failed, retry.
+    // TODO(antonm): Implement exponential backoff.
+    // TODO(antonm): Reconsider status codes, maybe only some require
+    // retry.
+    // TODO(antonm): Support Retry-After header.
+    on_retriable_failure();
+  } else {
+    PostToCallback(errorback, std::move(error));
+  }
+}
+
+}  // namespace
+
 void DeviceRegistrationInfo::DoCloudRequest(
     const char* method,
     const std::string& url,
     const base::DictionaryValue* body,
     CloudRequestCallback callback,
     CloudRequestErroback errorback) {
-  // TODO(antonm): Add retries for 5xx.
   // TODO(antonm): Add reauthorisation on access token expiration (do not
   // forget about 5xx when fetching new access token).
   // TODO(antonm): Add support for device removal.
 
-  chromeos::ErrorPtr error;
-
-  auto report_error = [errorback, &error] () {
-    auto cb = [errorback] (chromeos::Error* error_ptr) {
-      errorback.Run(*error_ptr);
-    };
-    base::MessageLoop::current()->PostTask(
-        FROM_HERE, base::Bind(cb, base::Owned(error.release())));
-  };
-
   std::string data;
   if (body)
     base::JSONWriter::Write(body, &data);
@@ -495,40 +570,26 @@
       chromeos::mime::parameters::kCharset,
       "utf-8")};
 
-  const std::unique_ptr<const chromeos::http::Response> response{
-      chromeos::http::SendRequest(
-          method,
-          url,
-          data.c_str(), data.size(),
-          mime_type.c_str(),
-          {GetAuthorizationHeader()},
-          transport_,
-          &error)};
-  if (!response) {
-    report_error();
-    return;
-  }
+  auto request_cb = [callback, errorback] (
+      const chromeos::http::Response& response) {
+    chromeos::ErrorPtr error;
 
-  int status_code{0};
-  std::unique_ptr<base::DictionaryValue> json_resp{
-      chromeos::http::ParseJsonResponse(response.get(), &status_code, &error)};
-  if (!json_resp) {
-    report_error();
-    return;
-  }
+    std::unique_ptr<base::DictionaryValue> json_resp{
+        chromeos::http::ParseJsonResponse(&response, nullptr, &error)};
+    if (!json_resp) {
+      PostToCallback(errorback, std::move(error));
+      return;
+    }
 
-  if (status_code >= chromeos::http::status_code::BadRequest) {
-    LOG(WARNING) << "Cloud request failed. Response code = " << status_code;
-    ParseGCDError(json_resp.get(), &error);
-    report_error();
-    return;
-  }
-
-  auto cb = [callback] (base::DictionaryValue* result) {
-    callback.Run(*result);
+    PostToCallback(callback, std::move(json_resp));
   };
-  base::MessageLoop::current()->PostTask(
-      FROM_HERE, base::Bind(cb, base::Owned(json_resp.release())));
+
+  SendRequestAsync(method, url,
+                   data, mime_type,
+                   {GetAuthorizationHeader()},
+                   transport_,
+                   7,
+                   base::Bind(request_cb), errorback);
 }
 
 void DeviceRegistrationInfo::StartDevice(chromeos::ErrorPtr* error) {