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