shill: HookTable collects async actions to be executed later

HookTable provides a facility for starting a set of generic
actions and polling for their completion.  For example, on shutdown,
each service gets disconnected.  A disconnect action may be
instantaneous or it may require some time to complete.  Users of
HookTable use the Add() function to provide a closure for starting
an action and a callback for pollings its completion.  When an event
occurs, the Run() function is called, which starts each action and
polls for completion.  Upon completion or timeout, Run() calls a
user-supplied callback to notify the caller of the state of actions.

BUG=chromium-os:22408
TEST=new unittests; ran all unittests.

Change-Id: Ia42816a2a2b3c252108665ec64bc3c68f886463b
Reviewed-on: https://gerrit.chromium.org/gerrit/22862
Commit-Ready: Gary Morain <gmorain@chromium.org>
Reviewed-by: Gary Morain <gmorain@chromium.org>
Tested-by: Gary Morain <gmorain@chromium.org>
diff --git a/Makefile b/Makefile
index 1be2edb..6e16a83 100644
--- a/Makefile
+++ b/Makefile
@@ -139,6 +139,7 @@
 	glib.o \
 	glib_io_ready_handler.o \
 	glib_io_input_handler.o \
+	hook_table.o \
 	http_proxy.o \
 	http_request.o \
 	http_url.o \
@@ -243,6 +244,7 @@
 	dhcp_provider_unittest.o \
 	dns_client_unittest.o \
 	error_unittest.o \
+	hook_table_unittest.o \
 	http_proxy_unittest.o \
 	http_request_unittest.o \
 	http_url_unittest.o \
diff --git a/hook_table.cc b/hook_table.cc
new file mode 100644
index 0000000..53dbc29
--- /dev/null
+++ b/hook_table.cc
@@ -0,0 +1,78 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shill/hook_table.h"
+
+#include <string>
+
+#include <base/bind.h>
+#include <base/callback.h>
+
+#include "shill/error.h"
+#include "shill/event_dispatcher.h"
+
+using base::Bind;
+using base::Closure;
+using base::ConstRef;
+using base::Unretained;
+using std::string;
+
+namespace shill {
+
+const int HookTable::kPollIterations = 10;
+
+HookTable::HookTable(EventDispatcher *event_dispatcher)
+    : event_dispatcher_(event_dispatcher),
+      iteration_counter_(0) {}
+
+void HookTable::Add(const string &name, const base::Closure &start,
+                    const base::Callback<bool()> &poll) {
+  hook_table_.insert(
+      HookTableMap::value_type(name, HookCallbacks(start, poll)));
+}
+
+void HookTable::Run(int timeout_seconds,
+                    const base::Callback<void(const Error &)> &done) {
+  // Run all the start actions in the hook table.
+  for (HookTableMap::const_iterator it = hook_table_.begin();
+       it != hook_table_.end(); ++it) {
+    it->second.start.Run();
+  }
+
+  // Start polling for completion.
+  iteration_counter_ = 0;
+  PollActions(timeout_seconds, done);
+}
+
+void HookTable::PollActions(int timeout_seconds,
+                            const base::Callback<void(const Error &)> &done) {
+  // Call all the poll functions in the hook table.
+  bool all_done = true;
+  for (HookTableMap::const_iterator it = hook_table_.begin();
+       it != hook_table_.end(); ++it) {
+    if (!it->second.poll.Run()) {
+      all_done = false;
+    }
+  }
+
+  if (all_done) {
+    done.Run(Error(Error::kSuccess));
+    return;
+  }
+
+  if (iteration_counter_ >= kPollIterations) {
+    done.Run(Error(Error::kOperationTimeout));
+    return;
+  }
+
+  // Some actions have not yet completed.  Queue this function to poll again
+  // later.
+  Closure poll_actions_cb = Bind(&HookTable::PollActions, Unretained(this),
+                                 timeout_seconds, ConstRef(done));
+  const uint64 delay_ms = (timeout_seconds * 1000) / kPollIterations;
+  event_dispatcher_->PostDelayedTask(poll_actions_cb, delay_ms);
+  iteration_counter_++;
+}
+
+}  // namespace shill
diff --git a/hook_table.h b/hook_table.h
new file mode 100644
index 0000000..237f132
--- /dev/null
+++ b/hook_table.h
@@ -0,0 +1,109 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SHILL_HOOK_TABLE_H_
+#define SHILL_HOOK_TABLE_H_
+
+// HookTable provides a facility for starting a set of generic actions and
+// polling for their completion.  For example, on shutdown, each service gets
+// disconnected.  A disconnect action may be instantaneous or it may require
+// some time to complete.  Users of this facility use the Add() function to
+// provide a closure for starting an action and a callback for polling its
+// completion.  When an event occurs, the Run() function is called, which starts
+// each action and polls for completion.  Upon completion or timeout, Run()
+// calls a user-supplied callback to notify the caller of the state of actions.
+//
+// Usage example.  Add an action to a hook table like this:
+//
+//   HookTable hook_table_(&event_dispatcher);
+//   Closure start_cb = Bind(&MyService::Disconnect, &my_service);
+//   Callback poll_cb = Bind(&MyService::IsConnected, &my_service);
+//   hook_table_.Add("MyService", start_cb, poll_cb);
+//
+// The code that catches an event runs the actions of the hook table like this:
+//
+//   Callback<void(const Error &)> done_cb =
+//     Bind(Manager::OnDisconnect, &manager);
+//   hook_table_.Run(kTimeout, done_cb);
+//
+// When |my_service| has completed its disconnect process,
+// Manager::OnDisconnect() gets called with Error::kSuccess.  If |my_service|
+// does not finish its disconnect processing before kTimeout, then it gets
+// called with kOperationTimeout.
+
+#include <map>
+#include <string>
+
+#include <base/basictypes.h>
+#include <base/callback.h>
+#include <gtest/gtest_prod.h>
+
+namespace shill {
+class Error;
+class EventDispatcher;
+
+class HookTable {
+ public:
+  explicit HookTable(EventDispatcher *event_dispatcher);
+
+  // Adds a closure to the hook table.  |name| should be unique; otherwise, a
+  // previous closure by the same name will NOT be replaced.  |start| will be
+  // called when Run() is called.  Run() will poll to see if the action has
+  // completed by calling |poll|, which should return true when the action has
+  // completed.
+  void Add(const std::string &name, const base::Closure &start,
+           const base::Callback<bool()> &poll);
+
+  // Runs the actions that have been added to the HookTable via Add().  It polls
+  // for completion up to |timeout_seconds|.  If all actions complete within the
+  // timeout period, |done| is called with a value of Error::kSuccess.
+  // Otherwise, it is called with Error::kOperationTimeout.
+  void Run(int timeout_seconds,
+           const base::Callback<void(const Error &)> &done);
+
+ private:
+  FRIEND_TEST(HookTableTest, ActionTimesOut);
+  FRIEND_TEST(HookTableTest, DelayedAction);
+  FRIEND_TEST(HookTableTest, MultipleActionsAllSucceed);
+  FRIEND_TEST(HookTableTest, MultipleActionsAndOneTimesOut);
+
+  // The |timeout_seconds| passed to Run() is divided into |kPollIterations|,
+  // polled once iteration.
+  static const int kPollIterations;
+
+  // For each action, there is a |start| and a |poll| callback, which are stored
+  // in this structure.
+  struct HookCallbacks {
+    HookCallbacks(const base::Closure &s, const base::Callback<bool()> &p)
+        : start(s), poll(p) {}
+    const base::Closure start;
+    const base::Callback<bool()> poll;
+  };
+
+  // Each action is stored in this table.  The key is |name| passed to Add().
+  typedef std::map<std::string, HookCallbacks> HookTableMap;
+
+  // Calls all the |poll| callbacks in |hook_table_|.  If all of them return
+  // true, then |done| is called with Error::kSuccess.  Otherwise, it queues
+  // itself in the |event_dispatcher_| to be called again later.  It repeats
+  // this process up to |kPollIterations|, after which if an action still has
+  // not completed, |done| is called with Error::kOperationTimeout.
+  void PollActions(int timeout_seconds,
+                   const base::Callback<void(const Error &)> &done);
+
+  // Each action is stored in this table.
+  HookTableMap hook_table_;
+
+  // Used for polling actions that do not complete immediately.
+  EventDispatcher *const event_dispatcher_;
+
+  // Counts the number of polling attempts.
+  int iteration_counter_;
+
+  DISALLOW_COPY_AND_ASSIGN(HookTable);
+};
+
+}  // namespace shill
+
+#endif  // SHILL_HOOK_TABLE_H_
diff --git a/hook_table_unittest.cc b/hook_table_unittest.cc
new file mode 100644
index 0000000..bb3ff14
--- /dev/null
+++ b/hook_table_unittest.cc
@@ -0,0 +1,160 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shill/hook_table.h"
+
+#include <base/bind.h>
+#include <base/callback.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "shill/error.h"
+#include "shill/mock_event_dispatcher.h"
+
+using base::Callback;
+using base::Closure;
+using base::Bind;
+using base::Unretained;
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Return;
+using ::testing::SaveArg;
+
+namespace shill {
+
+class HookTableTest : public testing::Test {
+ public:
+  MOCK_METHOD0(StartAction, void());
+  MOCK_METHOD0(PollAction, bool());
+  MOCK_METHOD1(DoneAction, void(const Error &));
+
+ protected:
+  HookTableTest()
+      : hook_table_(&event_dispatcher_) {}
+
+  MockEventDispatcher event_dispatcher_;
+  HookTable hook_table_;
+};
+
+MATCHER(IsSuccess, "") {
+  return arg.IsSuccess();
+}
+
+MATCHER(IsFailure, "") {
+  return arg.IsFailure();
+}
+
+TEST_F(HookTableTest, ActionCompletesImmediately) {
+  EXPECT_CALL(*this, StartAction());
+  EXPECT_CALL(*this, PollAction()).WillOnce(Return(true));
+  EXPECT_CALL(*this, DoneAction(IsSuccess()));
+  EXPECT_CALL(event_dispatcher_, PostDelayedTask(_, _)).Times(0);
+  Closure start_cb = Bind(&HookTableTest::StartAction, Unretained(this));
+  Callback<bool()> poll_cb = Bind(&HookTableTest::PollAction, Unretained(this));
+  Callback<void(const Error &)> done_cb = Bind(&HookTableTest::DoneAction,
+                                               Unretained(this));
+  hook_table_.Add("test", start_cb, poll_cb);
+  hook_table_.Run(0, done_cb);
+}
+
+TEST_F(HookTableTest, DelayedAction) {
+  Closure pending_cb;
+  const int kTimeout = 10;
+  const int64 kExpectedDelay = kTimeout * 1000 / HookTable::kPollIterations;
+  EXPECT_CALL(*this, StartAction());
+  {
+    InSequence s;
+    EXPECT_CALL(*this, PollAction()).WillOnce(Return(false));
+    EXPECT_CALL(event_dispatcher_, PostDelayedTask(_, kExpectedDelay))
+        .WillOnce(SaveArg<0>(&pending_cb));
+    EXPECT_CALL(*this, PollAction()).WillOnce(Return(true));
+    EXPECT_CALL(*this, DoneAction(IsSuccess()));
+  }
+  Closure start_cb = Bind(&HookTableTest::StartAction, Unretained(this));
+  Callback<bool()> poll_cb = Bind(&HookTableTest::PollAction, Unretained(this));
+  Callback<void(const Error &)> done_cb = Bind(&HookTableTest::DoneAction,
+                                               Unretained(this));
+
+  hook_table_.Add("test", start_cb, poll_cb);
+  hook_table_.Run(kTimeout, done_cb);
+  ASSERT_FALSE(pending_cb.is_null());
+  pending_cb.Run();
+}
+
+TEST_F(HookTableTest, ActionTimesOut) {
+  Closure pending_cb;
+  const int kTimeout = 10;
+  const int64 kExpectedDelay = kTimeout * 1000 / HookTable::kPollIterations;
+  EXPECT_CALL(*this, StartAction());
+  EXPECT_CALL(*this, PollAction()).WillRepeatedly(Return(false));
+  EXPECT_CALL(event_dispatcher_, PostDelayedTask(_, kExpectedDelay))
+      .Times(HookTable::kPollIterations)
+      .WillRepeatedly(SaveArg<0>(&pending_cb));
+  EXPECT_CALL(*this, DoneAction(IsFailure()));
+
+  Closure start_cb = Bind(&HookTableTest::StartAction, Unretained(this));
+  Callback<bool()> poll_cb = Bind(&HookTableTest::PollAction, Unretained(this));
+  Callback<void(const Error &)> done_cb = Bind(&HookTableTest::DoneAction,
+                                               Unretained(this));
+
+  hook_table_.Add("test", start_cb, poll_cb);
+  hook_table_.Run(kTimeout, done_cb);
+  for (int i = 0; i < HookTable::kPollIterations; ++i) {
+    ASSERT_FALSE(pending_cb.is_null());
+    pending_cb.Run();
+  }
+}
+
+TEST_F(HookTableTest, MultipleActionsAllSucceed) {
+  Closure pending_cb;
+  const int kTimeout = 10;
+  EXPECT_CALL(*this, StartAction()).Times(3);
+  EXPECT_CALL(*this, PollAction())
+      .Times(3)
+      .WillRepeatedly(Return(true));
+  EXPECT_CALL(event_dispatcher_, PostDelayedTask(_, _)).Times(0);
+  EXPECT_CALL(*this, DoneAction(IsSuccess()));
+
+  Closure start_cb = Bind(&HookTableTest::StartAction, Unretained(this));
+  Callback<bool()> poll_cb = Bind(&HookTableTest::PollAction, Unretained(this));
+  Callback<void(const Error &)> done_cb = Bind(&HookTableTest::DoneAction,
+                                               Unretained(this));
+
+  hook_table_.Add("test1", start_cb, poll_cb);
+  hook_table_.Add("test2", start_cb, poll_cb);
+  hook_table_.Add("test3", start_cb, poll_cb);
+  hook_table_.Run(kTimeout, done_cb);
+}
+
+TEST_F(HookTableTest, MultipleActionsAndOneTimesOut) {
+  Closure pending_cb;
+  const int kTimeout = 10;
+  const int64 kExpectedDelay = kTimeout * 1000 / HookTable::kPollIterations;
+  EXPECT_CALL(*this, StartAction()).Times(3);
+  EXPECT_CALL(*this, PollAction())
+      .Times((HookTable::kPollIterations + 1) * 3)
+      .WillOnce(Return(true))
+      .WillOnce(Return(true))
+      .WillRepeatedly(Return(false));
+  EXPECT_CALL(event_dispatcher_, PostDelayedTask(_, kExpectedDelay))
+      .Times(HookTable::kPollIterations)
+      .WillRepeatedly(SaveArg<0>(&pending_cb));
+  EXPECT_CALL(*this, DoneAction(IsFailure()));
+
+  Closure start_cb = Bind(&HookTableTest::StartAction, Unretained(this));
+  Callback<bool()> poll_cb = Bind(&HookTableTest::PollAction, Unretained(this));
+  Callback<void(const Error &)> done_cb = Bind(&HookTableTest::DoneAction,
+                                               Unretained(this));
+
+  hook_table_.Add("test1", start_cb, poll_cb);
+  hook_table_.Add("test2", start_cb, poll_cb);
+  hook_table_.Add("test3", start_cb, poll_cb);
+  hook_table_.Run(kTimeout, done_cb);
+  for (int i = 0; i < HookTable::kPollIterations; ++i) {
+    ASSERT_FALSE(pending_cb.is_null());
+    pending_cb.Run();
+  }
+}
+
+}  // namespace shill