buffet: Serialize/queue device state updates to the cloud server

Make sure there are no simultaneous device state patche requests
sent to the GCD server. As long as one request is pending, postpone
all subsequent requests until the previous request finishes.

Since DoCloudRequest() performs all the necessary retries and transient
error handling, no additional error handling is required in
PublishStateUpdates() method.

BUG=brillo:1195
TEST=`FEATURES=test emerge-link buffet`
     `test_that -b link 100.96.49.59 e:buffet_.*`

Change-Id: Ia65f7c9f844580a01aaef89271373b8ca710ef19
Reviewed-on: https://chromium-review.googlesource.com/280861
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Commit-Queue: Vitaly Buka <vitalybuka@chromium.org>
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc
index 00899ce..2d9fb80 100644
--- a/buffet/device_registration_info.cc
+++ b/buffet/device_registration_info.cc
@@ -887,7 +887,10 @@
 }
 
 void DeviceRegistrationInfo::PublishStateUpdates() {
-  VLOG(1) << "PublishStateUpdates";
+  // If we have pending state update requests, don't send any more for now.
+  if (device_state_update_pending_)
+    return;
+
   const std::vector<StateChange> state_changes{
       state_manager_->GetAndClearRecordedStateChanges()};
   if (state_changes.empty())
@@ -921,11 +924,24 @@
                  std::to_string(base::Time::Now().ToJavaTime()));
   body.Set("patches", patches.release());
 
+  device_state_update_pending_ = true;
   DoCloudRequest(
-      chromeos::http::request_type::kPost,
-      GetDeviceURL("patchState"),
-      &body,
-      base::Bind(&IgnoreCloudResult), base::Bind(&IgnoreCloudError));
+      chromeos::http::request_type::kPost, GetDeviceURL("patchState"), &body,
+      base::Bind(&DeviceRegistrationInfo::OnPublishStateSuccess, AsWeakPtr()),
+      base::Bind(&DeviceRegistrationInfo::OnPublishStateError, AsWeakPtr()));
+}
+
+void DeviceRegistrationInfo::OnPublishStateSuccess(
+    const base::DictionaryValue& reply) {
+  device_state_update_pending_ = false;
+  // See if there were more pending state updates since the previous request
+  // had been sent out.
+  PublishStateUpdates();
+}
+
+void DeviceRegistrationInfo::OnPublishStateError(const chromeos::Error* error) {
+  LOG(ERROR) << "Permanent failure while trying to update device state";
+  device_state_update_pending_ = false;
 }
 
 void DeviceRegistrationInfo::SetRegistrationStatus(
diff --git a/buffet/device_registration_info.h b/buffet/device_registration_info.h
index c2f28b0..85b4b65 100644
--- a/buffet/device_registration_info.h
+++ b/buffet/device_registration_info.h
@@ -239,6 +239,8 @@
   void PublishCommand(const base::DictionaryValue& command);
 
   void PublishStateUpdates();
+  void OnPublishStateSuccess(const base::DictionaryValue& reply);
+  void OnPublishStateError(const chromeos::Error* error);
 
   // If unrecoverable error occurred (e.g. error parsing command instance),
   // notify the server that the command is aborted by the device.
@@ -286,6 +288,10 @@
   std::unique_ptr<chromeos::BackoffEntry::Policy> cloud_backoff_policy_;
   std::unique_ptr<chromeos::BackoffEntry> cloud_backoff_entry_;
 
+  // Flag set to true while a device state update patch request is in flight
+  // to the cloud server.
+  bool device_state_update_pending_{false};
+
   const bool notifications_enabled_;
   std::unique_ptr<NotificationChannel> primary_notification_channel_;
   std::unique_ptr<PullChannel> pull_channel_;