diff options
author | rlarocque@chromium.org <rlarocque@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-02 23:16:34 +0000 |
---|---|---|
committer | rlarocque@chromium.org <rlarocque@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-02 23:16:34 +0000 |
commit | d1929aaf01ac9c3166d31b3eeaaf2676629cebcb (patch) | |
tree | 4218d03dcb01b02a41a2eef448e5009202035855 /sync | |
parent | 713eac6708cdadc256d996e6695a4ee9d09212d8 (diff) | |
download | chromium_src-d1929aaf01ac9c3166d31b3eeaaf2676629cebcb.zip chromium_src-d1929aaf01ac9c3166d31b3eeaaf2676629cebcb.tar.gz chromium_src-d1929aaf01ac9c3166d31b3eeaaf2676629cebcb.tar.bz2 |
Revert of Move some sync/notifier to components/invalidation (https://codereview.chromium.org/294123004/)
Reason for revert:
Passed the CQ but broke the buildbot.
Original issue's description:
> Move some sync/notifier to components/invalidation
>
> Moves many of the files in sync/notifier to components/invalidation.
>
> This change does not introduce any new dependencies. The relevant
> dependency rules both before and after this change should be:
> - chrome/browser/invalidation and chrome in general depend on
> components/invalidation.
> - components/invalidation depends on sync/notifier and sync in
> general.
> - sync/notifier, components/invalidation, and various parts of
> chrome all depend on sync/internal_api/public.
>
> The eventual goal is to move all of sync/notifier into
> components/invalidation. The invalidation-related parts of
> sync/internal_api/public should be moved to components/invalidation,
> too. This will allow us to remove the deopendencies from
> components/invalidation to sync, and remove sync's dependencies on
> cacheinvalidation and libjingle.
>
> This change is a regression in terms of shared library componentization.
> the files in the sync/notifier folder could be built as a shared
> library. The files in compononents/invalidation do not support this
> yet. The SYNC_EXPORT declarations in the moved files have been changed
> to INVALIDATION_EXPORT so as to not lose this information, but the
> macros are currently #defined to no-ops.
>
> This change does not attempt to rename any classes or namespaces.
> Many of the files ported from sync/notifier still use the syncer
> namespace. Some, like SyncSystemResources, still have names tied
> to their sync heritage. This will be addressed in future CLs.
>
> Some non-trivial or non-obvious changes include:
> - invalidator_state.h was moved to sync/internal_api/public/base so it
> could be shared by both sync/ and components/invalidation. This should
> be fixed in a future CL.
> - FromNotifierReason was split out of invalidator_state.h and moved to
> the newly-created components/invalidator_reason_util.h
>
> TBR=zea,rtenneti,mallinath,dcheng
> BUG=259559
>
> Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=274350
TBR=pavely@chromium.org,dcheng@chromium.org,zea@chromium.org,rtenneti@chromium.org,mallinath@chromium.org,maniscalco@chromium.org
NOTREECHECKS=true
NOTRY=true
BUG=259559
Review URL: https://codereview.chromium.org/308413002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@274364 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'sync')
55 files changed, 7214 insertions, 55 deletions
diff --git a/sync/internal_api/public/base/invalidator_state.cc b/sync/internal_api/public/base/invalidator_state.cc deleted file mode 100644 index e42ca63..0000000 --- a/sync/internal_api/public/base/invalidator_state.cc +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2014 The Chromium 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 "sync/internal_api/public/base/invalidator_state.h" - -#include "base/logging.h" - -namespace syncer { - -const char* InvalidatorStateToString(InvalidatorState state) { - switch (state) { - case TRANSIENT_INVALIDATION_ERROR: - return "TRANSIENT_INVALIDATION_ERROR"; - case INVALIDATION_CREDENTIALS_REJECTED: - return "INVALIDATION_CREDENTIALS_REJECTED"; - case INVALIDATIONS_ENABLED: - return "INVALIDATIONS_ENABLED"; - default: - NOTREACHED(); - return "UNKNOWN_INVALIDATOR_STATE"; - } -} - -} // namespace syncer diff --git a/sync/internal_api/public/test/fake_sync_manager.h b/sync/internal_api/public/test/fake_sync_manager.h index 31f5d412..9b2dbea 100644 --- a/sync/internal_api/public/test/fake_sync_manager.h +++ b/sync/internal_api/public/test/fake_sync_manager.h @@ -12,6 +12,7 @@ #include "sync/internal_api/public/sync_manager.h" #include "sync/internal_api/public/test/null_sync_core_proxy.h" #include "sync/internal_api/public/test/test_user_share.h" +#include "sync/notifier/invalidator_registrar.h" namespace base { class SequencedTaskRunner; @@ -156,6 +157,9 @@ class FakeSyncManager : public SyncManager { // The set of types that have been enabled. ModelTypeSet enabled_types_; + // Faked invalidator state. + InvalidatorRegistrar registrar_; + // The types for which a refresh was most recently requested. ModelTypeSet last_refresh_request_types_; diff --git a/sync/internal_api/sync_manager_impl.h b/sync/internal_api/sync_manager_impl.h index dd6e191..2e02b60 100644 --- a/sync/internal_api/sync_manager_impl.h +++ b/sync/internal_api/sync_manager_impl.h @@ -19,13 +19,13 @@ #include "sync/internal_api/js_sync_encryption_handler_observer.h" #include "sync/internal_api/js_sync_manager_observer.h" #include "sync/internal_api/protocol_event_buffer.h" -#include "sync/internal_api/public/base/invalidator_state.h" #include "sync/internal_api/public/sync_core_proxy.h" #include "sync/internal_api/public/sync_manager.h" #include "sync/internal_api/public/user_share.h" #include "sync/internal_api/sync_encryption_handler_impl.h" #include "sync/js/js_backend.h" #include "sync/notifier/invalidation_handler.h" +#include "sync/notifier/invalidator_state.h" #include "sync/syncable/directory_change_delegate.h" #include "sync/util/cryptographer.h" #include "sync/util/time.h" diff --git a/sync/internal_api/sync_manager_impl_unittest.cc b/sync/internal_api/sync_manager_impl_unittest.cc index a3eff02..20b7685 100644 --- a/sync/internal_api/sync_manager_impl_unittest.cc +++ b/sync/internal_api/sync_manager_impl_unittest.cc @@ -45,6 +45,7 @@ #include "sync/js/js_backend.h" #include "sync/js/js_event_handler.h" #include "sync/js/js_test_util.h" +#include "sync/notifier/fake_invalidation_handler.h" #include "sync/notifier/invalidation_handler.h" #include "sync/notifier/invalidator.h" #include "sync/protocol/bookmark_specifics.pb.h" diff --git a/sync/internal_api/test/fake_sync_manager.cc b/sync/internal_api/test/fake_sync_manager.cc index 7526795..9374cd1 100644 --- a/sync/internal_api/test/fake_sync_manager.cc +++ b/sync/internal_api/test/fake_sync_manager.cc @@ -14,11 +14,11 @@ #include "base/sequenced_task_runner.h" #include "base/single_thread_task_runner.h" #include "base/thread_task_runner_handle.h" -#include "sync/internal_api/public/base/invalidator_state.h" #include "sync/internal_api/public/http_post_provider_factory.h" #include "sync/internal_api/public/internal_components_factory.h" #include "sync/internal_api/public/util/weak_handle.h" #include "sync/notifier/invalidator.h" +#include "sync/notifier/invalidator_state.h" #include "sync/notifier/object_id_invalidation_map.h" #include "sync/syncable/directory.h" #include "sync/test/fake_sync_encryption_handler.h" diff --git a/sync/notifier/DEPS b/sync/notifier/DEPS index 0856674..c16c291 100644 --- a/sync/notifier/DEPS +++ b/sync/notifier/DEPS @@ -1,7 +1,20 @@ include_rules = [ "+google/cacheinvalidation", + "+jingle/notifier", + "+net/base/backoff_entry.h", + "+net/base/mock_host_resolver.h", + "+net/base/network_change_notifier.h", + "+net/http/http_status_code.h", + "+net/url_request", "+sync/base", "+sync/internal_api/public/base", "+sync/internal_api/public/util", + "+sync/protocol/service_constants.h", + "+sync/util", + + # unit tests depend on talk/base. + "+talk/base", + # sync_notifier depends on the xmpp part of libjingle. + "+talk/xmpp", ] diff --git a/sync/notifier/fake_invalidation_handler.cc b/sync/notifier/fake_invalidation_handler.cc new file mode 100644 index 0000000..e6497a4 --- /dev/null +++ b/sync/notifier/fake_invalidation_handler.cc @@ -0,0 +1,40 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/fake_invalidation_handler.h" + +namespace syncer { + +FakeInvalidationHandler::FakeInvalidationHandler() + : state_(DEFAULT_INVALIDATION_ERROR), + invalidation_count_(0) {} + +FakeInvalidationHandler::~FakeInvalidationHandler() {} + +InvalidatorState FakeInvalidationHandler::GetInvalidatorState() const { + return state_; +} + +const ObjectIdInvalidationMap& +FakeInvalidationHandler::GetLastInvalidationMap() const { + return last_invalidation_map_; +} + +int FakeInvalidationHandler::GetInvalidationCount() const { + return invalidation_count_; +} + +void FakeInvalidationHandler::OnInvalidatorStateChange(InvalidatorState state) { + state_ = state; +} + +void FakeInvalidationHandler::OnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + last_invalidation_map_ = invalidation_map; + ++invalidation_count_; +} + +std::string FakeInvalidationHandler::GetOwnerName() const { return "Fake"; } + +} // namespace syncer diff --git a/sync/notifier/fake_invalidation_handler.h b/sync/notifier/fake_invalidation_handler.h new file mode 100644 index 0000000..985de50 --- /dev/null +++ b/sync/notifier/fake_invalidation_handler.h @@ -0,0 +1,42 @@ +// Copyright 2014 The Chromium 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 SYNC_NOTIFIER_FAKE_SYNC_NOTIFIER_OBSERVER_H_ +#define SYNC_NOTIFIER_FAKE_SYNC_NOTIFIER_OBSERVER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "sync/notifier/invalidation_handler.h" +#include "sync/notifier/object_id_invalidation_map.h" + +namespace syncer { + +class FakeInvalidationHandler : public InvalidationHandler { + public: + FakeInvalidationHandler(); + virtual ~FakeInvalidationHandler(); + + InvalidatorState GetInvalidatorState() const; + const ObjectIdInvalidationMap& GetLastInvalidationMap() const; + int GetInvalidationCount() const; + + // InvalidationHandler implementation. + virtual void OnInvalidatorStateChange(InvalidatorState state) OVERRIDE; + virtual void OnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) OVERRIDE; + virtual std::string GetOwnerName() const OVERRIDE; + + private: + InvalidatorState state_; + ObjectIdInvalidationMap last_invalidation_map_; + int invalidation_count_; + + DISALLOW_COPY_AND_ASSIGN(FakeInvalidationHandler); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_FAKE_SYNC_NOTIFIER_OBSERVER_H_ diff --git a/sync/notifier/fake_invalidation_state_tracker.cc b/sync/notifier/fake_invalidation_state_tracker.cc new file mode 100644 index 0000000..681bda9 --- /dev/null +++ b/sync/notifier/fake_invalidation_state_tracker.cc @@ -0,0 +1,55 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/fake_invalidation_state_tracker.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/location.h" +#include "base/task_runner.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +const int64 FakeInvalidationStateTracker::kMinVersion = kint64min; + +FakeInvalidationStateTracker::FakeInvalidationStateTracker() {} + +FakeInvalidationStateTracker::~FakeInvalidationStateTracker() {} + +void FakeInvalidationStateTracker::ClearAndSetNewClientId( + const std::string& client_id) { + Clear(); + invalidator_client_id_ = client_id; +} + +std::string FakeInvalidationStateTracker::GetInvalidatorClientId() const { + return invalidator_client_id_; +} + +void FakeInvalidationStateTracker::SetBootstrapData( + const std::string& data) { + bootstrap_data_ = data; +} + +std::string FakeInvalidationStateTracker::GetBootstrapData() const { + return bootstrap_data_; +} + +void FakeInvalidationStateTracker::SetSavedInvalidations( + const UnackedInvalidationsMap& states) { + unacked_invalidations_map_ = states; +} + +UnackedInvalidationsMap +FakeInvalidationStateTracker::GetSavedInvalidations() const { + return unacked_invalidations_map_; +} + +void FakeInvalidationStateTracker::Clear() { + invalidator_client_id_.clear(); + bootstrap_data_.clear(); +} + +} // namespace syncer diff --git a/sync/notifier/fake_invalidation_state_tracker.h b/sync/notifier/fake_invalidation_state_tracker.h new file mode 100644 index 0000000..cd19134 --- /dev/null +++ b/sync/notifier/fake_invalidation_state_tracker.h @@ -0,0 +1,42 @@ +// Copyright 2014 The Chromium 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 SYNC_NOTIFIER_FAKE_INVALIDATION_STATE_TRACKER_H_ +#define SYNC_NOTIFIER_FAKE_INVALIDATION_STATE_TRACKER_H_ + +#include "base/memory/weak_ptr.h" +#include "sync/notifier/invalidation_state_tracker.h" + +namespace syncer { + +// InvalidationStateTracker implementation that simply keeps track of +// the max versions and invalidation state in memory. +class FakeInvalidationStateTracker + : public InvalidationStateTracker, + public base::SupportsWeakPtr<FakeInvalidationStateTracker> { + public: + FakeInvalidationStateTracker(); + virtual ~FakeInvalidationStateTracker(); + + // InvalidationStateTracker implementation. + virtual void ClearAndSetNewClientId(const std::string& client_id) OVERRIDE; + virtual std::string GetInvalidatorClientId() const OVERRIDE; + virtual void SetBootstrapData(const std::string& data) OVERRIDE; + virtual std::string GetBootstrapData() const OVERRIDE; + virtual void SetSavedInvalidations( + const UnackedInvalidationsMap& states) OVERRIDE; + virtual UnackedInvalidationsMap GetSavedInvalidations() const OVERRIDE; + virtual void Clear() OVERRIDE; + + static const int64 kMinVersion; + + private: + std::string invalidator_client_id_; + std::string bootstrap_data_; + UnackedInvalidationsMap unacked_invalidations_map_; +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_FAKE_INVALIDATION_STATE_TRACKER_H_ diff --git a/sync/notifier/fake_invalidator.cc b/sync/notifier/fake_invalidator.cc new file mode 100644 index 0000000..cec6f52 --- /dev/null +++ b/sync/notifier/fake_invalidator.cc @@ -0,0 +1,69 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/fake_invalidator.h" + +#include "sync/notifier/object_id_invalidation_map.h" + +namespace syncer { + +FakeInvalidator::FakeInvalidator() {} + +FakeInvalidator::~FakeInvalidator() {} + +bool FakeInvalidator::IsHandlerRegistered(InvalidationHandler* handler) const { + return registrar_.IsHandlerRegisteredForTest(handler); +} + +ObjectIdSet FakeInvalidator::GetRegisteredIds( + InvalidationHandler* handler) const { + return registrar_.GetRegisteredIds(handler); +} + +const std::string& FakeInvalidator::GetCredentialsEmail() const { + return email_; +} + +const std::string& FakeInvalidator::GetCredentialsToken() const { + return token_; +} + +void FakeInvalidator::EmitOnInvalidatorStateChange(InvalidatorState state) { + registrar_.UpdateInvalidatorState(state); +} + +void FakeInvalidator::EmitOnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + registrar_.DispatchInvalidationsToHandlers(invalidation_map); +} + +void FakeInvalidator::RegisterHandler(InvalidationHandler* handler) { + registrar_.RegisterHandler(handler); +} + +void FakeInvalidator::UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) { + registrar_.UpdateRegisteredIds(handler, ids); +} + +void FakeInvalidator::UnregisterHandler(InvalidationHandler* handler) { + registrar_.UnregisterHandler(handler); +} + +InvalidatorState FakeInvalidator::GetInvalidatorState() const { + return registrar_.GetInvalidatorState(); +} + +void FakeInvalidator::UpdateCredentials( + const std::string& email, const std::string& token) { + email_ = email; + token_ = token; +} + +void FakeInvalidator::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const { + base::DictionaryValue value; + callback.Run(value); +} +} // namespace syncer diff --git a/sync/notifier/fake_invalidator.h b/sync/notifier/fake_invalidator.h new file mode 100644 index 0000000..25d03e2 --- /dev/null +++ b/sync/notifier/fake_invalidator.h @@ -0,0 +1,53 @@ +// Copyright 2014 The Chromium 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 SYNC_NOTIFIER_FAKE_INVALIDATOR_H_ +#define SYNC_NOTIFIER_FAKE_INVALIDATOR_H_ + +#include <string> + +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "sync/notifier/invalidation_util.h" +#include "sync/notifier/invalidator.h" +#include "sync/notifier/invalidator_registrar.h" + +namespace syncer { + +class FakeInvalidator : public Invalidator { + public: + FakeInvalidator(); + virtual ~FakeInvalidator(); + + bool IsHandlerRegistered(InvalidationHandler* handler) const; + ObjectIdSet GetRegisteredIds(InvalidationHandler* handler) const; + const std::string& GetUniqueId() const; + const std::string& GetCredentialsEmail() const; + const std::string& GetCredentialsToken() const; + + void EmitOnInvalidatorStateChange(InvalidatorState state); + void EmitOnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map); + + virtual void RegisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual void UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) OVERRIDE; + virtual void UnregisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual InvalidatorState GetInvalidatorState() const OVERRIDE; + virtual void UpdateCredentials( + const std::string& email, const std::string& token) OVERRIDE; + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const + OVERRIDE; + + private: + InvalidatorRegistrar registrar_; + std::string state_; + std::string email_; + std::string token_; +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_FAKE_INVALIDATOR_H_ diff --git a/sync/notifier/fake_invalidator_unittest.cc b/sync/notifier/fake_invalidator_unittest.cc new file mode 100644 index 0000000..c48098a --- /dev/null +++ b/sync/notifier/fake_invalidator_unittest.cc @@ -0,0 +1,63 @@ +// Copyright 2014 The Chromium 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 "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "sync/notifier/fake_invalidator.h" +#include "sync/notifier/invalidator_test_template.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +namespace { + +class FakeInvalidatorTestDelegate { + public: + FakeInvalidatorTestDelegate() {} + + ~FakeInvalidatorTestDelegate() { + DestroyInvalidator(); + } + + void CreateInvalidator( + const std::string& invalidator_client_id, + const std::string& initial_state, + const base::WeakPtr<InvalidationStateTracker>& + invalidation_state_tracker) { + DCHECK(!invalidator_.get()); + invalidator_.reset(new FakeInvalidator()); + } + + FakeInvalidator* GetInvalidator() { + return invalidator_.get(); + } + + void DestroyInvalidator() { + invalidator_.reset(); + } + + void WaitForInvalidator() { + // Do Nothing. + } + + void TriggerOnInvalidatorStateChange(InvalidatorState state) { + invalidator_->EmitOnInvalidatorStateChange(state); + } + + void TriggerOnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + invalidator_->EmitOnIncomingInvalidation(invalidation_map); + } + + private: + scoped_ptr<FakeInvalidator> invalidator_; +}; + +INSTANTIATE_TYPED_TEST_CASE_P( + FakeInvalidatorTest, InvalidatorTest, + FakeInvalidatorTestDelegate); + +} // namespace + +} // namespace syncer diff --git a/sync/notifier/gcm_network_channel.cc b/sync/notifier/gcm_network_channel.cc new file mode 100644 index 0000000..59f1a57 --- /dev/null +++ b/sync/notifier/gcm_network_channel.cc @@ -0,0 +1,430 @@ +// Copyright 2014 The Chromium 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 "base/base64.h" +#include "base/i18n/time_formatting.h" +#include "base/metrics/histogram.h" +#include "base/sha1.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#if !defined(OS_ANDROID) +// channel_common.proto defines ANDROID constant that conflicts with Android +// build. At the same time TiclInvalidationService is not used on Android so it +// is safe to exclude these protos from Android build. +#include "google/cacheinvalidation/android_channel.pb.h" +#include "google/cacheinvalidation/channel_common.pb.h" +#include "google/cacheinvalidation/types.pb.h" +#endif +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_status.h" +#include "sync/notifier/gcm_network_channel.h" +#include "sync/notifier/gcm_network_channel_delegate.h" + +namespace syncer { + +namespace { + +const char kCacheInvalidationEndpointUrl[] = + "https://clients4.google.com/invalidation/android/request/"; +const char kCacheInvalidationPackageName[] = "com.google.chrome.invalidations"; + +// Register backoff policy. +const net::BackoffEntry::Policy kRegisterBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + 0, + + // Initial delay for exponential back-off in ms. + 2000, // 2 seconds. + + // Factor by which the waiting time will be multiplied. + 2, + + // Fuzzing percentage. ex: 10% will spread requests randomly + // between 90%-100% of the calculated time. + 0.2, // 20%. + + // Maximum amount of time we are willing to delay our request in ms. + 1000 * 3600 * 4, // 4 hours. + + // Time to keep an entry from being discarded even when it + // has no significant state, -1 to never discard. + -1, + + // Don't use initial delay unless the last request was an error. + false, +}; + +// Incoming message status values for UMA_HISTOGRAM. +enum IncomingMessageStatus { + INCOMING_MESSAGE_SUCCESS, + MESSAGE_EMPTY, // GCM message's content is missing or empty. + INVALID_ENCODING, // Base64Decode failed. + INVALID_PROTO, // Parsing protobuf failed. + + // This enum is used in UMA_HISTOGRAM_ENUMERATION. Insert new values above + // this line. + INCOMING_MESSAGE_STATUS_COUNT +}; + +// Outgoing message status values for UMA_HISTOGRAM. +enum OutgoingMessageStatus { + OUTGOING_MESSAGE_SUCCESS, + MESSAGE_DISCARDED, // New message started before old one was sent. + ACCESS_TOKEN_FAILURE, // Requeting access token failed. + POST_FAILURE, // HTTP Post failed. + + // This enum is used in UMA_HISTOGRAM_ENUMERATION. Insert new values above + // this line. + OUTGOING_MESSAGE_STATUS_COUNT +}; + +const char kIncomingMessageStatusHistogram[] = + "GCMInvalidations.IncomingMessageStatus"; +const char kOutgoingMessageStatusHistogram[] = + "GCMInvalidations.OutgoingMessageStatus"; + +void RecordIncomingMessageStatus(IncomingMessageStatus status) { + UMA_HISTOGRAM_ENUMERATION(kIncomingMessageStatusHistogram, + status, + INCOMING_MESSAGE_STATUS_COUNT); +} + +void RecordOutgoingMessageStatus(OutgoingMessageStatus status) { + UMA_HISTOGRAM_ENUMERATION(kOutgoingMessageStatusHistogram, + status, + OUTGOING_MESSAGE_STATUS_COUNT); +} + +} // namespace + +GCMNetworkChannel::GCMNetworkChannel( + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + scoped_ptr<GCMNetworkChannelDelegate> delegate) + : request_context_getter_(request_context_getter), + delegate_(delegate.Pass()), + register_backoff_entry_(new net::BackoffEntry(&kRegisterBackoffPolicy)), + diagnostic_info_(this), + weak_factory_(this) { + net::NetworkChangeNotifier::AddNetworkChangeObserver(this); + delegate_->Initialize(); + Register(); +} + +GCMNetworkChannel::~GCMNetworkChannel() { + net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this); +} + +void GCMNetworkChannel::Register() { + delegate_->Register(base::Bind(&GCMNetworkChannel::OnRegisterComplete, + weak_factory_.GetWeakPtr())); +} + +void GCMNetworkChannel::OnRegisterComplete( + const std::string& registration_id, + gcm::GCMClient::Result result) { + DCHECK(CalledOnValidThread()); + if (result == gcm::GCMClient::SUCCESS) { + DCHECK(!registration_id.empty()); + DVLOG(2) << "Got registration_id"; + register_backoff_entry_->Reset(); + registration_id_ = registration_id; + if (!cached_message_.empty()) + RequestAccessToken(); + } else { + DVLOG(2) << "Register failed: " << result; + // Retry in case of transient error. + switch (result) { + case gcm::GCMClient::NETWORK_ERROR: + case gcm::GCMClient::SERVER_ERROR: + case gcm::GCMClient::TTL_EXCEEDED: + case gcm::GCMClient::UNKNOWN_ERROR: { + register_backoff_entry_->InformOfRequest(false); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&GCMNetworkChannel::Register, + weak_factory_.GetWeakPtr()), + register_backoff_entry_->GetTimeUntilRelease()); + break; + } + default: + break; + } + } + diagnostic_info_.registration_id_ = registration_id_; + diagnostic_info_.registration_result_ = result; +} + +void GCMNetworkChannel::SendMessage(const std::string& message) { + DCHECK(CalledOnValidThread()); + DCHECK(!message.empty()); + DVLOG(2) << "SendMessage"; + diagnostic_info_.sent_messages_count_++; + if (!cached_message_.empty()) { + RecordOutgoingMessageStatus(MESSAGE_DISCARDED); + } + cached_message_ = message; + + if (!registration_id_.empty()) { + RequestAccessToken(); + } +} + +void GCMNetworkChannel::RequestAccessToken() { + DCHECK(CalledOnValidThread()); + delegate_->RequestToken(base::Bind(&GCMNetworkChannel::OnGetTokenComplete, + weak_factory_.GetWeakPtr())); +} + +void GCMNetworkChannel::OnGetTokenComplete( + const GoogleServiceAuthError& error, + const std::string& token) { + DCHECK(CalledOnValidThread()); + if (cached_message_.empty()) { + // Nothing to do. + return; + } + + if (error.state() != GoogleServiceAuthError::NONE) { + // Requesting access token failed. Persistent errors will be reported by + // token service. Just drop this request, cacheinvalidations will retry + // sending message and at that time we'll retry requesting access token. + DVLOG(1) << "RequestAccessToken failed: " << error.ToString(); + RecordOutgoingMessageStatus(ACCESS_TOKEN_FAILURE); + // Message won't get sent because of connection failure. Let's retry once + // connection is restored. + if (error.state() == GoogleServiceAuthError::CONNECTION_FAILED) + NotifyStateChange(TRANSIENT_INVALIDATION_ERROR); + cached_message_.clear(); + return; + } + DCHECK(!token.empty()); + // Save access token in case POST fails and we need to invalidate it. + access_token_ = token; + + DVLOG(2) << "Got access token, sending message"; + fetcher_.reset(net::URLFetcher::Create( + BuildUrl(registration_id_), net::URLFetcher::POST, this)); + fetcher_->SetRequestContext(request_context_getter_); + const std::string auth_header("Authorization: Bearer " + access_token_); + fetcher_->AddExtraRequestHeader(auth_header); + if (!echo_token_.empty()) { + const std::string echo_header("echo-token: " + echo_token_); + fetcher_->AddExtraRequestHeader(echo_header); + } + fetcher_->SetUploadData("application/x-protobuffer", cached_message_); + fetcher_->Start(); + // Clear message to prevent accidentally resending it in the future. + cached_message_.clear(); +} + +void GCMNetworkChannel::OnURLFetchComplete(const net::URLFetcher* source) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(fetcher_, source); + // Free fetcher at the end of function. + scoped_ptr<net::URLFetcher> fetcher = fetcher_.Pass(); + + net::URLRequestStatus status = fetcher->GetStatus(); + diagnostic_info_.last_post_response_code_ = + status.is_success() ? source->GetResponseCode() : status.error(); + + if (status.is_success() && + fetcher->GetResponseCode() == net::HTTP_UNAUTHORIZED) { + DVLOG(1) << "URLFetcher failure: HTTP_UNAUTHORIZED"; + delegate_->InvalidateToken(access_token_); + } + + if (!status.is_success() || + (fetcher->GetResponseCode() != net::HTTP_OK && + fetcher->GetResponseCode() != net::HTTP_NO_CONTENT)) { + DVLOG(1) << "URLFetcher failure"; + RecordOutgoingMessageStatus(POST_FAILURE); + NotifyStateChange(TRANSIENT_INVALIDATION_ERROR); + return; + } + + RecordOutgoingMessageStatus(OUTGOING_MESSAGE_SUCCESS); + NotifyStateChange(INVALIDATIONS_ENABLED); + DVLOG(2) << "URLFetcher success"; +} + +void GCMNetworkChannel::OnIncomingMessage(const std::string& message, + const std::string& echo_token) { +#if !defined(OS_ANDROID) + if (!echo_token.empty()) + echo_token_ = echo_token; + diagnostic_info_.last_message_empty_echo_token_ = echo_token.empty(); + diagnostic_info_.last_message_received_time_ = base::Time::Now(); + + if (message.empty()) { + RecordIncomingMessageStatus(MESSAGE_EMPTY); + return; + } + std::string data; + if (!Base64DecodeURLSafe(message, &data)) { + RecordIncomingMessageStatus(INVALID_ENCODING); + return; + } + ipc::invalidation::AddressedAndroidMessage android_message; + if (!android_message.ParseFromString(data) || + !android_message.has_message()) { + RecordIncomingMessageStatus(INVALID_PROTO); + return; + } + DVLOG(2) << "Deliver incoming message"; + RecordIncomingMessageStatus(INCOMING_MESSAGE_SUCCESS); + DeliverIncomingMessage(android_message.message()); +#else + // This code shouldn't be invoked on Android. + NOTREACHED(); +#endif +} + +void GCMNetworkChannel::OnNetworkChanged( + net::NetworkChangeNotifier::ConnectionType connection_type) { + // Network connection is restored. Let's notify cacheinvalidations so it has + // chance to retry. + if (connection_type != net::NetworkChangeNotifier::CONNECTION_NONE) + NotifyStateChange(INVALIDATIONS_ENABLED); +} + +GURL GCMNetworkChannel::BuildUrl(const std::string& registration_id) { + DCHECK(!registration_id.empty()); + +#if !defined(OS_ANDROID) + ipc::invalidation::EndpointId endpoint_id; + endpoint_id.set_c2dm_registration_id(registration_id); + endpoint_id.set_client_key(std::string()); + endpoint_id.set_package_name(kCacheInvalidationPackageName); + endpoint_id.mutable_channel_version()->set_major_version( + ipc::invalidation::INITIAL); + std::string endpoint_id_buffer; + endpoint_id.SerializeToString(&endpoint_id_buffer); + + ipc::invalidation::NetworkEndpointId network_endpoint_id; + network_endpoint_id.set_network_address( + ipc::invalidation::NetworkEndpointId_NetworkAddress_ANDROID); + network_endpoint_id.set_client_address(endpoint_id_buffer); + std::string network_endpoint_id_buffer; + network_endpoint_id.SerializeToString(&network_endpoint_id_buffer); + + std::string base64URLPiece; + Base64EncodeURLSafe(network_endpoint_id_buffer, &base64URLPiece); + + std::string url(kCacheInvalidationEndpointUrl); + url += base64URLPiece; + return GURL(url); +#else + // This code shouldn't be invoked on Android. + NOTREACHED(); + return GURL(); +#endif +} + +void GCMNetworkChannel::Base64EncodeURLSafe(const std::string& input, + std::string* output) { + base::Base64Encode(input, output); + // Covert to url safe alphabet. + base::ReplaceChars(*output, "+", "-", output); + base::ReplaceChars(*output, "/", "_", output); + // Trim padding. + size_t padding_size = 0; + for (size_t i = output->size(); i > 0 && (*output)[i - 1] == '='; --i) + ++padding_size; + output->resize(output->size() - padding_size); +} + +bool GCMNetworkChannel::Base64DecodeURLSafe(const std::string& input, + std::string* output) { + // Add padding. + size_t padded_size = (input.size() + 3) - (input.size() + 3) % 4; + std::string padded_input(input); + padded_input.resize(padded_size, '='); + // Convert to standard base64 alphabet. + base::ReplaceChars(padded_input, "-", "+", &padded_input); + base::ReplaceChars(padded_input, "_", "/", &padded_input); + return base::Base64Decode(padded_input, output); +} + +void GCMNetworkChannel::SetMessageReceiver( + invalidation::MessageCallback* incoming_receiver) { + delegate_->SetMessageReceiver(base::Bind( + &GCMNetworkChannel::OnIncomingMessage, weak_factory_.GetWeakPtr())); + SyncNetworkChannel::SetMessageReceiver(incoming_receiver); +} + +void GCMNetworkChannel::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) { + callback.Run(*diagnostic_info_.CollectDebugData()); +} + +void GCMNetworkChannel::UpdateCredentials(const std::string& email, + const std::string& token) { + // Do nothing. We get access token by requesting it for every message. +} + +int GCMNetworkChannel::GetInvalidationClientType() { +#if defined(OS_IOS) + return ipc::invalidation::ClientType::CHROME_SYNC_GCM_IOS; +#else + return ipc::invalidation::ClientType::CHROME_SYNC_GCM_DESKTOP; +#endif +} + +void GCMNetworkChannel::ResetRegisterBackoffEntryForTest( + const net::BackoffEntry::Policy* policy) { + register_backoff_entry_.reset(new net::BackoffEntry(policy)); +} + +GCMNetworkChannelDiagnostic::GCMNetworkChannelDiagnostic( + GCMNetworkChannel* parent) + : parent_(parent), + last_message_empty_echo_token_(false), + last_post_response_code_(0), + registration_result_(gcm::GCMClient::UNKNOWN_ERROR), + sent_messages_count_(0) {} + +scoped_ptr<base::DictionaryValue> +GCMNetworkChannelDiagnostic::CollectDebugData() const { + scoped_ptr<base::DictionaryValue> status(new base::DictionaryValue); + status->SetString("GCMNetworkChannel.Channel", "GCM"); + std::string reg_id_hash = base::SHA1HashString(registration_id_); + status->SetString("GCMNetworkChannel.HashedRegistrationID", + base::HexEncode(reg_id_hash.c_str(), reg_id_hash.size())); + status->SetString("GCMNetworkChannel.RegistrationResult", + GCMClientResultToString(registration_result_)); + status->SetBoolean("GCMNetworkChannel.HadLastMessageEmptyEchoToken", + last_message_empty_echo_token_); + status->SetString( + "GCMNetworkChannel.LastMessageReceivedTime", + base::TimeFormatShortDateAndTime(last_message_received_time_)); + status->SetInteger("GCMNetworkChannel.LastPostResponseCode", + last_post_response_code_); + status->SetInteger("GCMNetworkChannel.SentMessages", sent_messages_count_); + status->SetInteger("GCMNetworkChannel.ReceivedMessages", + parent_->GetReceivedMessagesCount()); + return status.Pass(); +} + +std::string GCMNetworkChannelDiagnostic::GCMClientResultToString( + const gcm::GCMClient::Result result) const { +#define ENUM_CASE(x) case x: return #x; break; + switch (result) { + ENUM_CASE(gcm::GCMClient::SUCCESS); + ENUM_CASE(gcm::GCMClient::NETWORK_ERROR); + ENUM_CASE(gcm::GCMClient::SERVER_ERROR); + ENUM_CASE(gcm::GCMClient::TTL_EXCEEDED); + ENUM_CASE(gcm::GCMClient::UNKNOWN_ERROR); + ENUM_CASE(gcm::GCMClient::NOT_SIGNED_IN); + ENUM_CASE(gcm::GCMClient::INVALID_PARAMETER); + ENUM_CASE(gcm::GCMClient::ASYNC_OPERATION_PENDING); + ENUM_CASE(gcm::GCMClient::GCM_DISABLED); + } + NOTREACHED(); + return ""; +} + +} // namespace syncer diff --git a/sync/notifier/gcm_network_channel.h b/sync/notifier/gcm_network_channel.h new file mode 100644 index 0000000..5f86ce7 --- /dev/null +++ b/sync/notifier/gcm_network_channel.h @@ -0,0 +1,135 @@ +// Copyright 2014 The Chromium 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 SYNC_NOTIFIER_GCM_NETWORK_CHANNEL_H_ +#define SYNC_NOTIFIER_GCM_NETWORK_CHANNEL_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "net/base/backoff_entry.h" +#include "net/base/network_change_notifier.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "sync/base/sync_export.h" +#include "sync/notifier/gcm_network_channel_delegate.h" +#include "sync/notifier/sync_system_resources.h" +#include "url/gurl.h" + +class GoogleServiceAuthError; + +namespace syncer { +class GCMNetworkChannel; + +// POD with copy of some statuses for debugging purposes. +struct GCMNetworkChannelDiagnostic { + explicit GCMNetworkChannelDiagnostic(GCMNetworkChannel* parent); + + // Collect all the internal variables in a single readable dictionary. + scoped_ptr<base::DictionaryValue> CollectDebugData() const; + + // TODO(pavely): Move this toString to a more appropiate place in GCMClient. + std::string GCMClientResultToString( + const gcm::GCMClient::Result result) const; + + GCMNetworkChannel* parent_; + bool last_message_empty_echo_token_; + base::Time last_message_received_time_; + int last_post_response_code_; + std::string registration_id_; + gcm::GCMClient::Result registration_result_; + int sent_messages_count_; +}; + +// GCMNetworkChannel is an implementation of SyncNetworkChannel that routes +// messages through GCMService. +class SYNC_EXPORT_PRIVATE GCMNetworkChannel + : public SyncNetworkChannel, + public net::URLFetcherDelegate, + public net::NetworkChangeNotifier::NetworkChangeObserver, + public base::NonThreadSafe { + public: + GCMNetworkChannel( + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + scoped_ptr<GCMNetworkChannelDelegate> delegate); + + virtual ~GCMNetworkChannel(); + + // invalidation::NetworkChannel implementation. + virtual void SendMessage(const std::string& message) OVERRIDE; + virtual void SetMessageReceiver( + invalidation::MessageCallback* incoming_receiver) OVERRIDE; + + // SyncNetworkChannel implementation. + virtual void UpdateCredentials(const std::string& email, + const std::string& token) OVERRIDE; + virtual int GetInvalidationClientType() OVERRIDE; + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) OVERRIDE; + + // URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + // NetworkChangeObserver implementation. + virtual void OnNetworkChanged( + net::NetworkChangeNotifier::ConnectionType connection_type) OVERRIDE; + + protected: + void ResetRegisterBackoffEntryForTest( + const net::BackoffEntry::Policy* policy); + + virtual GURL BuildUrl(const std::string& registration_id); + + private: + friend class GCMNetworkChannelTest; + void Register(); + void OnRegisterComplete(const std::string& registration_id, + gcm::GCMClient::Result result); + void RequestAccessToken(); + void OnGetTokenComplete(const GoogleServiceAuthError& error, + const std::string& token); + void OnIncomingMessage(const std::string& message, + const std::string& echo_token); + + // Base64 encoding/decoding with URL safe alphabet. + // http://tools.ietf.org/html/rfc4648#page-7 + static void Base64EncodeURLSafe(const std::string& input, + std::string* output); + static bool Base64DecodeURLSafe(const std::string& input, + std::string* output); + + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + scoped_ptr<GCMNetworkChannelDelegate> delegate_; + + // Message is saved until all conditions are met: there is valid + // registration_id and access_token. + std::string cached_message_; + + // Access token is saved because in case of auth failure from server we need + // to invalidate it. + std::string access_token_; + + // GCM registration_id is requested one at startup and never refreshed until + // next restart. + std::string registration_id_; + scoped_ptr<net::BackoffEntry> register_backoff_entry_; + + scoped_ptr<net::URLFetcher> fetcher_; + + // cacheinvalidation client receives echo_token with incoming message from + // GCM and shuld include it in headers with outgoing message over http. + std::string echo_token_; + + GCMNetworkChannelDiagnostic diagnostic_info_; + + base::WeakPtrFactory<GCMNetworkChannel> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(GCMNetworkChannel); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_GCM_NETWORK_CHANNEL_H_ diff --git a/sync/notifier/gcm_network_channel_delegate.h b/sync/notifier/gcm_network_channel_delegate.h new file mode 100644 index 0000000..36485a1 --- /dev/null +++ b/sync/notifier/gcm_network_channel_delegate.h @@ -0,0 +1,51 @@ +// Copyright 2014 The Chromium 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 SYNC_NOTIFIER_GCM_NETWORK_CHANNEL_DELEGATE_H_ +#define SYNC_NOTIFIER_GCM_NETWORK_CHANNEL_DELEGATE_H_ + +#include <string> + +#include "base/callback.h" +#include "google_apis/gcm/gcm_client.h" + +class GoogleServiceAuthError; + +namespace syncer { + +// Delegate for GCMNetworkChannel. +// GCMNetworkChannel needs Register to register with GCM client and obtain gcm +// registration id. This id is used for building URL to cache invalidation +// endpoint. +// It needs RequestToken and InvalidateToken to get access token to include it +// in HTTP message to server. +// GCMNetworkChannel lives on IO thread therefore calls will be made on IO +// thread and callbacks should be invoked there as well. +class GCMNetworkChannelDelegate { + public: + typedef base::Callback<void(const GoogleServiceAuthError& error, + const std::string& token)> RequestTokenCallback; + typedef base::Callback<void(const std::string& registration_id, + gcm::GCMClient::Result result)> RegisterCallback; + typedef base::Callback<void(const std::string& message, + const std::string& echo_token)> MessageCallback; + + virtual ~GCMNetworkChannelDelegate() {} + + virtual void Initialize() = 0; + // Request access token. Callback should be called either with access token or + // error code. + virtual void RequestToken(RequestTokenCallback callback) = 0; + // Invalidate access token that was rejected by server. + virtual void InvalidateToken(const std::string& token) = 0; + + // Request registration_id from GCMService. Callback should be called with + // either registration id or error code. + virtual void Register(RegisterCallback callback) = 0; + // Provide callback for incoming messages from GCM. + virtual void SetMessageReceiver(MessageCallback callback) = 0; +}; +} // namespace syncer + +#endif // SYNC_NOTIFIER_GCM_NETWORK_CHANNEL_DELEGATE_H_ diff --git a/sync/notifier/gcm_network_channel_unittest.cc b/sync/notifier/gcm_network_channel_unittest.cc new file mode 100644 index 0000000..2564d0b --- /dev/null +++ b/sync/notifier/gcm_network_channel_unittest.cc @@ -0,0 +1,494 @@ +// Copyright 2014 The Chromium 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 "base/run_loop.h" +#include "base/strings/string_util.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_test_util.h" +#include "sync/notifier/gcm_network_channel.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +class TestGCMNetworkChannelDelegate : public GCMNetworkChannelDelegate { + public: + TestGCMNetworkChannelDelegate() + : register_call_count_(0) {} + + virtual void Initialize() OVERRIDE {} + + virtual void RequestToken(RequestTokenCallback callback) OVERRIDE { + request_token_callback = callback; + } + + virtual void InvalidateToken(const std::string& token) OVERRIDE { + invalidated_token = token; + } + + virtual void Register(RegisterCallback callback) OVERRIDE { + ++register_call_count_; + register_callback = callback; + } + + virtual void SetMessageReceiver(MessageCallback callback) OVERRIDE { + message_callback = callback; + } + + RequestTokenCallback request_token_callback; + std::string invalidated_token; + RegisterCallback register_callback; + int register_call_count_; + MessageCallback message_callback; +}; + +// Backoff policy for test. Run first 5 retries without delay. +const net::BackoffEntry::Policy kTestBackoffPolicy = { + // Number of initial errors (in sequence) to ignore before applying + // exponential back-off rules. + 5, + + // Initial delay for exponential back-off in ms. + 2000, // 2 seconds. + + // Factor by which the waiting time will be multiplied. + 2, + + // Fuzzing percentage. ex: 10% will spread requests randomly + // between 90%-100% of the calculated time. + 0.2, // 20%. + + // Maximum amount of time we are willing to delay our request in ms. + 1000 * 3600 * 4, // 4 hours. + + // Time to keep an entry from being discarded even when it + // has no significant state, -1 to never discard. + -1, + + // Don't use initial delay unless the last request was an error. + false, +}; + +class TestGCMNetworkChannel : public GCMNetworkChannel { + public: + TestGCMNetworkChannel( + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + scoped_ptr<GCMNetworkChannelDelegate> delegate) + : GCMNetworkChannel(request_context_getter, delegate.Pass()) { + ResetRegisterBackoffEntryForTest(&kTestBackoffPolicy); + } + + protected: + // On Android GCMNetworkChannel::BuildUrl hits NOTREACHED(). I still want + // tests to run. + virtual GURL BuildUrl(const std::string& registration_id) OVERRIDE { + return GURL("http://test.url.com"); + } +}; + +class GCMNetworkChannelTest; + +// Test needs to capture setting echo-token header on http request. +// This class is going to do that. +class TestNetworkChannelURLFetcher : public net::FakeURLFetcher { + public: + TestNetworkChannelURLFetcher(GCMNetworkChannelTest* test, + const GURL& url, + net::URLFetcherDelegate* delegate, + const std::string& response_data, + net::HttpStatusCode response_code, + net::URLRequestStatus::Status status) + : net::FakeURLFetcher(url, + delegate, + response_data, + response_code, + status), + test_(test) {} + + virtual void AddExtraRequestHeader(const std::string& header_line) OVERRIDE; + + private: + GCMNetworkChannelTest* test_; +}; + +class GCMNetworkChannelTest + : public ::testing::Test, + public SyncNetworkChannel::Observer { + public: + GCMNetworkChannelTest() + : delegate_(NULL), + url_fetchers_created_count_(0), + last_invalidator_state_(TRANSIENT_INVALIDATION_ERROR) {} + + virtual ~GCMNetworkChannelTest() { + } + + virtual void SetUp() { + request_context_getter_ = new net::TestURLRequestContextGetter( + base::MessageLoopProxy::current()); + // Ownership of delegate goes to GCNMentworkChannel but test needs pointer + // to it. + delegate_ = new TestGCMNetworkChannelDelegate(); + scoped_ptr<GCMNetworkChannelDelegate> delegate(delegate_); + gcm_network_channel_.reset(new TestGCMNetworkChannel( + request_context_getter_, + delegate.Pass())); + gcm_network_channel_->AddObserver(this); + gcm_network_channel_->SetMessageReceiver( + invalidation::NewPermanentCallback( + this, &GCMNetworkChannelTest::OnIncomingMessage)); + url_fetcher_factory_.reset(new net::FakeURLFetcherFactory(NULL, + base::Bind(&GCMNetworkChannelTest::CreateURLFetcher, + base::Unretained(this)))); + } + + virtual void TearDown() { + gcm_network_channel_->RemoveObserver(this); + } + + // Helper functions to call private methods from test + GURL BuildUrl(const std::string& registration_id) { + return gcm_network_channel_->GCMNetworkChannel::BuildUrl(registration_id); + } + + static void Base64EncodeURLSafe(const std::string& input, + std::string* output) { + GCMNetworkChannel::Base64EncodeURLSafe(input, output); + } + + static bool Base64DecodeURLSafe(const std::string& input, + std::string* output) { + return GCMNetworkChannel::Base64DecodeURLSafe(input, output); + } + + virtual void OnNetworkChannelStateChanged( + InvalidatorState invalidator_state) OVERRIDE { + last_invalidator_state_ = invalidator_state; + } + + void OnIncomingMessage(std::string incoming_message) { + } + + GCMNetworkChannel* network_channel() { + return gcm_network_channel_.get(); + } + + TestGCMNetworkChannelDelegate* delegate() { + return delegate_; + } + + int url_fetchers_created_count() { + return url_fetchers_created_count_; + } + + net::FakeURLFetcherFactory* url_fetcher_factory() { + return url_fetcher_factory_.get(); + } + + scoped_ptr<net::FakeURLFetcher> CreateURLFetcher( + const GURL& url, + net::URLFetcherDelegate* delegate, + const std::string& response_data, + net::HttpStatusCode response_code, + net::URLRequestStatus::Status status) { + ++url_fetchers_created_count_; + return scoped_ptr<net::FakeURLFetcher>(new TestNetworkChannelURLFetcher( + this, url, delegate, response_data, response_code, status)); + } + + void set_last_echo_token(const std::string& echo_token) { + last_echo_token_ = echo_token; + } + + const std::string& get_last_echo_token() { + return last_echo_token_; + } + + InvalidatorState get_last_invalidator_state() { + return last_invalidator_state_; + } + + void RunLoopUntilIdle() { + base::RunLoop run_loop; + run_loop.RunUntilIdle(); + } + + private: + base::MessageLoop message_loop_; + TestGCMNetworkChannelDelegate* delegate_; + scoped_ptr<GCMNetworkChannel> gcm_network_channel_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + scoped_ptr<net::FakeURLFetcherFactory> url_fetcher_factory_; + int url_fetchers_created_count_; + std::string last_echo_token_; + InvalidatorState last_invalidator_state_; +}; + +void TestNetworkChannelURLFetcher::AddExtraRequestHeader( + const std::string& header_line) { + net::FakeURLFetcher::AddExtraRequestHeader(header_line); + std::string header_name("echo-token: "); + std::string echo_token; + if (StartsWithASCII(header_line, header_name, false)) { + echo_token = header_line; + ReplaceFirstSubstringAfterOffset( + &echo_token, 0, header_name, std::string()); + test_->set_last_echo_token(echo_token); + } +} + +TEST_F(GCMNetworkChannelTest, HappyCase) { + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, get_last_invalidator_state()); + EXPECT_FALSE(delegate()->message_callback.is_null()); + url_fetcher_factory()->SetFakeResponse(GURL("http://test.url.com"), + std::string(), + net::HTTP_NO_CONTENT, + net::URLRequestStatus::SUCCESS); + + // After construction GCMNetworkChannel should have called Register. + EXPECT_FALSE(delegate()->register_callback.is_null()); + // Return valid registration id. + delegate()->register_callback.Run("registration.id", gcm::GCMClient::SUCCESS); + + network_channel()->SendMessage("abra.cadabra"); + // SendMessage should have triggered RequestToken. No HTTP request should be + // started yet. + EXPECT_FALSE(delegate()->request_token_callback.is_null()); + EXPECT_EQ(url_fetchers_created_count(), 0); + // Return valid access token. This should trigger HTTP request. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 1); + + // Return another access token. Message should be cleared by now and shouldn't + // be sent. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token2"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 1); + EXPECT_EQ(INVALIDATIONS_ENABLED, get_last_invalidator_state()); +} + +TEST_F(GCMNetworkChannelTest, FailedRegister) { + // After construction GCMNetworkChannel should have called Register. + EXPECT_FALSE(delegate()->register_callback.is_null()); + EXPECT_EQ(1, delegate()->register_call_count_); + // Return transient error from Register call. + delegate()->register_callback.Run("", gcm::GCMClient::NETWORK_ERROR); + RunLoopUntilIdle(); + // GcmNetworkChannel should have scheduled Register retry. + EXPECT_EQ(2, delegate()->register_call_count_); + // Return persistent error from Register call. + delegate()->register_callback.Run("", gcm::GCMClient::NOT_SIGNED_IN); + RunLoopUntilIdle(); + // GcmNetworkChannel should give up trying. + EXPECT_EQ(2, delegate()->register_call_count_); + + network_channel()->SendMessage("abra.cadabra"); + // SendMessage shouldn't trigger RequestToken. + EXPECT_TRUE(delegate()->request_token_callback.is_null()); + EXPECT_EQ(0, url_fetchers_created_count()); +} + +TEST_F(GCMNetworkChannelTest, RegisterFinishesAfterSendMessage) { + url_fetcher_factory()->SetFakeResponse(GURL("http://test.url.com"), + "", + net::HTTP_NO_CONTENT, + net::URLRequestStatus::SUCCESS); + + // After construction GCMNetworkChannel should have called Register. + EXPECT_FALSE(delegate()->register_callback.is_null()); + + network_channel()->SendMessage("abra.cadabra"); + // SendMessage shouldn't trigger RequestToken. + EXPECT_TRUE(delegate()->request_token_callback.is_null()); + EXPECT_EQ(url_fetchers_created_count(), 0); + + // Return valid registration id. + delegate()->register_callback.Run("registration.id", gcm::GCMClient::SUCCESS); + + EXPECT_FALSE(delegate()->request_token_callback.is_null()); + EXPECT_EQ(url_fetchers_created_count(), 0); + // Return valid access token. This should trigger HTTP request. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 1); +} + +TEST_F(GCMNetworkChannelTest, RequestTokenFailure) { + // After construction GCMNetworkChannel should have called Register. + EXPECT_FALSE(delegate()->register_callback.is_null()); + // Return valid registration id. + delegate()->register_callback.Run("registration.id", gcm::GCMClient::SUCCESS); + + network_channel()->SendMessage("abra.cadabra"); + // SendMessage should have triggered RequestToken. No HTTP request should be + // started yet. + EXPECT_FALSE(delegate()->request_token_callback.is_null()); + EXPECT_EQ(url_fetchers_created_count(), 0); + // RequestToken returns failure. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::FromConnectionError(1), ""); + + // Should be no HTTP requests. + EXPECT_EQ(url_fetchers_created_count(), 0); +} + +TEST_F(GCMNetworkChannelTest, AuthErrorFromServer) { + // Setup fake response to return AUTH_ERROR. + url_fetcher_factory()->SetFakeResponse(GURL("http://test.url.com"), + "", + net::HTTP_UNAUTHORIZED, + net::URLRequestStatus::SUCCESS); + + // After construction GCMNetworkChannel should have called Register. + EXPECT_FALSE(delegate()->register_callback.is_null()); + // Return valid registration id. + delegate()->register_callback.Run("registration.id", gcm::GCMClient::SUCCESS); + + network_channel()->SendMessage("abra.cadabra"); + // SendMessage should have triggered RequestToken. No HTTP request should be + // started yet. + EXPECT_FALSE(delegate()->request_token_callback.is_null()); + EXPECT_EQ(url_fetchers_created_count(), 0); + // Return valid access token. This should trigger HTTP request. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 1); + EXPECT_EQ(delegate()->invalidated_token, "access.token"); +} + +// Following two tests are to check for memory leaks/crashes when Register and +// RequestToken don't complete by the teardown. +TEST_F(GCMNetworkChannelTest, RegisterNeverCompletes) { + network_channel()->SendMessage("abra.cadabra"); + // Register should be called by now. Let's not complete and see what happens. + EXPECT_FALSE(delegate()->register_callback.is_null()); +} + +TEST_F(GCMNetworkChannelTest, RequestTokenNeverCompletes) { + network_channel()->SendMessage("abra.cadabra"); + // Return valid registration id. + delegate()->register_callback.Run("registration.id", gcm::GCMClient::SUCCESS); + // RequestToken should be called by now. Let's not complete and see what + // happens. + EXPECT_FALSE(delegate()->request_token_callback.is_null()); +} + +TEST_F(GCMNetworkChannelTest, Base64EncodeDecode) { + std::string input; + std::string plain; + std::string base64; + // Empty string. + Base64EncodeURLSafe(input, &base64); + EXPECT_TRUE(base64.empty()); + EXPECT_TRUE(Base64DecodeURLSafe(base64, &plain)); + EXPECT_EQ(input, plain); + // String length: 1..7. + for (int length = 1; length < 8; length++) { + input = "abra.cadabra"; + input.resize(length); + Base64EncodeURLSafe(input, &base64); + // Ensure no padding at the end. + EXPECT_NE(base64[base64.size() - 1], '='); + EXPECT_TRUE(Base64DecodeURLSafe(base64, &plain)); + EXPECT_EQ(input, plain); + } + // Presence of '-', '_'. + input = "\xfb\xff"; + Base64EncodeURLSafe(input, &base64); + EXPECT_EQ("-_8", base64); + EXPECT_TRUE(Base64DecodeURLSafe(base64, &plain)); + EXPECT_EQ(input, plain); +} + +TEST_F(GCMNetworkChannelTest, TransientError) { + EXPECT_FALSE(delegate()->message_callback.is_null()); + // POST will fail. + url_fetcher_factory()->SetFakeResponse(GURL("http://test.url.com"), + std::string(), + net::HTTP_SERVICE_UNAVAILABLE, + net::URLRequestStatus::SUCCESS); + + delegate()->register_callback.Run("registration.id", gcm::GCMClient::SUCCESS); + + network_channel()->SendMessage("abra.cadabra"); + EXPECT_FALSE(delegate()->request_token_callback.is_null()); + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 1); + // Failing HTTP POST should cause TRANSIENT_INVALIDATION_ERROR. + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, get_last_invalidator_state()); + // Network change to CONNECTION_NONE shouldn't affect invalidator state. + network_channel()->OnNetworkChanged( + net::NetworkChangeNotifier::CONNECTION_NONE); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, get_last_invalidator_state()); + // Network change to something else should trigger retry. + network_channel()->OnNetworkChanged( + net::NetworkChangeNotifier::CONNECTION_WIFI); + EXPECT_EQ(INVALIDATIONS_ENABLED, get_last_invalidator_state()); + network_channel()->OnNetworkChanged( + net::NetworkChangeNotifier::CONNECTION_NONE); + EXPECT_EQ(INVALIDATIONS_ENABLED, get_last_invalidator_state()); +} + +#if !defined(OS_ANDROID) +TEST_F(GCMNetworkChannelTest, BuildUrl) { + GURL url = BuildUrl("registration.id"); + EXPECT_TRUE(url.SchemeIsHTTPOrHTTPS()); + EXPECT_FALSE(url.host().empty()); + EXPECT_FALSE(url.path().empty()); + std::vector<std::string> parts; + Tokenize(url.path(), "/", &parts); + std::string buffer; + EXPECT_TRUE(Base64DecodeURLSafe(parts[parts.size() - 1], &buffer)); +} + +TEST_F(GCMNetworkChannelTest, EchoToken) { + url_fetcher_factory()->SetFakeResponse(GURL("http://test.url.com"), + std::string(), + net::HTTP_OK, + net::URLRequestStatus::SUCCESS); + // After construction GCMNetworkChannel should have called Register. + // Return valid registration id. + delegate()->register_callback.Run("registration.id", gcm::GCMClient::SUCCESS); + + network_channel()->SendMessage("abra.cadabra"); + // Return valid access token. This should trigger HTTP request. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 1); + EXPECT_TRUE(get_last_echo_token().empty()); + + // Trigger response. + delegate()->message_callback.Run("abra.cadabra", "echo.token"); + // Send another message. + network_channel()->SendMessage("abra.cadabra"); + // Return valid access token. This should trigger HTTP request. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 2); + EXPECT_EQ("echo.token", get_last_echo_token()); + + // Trigger response with empty echo token. + delegate()->message_callback.Run("abra.cadabra", ""); + // Send another message. + network_channel()->SendMessage("abra.cadabra"); + // Return valid access token. This should trigger HTTP request. + delegate()->request_token_callback.Run( + GoogleServiceAuthError::AuthErrorNone(), "access.token"); + RunLoopUntilIdle(); + EXPECT_EQ(url_fetchers_created_count(), 3); + // Echo_token should be from second message. + EXPECT_EQ("echo.token", get_last_echo_token()); +} +#endif + +} // namespace syncer diff --git a/sync/notifier/invalidation_handler.h b/sync/notifier/invalidation_handler.h index a999cf6..29843904 100644 --- a/sync/notifier/invalidation_handler.h +++ b/sync/notifier/invalidation_handler.h @@ -6,7 +6,7 @@ #define SYNC_NOTIFIER_INVALIDATION_HANDLER_H_ #include "sync/base/sync_export.h" -#include "sync/internal_api/public/base/invalidator_state.h" +#include "sync/notifier/invalidator_state.h" namespace syncer { diff --git a/sync/notifier/invalidation_notifier.cc b/sync/notifier/invalidation_notifier.cc new file mode 100644 index 0000000..7e3e8a3 --- /dev/null +++ b/sync/notifier/invalidation_notifier.cc @@ -0,0 +1,93 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/invalidation_notifier.h" + +#include "base/bind.h" +#include "base/logging.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "google/cacheinvalidation/include/invalidation-client-factory.h" +#include "jingle/notifier/listener/push_client.h" +#include "net/url_request/url_request_context.h" +#include "sync/notifier/invalidation_handler.h" +#include "talk/xmpp/jid.h" +#include "talk/xmpp/xmppclientsettings.h" + +namespace syncer { + +InvalidationNotifier::InvalidationNotifier( + scoped_ptr<SyncNetworkChannel> network_channel, + const std::string& invalidator_client_id, + const UnackedInvalidationsMap& saved_invalidations, + const std::string& invalidation_bootstrap_data, + const WeakHandle<InvalidationStateTracker>& invalidation_state_tracker, + const std::string& client_info) + : state_(STOPPED), + saved_invalidations_(saved_invalidations), + invalidation_state_tracker_(invalidation_state_tracker), + client_info_(client_info), + invalidator_client_id_(invalidator_client_id), + invalidation_bootstrap_data_(invalidation_bootstrap_data), + invalidation_listener_(network_channel.Pass()) { +} + +InvalidationNotifier::~InvalidationNotifier() { + DCHECK(CalledOnValidThread()); +} + +void InvalidationNotifier::RegisterHandler(InvalidationHandler* handler) { + DCHECK(CalledOnValidThread()); + registrar_.RegisterHandler(handler); +} + +void InvalidationNotifier::UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) { + DCHECK(CalledOnValidThread()); + registrar_.UpdateRegisteredIds(handler, ids); + invalidation_listener_.UpdateRegisteredIds(registrar_.GetAllRegisteredIds()); +} + +void InvalidationNotifier::UnregisterHandler(InvalidationHandler* handler) { + DCHECK(CalledOnValidThread()); + registrar_.UnregisterHandler(handler); +} + +InvalidatorState InvalidationNotifier::GetInvalidatorState() const { + DCHECK(CalledOnValidThread()); + return registrar_.GetInvalidatorState(); +} + +void InvalidationNotifier::UpdateCredentials( + const std::string& email, const std::string& token) { + if (state_ == STOPPED) { + invalidation_listener_.Start( + base::Bind(&invalidation::CreateInvalidationClient), + invalidator_client_id_, client_info_, invalidation_bootstrap_data_, + saved_invalidations_, + invalidation_state_tracker_, + this); + state_ = STARTED; + } + invalidation_listener_.UpdateCredentials(email, token); +} + +void InvalidationNotifier::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const { + DCHECK(CalledOnValidThread()); + invalidation_listener_.RequestDetailedStatus(callback); +} + +void InvalidationNotifier::OnInvalidate( + const ObjectIdInvalidationMap& invalidation_map) { + DCHECK(CalledOnValidThread()); + registrar_.DispatchInvalidationsToHandlers(invalidation_map); +} + +void InvalidationNotifier::OnInvalidatorStateChange(InvalidatorState state) { + DCHECK(CalledOnValidThread()); + registrar_.UpdateInvalidatorState(state); +} + +} // namespace syncer diff --git a/sync/notifier/invalidation_notifier.h b/sync/notifier/invalidation_notifier.h new file mode 100644 index 0000000..cd60a06 --- /dev/null +++ b/sync/notifier/invalidation_notifier.h @@ -0,0 +1,108 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// An implementation of Invalidator that wraps an invalidation +// client. Handles the details of connecting to XMPP and hooking it +// up to the invalidation client. +// +// You probably don't want to use this directly; use +// NonBlockingInvalidator. + +#ifndef SYNC_NOTIFIER_INVALIDATION_NOTIFIER_H_ +#define SYNC_NOTIFIER_INVALIDATION_NOTIFIER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "sync/base/sync_export.h" +#include "sync/internal_api/public/base/model_type.h" +#include "sync/internal_api/public/util/weak_handle.h" +#include "sync/notifier/invalidation_state_tracker.h" +#include "sync/notifier/invalidator.h" +#include "sync/notifier/invalidator_registrar.h" +#include "sync/notifier/sync_invalidation_listener.h" + +namespace notifier { +class PushClient; +} // namespace notifier + +namespace syncer { + +// This class must live on the IO thread. +class SYNC_EXPORT_PRIVATE InvalidationNotifier + : public Invalidator, + public SyncInvalidationListener::Delegate, + public base::NonThreadSafe { + public: + // |invalidation_state_tracker| must be initialized. + InvalidationNotifier( + scoped_ptr<SyncNetworkChannel> network_channel, + const std::string& invalidator_client_id, + const UnackedInvalidationsMap& saved_invalidations, + const std::string& invalidation_bootstrap_data, + const WeakHandle<InvalidationStateTracker>& + invalidation_state_tracker, + const std::string& client_info); + + virtual ~InvalidationNotifier(); + + // Invalidator implementation. + virtual void RegisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual void UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) OVERRIDE; + virtual void UnregisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual InvalidatorState GetInvalidatorState() const OVERRIDE; + virtual void UpdateCredentials( + const std::string& email, const std::string& token) OVERRIDE; + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const + OVERRIDE; + + // SyncInvalidationListener::Delegate implementation. + virtual void OnInvalidate( + const ObjectIdInvalidationMap& invalidation_map) OVERRIDE; + virtual void OnInvalidatorStateChange(InvalidatorState state) OVERRIDE; + + private: + // We start off in the STOPPED state. When we get our initial + // credentials, we connect and move to the CONNECTING state. When + // we're connected we start the invalidation client and move to the + // STARTED state. We never go back to a previous state. + enum State { + STOPPED, + CONNECTING, + STARTED + }; + State state_; + + InvalidatorRegistrar registrar_; + + // Passed to |invalidation_listener_|. + const UnackedInvalidationsMap saved_invalidations_; + + // Passed to |invalidation_listener_|. + const WeakHandle<InvalidationStateTracker> + invalidation_state_tracker_; + + // Passed to |invalidation_listener_|. + const std::string client_info_; + + // The client ID to pass to |invalidation_listener_|. + const std::string invalidator_client_id_; + + // The initial bootstrap data to pass to |invalidation_listener_|. + const std::string invalidation_bootstrap_data_; + + // The invalidation listener. + SyncInvalidationListener invalidation_listener_; + + DISALLOW_COPY_AND_ASSIGN(InvalidationNotifier); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_INVALIDATION_NOTIFIER_H_ diff --git a/sync/notifier/invalidation_notifier_unittest.cc b/sync/notifier/invalidation_notifier_unittest.cc new file mode 100644 index 0000000..75c6b3a --- /dev/null +++ b/sync/notifier/invalidation_notifier_unittest.cc @@ -0,0 +1,92 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/invalidation_notifier.h" + +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "jingle/notifier/base/fake_base_task.h" +#include "jingle/notifier/base/notifier_options.h" +#include "jingle/notifier/listener/fake_push_client.h" +#include "net/url_request/url_request_test_util.h" +#include "sync/internal_api/public/base/model_type.h" +#include "sync/internal_api/public/util/weak_handle.h" +#include "sync/notifier/fake_invalidation_handler.h" +#include "sync/notifier/fake_invalidation_state_tracker.h" +#include "sync/notifier/invalidation_state_tracker.h" +#include "sync/notifier/invalidator_test_template.h" +#include "sync/notifier/push_client_channel.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +namespace { + +class InvalidationNotifierTestDelegate { + public: + InvalidationNotifierTestDelegate() {} + + ~InvalidationNotifierTestDelegate() { + DestroyInvalidator(); + } + + void CreateInvalidator( + const std::string& invalidator_client_id, + const std::string& initial_state, + const base::WeakPtr<InvalidationStateTracker>& + invalidation_state_tracker) { + DCHECK(!invalidator_.get()); + scoped_ptr<notifier::PushClient> push_client( + new notifier::FakePushClient()); + scoped_ptr<SyncNetworkChannel> network_channel( + new PushClientChannel(push_client.Pass())); + invalidator_.reset( + new InvalidationNotifier( + network_channel.Pass(), + invalidator_client_id, + UnackedInvalidationsMap(), + initial_state, + MakeWeakHandle(invalidation_state_tracker), + "fake_client_info")); + } + + Invalidator* GetInvalidator() { + return invalidator_.get(); + } + + void DestroyInvalidator() { + // Stopping the invalidation notifier stops its scheduler, which deletes + // any pending tasks without running them. Some tasks "run and delete" + // another task, so they must be run in order to avoid leaking the inner + // task. Stopping does not schedule any tasks, so it's both necessary and + // sufficient to drain the task queue before stopping the notifier. + message_loop_.RunUntilIdle(); + invalidator_.reset(); + } + + void WaitForInvalidator() { + message_loop_.RunUntilIdle(); + } + + void TriggerOnInvalidatorStateChange(InvalidatorState state) { + invalidator_->OnInvalidatorStateChange(state); + } + + void TriggerOnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + invalidator_->OnInvalidate(invalidation_map); + } + + private: + base::MessageLoop message_loop_; + scoped_ptr<InvalidationNotifier> invalidator_; +}; + +INSTANTIATE_TYPED_TEST_CASE_P( + InvalidationNotifierTest, InvalidatorTest, + InvalidationNotifierTestDelegate); + +} // namespace + +} // namespace syncer diff --git a/sync/notifier/invalidation_state_tracker.cc b/sync/notifier/invalidation_state_tracker.cc deleted file mode 100644 index ab3ce17..0000000 --- a/sync/notifier/invalidation_state_tracker.cc +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2014 The Chromium 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 "sync/notifier/invalidation_state_tracker.h" - -namespace syncer { - -InvalidationStateTracker::InvalidationStateTracker() {} - -InvalidationStateTracker::~InvalidationStateTracker() {} - -} // namespace syncer diff --git a/sync/notifier/invalidation_state_tracker.h b/sync/notifier/invalidation_state_tracker.h index 6f4c133..22e3e6f 100644 --- a/sync/notifier/invalidation_state_tracker.h +++ b/sync/notifier/invalidation_state_tracker.h @@ -32,8 +32,8 @@ namespace syncer { class SYNC_EXPORT InvalidationStateTracker { public: - InvalidationStateTracker(); - virtual ~InvalidationStateTracker(); + InvalidationStateTracker() {} + virtual ~InvalidationStateTracker() {} // The per-client unique ID used to register the invalidation client with the // server. This is used to squelch invalidation notifications that originate diff --git a/sync/notifier/invalidator.h b/sync/notifier/invalidator.h index 0ed26e2..3c50fe6 100644 --- a/sync/notifier/invalidator.h +++ b/sync/notifier/invalidator.h @@ -12,9 +12,9 @@ #include <string> #include "sync/base/sync_export.h" -#include "sync/internal_api/public/base/invalidator_state.h" #include "sync/internal_api/public/base/model_type.h" #include "sync/notifier/invalidation_util.h" +#include "sync/notifier/invalidator_state.h" namespace syncer { class InvalidationHandler; diff --git a/sync/notifier/invalidator_registrar.cc b/sync/notifier/invalidator_registrar.cc new file mode 100644 index 0000000..27f206b --- /dev/null +++ b/sync/notifier/invalidator_registrar.cc @@ -0,0 +1,149 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/invalidator_registrar.h" + +#include <cstddef> +#include <iterator> +#include <utility> + +#include "base/logging.h" +#include "sync/notifier/object_id_invalidation_map.h" + +namespace syncer { + +InvalidatorRegistrar::InvalidatorRegistrar() + : state_(DEFAULT_INVALIDATION_ERROR) {} + +InvalidatorRegistrar::~InvalidatorRegistrar() { + DCHECK(thread_checker_.CalledOnValidThread()); + CHECK(!handlers_.might_have_observers()); + CHECK(handler_to_ids_map_.empty()); +} + +void InvalidatorRegistrar::RegisterHandler(InvalidationHandler* handler) { + DCHECK(thread_checker_.CalledOnValidThread()); + CHECK(handler); + CHECK(!handlers_.HasObserver(handler)); + handlers_.AddObserver(handler); +} + +void InvalidatorRegistrar::UpdateRegisteredIds( + InvalidationHandler* handler, + const ObjectIdSet& ids) { + DCHECK(thread_checker_.CalledOnValidThread()); + CHECK(handler); + CHECK(handlers_.HasObserver(handler)); + + for (HandlerIdsMap::const_iterator it = handler_to_ids_map_.begin(); + it != handler_to_ids_map_.end(); ++it) { + if (it->first == handler) { + continue; + } + + std::vector<invalidation::ObjectId> intersection; + std::set_intersection( + it->second.begin(), it->second.end(), + ids.begin(), ids.end(), + std::inserter(intersection, intersection.end()), + ObjectIdLessThan()); + CHECK(intersection.empty()) + << "Duplicate registration: trying to register " + << ObjectIdToString(*intersection.begin()) << " for " + << handler << " when it's already registered for " + << it->first; + } + + if (ids.empty()) { + handler_to_ids_map_.erase(handler); + } else { + handler_to_ids_map_[handler] = ids; + } +} + +void InvalidatorRegistrar::UnregisterHandler(InvalidationHandler* handler) { + DCHECK(thread_checker_.CalledOnValidThread()); + CHECK(handler); + CHECK(handlers_.HasObserver(handler)); + handlers_.RemoveObserver(handler); + handler_to_ids_map_.erase(handler); +} + +ObjectIdSet InvalidatorRegistrar::GetRegisteredIds( + InvalidationHandler* handler) const { + DCHECK(thread_checker_.CalledOnValidThread()); + HandlerIdsMap::const_iterator lookup = handler_to_ids_map_.find(handler); + if (lookup != handler_to_ids_map_.end()) { + return lookup->second; + } else { + return ObjectIdSet(); + } +} + +ObjectIdSet InvalidatorRegistrar::GetAllRegisteredIds() const { + DCHECK(thread_checker_.CalledOnValidThread()); + ObjectIdSet registered_ids; + for (HandlerIdsMap::const_iterator it = handler_to_ids_map_.begin(); + it != handler_to_ids_map_.end(); ++it) { + registered_ids.insert(it->second.begin(), it->second.end()); + } + return registered_ids; +} + +void InvalidatorRegistrar::DispatchInvalidationsToHandlers( + const ObjectIdInvalidationMap& invalidation_map) { + DCHECK(thread_checker_.CalledOnValidThread()); + // If we have no handlers, there's nothing to do. + if (!handlers_.might_have_observers()) { + return; + } + + for (HandlerIdsMap::iterator it = handler_to_ids_map_.begin(); + it != handler_to_ids_map_.end(); ++it) { + ObjectIdInvalidationMap to_emit = + invalidation_map.GetSubsetWithObjectIds(it->second); + if (!to_emit.Empty()) { + it->first->OnIncomingInvalidation(to_emit); + } + } +} + +void InvalidatorRegistrar::UpdateInvalidatorState(InvalidatorState state) { + DCHECK(thread_checker_.CalledOnValidThread()); + DVLOG(1) << "New invalidator state: " << InvalidatorStateToString(state_) + << " -> " << InvalidatorStateToString(state); + state_ = state; + FOR_EACH_OBSERVER(InvalidationHandler, handlers_, + OnInvalidatorStateChange(state)); +} + +InvalidatorState InvalidatorRegistrar::GetInvalidatorState() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return state_; +} + +std::map<std::string, ObjectIdSet> +InvalidatorRegistrar::GetSanitizedHandlersIdsMap() { + DCHECK(thread_checker_.CalledOnValidThread()); + std::map<std::string, ObjectIdSet> clean_handlers_to_ids; + for (HandlerIdsMap::const_iterator it = handler_to_ids_map_.begin(); + it != handler_to_ids_map_.end(); + ++it) { + clean_handlers_to_ids[it->first->GetOwnerName()] = ObjectIdSet(it->second); + } + return clean_handlers_to_ids; +} + +bool InvalidatorRegistrar::IsHandlerRegisteredForTest( + InvalidationHandler* handler) const { + DCHECK(thread_checker_.CalledOnValidThread()); + return handlers_.HasObserver(handler); +} + +void InvalidatorRegistrar::DetachFromThreadForTest() { + DCHECK(thread_checker_.CalledOnValidThread()); + thread_checker_.DetachFromThread(); +} + +} // namespace syncer diff --git a/sync/notifier/invalidator_registrar.h b/sync/notifier/invalidator_registrar.h new file mode 100644 index 0000000..2b636d7 --- /dev/null +++ b/sync/notifier/invalidator_registrar.h @@ -0,0 +1,97 @@ +// Copyright 2014 The Chromium 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 SYNC_NOTIFIER_INVALIDATOR_REGISTRAR_H_ +#define SYNC_NOTIFIER_INVALIDATOR_REGISTRAR_H_ + +#include <map> + +#include "base/basictypes.h" +#include "base/observer_list.h" +#include "base/threading/thread_checker.h" +#include "sync/base/sync_export.h" +#include "sync/notifier/invalidation_handler.h" +#include "sync/notifier/invalidation_util.h" + +namespace invalidation { +class ObjectId; +} // namespace invalidation + +namespace syncer { + +class ObjectIdInvalidationMap; + +// A helper class for implementations of the Invalidator interface. It helps +// keep track of registered handlers and which object ID registrations are +// associated with which handlers, so implementors can just reuse the logic +// here to dispatch invalidations and other interesting notifications. +class SYNC_EXPORT InvalidatorRegistrar { + public: + InvalidatorRegistrar(); + + // It is an error to have registered handlers on destruction. + ~InvalidatorRegistrar(); + + // Starts sending notifications to |handler|. |handler| must not be NULL, + // and it must already be registered. + void RegisterHandler(InvalidationHandler* handler); + + // Updates the set of ObjectIds associated with |handler|. |handler| must + // not be NULL, and must already be registered. An ID must be registered for + // at most one handler. + void UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids); + + // Stops sending notifications to |handler|. |handler| must not be NULL, and + // it must already be registered. Note that this doesn't unregister the IDs + // associated with |handler|. + void UnregisterHandler(InvalidationHandler* handler); + + ObjectIdSet GetRegisteredIds(InvalidationHandler* handler) const; + + // Returns the set of all IDs that are registered to some handler (even + // handlers that have been unregistered). + ObjectIdSet GetAllRegisteredIds() const; + + // Sorts incoming invalidations into a bucket for each handler and then + // dispatches the batched invalidations to the corresponding handler. + // Invalidations for IDs with no corresponding handler are dropped, as are + // invalidations for handlers that are not added. + void DispatchInvalidationsToHandlers( + const ObjectIdInvalidationMap& invalidation_map); + + // Updates the invalidator state to the given one and then notifies + // all handlers. Note that the order is important; handlers that + // call GetInvalidatorState() when notified will see the new state. + void UpdateInvalidatorState(InvalidatorState state); + + // Returns the current invalidator state. When called from within + // InvalidationHandler::OnInvalidatorStateChange(), this returns the + // updated state. + InvalidatorState GetInvalidatorState() const; + + // Gets a new map for the name of invalidator handlers and their + // objects id. This is used by the InvalidatorLogger to be able + // to display every registered handlers and its objectsIds. + std::map<std::string, ObjectIdSet> GetSanitizedHandlersIdsMap(); + + bool IsHandlerRegisteredForTest(InvalidationHandler* handler) const; + + // Needed for death tests. + void DetachFromThreadForTest(); + + private: + typedef std::map<InvalidationHandler*, ObjectIdSet> HandlerIdsMap; + + base::ThreadChecker thread_checker_; + ObserverList<InvalidationHandler> handlers_; + HandlerIdsMap handler_to_ids_map_; + InvalidatorState state_; + + DISALLOW_COPY_AND_ASSIGN(InvalidatorRegistrar); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_INVALIDATOR_REGISTRAR_H_ diff --git a/sync/notifier/invalidator_registrar_unittest.cc b/sync/notifier/invalidator_registrar_unittest.cc new file mode 100644 index 0000000..24e1908 --- /dev/null +++ b/sync/notifier/invalidator_registrar_unittest.cc @@ -0,0 +1,163 @@ +// Copyright 2014 The Chromium 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 "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "google/cacheinvalidation/types.pb.h" +#include "sync/notifier/fake_invalidation_handler.h" +#include "sync/notifier/invalidator_registrar.h" +#include "sync/notifier/invalidator_test_template.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +namespace { + +// We test InvalidatorRegistrar by wrapping it in an Invalidator and +// running the usual Invalidator tests. + +// Thin Invalidator wrapper around InvalidatorRegistrar. +class RegistrarInvalidator : public Invalidator { + public: + RegistrarInvalidator() {} + virtual ~RegistrarInvalidator() {} + + InvalidatorRegistrar* GetRegistrar() { + return ®istrar_; + } + + // Invalidator implementation. + virtual void RegisterHandler(InvalidationHandler* handler) OVERRIDE { + registrar_.RegisterHandler(handler); + } + + virtual void UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) OVERRIDE { + registrar_.UpdateRegisteredIds(handler, ids); + } + + virtual void UnregisterHandler(InvalidationHandler* handler) OVERRIDE { + registrar_.UnregisterHandler(handler); + } + + virtual InvalidatorState GetInvalidatorState() const OVERRIDE { + return registrar_.GetInvalidatorState(); + } + + virtual void UpdateCredentials( + const std::string& email, const std::string& token) OVERRIDE { + // Do nothing. + } + + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> call) const OVERRIDE { + // Do nothing. + } + + private: + InvalidatorRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(RegistrarInvalidator); +}; + +class RegistrarInvalidatorTestDelegate { + public: + RegistrarInvalidatorTestDelegate() {} + + ~RegistrarInvalidatorTestDelegate() { + DestroyInvalidator(); + } + + void CreateInvalidator( + const std::string& invalidator_client_id, + const std::string& initial_state, + const base::WeakPtr<InvalidationStateTracker>& + invalidation_state_tracker) { + DCHECK(!invalidator_.get()); + invalidator_.reset(new RegistrarInvalidator()); + } + + RegistrarInvalidator* GetInvalidator() { + return invalidator_.get(); + } + + void DestroyInvalidator() { + invalidator_.reset(); + } + + void WaitForInvalidator() { + // Do nothing. + } + + void TriggerOnInvalidatorStateChange(InvalidatorState state) { + invalidator_->GetRegistrar()->UpdateInvalidatorState(state); + } + + void TriggerOnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + invalidator_->GetRegistrar()->DispatchInvalidationsToHandlers( + invalidation_map); + } + + private: + scoped_ptr<RegistrarInvalidator> invalidator_; +}; + +INSTANTIATE_TYPED_TEST_CASE_P( + RegistrarInvalidatorTest, InvalidatorTest, + RegistrarInvalidatorTestDelegate); + +class InvalidatorRegistrarTest : public testing::Test {}; + +// Technically the tests below can be part of InvalidatorTest, but we +// want to keep the number of death tests down. + +// When we expect a death via CHECK(), we can't match against the +// CHECK() message since they are removed in official builds. + +#if GTEST_HAS_DEATH_TEST +// Having registered handlers on destruction should cause a CHECK. +TEST_F(InvalidatorRegistrarTest, RegisteredHandlerOnDestruction) { + scoped_ptr<InvalidatorRegistrar> registrar(new InvalidatorRegistrar()); + FakeInvalidationHandler handler; + + registrar->RegisterHandler(&handler); + + EXPECT_DEATH({ registrar.reset(); }, ""); + + ASSERT_TRUE(registrar.get()); + registrar->UnregisterHandler(&handler); +} + +// Multiple registrations by different handlers on the same object ID should +// cause a CHECK. +TEST_F(InvalidatorRegistrarTest, MultipleRegistration) { + const invalidation::ObjectId id1(ipc::invalidation::ObjectSource::TEST, "a"); + const invalidation::ObjectId id2(ipc::invalidation::ObjectSource::TEST, "a"); + + InvalidatorRegistrar registrar; + + FakeInvalidationHandler handler1; + registrar.RegisterHandler(&handler1); + + FakeInvalidationHandler handler2; + registrar.RegisterHandler(&handler2); + + ObjectIdSet ids; + ids.insert(id1); + ids.insert(id2); + registrar.UpdateRegisteredIds(&handler1, ids); + + registrar.DetachFromThreadForTest(); + EXPECT_DEATH({ registrar.UpdateRegisteredIds(&handler2, ids); }, ""); + + registrar.UnregisterHandler(&handler2); + registrar.UnregisterHandler(&handler1); +} +#endif // GTEST_HAS_DEATH_TEST + +} // namespace + +} // namespace syncer diff --git a/sync/notifier/invalidator_state.cc b/sync/notifier/invalidator_state.cc new file mode 100644 index 0000000..b57f4d0 --- /dev/null +++ b/sync/notifier/invalidator_state.cc @@ -0,0 +1,55 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/invalidator_state.h" + +#include "base/logging.h" + +namespace syncer { + +const char* InvalidatorStateToString(InvalidatorState state) { + switch (state) { + case TRANSIENT_INVALIDATION_ERROR: + return "TRANSIENT_INVALIDATION_ERROR"; + case INVALIDATION_CREDENTIALS_REJECTED: + return "INVALIDATION_CREDENTIALS_REJECTED"; + case INVALIDATIONS_ENABLED: + return "INVALIDATIONS_ENABLED"; + default: + NOTREACHED(); + return "UNKNOWN_INVALIDATOR_STATE"; + } +} + +InvalidatorState FromNotifierReason( + notifier::NotificationsDisabledReason reason) { + switch (reason) { + case notifier::NO_NOTIFICATION_ERROR: + return INVALIDATIONS_ENABLED; + case notifier::TRANSIENT_NOTIFICATION_ERROR: + return TRANSIENT_INVALIDATION_ERROR; + case notifier::NOTIFICATION_CREDENTIALS_REJECTED: + return INVALIDATION_CREDENTIALS_REJECTED; + default: + NOTREACHED(); + return DEFAULT_INVALIDATION_ERROR; + } +} + +notifier::NotificationsDisabledReason ToNotifierReasonForTest( + InvalidatorState state) { + switch (state) { + case TRANSIENT_INVALIDATION_ERROR: + return notifier::TRANSIENT_NOTIFICATION_ERROR; + case INVALIDATION_CREDENTIALS_REJECTED: + return notifier::NOTIFICATION_CREDENTIALS_REJECTED; + case INVALIDATIONS_ENABLED: + // Fall through. + default: + NOTREACHED(); + return notifier::TRANSIENT_NOTIFICATION_ERROR; + } +} + +} // namespace syncer diff --git a/sync/internal_api/public/base/invalidator_state.h b/sync/notifier/invalidator_state.h index 3a1c7ee..a269661 100644 --- a/sync/internal_api/public/base/invalidator_state.h +++ b/sync/notifier/invalidator_state.h @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef SYNC_INTERNAL_API_PUBLIC_BASE_INVALIDATOR_STATE_H_ -#define SYNC_INTERNAL_API_PUBLIC_BASE_INVALIDATOR_STATE_H_ +#ifndef SYNC_NOTIFIER_INVALIDATOR_STATE_H_ +#define SYNC_NOTIFIER_INVALIDATOR_STATE_H_ +#include "jingle/notifier/listener/push_client_observer.h" #include "sync/base/sync_export.h" namespace syncer { @@ -25,6 +26,13 @@ enum InvalidatorState { SYNC_EXPORT const char* InvalidatorStateToString(InvalidatorState state); +InvalidatorState FromNotifierReason( + notifier::NotificationsDisabledReason reason); + +// Should not be called when |state| == INVALIDATIONS_ENABLED. +SYNC_EXPORT_PRIVATE notifier::NotificationsDisabledReason + ToNotifierReasonForTest(InvalidatorState state); + } // namespace syncer -#endif // SYNC_INTERNAL_API_PUBLIC_BASE_INVALIDATOR_STATE_H_ +#endif // SYNC_NOTIFIER_INVALIDATOR_STATE_H_ diff --git a/sync/notifier/invalidator_test_template.cc b/sync/notifier/invalidator_test_template.cc new file mode 100644 index 0000000..0a93cf3 --- /dev/null +++ b/sync/notifier/invalidator_test_template.cc @@ -0,0 +1,28 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/invalidator_test_template.h" + +namespace syncer { +namespace internal { + +BoundFakeInvalidationHandler::BoundFakeInvalidationHandler( + const Invalidator& invalidator) + : invalidator_(invalidator), + last_retrieved_state_(DEFAULT_INVALIDATION_ERROR) {} + +BoundFakeInvalidationHandler::~BoundFakeInvalidationHandler() {} + +InvalidatorState BoundFakeInvalidationHandler::GetLastRetrievedState() const { + return last_retrieved_state_; +} + +void BoundFakeInvalidationHandler::OnInvalidatorStateChange( + InvalidatorState state) { + FakeInvalidationHandler::OnInvalidatorStateChange(state); + last_retrieved_state_ = invalidator_.GetInvalidatorState(); +} + +} // namespace internal +} // namespace syncer diff --git a/sync/notifier/invalidator_test_template.h b/sync/notifier/invalidator_test_template.h new file mode 100644 index 0000000..c153088 --- /dev/null +++ b/sync/notifier/invalidator_test_template.h @@ -0,0 +1,377 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This class defines tests that implementations of Invalidator should pass in +// order to be conformant. Here's how you use it to test your implementation. +// +// Say your class is called MyInvalidator. Then you need to define a class +// called MyInvalidatorTestDelegate in my_sync_notifier_unittest.cc like this: +// +// class MyInvalidatorTestDelegate { +// public: +// MyInvalidatorTestDelegate() ... +// +// ~MyInvalidatorTestDelegate() { +// // DestroyInvalidator() may not be explicitly called by tests. +// DestroyInvalidator(); +// } +// +// // Create the Invalidator implementation with the given parameters. +// void CreateInvalidator( +// const std::string& initial_state, +// const base::WeakPtr<InvalidationStateTracker>& +// invalidation_state_tracker) { +// ... +// } +// +// // Should return the Invalidator implementation. Only called after +// // CreateInvalidator and before DestroyInvalidator. +// MyInvalidator* GetInvalidator() { +// ... +// } +// +// // Destroy the Invalidator implementation. +// void DestroyInvalidator() { +// ... +// } +// +// // Called after a call to SetUniqueId(), or UpdateCredentials() on the +// // Invalidator implementation. Should block until the effects of the +// // call are visible on the current thread. +// void WaitForInvalidator() { +// ... +// } +// +// // The Trigger* functions below should block until the effects of +// // the call are visible on the current thread. +// +// // Should cause OnInvalidatorStateChange() to be called on all +// // observers of the Invalidator implementation with the given +// // parameters. +// void TriggerOnInvalidatorStateChange(InvalidatorState state) { +// ... +// } +// +// // Should cause OnIncomingInvalidation() to be called on all +// // observers of the Invalidator implementation with the given +// // parameters. +// void TriggerOnIncomingInvalidation( +// const ObjectIdInvalidationMap& invalidation_map) { +// ... +// } +// }; +// +// The InvalidatorTest test harness will have a member variable of +// this delegate type and will call its functions in the various +// tests. +// +// Then you simply #include this file as well as gtest.h and add the +// following statement to my_sync_notifier_unittest.cc: +// +// INSTANTIATE_TYPED_TEST_CASE_P( +// MyInvalidator, InvalidatorTest, MyInvalidatorTestDelegate); +// +// Easy! + +#ifndef SYNC_NOTIFIER_INVALIDATOR_TEST_TEMPLATE_H_ +#define SYNC_NOTIFIER_INVALIDATOR_TEST_TEMPLATE_H_ + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "google/cacheinvalidation/include/types.h" +#include "google/cacheinvalidation/types.pb.h" +#include "sync/internal_api/public/base/object_id_invalidation_map_test_util.h" +#include "sync/notifier/fake_invalidation_handler.h" +#include "sync/notifier/fake_invalidation_state_tracker.h" +#include "sync/notifier/invalidator.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +template <typename InvalidatorTestDelegate> +class InvalidatorTest : public testing::Test { + protected: + InvalidatorTest() + : id1(ipc::invalidation::ObjectSource::TEST, "a"), + id2(ipc::invalidation::ObjectSource::TEST, "b"), + id3(ipc::invalidation::ObjectSource::TEST, "c"), + id4(ipc::invalidation::ObjectSource::TEST, "d") { + } + + Invalidator* CreateAndInitializeInvalidator() { + this->delegate_.CreateInvalidator("fake_invalidator_client_id", + "fake_initial_state", + this->fake_tracker_.AsWeakPtr()); + Invalidator* const invalidator = this->delegate_.GetInvalidator(); + + this->delegate_.WaitForInvalidator(); + invalidator->UpdateCredentials("foo@bar.com", "fake_token"); + this->delegate_.WaitForInvalidator(); + + return invalidator; + } + + FakeInvalidationStateTracker fake_tracker_; + InvalidatorTestDelegate delegate_; + + const invalidation::ObjectId id1; + const invalidation::ObjectId id2; + const invalidation::ObjectId id3; + const invalidation::ObjectId id4; +}; + +TYPED_TEST_CASE_P(InvalidatorTest); + +// Initialize the invalidator, register a handler, register some IDs for that +// handler, and then unregister the handler, dispatching invalidations in +// between. The handler should only see invalidations when its registered and +// its IDs are registered. +TYPED_TEST_P(InvalidatorTest, Basic) { + Invalidator* const invalidator = this->CreateAndInitializeInvalidator(); + + FakeInvalidationHandler handler; + + invalidator->RegisterHandler(&handler); + + ObjectIdInvalidationMap invalidation_map; + invalidation_map.Insert(Invalidation::Init(this->id1, 1, "1")); + invalidation_map.Insert(Invalidation::Init(this->id2, 2, "2")); + invalidation_map.Insert(Invalidation::Init(this->id3, 3, "3")); + + // Should be ignored since no IDs are registered to |handler|. + this->delegate_.TriggerOnIncomingInvalidation(invalidation_map); + EXPECT_EQ(0, handler.GetInvalidationCount()); + + ObjectIdSet ids; + ids.insert(this->id1); + ids.insert(this->id2); + invalidator->UpdateRegisteredIds(&handler, ids); + + this->delegate_.TriggerOnInvalidatorStateChange(INVALIDATIONS_ENABLED); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler.GetInvalidatorState()); + + ObjectIdInvalidationMap expected_invalidations; + expected_invalidations.Insert(Invalidation::Init(this->id1, 1, "1")); + expected_invalidations.Insert(Invalidation::Init(this->id2, 2, "2")); + + this->delegate_.TriggerOnIncomingInvalidation(invalidation_map); + EXPECT_EQ(1, handler.GetInvalidationCount()); + EXPECT_THAT(expected_invalidations, Eq(handler.GetLastInvalidationMap())); + + ids.erase(this->id1); + ids.insert(this->id3); + invalidator->UpdateRegisteredIds(&handler, ids); + + expected_invalidations = ObjectIdInvalidationMap(); + expected_invalidations.Insert(Invalidation::Init(this->id2, 2, "2")); + expected_invalidations.Insert(Invalidation::Init(this->id3, 3, "3")); + + // Removed object IDs should not be notified, newly-added ones should. + this->delegate_.TriggerOnIncomingInvalidation(invalidation_map); + EXPECT_EQ(2, handler.GetInvalidationCount()); + EXPECT_THAT(expected_invalidations, Eq(handler.GetLastInvalidationMap())); + + this->delegate_.TriggerOnInvalidatorStateChange(TRANSIENT_INVALIDATION_ERROR); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, + handler.GetInvalidatorState()); + + this->delegate_.TriggerOnInvalidatorStateChange( + INVALIDATION_CREDENTIALS_REJECTED); + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, + handler.GetInvalidatorState()); + + invalidator->UnregisterHandler(&handler); + + // Should be ignored since |handler| isn't registered anymore. + this->delegate_.TriggerOnIncomingInvalidation(invalidation_map); + EXPECT_EQ(2, handler.GetInvalidationCount()); +} + +// Register handlers and some IDs for those handlers, register a handler with +// no IDs, and register a handler with some IDs but unregister it. Then, +// dispatch some invalidations and invalidations. Handlers that are registered +// should get invalidations, and the ones that have registered IDs should +// receive invalidations for those IDs. +TYPED_TEST_P(InvalidatorTest, MultipleHandlers) { + Invalidator* const invalidator = this->CreateAndInitializeInvalidator(); + + FakeInvalidationHandler handler1; + FakeInvalidationHandler handler2; + FakeInvalidationHandler handler3; + FakeInvalidationHandler handler4; + + invalidator->RegisterHandler(&handler1); + invalidator->RegisterHandler(&handler2); + invalidator->RegisterHandler(&handler3); + invalidator->RegisterHandler(&handler4); + + { + ObjectIdSet ids; + ids.insert(this->id1); + ids.insert(this->id2); + invalidator->UpdateRegisteredIds(&handler1, ids); + } + + { + ObjectIdSet ids; + ids.insert(this->id3); + invalidator->UpdateRegisteredIds(&handler2, ids); + } + + // Don't register any IDs for handler3. + + { + ObjectIdSet ids; + ids.insert(this->id4); + invalidator->UpdateRegisteredIds(&handler4, ids); + } + + invalidator->UnregisterHandler(&handler4); + + this->delegate_.TriggerOnInvalidatorStateChange(INVALIDATIONS_ENABLED); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler1.GetInvalidatorState()); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler2.GetInvalidatorState()); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler3.GetInvalidatorState()); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler4.GetInvalidatorState()); + + { + ObjectIdInvalidationMap invalidation_map; + invalidation_map.Insert(Invalidation::Init(this->id1, 1, "1")); + invalidation_map.Insert(Invalidation::Init(this->id2, 2, "2")); + invalidation_map.Insert(Invalidation::Init(this->id3, 3, "3")); + invalidation_map.Insert(Invalidation::Init(this->id4, 4, "4")); + + this->delegate_.TriggerOnIncomingInvalidation(invalidation_map); + + ObjectIdInvalidationMap expected_invalidations; + expected_invalidations.Insert(Invalidation::Init(this->id1, 1, "1")); + expected_invalidations.Insert(Invalidation::Init(this->id2, 2, "2")); + + EXPECT_EQ(1, handler1.GetInvalidationCount()); + EXPECT_THAT(expected_invalidations, Eq(handler1.GetLastInvalidationMap())); + + expected_invalidations = ObjectIdInvalidationMap(); + expected_invalidations.Insert(Invalidation::Init(this->id3, 3, "3")); + + EXPECT_EQ(1, handler2.GetInvalidationCount()); + EXPECT_THAT(expected_invalidations, Eq(handler2.GetLastInvalidationMap())); + + EXPECT_EQ(0, handler3.GetInvalidationCount()); + EXPECT_EQ(0, handler4.GetInvalidationCount()); + } + + this->delegate_.TriggerOnInvalidatorStateChange(TRANSIENT_INVALIDATION_ERROR); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler1.GetInvalidatorState()); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler2.GetInvalidatorState()); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler3.GetInvalidatorState()); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler4.GetInvalidatorState()); + + invalidator->UnregisterHandler(&handler3); + invalidator->UnregisterHandler(&handler2); + invalidator->UnregisterHandler(&handler1); +} + +// Make sure that passing an empty set to UpdateRegisteredIds clears the +// corresponding entries for the handler. +TYPED_TEST_P(InvalidatorTest, EmptySetUnregisters) { + Invalidator* const invalidator = this->CreateAndInitializeInvalidator(); + + FakeInvalidationHandler handler1; + + // Control observer. + FakeInvalidationHandler handler2; + + invalidator->RegisterHandler(&handler1); + invalidator->RegisterHandler(&handler2); + + { + ObjectIdSet ids; + ids.insert(this->id1); + ids.insert(this->id2); + invalidator->UpdateRegisteredIds(&handler1, ids); + } + + { + ObjectIdSet ids; + ids.insert(this->id3); + invalidator->UpdateRegisteredIds(&handler2, ids); + } + + // Unregister the IDs for the first observer. It should not receive any + // further invalidations. + invalidator->UpdateRegisteredIds(&handler1, ObjectIdSet()); + + this->delegate_.TriggerOnInvalidatorStateChange(INVALIDATIONS_ENABLED); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler1.GetInvalidatorState()); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler2.GetInvalidatorState()); + + { + ObjectIdInvalidationMap invalidation_map; + invalidation_map.Insert(Invalidation::Init(this->id1, 1, "1")); + invalidation_map.Insert(Invalidation::Init(this->id2, 2, "2")); + invalidation_map.Insert(Invalidation::Init(this->id3, 3, "3")); + this->delegate_.TriggerOnIncomingInvalidation(invalidation_map); + EXPECT_EQ(0, handler1.GetInvalidationCount()); + EXPECT_EQ(1, handler2.GetInvalidationCount()); + } + + this->delegate_.TriggerOnInvalidatorStateChange(TRANSIENT_INVALIDATION_ERROR); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler1.GetInvalidatorState()); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler2.GetInvalidatorState()); + + invalidator->UnregisterHandler(&handler2); + invalidator->UnregisterHandler(&handler1); +} + +namespace internal { + +// A FakeInvalidationHandler that is "bound" to a specific +// Invalidator. This is for cross-referencing state information with +// the bound Invalidator. +class BoundFakeInvalidationHandler : public FakeInvalidationHandler { + public: + explicit BoundFakeInvalidationHandler(const Invalidator& invalidator); + virtual ~BoundFakeInvalidationHandler(); + + // Returns the last return value of GetInvalidatorState() on the + // bound invalidator from the last time the invalidator state + // changed. + InvalidatorState GetLastRetrievedState() const; + + // InvalidationHandler implementation. + virtual void OnInvalidatorStateChange(InvalidatorState state) OVERRIDE; + + private: + const Invalidator& invalidator_; + InvalidatorState last_retrieved_state_; + + DISALLOW_COPY_AND_ASSIGN(BoundFakeInvalidationHandler); +}; + +} // namespace internal + +TYPED_TEST_P(InvalidatorTest, GetInvalidatorStateAlwaysCurrent) { + Invalidator* const invalidator = this->CreateAndInitializeInvalidator(); + + internal::BoundFakeInvalidationHandler handler(*invalidator); + invalidator->RegisterHandler(&handler); + + this->delegate_.TriggerOnInvalidatorStateChange(INVALIDATIONS_ENABLED); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler.GetInvalidatorState()); + EXPECT_EQ(INVALIDATIONS_ENABLED, handler.GetLastRetrievedState()); + + this->delegate_.TriggerOnInvalidatorStateChange(TRANSIENT_INVALIDATION_ERROR); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler.GetInvalidatorState()); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, handler.GetLastRetrievedState()); + + invalidator->UnregisterHandler(&handler); +} + +REGISTER_TYPED_TEST_CASE_P(InvalidatorTest, + Basic, MultipleHandlers, EmptySetUnregisters, + GetInvalidatorStateAlwaysCurrent); + +} // namespace syncer + +#endif // SYNC_NOTIFIER_INVALIDATOR_TEST_TEMPLATE_H_ diff --git a/sync/notifier/non_blocking_invalidator.cc b/sync/notifier/non_blocking_invalidator.cc new file mode 100644 index 0000000..b155c35 --- /dev/null +++ b/sync/notifier/non_blocking_invalidator.cc @@ -0,0 +1,367 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/non_blocking_invalidator.h" + +#include <cstddef> + +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/single_thread_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "base/threading/thread.h" +#include "jingle/notifier/listener/push_client.h" +#include "sync/internal_api/public/util/weak_handle.h" +#include "sync/notifier/gcm_network_channel_delegate.h" +#include "sync/notifier/invalidation_handler.h" +#include "sync/notifier/invalidation_notifier.h" +#include "sync/notifier/object_id_invalidation_map.h" +#include "sync/notifier/sync_system_resources.h" + +namespace syncer { + +struct NonBlockingInvalidator::InitializeOptions { + InitializeOptions( + NetworkChannelCreator network_channel_creator, + const std::string& invalidator_client_id, + const UnackedInvalidationsMap& saved_invalidations, + const std::string& invalidation_bootstrap_data, + const WeakHandle<InvalidationStateTracker>& + invalidation_state_tracker, + const std::string& client_info, + scoped_refptr<net::URLRequestContextGetter> request_context_getter) + : network_channel_creator(network_channel_creator), + invalidator_client_id(invalidator_client_id), + saved_invalidations(saved_invalidations), + invalidation_bootstrap_data(invalidation_bootstrap_data), + invalidation_state_tracker(invalidation_state_tracker), + client_info(client_info), + request_context_getter(request_context_getter) { + } + + NetworkChannelCreator network_channel_creator; + std::string invalidator_client_id; + UnackedInvalidationsMap saved_invalidations; + std::string invalidation_bootstrap_data; + WeakHandle<InvalidationStateTracker> invalidation_state_tracker; + std::string client_info; + scoped_refptr<net::URLRequestContextGetter> request_context_getter; +}; + +namespace { +// This class provides a wrapper for a logging class in order to receive +// callbacks across threads, without having to worry about owner threads. +class CallbackProxy { + public: + explicit CallbackProxy( + base::Callback<void(const base::DictionaryValue&)> callback); + ~CallbackProxy(); + + void Run(const base::DictionaryValue& value); + + private: + static void DoRun(base::Callback<void(const base::DictionaryValue&)> callback, + scoped_ptr<base::DictionaryValue> value); + + base::Callback<void(const base::DictionaryValue&)> callback_; + scoped_refptr<base::SingleThreadTaskRunner> running_thread_; + + DISALLOW_COPY_AND_ASSIGN(CallbackProxy); +}; + +CallbackProxy::CallbackProxy( + base::Callback<void(const base::DictionaryValue&)> callback) + : callback_(callback), + running_thread_(base::ThreadTaskRunnerHandle::Get()) {} + +CallbackProxy::~CallbackProxy() {} + +void CallbackProxy::DoRun( + base::Callback<void(const base::DictionaryValue&)> callback, + scoped_ptr<base::DictionaryValue> value) { + callback.Run(*value); +} + +void CallbackProxy::Run(const base::DictionaryValue& value) { + scoped_ptr<base::DictionaryValue> copied(value.DeepCopy()); + running_thread_->PostTask( + FROM_HERE, + base::Bind(&CallbackProxy::DoRun, callback_, base::Passed(&copied))); +} +} + +class NonBlockingInvalidator::Core + : public base::RefCountedThreadSafe<NonBlockingInvalidator::Core>, + // InvalidationHandler to observe the InvalidationNotifier we create. + public InvalidationHandler { + public: + // Called on parent thread. |delegate_observer| should be initialized. + explicit Core( + const WeakHandle<NonBlockingInvalidator>& delegate_observer); + + // Helpers called on I/O thread. + void Initialize( + const NonBlockingInvalidator::InitializeOptions& initialize_options); + void Teardown(); + void UpdateRegisteredIds(const ObjectIdSet& ids); + void UpdateCredentials(const std::string& email, const std::string& token); + void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const; + + // InvalidationHandler implementation (all called on I/O thread by + // InvalidationNotifier). + virtual void OnInvalidatorStateChange(InvalidatorState reason) OVERRIDE; + virtual void OnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) OVERRIDE; + virtual std::string GetOwnerName() const OVERRIDE; + + private: + friend class + base::RefCountedThreadSafe<NonBlockingInvalidator::Core>; + // Called on parent or I/O thread. + virtual ~Core(); + + // The variables below should be used only on the I/O thread. + const WeakHandle<NonBlockingInvalidator> delegate_observer_; + scoped_ptr<InvalidationNotifier> invalidation_notifier_; + scoped_refptr<base::SingleThreadTaskRunner> network_task_runner_; + + DISALLOW_COPY_AND_ASSIGN(Core); +}; + +NonBlockingInvalidator::Core::Core( + const WeakHandle<NonBlockingInvalidator>& delegate_observer) + : delegate_observer_(delegate_observer) { + DCHECK(delegate_observer_.IsInitialized()); +} + +NonBlockingInvalidator::Core::~Core() { +} + +void NonBlockingInvalidator::Core::Initialize( + const NonBlockingInvalidator::InitializeOptions& initialize_options) { + DCHECK(initialize_options.request_context_getter.get()); + network_task_runner_ = + initialize_options.request_context_getter->GetNetworkTaskRunner(); + DCHECK(network_task_runner_->BelongsToCurrentThread()); + scoped_ptr<SyncNetworkChannel> network_channel = + initialize_options.network_channel_creator.Run(); + invalidation_notifier_.reset( + new InvalidationNotifier( + network_channel.Pass(), + initialize_options.invalidator_client_id, + initialize_options.saved_invalidations, + initialize_options.invalidation_bootstrap_data, + initialize_options.invalidation_state_tracker, + initialize_options.client_info)); + invalidation_notifier_->RegisterHandler(this); +} + +void NonBlockingInvalidator::Core::Teardown() { + DCHECK(network_task_runner_->BelongsToCurrentThread()); + invalidation_notifier_->UnregisterHandler(this); + invalidation_notifier_.reset(); + network_task_runner_ = NULL; +} + +void NonBlockingInvalidator::Core::UpdateRegisteredIds(const ObjectIdSet& ids) { + DCHECK(network_task_runner_->BelongsToCurrentThread()); + invalidation_notifier_->UpdateRegisteredIds(this, ids); +} + +void NonBlockingInvalidator::Core::UpdateCredentials(const std::string& email, + const std::string& token) { + DCHECK(network_task_runner_->BelongsToCurrentThread()); + invalidation_notifier_->UpdateCredentials(email, token); +} + +void NonBlockingInvalidator::Core::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const { + DCHECK(network_task_runner_->BelongsToCurrentThread()); + invalidation_notifier_->RequestDetailedStatus(callback); +} + +void NonBlockingInvalidator::Core::OnInvalidatorStateChange( + InvalidatorState reason) { + DCHECK(network_task_runner_->BelongsToCurrentThread()); + delegate_observer_.Call(FROM_HERE, + &NonBlockingInvalidator::OnInvalidatorStateChange, + reason); +} + +void NonBlockingInvalidator::Core::OnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + DCHECK(network_task_runner_->BelongsToCurrentThread()); + delegate_observer_.Call(FROM_HERE, + &NonBlockingInvalidator::OnIncomingInvalidation, + invalidation_map); +} + +std::string NonBlockingInvalidator::Core::GetOwnerName() const { + return "Sync"; +} + +NonBlockingInvalidator::NonBlockingInvalidator( + NetworkChannelCreator network_channel_creator, + const std::string& invalidator_client_id, + const UnackedInvalidationsMap& saved_invalidations, + const std::string& invalidation_bootstrap_data, + InvalidationStateTracker* invalidation_state_tracker, + const std::string& client_info, + const scoped_refptr<net::URLRequestContextGetter>& request_context_getter) + : invalidation_state_tracker_(invalidation_state_tracker), + parent_task_runner_(base::ThreadTaskRunnerHandle::Get()), + network_task_runner_(request_context_getter->GetNetworkTaskRunner()), + weak_ptr_factory_(this) { + core_ = new Core(MakeWeakHandle(weak_ptr_factory_.GetWeakPtr())); + + InitializeOptions initialize_options( + network_channel_creator, + invalidator_client_id, + saved_invalidations, + invalidation_bootstrap_data, + MakeWeakHandle(weak_ptr_factory_.GetWeakPtr()), + client_info, + request_context_getter); + + if (!network_task_runner_->PostTask( + FROM_HERE, + base::Bind( + &NonBlockingInvalidator::Core::Initialize, + core_.get(), + initialize_options))) { + NOTREACHED(); + } +} + +NonBlockingInvalidator::~NonBlockingInvalidator() { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + if (!network_task_runner_->PostTask( + FROM_HERE, + base::Bind(&NonBlockingInvalidator::Core::Teardown, + core_.get()))) { + DVLOG(1) << "Network thread stopped before invalidator is destroyed."; + } +} + +void NonBlockingInvalidator::RegisterHandler(InvalidationHandler* handler) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + registrar_.RegisterHandler(handler); +} + +void NonBlockingInvalidator::UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + registrar_.UpdateRegisteredIds(handler, ids); + if (!network_task_runner_->PostTask( + FROM_HERE, + base::Bind( + &NonBlockingInvalidator::Core::UpdateRegisteredIds, + core_.get(), + registrar_.GetAllRegisteredIds()))) { + NOTREACHED(); + } +} + +void NonBlockingInvalidator::UnregisterHandler(InvalidationHandler* handler) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + registrar_.UnregisterHandler(handler); +} + +InvalidatorState NonBlockingInvalidator::GetInvalidatorState() const { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + return registrar_.GetInvalidatorState(); +} + +void NonBlockingInvalidator::UpdateCredentials(const std::string& email, + const std::string& token) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + if (!network_task_runner_->PostTask( + FROM_HERE, + base::Bind(&NonBlockingInvalidator::Core::UpdateCredentials, + core_.get(), email, token))) { + NOTREACHED(); + } +} + +void NonBlockingInvalidator::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + base::Callback<void(const base::DictionaryValue&)> proxy_callback = + base::Bind(&CallbackProxy::Run, base::Owned(new CallbackProxy(callback))); + if (!network_task_runner_->PostTask( + FROM_HERE, + base::Bind(&NonBlockingInvalidator::Core::RequestDetailedStatus, + core_.get(), + proxy_callback))) { + NOTREACHED(); + } +} + +NetworkChannelCreator + NonBlockingInvalidator::MakePushClientChannelCreator( + const notifier::NotifierOptions& notifier_options) { + return base::Bind(SyncNetworkChannel::CreatePushClientChannel, + notifier_options); +} + +NetworkChannelCreator NonBlockingInvalidator::MakeGCMNetworkChannelCreator( + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + scoped_ptr<GCMNetworkChannelDelegate> delegate) { + return base::Bind(&SyncNetworkChannel::CreateGCMNetworkChannel, + request_context_getter, + base::Passed(&delegate)); +} + +void NonBlockingInvalidator::ClearAndSetNewClientId(const std::string& data) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + invalidation_state_tracker_->ClearAndSetNewClientId(data); +} + +std::string NonBlockingInvalidator::GetInvalidatorClientId() const { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + return invalidation_state_tracker_->GetInvalidatorClientId(); +} + +void NonBlockingInvalidator::SetBootstrapData(const std::string& data) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + invalidation_state_tracker_->SetBootstrapData(data); +} + +std::string NonBlockingInvalidator::GetBootstrapData() const { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + return invalidation_state_tracker_->GetBootstrapData(); +} + +void NonBlockingInvalidator::SetSavedInvalidations( + const UnackedInvalidationsMap& states) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + invalidation_state_tracker_->SetSavedInvalidations(states); +} + +UnackedInvalidationsMap NonBlockingInvalidator::GetSavedInvalidations() const { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + return invalidation_state_tracker_->GetSavedInvalidations(); +} + +void NonBlockingInvalidator::Clear() { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + invalidation_state_tracker_->Clear(); +} + +void NonBlockingInvalidator::OnInvalidatorStateChange(InvalidatorState state) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + registrar_.UpdateInvalidatorState(state); +} + +void NonBlockingInvalidator::OnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + DCHECK(parent_task_runner_->BelongsToCurrentThread()); + registrar_.DispatchInvalidationsToHandlers(invalidation_map); +} + +std::string NonBlockingInvalidator::GetOwnerName() const { return "Sync"; } + +} // namespace syncer diff --git a/sync/notifier/non_blocking_invalidator.h b/sync/notifier/non_blocking_invalidator.h new file mode 100644 index 0000000..d08dfb9 --- /dev/null +++ b/sync/notifier/non_blocking_invalidator.h @@ -0,0 +1,114 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// An implementation of SyncNotifier that wraps InvalidationNotifier +// on its own thread. + +#ifndef SYNC_NOTIFIER_NON_BLOCKING_INVALIDATOR_H_ +#define SYNC_NOTIFIER_NON_BLOCKING_INVALIDATOR_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "jingle/notifier/base/notifier_options.h" +#include "sync/base/sync_export.h" +#include "sync/notifier/invalidation_state_tracker.h" +#include "sync/notifier/invalidator.h" +#include "sync/notifier/invalidator_registrar.h" +#include "sync/notifier/invalidator_state.h" +#include "sync/notifier/unacked_invalidation_set.h" + +namespace base { +class SingleThreadTaskRunner; +} // namespace base + +namespace syncer { +class SyncNetworkChannel; +class GCMNetworkChannelDelegate; + +// Callback type for function that creates SyncNetworkChannel. This function +// gets passed into NonBlockingInvalidator constructor. +typedef base::Callback<scoped_ptr<SyncNetworkChannel>(void)> + NetworkChannelCreator; + +class SYNC_EXPORT_PRIVATE NonBlockingInvalidator + : public Invalidator, + public InvalidationStateTracker { + public: + // |invalidation_state_tracker| must be initialized and must outlive |this|. + NonBlockingInvalidator( + NetworkChannelCreator network_channel_creator, + const std::string& invalidator_client_id, + const UnackedInvalidationsMap& saved_invalidations, + const std::string& invalidation_bootstrap_data, + InvalidationStateTracker* invalidation_state_tracker, + const std::string& client_info, + const scoped_refptr<net::URLRequestContextGetter>& + request_context_getter); + + virtual ~NonBlockingInvalidator(); + + // Invalidator implementation. + virtual void RegisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual void UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) OVERRIDE; + virtual void UnregisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual InvalidatorState GetInvalidatorState() const OVERRIDE; + virtual void UpdateCredentials( + const std::string& email, const std::string& token) OVERRIDE; + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const + OVERRIDE; + + // Static functions to construct callback that creates network channel for + // SyncSystemResources. The goal is to pass network channel to invalidator at + // the same time not exposing channel specific parameters to invalidator and + // channel implementation to client of invalidator. + static NetworkChannelCreator MakePushClientChannelCreator( + const notifier::NotifierOptions& notifier_options); + static NetworkChannelCreator MakeGCMNetworkChannelCreator( + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + scoped_ptr<GCMNetworkChannelDelegate> delegate); + + // These methods are forwarded to the invalidation_state_tracker_. + virtual void ClearAndSetNewClientId(const std::string& data) OVERRIDE; + virtual std::string GetInvalidatorClientId() const OVERRIDE; + virtual void SetBootstrapData(const std::string& data) OVERRIDE; + virtual std::string GetBootstrapData() const OVERRIDE; + virtual void SetSavedInvalidations( + const UnackedInvalidationsMap& states) OVERRIDE; + virtual UnackedInvalidationsMap GetSavedInvalidations() const OVERRIDE; + virtual void Clear() OVERRIDE; + + private: + void OnInvalidatorStateChange(InvalidatorState state); + void OnIncomingInvalidation(const ObjectIdInvalidationMap& invalidation_map); + std::string GetOwnerName() const; + + friend class NonBlockingInvalidatorTestDelegate; + + struct InitializeOptions; + class Core; + + InvalidatorRegistrar registrar_; + InvalidationStateTracker* invalidation_state_tracker_; + + // The real guts of NonBlockingInvalidator, which allows this class to live + // completely on the parent thread. + scoped_refptr<Core> core_; + scoped_refptr<base::SingleThreadTaskRunner> parent_task_runner_; + scoped_refptr<base::SingleThreadTaskRunner> network_task_runner_; + + base::WeakPtrFactory<NonBlockingInvalidator> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(NonBlockingInvalidator); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_NON_BLOCKING_INVALIDATOR_H_ diff --git a/sync/notifier/non_blocking_invalidator_unittest.cc b/sync/notifier/non_blocking_invalidator_unittest.cc new file mode 100644 index 0000000..47043ec --- /dev/null +++ b/sync/notifier/non_blocking_invalidator_unittest.cc @@ -0,0 +1,98 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/non_blocking_invalidator.h" + +#include "base/bind_helpers.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/threading/thread.h" +#include "google/cacheinvalidation/types.pb.h" +#include "jingle/notifier/base/fake_base_task.h" +#include "net/url_request/url_request_test_util.h" +#include "sync/notifier/fake_invalidation_handler.h" +#include "sync/notifier/invalidation_state_tracker.h" +#include "sync/notifier/invalidator_test_template.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +class NonBlockingInvalidatorTestDelegate { + public: + NonBlockingInvalidatorTestDelegate() : io_thread_("IO thread") {} + + ~NonBlockingInvalidatorTestDelegate() { + DestroyInvalidator(); + } + + void CreateInvalidator( + const std::string& invalidator_client_id, + const std::string& initial_state, + const base::WeakPtr<InvalidationStateTracker>& + invalidation_state_tracker) { + DCHECK(!invalidator_.get()); + base::Thread::Options options; + options.message_loop_type = base::MessageLoop::TYPE_IO; + io_thread_.StartWithOptions(options); + request_context_getter_ = + new net::TestURLRequestContextGetter(io_thread_.message_loop_proxy()); + notifier::NotifierOptions notifier_options; + notifier_options.request_context_getter = request_context_getter_; + NetworkChannelCreator network_channel_creator = + NonBlockingInvalidator::MakePushClientChannelCreator(notifier_options); + invalidator_.reset( + new NonBlockingInvalidator( + network_channel_creator, + invalidator_client_id, + UnackedInvalidationsMap(), + initial_state, + invalidation_state_tracker.get(), + "fake_client_info", + request_context_getter_)); + } + + Invalidator* GetInvalidator() { + return invalidator_.get(); + } + + void DestroyInvalidator() { + invalidator_.reset(); + request_context_getter_ = NULL; + io_thread_.Stop(); + message_loop_.RunUntilIdle(); + } + + void WaitForInvalidator() { + base::RunLoop run_loop; + ASSERT_TRUE( + io_thread_.message_loop_proxy()->PostTaskAndReply( + FROM_HERE, + base::Bind(&base::DoNothing), + run_loop.QuitClosure())); + run_loop.Run(); + } + + void TriggerOnInvalidatorStateChange(InvalidatorState state) { + invalidator_->OnInvalidatorStateChange(state); + } + + void TriggerOnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + invalidator_->OnIncomingInvalidation(invalidation_map); + } + + private: + base::MessageLoop message_loop_; + base::Thread io_thread_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + scoped_ptr<NonBlockingInvalidator> invalidator_; +}; + +INSTANTIATE_TYPED_TEST_CASE_P( + NonBlockingInvalidatorTest, InvalidatorTest, + NonBlockingInvalidatorTestDelegate); + +} // namespace syncer diff --git a/sync/notifier/p2p_invalidator.cc b/sync/notifier/p2p_invalidator.cc new file mode 100644 index 0000000..1eb0596 --- /dev/null +++ b/sync/notifier/p2p_invalidator.cc @@ -0,0 +1,299 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/p2p_invalidator.h" + +#include <algorithm> +#include <iterator> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "jingle/notifier/listener/push_client.h" +#include "sync/notifier/invalidation_handler.h" +#include "sync/notifier/invalidation_util.h" +#include "sync/notifier/object_id_invalidation_map.h" + +namespace syncer { + +const char kSyncP2PNotificationChannel[] = "http://www.google.com/chrome/sync"; + +namespace { + +const char kNotifySelf[] = "notifySelf"; +const char kNotifyOthers[] = "notifyOthers"; +const char kNotifyAll[] = "notifyAll"; + +const char kSenderIdKey[] = "senderId"; +const char kNotificationTypeKey[] = "notificationType"; +const char kInvalidationsKey[] = "invalidations"; + +} // namespace + +std::string P2PNotificationTargetToString(P2PNotificationTarget target) { + switch (target) { + case NOTIFY_SELF: + return kNotifySelf; + case NOTIFY_OTHERS: + return kNotifyOthers; + case NOTIFY_ALL: + return kNotifyAll; + default: + NOTREACHED(); + return std::string(); + } +} + +P2PNotificationTarget P2PNotificationTargetFromString( + const std::string& target_str) { + if (target_str == kNotifySelf) { + return NOTIFY_SELF; + } + if (target_str == kNotifyOthers) { + return NOTIFY_OTHERS; + } + if (target_str == kNotifyAll) { + return NOTIFY_ALL; + } + LOG(WARNING) << "Could not parse " << target_str; + return NOTIFY_SELF; +} + +P2PNotificationData::P2PNotificationData() + : target_(NOTIFY_SELF) {} + +P2PNotificationData::P2PNotificationData( + const std::string& sender_id, + P2PNotificationTarget target, + const ObjectIdInvalidationMap& invalidation_map) + : sender_id_(sender_id), + target_(target), + invalidation_map_(invalidation_map) {} + +P2PNotificationData::~P2PNotificationData() {} + +bool P2PNotificationData::IsTargeted(const std::string& id) const { + switch (target_) { + case NOTIFY_SELF: + return sender_id_ == id; + case NOTIFY_OTHERS: + return sender_id_ != id; + case NOTIFY_ALL: + return true; + default: + NOTREACHED(); + return false; + } +} + +const ObjectIdInvalidationMap& +P2PNotificationData::GetIdInvalidationMap() const { + return invalidation_map_; +} + +bool P2PNotificationData::Equals(const P2PNotificationData& other) const { + return + (sender_id_ == other.sender_id_) && + (target_ == other.target_) && + (invalidation_map_ == other.invalidation_map_); +} + +std::string P2PNotificationData::ToString() const { + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue()); + dict->SetString(kSenderIdKey, sender_id_); + dict->SetString(kNotificationTypeKey, + P2PNotificationTargetToString(target_)); + dict->Set(kInvalidationsKey, invalidation_map_.ToValue().release()); + std::string json; + base::JSONWriter::Write(dict.get(), &json); + return json; +} + +bool P2PNotificationData::ResetFromString(const std::string& str) { + scoped_ptr<base::Value> data_value(base::JSONReader::Read(str)); + const base::DictionaryValue* data_dict = NULL; + if (!data_value.get() || !data_value->GetAsDictionary(&data_dict)) { + LOG(WARNING) << "Could not parse " << str << " as a dictionary"; + return false; + } + if (!data_dict->GetString(kSenderIdKey, &sender_id_)) { + LOG(WARNING) << "Could not find string value for " << kSenderIdKey; + } + std::string target_str; + if (!data_dict->GetString(kNotificationTypeKey, &target_str)) { + LOG(WARNING) << "Could not find string value for " + << kNotificationTypeKey; + } + target_ = P2PNotificationTargetFromString(target_str); + const base::ListValue* invalidation_map_list = NULL; + if (!data_dict->GetList(kInvalidationsKey, &invalidation_map_list) || + !invalidation_map_.ResetFromValue(*invalidation_map_list)) { + LOG(WARNING) << "Could not parse " << kInvalidationsKey; + } + return true; +} + +P2PInvalidator::P2PInvalidator(scoped_ptr<notifier::PushClient> push_client, + const std::string& invalidator_client_id, + P2PNotificationTarget send_notification_target) + : push_client_(push_client.Pass()), + invalidator_client_id_(invalidator_client_id), + logged_in_(false), + notifications_enabled_(false), + send_notification_target_(send_notification_target) { + DCHECK(send_notification_target_ == NOTIFY_OTHERS || + send_notification_target_ == NOTIFY_ALL); + push_client_->AddObserver(this); +} + +P2PInvalidator::~P2PInvalidator() { + DCHECK(thread_checker_.CalledOnValidThread()); + push_client_->RemoveObserver(this); +} + +void P2PInvalidator::RegisterHandler(InvalidationHandler* handler) { + DCHECK(thread_checker_.CalledOnValidThread()); + registrar_.RegisterHandler(handler); +} + +void P2PInvalidator::UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) { + DCHECK(thread_checker_.CalledOnValidThread()); + ObjectIdSet new_ids; + const ObjectIdSet& old_ids = registrar_.GetRegisteredIds(handler); + std::set_difference(ids.begin(), ids.end(), + old_ids.begin(), old_ids.end(), + std::inserter(new_ids, new_ids.end()), + ObjectIdLessThan()); + registrar_.UpdateRegisteredIds(handler, ids); + const P2PNotificationData notification_data( + invalidator_client_id_, + send_notification_target_, + ObjectIdInvalidationMap::InvalidateAll(ids)); + SendNotificationData(notification_data); +} + +void P2PInvalidator::UnregisterHandler(InvalidationHandler* handler) { + DCHECK(thread_checker_.CalledOnValidThread()); + registrar_.UnregisterHandler(handler); +} + +InvalidatorState P2PInvalidator::GetInvalidatorState() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return registrar_.GetInvalidatorState(); +} + +void P2PInvalidator::UpdateCredentials( + const std::string& email, const std::string& token) { + DCHECK(thread_checker_.CalledOnValidThread()); + notifier::Subscription subscription; + subscription.channel = kSyncP2PNotificationChannel; + // There may be some subtle issues around case sensitivity of the + // from field, but it doesn't matter too much since this is only + // used in p2p mode (which is only used in testing). + subscription.from = email; + push_client_->UpdateSubscriptions( + notifier::SubscriptionList(1, subscription)); + // If already logged in, the new credentials will take effect on the + // next reconnection. + push_client_->UpdateCredentials(email, token); + logged_in_ = true; +} + +void P2PInvalidator::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const { + DCHECK(thread_checker_.CalledOnValidThread()); + // TODO(mferreria): Make the P2P Invalidator work. + scoped_ptr<base::DictionaryValue> value(new base::DictionaryValue()); + callback.Run(*value); +} + +void P2PInvalidator::SendInvalidation(const ObjectIdSet& ids) { + DCHECK(thread_checker_.CalledOnValidThread()); + ObjectIdInvalidationMap invalidation_map = + ObjectIdInvalidationMap::InvalidateAll(ids); + const P2PNotificationData notification_data( + invalidator_client_id_, send_notification_target_, invalidation_map); + SendNotificationData(notification_data); +} + +void P2PInvalidator::OnNotificationsEnabled() { + DCHECK(thread_checker_.CalledOnValidThread()); + bool just_turned_on = (notifications_enabled_ == false); + notifications_enabled_ = true; + registrar_.UpdateInvalidatorState(INVALIDATIONS_ENABLED); + if (just_turned_on) { + const P2PNotificationData notification_data( + invalidator_client_id_, + NOTIFY_SELF, + ObjectIdInvalidationMap::InvalidateAll( + registrar_.GetAllRegisteredIds())); + SendNotificationData(notification_data); + } +} + +void P2PInvalidator::OnNotificationsDisabled( + notifier::NotificationsDisabledReason reason) { + DCHECK(thread_checker_.CalledOnValidThread()); + registrar_.UpdateInvalidatorState(FromNotifierReason(reason)); +} + +void P2PInvalidator::OnIncomingNotification( + const notifier::Notification& notification) { + DCHECK(thread_checker_.CalledOnValidThread()); + DVLOG(1) << "Received notification " << notification.ToString(); + if (!logged_in_) { + DVLOG(1) << "Not logged in yet -- not emitting notification"; + return; + } + if (!notifications_enabled_) { + DVLOG(1) << "Notifications not on -- not emitting notification"; + return; + } + if (notification.channel != kSyncP2PNotificationChannel) { + LOG(WARNING) << "Notification from unexpected source " + << notification.channel; + } + P2PNotificationData notification_data; + if (!notification_data.ResetFromString(notification.data)) { + LOG(WARNING) << "Could not parse notification data from " + << notification.data; + notification_data = P2PNotificationData( + invalidator_client_id_, + NOTIFY_ALL, + ObjectIdInvalidationMap::InvalidateAll( + registrar_.GetAllRegisteredIds())); + } + if (!notification_data.IsTargeted(invalidator_client_id_)) { + DVLOG(1) << "Not a target of the notification -- " + << "not emitting notification"; + return; + } + registrar_.DispatchInvalidationsToHandlers( + notification_data.GetIdInvalidationMap()); +} + +void P2PInvalidator::SendNotificationDataForTest( + const P2PNotificationData& notification_data) { + DCHECK(thread_checker_.CalledOnValidThread()); + SendNotificationData(notification_data); +} + +void P2PInvalidator::SendNotificationData( + const P2PNotificationData& notification_data) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (notification_data.GetIdInvalidationMap().Empty()) { + DVLOG(1) << "Not sending XMPP notification with empty state map: " + << notification_data.ToString(); + return; + } + notifier::Notification notification; + notification.channel = kSyncP2PNotificationChannel; + notification.data = notification_data.ToString(); + DVLOG(1) << "Sending XMPP notification: " << notification.ToString(); + push_client_->SendNotification(notification); +} + +} // namespace syncer diff --git a/sync/notifier/p2p_invalidator.h b/sync/notifier/p2p_invalidator.h new file mode 100644 index 0000000..8132d8c --- /dev/null +++ b/sync/notifier/p2p_invalidator.h @@ -0,0 +1,150 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// An invalidator that uses p2p invalidations based on XMPP push +// notifications. Used only for sync integration tests. + +#ifndef SYNC_NOTIFIER_P2P_INVALIDATOR_H_ +#define SYNC_NOTIFIER_P2P_INVALIDATOR_H_ + +#include <string> + +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/threading/thread_checker.h" +#include "jingle/notifier/base/notifier_options.h" +#include "jingle/notifier/listener/push_client.h" +#include "jingle/notifier/listener/push_client_observer.h" +#include "sync/base/sync_export.h" +#include "sync/internal_api/public/base/model_type.h" +#include "sync/notifier/invalidator.h" +#include "sync/notifier/invalidator_registrar.h" +#include "sync/notifier/invalidator_state.h" +#include "sync/notifier/object_id_invalidation_map.h" + +namespace notifier { +class PushClient; +} // namespace notifier + +namespace syncer { + +// The channel to use for sync notifications. +SYNC_EXPORT extern const char kSyncP2PNotificationChannel[]; + +// The intended recipient(s) of a P2P notification. +enum P2PNotificationTarget { + NOTIFY_SELF, + FIRST_NOTIFICATION_TARGET = NOTIFY_SELF, + NOTIFY_OTHERS, + NOTIFY_ALL, + LAST_NOTIFICATION_TARGET = NOTIFY_ALL +}; + +SYNC_EXPORT_PRIVATE std::string P2PNotificationTargetToString( + P2PNotificationTarget target); + +// If |target_str| can't be parsed, assumes NOTIFY_SELF. +SYNC_EXPORT_PRIVATE P2PNotificationTarget P2PNotificationTargetFromString( + const std::string& target_str); + +// Helper notification data class that can be serialized to and +// deserialized from a string. +class SYNC_EXPORT_PRIVATE P2PNotificationData { + public: + // Initializes with an empty sender ID, target set to NOTIFY_SELF, + // and empty changed types. + P2PNotificationData(); + P2PNotificationData(const std::string& sender_id, + P2PNotificationTarget target, + const ObjectIdInvalidationMap& invalidation_map); + + ~P2PNotificationData(); + + // Returns true if the given ID is targeted by this notification. + bool IsTargeted(const std::string& id) const; + + const ObjectIdInvalidationMap& GetIdInvalidationMap() const; + + bool Equals(const P2PNotificationData& other) const; + + std::string ToString() const; + + // Returns whether parsing |str| was successful. If parsing was + // unsuccessful, the state of the notification is undefined. + bool ResetFromString(const std::string& str); + + private: + // The unique ID of the client that sent the notification. + std::string sender_id_; + // The intendent recipient(s) of the notification. + P2PNotificationTarget target_; + // The invalidation map for the notification. + ObjectIdInvalidationMap invalidation_map_; +}; + +class SYNC_EXPORT_PRIVATE P2PInvalidator + : public Invalidator, + public NON_EXPORTED_BASE(notifier::PushClientObserver) { + public: + // The |send_notification_target| parameter was added to allow us to send + // self-notifications in some cases, but not others. The value should be + // either NOTIFY_ALL to send notifications to all clients, or NOTIFY_OTHERS + // to send notifications to all clients except for the one that triggered the + // notification. See crbug.com/97780. + P2PInvalidator(scoped_ptr<notifier::PushClient> push_client, + const std::string& invalidator_client_id, + P2PNotificationTarget send_notification_target); + + virtual ~P2PInvalidator(); + + // Invalidator implementation. + virtual void RegisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual void UpdateRegisteredIds(InvalidationHandler* handler, + const ObjectIdSet& ids) OVERRIDE; + virtual void UnregisterHandler(InvalidationHandler* handler) OVERRIDE; + virtual InvalidatorState GetInvalidatorState() const OVERRIDE; + virtual void UpdateCredentials( + const std::string& email, const std::string& token) OVERRIDE; + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const + OVERRIDE; + + // PushClientObserver implementation. + virtual void OnNotificationsEnabled() OVERRIDE; + virtual void OnNotificationsDisabled( + notifier::NotificationsDisabledReason reason) OVERRIDE; + virtual void OnIncomingNotification( + const notifier::Notification& notification) OVERRIDE; + + void SendInvalidation(const ObjectIdSet& ids); + + void SendNotificationDataForTest( + const P2PNotificationData& notification_data); + + private: + void SendNotificationData(const P2PNotificationData& notification_data); + + base::ThreadChecker thread_checker_; + + InvalidatorRegistrar registrar_; + + // The push client. + scoped_ptr<notifier::PushClient> push_client_; + // Our unique ID. + std::string invalidator_client_id_; + // Whether we have called UpdateCredentials() yet. + bool logged_in_; + bool notifications_enabled_; + // Which set of clients should be sent notifications. + P2PNotificationTarget send_notification_target_; + + DISALLOW_COPY_AND_ASSIGN(P2PInvalidator); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_P2P_INVALIDATOR_H_ diff --git a/sync/notifier/p2p_invalidator_unittest.cc b/sync/notifier/p2p_invalidator_unittest.cc new file mode 100644 index 0000000..9f0b273 --- /dev/null +++ b/sync/notifier/p2p_invalidator_unittest.cc @@ -0,0 +1,355 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/p2p_invalidator.h" + +#include <cstddef> + +#include "jingle/notifier/listener/fake_push_client.h" +#include "sync/internal_api/public/base/model_type.h" +#include "sync/notifier/fake_invalidation_handler.h" +#include "sync/notifier/invalidator_test_template.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +namespace { + +class P2PInvalidatorTestDelegate { + public: + P2PInvalidatorTestDelegate() : fake_push_client_(NULL) {} + + ~P2PInvalidatorTestDelegate() { + DestroyInvalidator(); + } + + void CreateInvalidator( + const std::string& invalidator_client_id, + const std::string& initial_state, + const base::WeakPtr<InvalidationStateTracker>& + invalidation_state_tracker) { + DCHECK(!fake_push_client_); + DCHECK(!invalidator_.get()); + fake_push_client_ = new notifier::FakePushClient(); + invalidator_.reset( + new P2PInvalidator( + scoped_ptr<notifier::PushClient>(fake_push_client_), + invalidator_client_id, + NOTIFY_OTHERS)); + } + + P2PInvalidator* GetInvalidator() { + return invalidator_.get(); + } + + notifier::FakePushClient* GetPushClient() { + return fake_push_client_; + } + + void DestroyInvalidator() { + invalidator_.reset(); + fake_push_client_ = NULL; + } + + void WaitForInvalidator() { + // Do Nothing. + } + + void TriggerOnInvalidatorStateChange(InvalidatorState state) { + if (state == INVALIDATIONS_ENABLED) { + fake_push_client_->EnableNotifications(); + } else { + fake_push_client_->DisableNotifications(ToNotifierReasonForTest(state)); + } + } + + void TriggerOnIncomingInvalidation( + const ObjectIdInvalidationMap& invalidation_map) { + const P2PNotificationData notification_data( + std::string(), NOTIFY_ALL, invalidation_map); + notifier::Notification notification; + notification.channel = kSyncP2PNotificationChannel; + notification.data = notification_data.ToString(); + fake_push_client_->SimulateIncomingNotification(notification); + } + + private: + // Owned by |invalidator_|. + notifier::FakePushClient* fake_push_client_; + scoped_ptr<P2PInvalidator> invalidator_; +}; + +class P2PInvalidatorTest : public testing::Test { + protected: + P2PInvalidatorTest() + : next_sent_notification_to_reflect_(0) { + delegate_.CreateInvalidator("sender", + "fake_state", + base::WeakPtr<InvalidationStateTracker>()); + delegate_.GetInvalidator()->RegisterHandler(&fake_handler_); + } + + virtual ~P2PInvalidatorTest() { + delegate_.GetInvalidator()->UnregisterHandler(&fake_handler_); + } + + ObjectIdInvalidationMap MakeInvalidationMap(ModelTypeSet types) { + ObjectIdInvalidationMap invalidations; + ObjectIdSet ids = ModelTypeSetToObjectIdSet(types); + return ObjectIdInvalidationMap::InvalidateAll(ids); + } + + // Simulate receiving all the notifications we sent out since last + // time this was called. + void ReflectSentNotifications() { + const std::vector<notifier::Notification>& sent_notifications = + delegate_.GetPushClient()->sent_notifications(); + for(size_t i = next_sent_notification_to_reflect_; + i < sent_notifications.size(); ++i) { + delegate_.GetInvalidator()->OnIncomingNotification(sent_notifications[i]); + } + next_sent_notification_to_reflect_ = sent_notifications.size(); + } + + FakeInvalidationHandler fake_handler_; + P2PInvalidatorTestDelegate delegate_; + + private: + size_t next_sent_notification_to_reflect_; +}; + +// Make sure the P2PNotificationTarget <-> string conversions work. +TEST_F(P2PInvalidatorTest, P2PNotificationTarget) { + for (int i = FIRST_NOTIFICATION_TARGET; + i <= LAST_NOTIFICATION_TARGET; ++i) { + P2PNotificationTarget target = static_cast<P2PNotificationTarget>(i); + const std::string& target_str = P2PNotificationTargetToString(target); + EXPECT_FALSE(target_str.empty()); + EXPECT_EQ(target, P2PNotificationTargetFromString(target_str)); + } + EXPECT_EQ(NOTIFY_SELF, P2PNotificationTargetFromString("unknown")); +} + +// Make sure notification targeting works correctly. +TEST_F(P2PInvalidatorTest, P2PNotificationDataIsTargeted) { + { + const P2PNotificationData notification_data( + "sender", NOTIFY_SELF, ObjectIdInvalidationMap()); + EXPECT_TRUE(notification_data.IsTargeted("sender")); + EXPECT_FALSE(notification_data.IsTargeted("other1")); + EXPECT_FALSE(notification_data.IsTargeted("other2")); + } + { + const P2PNotificationData notification_data( + "sender", NOTIFY_OTHERS, ObjectIdInvalidationMap()); + EXPECT_FALSE(notification_data.IsTargeted("sender")); + EXPECT_TRUE(notification_data.IsTargeted("other1")); + EXPECT_TRUE(notification_data.IsTargeted("other2")); + } + { + const P2PNotificationData notification_data( + "sender", NOTIFY_ALL, ObjectIdInvalidationMap()); + EXPECT_TRUE(notification_data.IsTargeted("sender")); + EXPECT_TRUE(notification_data.IsTargeted("other1")); + EXPECT_TRUE(notification_data.IsTargeted("other2")); + } +} + +// Make sure the P2PNotificationData <-> string conversions work for a +// default-constructed P2PNotificationData. +TEST_F(P2PInvalidatorTest, P2PNotificationDataDefault) { + const P2PNotificationData notification_data; + EXPECT_TRUE(notification_data.IsTargeted(std::string())); + EXPECT_FALSE(notification_data.IsTargeted("other1")); + EXPECT_FALSE(notification_data.IsTargeted("other2")); + EXPECT_TRUE(notification_data.GetIdInvalidationMap().Empty()); + const std::string& notification_data_str = notification_data.ToString(); + EXPECT_EQ( + "{\"invalidations\":[],\"notificationType\":\"notifySelf\"," + "\"senderId\":\"\"}", notification_data_str); + + P2PNotificationData notification_data_parsed; + EXPECT_TRUE(notification_data_parsed.ResetFromString(notification_data_str)); + EXPECT_TRUE(notification_data.Equals(notification_data_parsed)); +} + +// Make sure the P2PNotificationData <-> string conversions work for a +// non-default-constructed P2PNotificationData. +TEST_F(P2PInvalidatorTest, P2PNotificationDataNonDefault) { + ObjectIdInvalidationMap invalidation_map = + ObjectIdInvalidationMap::InvalidateAll( + ModelTypeSetToObjectIdSet(ModelTypeSet(BOOKMARKS, THEMES))); + const P2PNotificationData notification_data("sender", + NOTIFY_ALL, + invalidation_map); + EXPECT_TRUE(notification_data.IsTargeted("sender")); + EXPECT_TRUE(notification_data.IsTargeted("other1")); + EXPECT_TRUE(notification_data.IsTargeted("other2")); + EXPECT_EQ(invalidation_map, notification_data.GetIdInvalidationMap()); + const std::string& notification_data_str = notification_data.ToString(); + EXPECT_EQ( + "{\"invalidations\":[" + "{\"isUnknownVersion\":true," + "\"objectId\":{\"name\":\"BOOKMARK\",\"source\":1004}}," + "{\"isUnknownVersion\":true," + "\"objectId\":{\"name\":\"THEME\",\"source\":1004}}" + "],\"notificationType\":\"notifyAll\"," + "\"senderId\":\"sender\"}", notification_data_str); + + P2PNotificationData notification_data_parsed; + EXPECT_TRUE(notification_data_parsed.ResetFromString(notification_data_str)); + EXPECT_TRUE(notification_data.Equals(notification_data_parsed)); +} + +// Set up the P2PInvalidator, simulate a successful connection, and send +// a notification with the default target (NOTIFY_OTHERS). The +// observer should receive only a notification from the call to +// UpdateEnabledTypes(). +TEST_F(P2PInvalidatorTest, NotificationsBasic) { + const ModelTypeSet enabled_types(BOOKMARKS, PREFERENCES); + + P2PInvalidator* const invalidator = delegate_.GetInvalidator(); + notifier::FakePushClient* const push_client = delegate_.GetPushClient(); + + invalidator->UpdateRegisteredIds(&fake_handler_, + ModelTypeSetToObjectIdSet(enabled_types)); + + const char kEmail[] = "foo@bar.com"; + const char kToken[] = "token"; + invalidator->UpdateCredentials(kEmail, kToken); + { + notifier::Subscription expected_subscription; + expected_subscription.channel = kSyncP2PNotificationChannel; + expected_subscription.from = kEmail; + EXPECT_TRUE(notifier::SubscriptionListsEqual( + push_client->subscriptions(), + notifier::SubscriptionList(1, expected_subscription))); + } + EXPECT_EQ(kEmail, push_client->email()); + EXPECT_EQ(kToken, push_client->token()); + + ReflectSentNotifications(); + push_client->EnableNotifications(); + EXPECT_EQ(INVALIDATIONS_ENABLED, fake_handler_.GetInvalidatorState()); + + ReflectSentNotifications(); + EXPECT_EQ(1, fake_handler_.GetInvalidationCount()); + EXPECT_THAT( + MakeInvalidationMap(enabled_types), + Eq(fake_handler_.GetLastInvalidationMap())); + + // Sent with target NOTIFY_OTHERS so should not be propagated to + // |fake_handler_|. + invalidator->SendInvalidation( + ModelTypeSetToObjectIdSet(ModelTypeSet(THEMES, APPS))); + + ReflectSentNotifications(); + EXPECT_EQ(1, fake_handler_.GetInvalidationCount()); +} + +// Set up the P2PInvalidator and send out notifications with various +// target settings. The notifications received by the observer should +// be consistent with the target settings. +TEST_F(P2PInvalidatorTest, SendNotificationData) { + const ModelTypeSet enabled_types(BOOKMARKS, PREFERENCES, THEMES); + const ModelTypeSet changed_types(THEMES, APPS); + const ModelTypeSet expected_types(THEMES); + + const ObjectIdInvalidationMap& invalidation_map = + MakeInvalidationMap(changed_types); + + P2PInvalidator* const invalidator = delegate_.GetInvalidator(); + notifier::FakePushClient* const push_client = delegate_.GetPushClient(); + + invalidator->UpdateRegisteredIds(&fake_handler_, + ModelTypeSetToObjectIdSet(enabled_types)); + + invalidator->UpdateCredentials("foo@bar.com", "fake_token"); + + ReflectSentNotifications(); + push_client->EnableNotifications(); + EXPECT_EQ(INVALIDATIONS_ENABLED, fake_handler_.GetInvalidatorState()); + + ReflectSentNotifications(); + EXPECT_EQ(1, fake_handler_.GetInvalidationCount()); + EXPECT_EQ(ModelTypeSetToObjectIdSet(enabled_types), + fake_handler_.GetLastInvalidationMap().GetObjectIds()); + + // Should be dropped. + invalidator->SendNotificationDataForTest(P2PNotificationData()); + ReflectSentNotifications(); + EXPECT_EQ(1, fake_handler_.GetInvalidationCount()); + + const ObjectIdSet& expected_ids = ModelTypeSetToObjectIdSet(expected_types); + + // Should be propagated. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender", NOTIFY_SELF, invalidation_map)); + ReflectSentNotifications(); + EXPECT_EQ(2, fake_handler_.GetInvalidationCount()); + EXPECT_EQ(expected_ids, + fake_handler_.GetLastInvalidationMap().GetObjectIds()); + + // Should be dropped. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender2", NOTIFY_SELF, invalidation_map)); + ReflectSentNotifications(); + EXPECT_EQ(2, fake_handler_.GetInvalidationCount()); + + // Should be dropped. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender", NOTIFY_SELF, ObjectIdInvalidationMap())); + ReflectSentNotifications(); + EXPECT_EQ(2, fake_handler_.GetInvalidationCount()); + + // Should be dropped. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender", NOTIFY_OTHERS, invalidation_map)); + ReflectSentNotifications(); + EXPECT_EQ(2, fake_handler_.GetInvalidationCount()); + + // Should be propagated. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender2", NOTIFY_OTHERS, invalidation_map)); + ReflectSentNotifications(); + EXPECT_EQ(3, fake_handler_.GetInvalidationCount()); + EXPECT_EQ(expected_ids, + fake_handler_.GetLastInvalidationMap().GetObjectIds()); + + // Should be dropped. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender2", NOTIFY_OTHERS, ObjectIdInvalidationMap())); + ReflectSentNotifications(); + EXPECT_EQ(3, fake_handler_.GetInvalidationCount()); + + // Should be propagated. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender", NOTIFY_ALL, invalidation_map)); + ReflectSentNotifications(); + EXPECT_EQ(4, fake_handler_.GetInvalidationCount()); + EXPECT_EQ(expected_ids, + fake_handler_.GetLastInvalidationMap().GetObjectIds()); + + // Should be propagated. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender2", NOTIFY_ALL, invalidation_map)); + ReflectSentNotifications(); + EXPECT_EQ(5, fake_handler_.GetInvalidationCount()); + EXPECT_EQ(expected_ids, + fake_handler_.GetLastInvalidationMap().GetObjectIds()); + + // Should be dropped. + invalidator->SendNotificationDataForTest( + P2PNotificationData("sender2", NOTIFY_ALL, ObjectIdInvalidationMap())); + ReflectSentNotifications(); + EXPECT_EQ(5, fake_handler_.GetInvalidationCount()); +} + +INSTANTIATE_TYPED_TEST_CASE_P( + P2PInvalidatorTest, InvalidatorTest, + P2PInvalidatorTestDelegate); + +} // namespace + +} // namespace syncer diff --git a/sync/notifier/push_client_channel.cc b/sync/notifier/push_client_channel.cc new file mode 100644 index 0000000..0bd07c9 --- /dev/null +++ b/sync/notifier/push_client_channel.cc @@ -0,0 +1,161 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/push_client_channel.h" + +#include "base/stl_util.h" +#include "google/cacheinvalidation/client_gateway.pb.h" +#include "google/cacheinvalidation/types.pb.h" +#include "jingle/notifier/listener/push_client.h" + +namespace syncer { + +namespace { + +const char kBotJid[] = "tango@bot.talk.google.com"; +const char kChannelName[] = "tango_raw"; + +} // namespace + +PushClientChannel::PushClientChannel( + scoped_ptr<notifier::PushClient> push_client) + : push_client_(push_client.Pass()), + scheduling_hash_(0), + sent_messages_count_(0) { + push_client_->AddObserver(this); + notifier::Subscription subscription; + subscription.channel = kChannelName; + subscription.from = ""; + notifier::SubscriptionList subscriptions; + subscriptions.push_back(subscription); + push_client_->UpdateSubscriptions(subscriptions); +} + +PushClientChannel::~PushClientChannel() { + push_client_->RemoveObserver(this); +} + +void PushClientChannel::UpdateCredentials( + const std::string& email, const std::string& token) { + push_client_->UpdateCredentials(email, token); +} + +int PushClientChannel::GetInvalidationClientType() { +#if defined(OS_IOS) + return ipc::invalidation::ClientType::CHROME_SYNC_IOS; +#else + return ipc::invalidation::ClientType::CHROME_SYNC; +#endif +} + +void PushClientChannel::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) { + callback.Run(*CollectDebugData()); +} + +void PushClientChannel::SendMessage(const std::string& message) { + std::string encoded_message; + EncodeMessage(&encoded_message, message, service_context_, scheduling_hash_); + + notifier::Recipient recipient; + recipient.to = kBotJid; + notifier::Notification notification; + notification.channel = kChannelName; + notification.recipients.push_back(recipient); + notification.data = encoded_message; + push_client_->SendNotification(notification); + sent_messages_count_++; +} + +void PushClientChannel::OnNotificationsEnabled() { + NotifyStateChange(INVALIDATIONS_ENABLED); +} + +void PushClientChannel::OnNotificationsDisabled( + notifier::NotificationsDisabledReason reason) { + NotifyStateChange(FromNotifierReason(reason)); +} + +void PushClientChannel::OnIncomingNotification( + const notifier::Notification& notification) { + std::string message; + std::string service_context; + int64 scheduling_hash; + if (!DecodeMessage( + notification.data, &message, &service_context, &scheduling_hash)) { + DLOG(ERROR) << "Could not parse ClientGatewayMessage"; + return; + } + if (DeliverIncomingMessage(message)) { + service_context_ = service_context; + scheduling_hash_ = scheduling_hash; + } +} + +const std::string& PushClientChannel::GetServiceContextForTest() const { + return service_context_; +} + +int64 PushClientChannel::GetSchedulingHashForTest() const { + return scheduling_hash_; +} + +std::string PushClientChannel::EncodeMessageForTest( + const std::string& message, + const std::string& service_context, + int64 scheduling_hash) { + std::string encoded_message; + EncodeMessage(&encoded_message, message, service_context, scheduling_hash); + return encoded_message; +} + +bool PushClientChannel::DecodeMessageForTest(const std::string& data, + std::string* message, + std::string* service_context, + int64* scheduling_hash) { + return DecodeMessage(data, message, service_context, scheduling_hash); +} + +void PushClientChannel::EncodeMessage(std::string* encoded_message, + const std::string& message, + const std::string& service_context, + int64 scheduling_hash) { + ipc::invalidation::ClientGatewayMessage envelope; + envelope.set_is_client_to_server(true); + if (!service_context.empty()) { + envelope.set_service_context(service_context); + envelope.set_rpc_scheduling_hash(scheduling_hash); + } + envelope.set_network_message(message); + envelope.SerializeToString(encoded_message); +} + +bool PushClientChannel::DecodeMessage(const std::string& data, + std::string* message, + std::string* service_context, + int64* scheduling_hash) { + ipc::invalidation::ClientGatewayMessage envelope; + if (!envelope.ParseFromString(data)) { + return false; + } + *message = envelope.network_message(); + if (envelope.has_service_context()) { + *service_context = envelope.service_context(); + } + if (envelope.has_rpc_scheduling_hash()) { + *scheduling_hash = envelope.rpc_scheduling_hash(); + } + return true; +} + +scoped_ptr<base::DictionaryValue> PushClientChannel::CollectDebugData() const { + scoped_ptr<base::DictionaryValue> status(new base::DictionaryValue); + status->SetString("PushClientChannel.NetworkChannel", "Push Client"); + status->SetInteger("PushClientChannel.SentMessages", sent_messages_count_); + status->SetInteger("PushClientChannel.ReceivedMessages", + SyncNetworkChannel::GetReceivedMessagesCount()); + return status.Pass(); +} + +} // namespace syncer diff --git a/sync/notifier/push_client_channel.h b/sync/notifier/push_client_channel.h new file mode 100644 index 0000000..b3e4bf2 --- /dev/null +++ b/sync/notifier/push_client_channel.h @@ -0,0 +1,91 @@ +// Copyright 2014 The Chromium 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 SYNC_NOTIFIER_PUSH_CLIENT_CHANNEL_H_ +#define SYNC_NOTIFIER_PUSH_CLIENT_CHANNEL_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "jingle/notifier/listener/push_client_observer.h" +#include "sync/base/sync_export.h" +#include "sync/notifier/sync_system_resources.h" + +namespace notifier { +class PushClient; +} // namespace notifier + +namespace syncer { + +// A PushClientChannel is an implementation of NetworkChannel that +// routes messages through a PushClient. +class SYNC_EXPORT_PRIVATE PushClientChannel + : public SyncNetworkChannel, + public NON_EXPORTED_BASE(notifier::PushClientObserver) { + public: + // |push_client| is guaranteed to be destroyed only when this object + // is destroyed. + explicit PushClientChannel(scoped_ptr<notifier::PushClient> push_client); + + virtual ~PushClientChannel(); + + // invalidation::NetworkChannel implementation. + virtual void SendMessage(const std::string& message) OVERRIDE; + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) OVERRIDE; + + // SyncNetworkChannel implementation. + // If not connected, connects with the given credentials. If + // already connected, the next connection attempt will use the given + // credentials. + virtual void UpdateCredentials(const std::string& email, + const std::string& token) OVERRIDE; + virtual int GetInvalidationClientType() OVERRIDE; + + // notifier::PushClient::Observer implementation. + virtual void OnNotificationsEnabled() OVERRIDE; + virtual void OnNotificationsDisabled( + notifier::NotificationsDisabledReason reason) OVERRIDE; + virtual void OnIncomingNotification( + const notifier::Notification& notification) OVERRIDE; + + const std::string& GetServiceContextForTest() const; + + int64 GetSchedulingHashForTest() const; + + static std::string EncodeMessageForTest(const std::string& message, + const std::string& service_context, + int64 scheduling_hash); + + static bool DecodeMessageForTest(const std::string& notification, + std::string* message, + std::string* service_context, + int64* scheduling_hash); + + private: + static void EncodeMessage(std::string* encoded_message, + const std::string& message, + const std::string& service_context, + int64 scheduling_hash); + static bool DecodeMessage(const std::string& data, + std::string* message, + std::string* service_context, + int64* scheduling_hash); + scoped_ptr<base::DictionaryValue> CollectDebugData() const; + + scoped_ptr<notifier::PushClient> push_client_; + std::string service_context_; + int64 scheduling_hash_; + + // This count is saved for displaying statatistics. + int sent_messages_count_; + + DISALLOW_COPY_AND_ASSIGN(PushClientChannel); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_PUSH_CLIENT_CHANNEL_H_ diff --git a/sync/notifier/push_client_channel_unittest.cc b/sync/notifier/push_client_channel_unittest.cc new file mode 100644 index 0000000..e58d20b --- /dev/null +++ b/sync/notifier/push_client_channel_unittest.cc @@ -0,0 +1,252 @@ +// Copyright 2014 The Chromium 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 <string> + +#include "base/compiler_specific.h" +#include "jingle/notifier/listener/fake_push_client.h" +#include "jingle/notifier/listener/notification_defines.h" +#include "sync/notifier/push_client_channel.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { +namespace { + +class PushClientChannelTest + : public ::testing::Test, + public SyncNetworkChannel::Observer { + protected: + PushClientChannelTest() + : fake_push_client_(new notifier::FakePushClient()), + push_client_channel_( + scoped_ptr<notifier::PushClient>(fake_push_client_)), + last_invalidator_state_(DEFAULT_INVALIDATION_ERROR) { + push_client_channel_.AddObserver(this); + push_client_channel_.SetMessageReceiver( + invalidation::NewPermanentCallback( + this, &PushClientChannelTest::OnIncomingMessage)); + push_client_channel_.SetSystemResources(NULL); + } + + virtual ~PushClientChannelTest() { + push_client_channel_.RemoveObserver(this); + } + + virtual void OnNetworkChannelStateChanged( + InvalidatorState invalidator_state) OVERRIDE { + last_invalidator_state_ = invalidator_state; + } + + void OnIncomingMessage(std::string incoming_message) { + last_message_ = incoming_message; + } + + notifier::FakePushClient* fake_push_client_; + PushClientChannel push_client_channel_; + std::string last_message_; + InvalidatorState last_invalidator_state_; +}; + +const char kMessage[] = "message"; +const char kServiceContext[] = "service context"; +const int64 kSchedulingHash = 100; + +// Make sure the channel subscribes to the correct notifications +// channel on construction. +TEST_F(PushClientChannelTest, Subscriptions) { + notifier::Subscription expected_subscription; + expected_subscription.channel = "tango_raw"; + EXPECT_TRUE(notifier::SubscriptionListsEqual( + fake_push_client_->subscriptions(), + notifier::SubscriptionList(1, expected_subscription))); +} + +// Call UpdateCredentials on the channel. It should propagate it to +// the push client. +TEST_F(PushClientChannelTest, UpdateCredentials) { + const char kEmail[] = "foo@bar.com"; + const char kToken[] = "token"; + EXPECT_TRUE(fake_push_client_->email().empty()); + EXPECT_TRUE(fake_push_client_->token().empty()); + push_client_channel_.UpdateCredentials(kEmail, kToken); + EXPECT_EQ(kEmail, fake_push_client_->email()); + EXPECT_EQ(kToken, fake_push_client_->token()); +} + +// Simulate push client state changes on the push client. It should +// propagate to the channel. +TEST_F(PushClientChannelTest, OnPushClientStateChange) { + EXPECT_EQ(DEFAULT_INVALIDATION_ERROR, last_invalidator_state_); + fake_push_client_->EnableNotifications(); + EXPECT_EQ(INVALIDATIONS_ENABLED, last_invalidator_state_); + fake_push_client_->DisableNotifications( + notifier::TRANSIENT_NOTIFICATION_ERROR); + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, last_invalidator_state_); + fake_push_client_->DisableNotifications( + notifier::NOTIFICATION_CREDENTIALS_REJECTED); + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, last_invalidator_state_); +} + +// Call SendMessage on the channel. It should propagate it to the +// push client. +TEST_F(PushClientChannelTest, SendMessage) { + EXPECT_TRUE(fake_push_client_->sent_notifications().empty()); + push_client_channel_.SendMessage(kMessage); + ASSERT_EQ(1u, fake_push_client_->sent_notifications().size()); + std::string expected_encoded_message = + PushClientChannel::EncodeMessageForTest( + kMessage, + push_client_channel_.GetServiceContextForTest(), + push_client_channel_.GetSchedulingHashForTest()); + ASSERT_EQ(expected_encoded_message, + fake_push_client_->sent_notifications()[0].data); +} + +// Encode a message with some context and then decode it. The decoded info +// should match the original info. +TEST_F(PushClientChannelTest, EncodeDecode) { + const std::string& data = PushClientChannel::EncodeMessageForTest( + kMessage, kServiceContext, kSchedulingHash); + std::string message; + std::string service_context; + int64 scheduling_hash = 0LL; + EXPECT_TRUE(PushClientChannel::DecodeMessageForTest( + data, &message, &service_context, &scheduling_hash)); + EXPECT_EQ(kMessage, message); + EXPECT_EQ(kServiceContext, service_context); + EXPECT_EQ(kSchedulingHash, scheduling_hash); +} + +// Encode a message with no context and then decode it. The decoded message +// should match the original message, but the context and hash should be +// untouched. +TEST_F(PushClientChannelTest, EncodeDecodeNoContext) { + const std::string& data = PushClientChannel::EncodeMessageForTest( + kMessage, std::string(), kSchedulingHash); + std::string message; + std::string service_context = kServiceContext; + int64 scheduling_hash = kSchedulingHash + 1; + EXPECT_TRUE(PushClientChannel::DecodeMessageForTest( + data, &message, &service_context, &scheduling_hash)); + EXPECT_EQ(kMessage, message); + EXPECT_EQ(kServiceContext, service_context); + EXPECT_EQ(kSchedulingHash + 1, scheduling_hash); +} + +// Decode an empty notification. It should result in an empty message +// but should leave the context and hash untouched. +TEST_F(PushClientChannelTest, DecodeEmpty) { + std::string message = kMessage; + std::string service_context = kServiceContext; + int64 scheduling_hash = kSchedulingHash; + EXPECT_TRUE(PushClientChannel::DecodeMessageForTest( + std::string(), &message, &service_context, &scheduling_hash)); + EXPECT_TRUE(message.empty()); + EXPECT_EQ(kServiceContext, service_context); + EXPECT_EQ(kSchedulingHash, scheduling_hash); +} + +// Try to decode a garbage notification. It should leave all its +// arguments untouched and return false. +TEST_F(PushClientChannelTest, DecodeGarbage) { + std::string data = "garbage"; + std::string message = kMessage; + std::string service_context = kServiceContext; + int64 scheduling_hash = kSchedulingHash; + EXPECT_FALSE(PushClientChannel::DecodeMessageForTest( + data, &message, &service_context, &scheduling_hash)); + EXPECT_EQ(kMessage, message); + EXPECT_EQ(kServiceContext, service_context); + EXPECT_EQ(kSchedulingHash, scheduling_hash); +} + +// Simulate an incoming notification. It should be decoded properly +// by the channel. +TEST_F(PushClientChannelTest, OnIncomingMessage) { + notifier::Notification notification; + notification.data = + PushClientChannel::EncodeMessageForTest( + kMessage, kServiceContext, kSchedulingHash); + fake_push_client_->SimulateIncomingNotification(notification); + + EXPECT_EQ(kServiceContext, + push_client_channel_.GetServiceContextForTest()); + EXPECT_EQ(kSchedulingHash, + push_client_channel_.GetSchedulingHashForTest()); + EXPECT_EQ(kMessage, last_message_); +} + +// Simulate an incoming notification with no receiver. It should be dropped by +// the channel. +TEST_F(PushClientChannelTest, OnIncomingMessageNoReceiver) { + push_client_channel_.SetMessageReceiver(NULL); + + notifier::Notification notification; + notification.data = PushClientChannel::EncodeMessageForTest( + kMessage, kServiceContext, kSchedulingHash); + fake_push_client_->SimulateIncomingNotification(notification); + + EXPECT_TRUE(push_client_channel_.GetServiceContextForTest().empty()); + EXPECT_EQ(static_cast<int64>(0), + push_client_channel_.GetSchedulingHashForTest()); + EXPECT_TRUE(last_message_.empty()); +} + +// Simulate an incoming garbage notification. It should be dropped by +// the channel. +TEST_F(PushClientChannelTest, OnIncomingMessageGarbage) { + notifier::Notification notification; + notification.data = "garbage"; + fake_push_client_->SimulateIncomingNotification(notification); + EXPECT_TRUE(push_client_channel_.GetServiceContextForTest().empty()); + EXPECT_EQ(static_cast<int64>(0), + push_client_channel_.GetSchedulingHashForTest()); + EXPECT_TRUE(last_message_.empty()); +} + +// Send a message, simulate an incoming message with context, and then +// send the same message again. The first sent message should not +// have any context, but the second sent message should have the +// context from the incoming emssage. +TEST_F(PushClientChannelTest, PersistedMessageState) { + push_client_channel_.SendMessage(kMessage); + ASSERT_EQ(1u, fake_push_client_->sent_notifications().size()); + { + std::string message; + std::string service_context; + int64 scheduling_hash = 0LL; + EXPECT_TRUE(PushClientChannel::DecodeMessageForTest( + fake_push_client_->sent_notifications()[0].data, + &message, + &service_context, + &scheduling_hash)); + EXPECT_EQ(kMessage, message); + EXPECT_TRUE(service_context.empty()); + EXPECT_EQ(0LL, scheduling_hash); + } + + notifier::Notification notification; + notification.data = PushClientChannel::EncodeMessageForTest( + kMessage, kServiceContext, kSchedulingHash); + fake_push_client_->SimulateIncomingNotification(notification); + + push_client_channel_.SendMessage(kMessage); + ASSERT_EQ(2u, fake_push_client_->sent_notifications().size()); + { + std::string message; + std::string service_context; + int64 scheduling_hash = 0LL; + EXPECT_TRUE(PushClientChannel::DecodeMessageForTest( + fake_push_client_->sent_notifications()[1].data, + &message, + &service_context, + &scheduling_hash)); + EXPECT_EQ(kMessage, message); + EXPECT_EQ(kServiceContext, service_context); + EXPECT_EQ(kSchedulingHash, scheduling_hash); + } +} + +} // namespace +} // namespace syncer diff --git a/sync/notifier/state_writer.h b/sync/notifier/state_writer.h new file mode 100644 index 0000000..d008fee --- /dev/null +++ b/sync/notifier/state_writer.h @@ -0,0 +1,25 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Simple interface for something that persists state. + +#ifndef SYNC_NOTIFIER_STATE_WRITER_H_ +#define SYNC_NOTIFIER_STATE_WRITER_H_ + +#include <string> + +#include "sync/base/sync_export.h" + +namespace syncer { + +class SYNC_EXPORT_PRIVATE StateWriter { + public: + virtual ~StateWriter() {} + + virtual void WriteState(const std::string& state) = 0; +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_STATE_WRITER_H_ diff --git a/sync/notifier/sync_invalidation_listener.cc b/sync/notifier/sync_invalidation_listener.cc new file mode 100644 index 0000000..ddf4233 --- /dev/null +++ b/sync/notifier/sync_invalidation_listener.cc @@ -0,0 +1,443 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/sync_invalidation_listener.h" + +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "base/tracked_objects.h" +#include "google/cacheinvalidation/include/invalidation-client.h" +#include "google/cacheinvalidation/include/types.h" +#include "jingle/notifier/listener/push_client.h" +#include "sync/notifier/invalidation_util.h" +#include "sync/notifier/object_id_invalidation_map.h" +#include "sync/notifier/registration_manager.h" + +namespace { + +const char kApplicationName[] = "chrome-sync"; + +} // namespace + +namespace syncer { + +SyncInvalidationListener::Delegate::~Delegate() {} + +SyncInvalidationListener::SyncInvalidationListener( + scoped_ptr<SyncNetworkChannel> network_channel) + : sync_network_channel_(network_channel.Pass()), + sync_system_resources_(sync_network_channel_.get(), this), + delegate_(NULL), + ticl_state_(DEFAULT_INVALIDATION_ERROR), + push_client_state_(DEFAULT_INVALIDATION_ERROR), + weak_ptr_factory_(this) { + DCHECK(CalledOnValidThread()); + sync_network_channel_->AddObserver(this); +} + +SyncInvalidationListener::~SyncInvalidationListener() { + DCHECK(CalledOnValidThread()); + sync_network_channel_->RemoveObserver(this); + Stop(); + DCHECK(!delegate_); +} + +void SyncInvalidationListener::Start( + const CreateInvalidationClientCallback& + create_invalidation_client_callback, + const std::string& client_id, const std::string& client_info, + const std::string& invalidation_bootstrap_data, + const UnackedInvalidationsMap& initial_unacked_invalidations, + const WeakHandle<InvalidationStateTracker>& invalidation_state_tracker, + Delegate* delegate) { + DCHECK(CalledOnValidThread()); + Stop(); + + sync_system_resources_.set_platform(client_info); + sync_system_resources_.Start(); + + // The Storage resource is implemented as a write-through cache. We populate + // it with the initial state on startup, so subsequent writes go to disk and + // update the in-memory cache, while reads just return the cached state. + sync_system_resources_.storage()->SetInitialState( + invalidation_bootstrap_data); + + unacked_invalidations_map_ = initial_unacked_invalidations; + invalidation_state_tracker_ = invalidation_state_tracker; + DCHECK(invalidation_state_tracker_.IsInitialized()); + + DCHECK(!delegate_); + DCHECK(delegate); + delegate_ = delegate; + + invalidation_client_.reset(create_invalidation_client_callback.Run( + &sync_system_resources_, + sync_network_channel_->GetInvalidationClientType(), + client_id, + kApplicationName, + this)); + invalidation_client_->Start(); + + registration_manager_.reset( + new RegistrationManager(invalidation_client_.get())); +} + +void SyncInvalidationListener::UpdateCredentials( + const std::string& email, const std::string& token) { + DCHECK(CalledOnValidThread()); + sync_network_channel_->UpdateCredentials(email, token); +} + +void SyncInvalidationListener::UpdateRegisteredIds(const ObjectIdSet& ids) { + DCHECK(CalledOnValidThread()); + registered_ids_ = ids; + // |ticl_state_| can go to INVALIDATIONS_ENABLED even without a + // working XMPP connection (as observed by us), so check it instead + // of GetState() (see http://crbug.com/139424). + if (ticl_state_ == INVALIDATIONS_ENABLED && registration_manager_) { + DoRegistrationUpdate(); + } +} + +void SyncInvalidationListener::Ready( + invalidation::InvalidationClient* client) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + ticl_state_ = INVALIDATIONS_ENABLED; + EmitStateChange(); + DoRegistrationUpdate(); +} + +void SyncInvalidationListener::Invalidate( + invalidation::InvalidationClient* client, + const invalidation::Invalidation& invalidation, + const invalidation::AckHandle& ack_handle) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + client->Acknowledge(ack_handle); + + const invalidation::ObjectId& id = invalidation.object_id(); + + std::string payload; + // payload() CHECK()'s has_payload(), so we must check it ourselves first. + if (invalidation.has_payload()) + payload = invalidation.payload(); + + DVLOG(2) << "Received invalidation with version " << invalidation.version() + << " for " << ObjectIdToString(id); + + ObjectIdInvalidationMap invalidations; + Invalidation inv = Invalidation::Init(id, invalidation.version(), payload); + inv.set_ack_handler(GetThisAsAckHandler()); + invalidations.Insert(inv); + + DispatchInvalidations(invalidations); +} + +void SyncInvalidationListener::InvalidateUnknownVersion( + invalidation::InvalidationClient* client, + const invalidation::ObjectId& object_id, + const invalidation::AckHandle& ack_handle) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + DVLOG(1) << "InvalidateUnknownVersion"; + client->Acknowledge(ack_handle); + + ObjectIdInvalidationMap invalidations; + Invalidation unknown_version = Invalidation::InitUnknownVersion(object_id); + unknown_version.set_ack_handler(GetThisAsAckHandler()); + invalidations.Insert(unknown_version); + + DispatchInvalidations(invalidations); +} + +// This should behave as if we got an invalidation with version +// UNKNOWN_OBJECT_VERSION for all known data types. +void SyncInvalidationListener::InvalidateAll( + invalidation::InvalidationClient* client, + const invalidation::AckHandle& ack_handle) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + DVLOG(1) << "InvalidateAll"; + client->Acknowledge(ack_handle); + + ObjectIdInvalidationMap invalidations; + for (ObjectIdSet::iterator it = registered_ids_.begin(); + it != registered_ids_.end(); ++it) { + Invalidation unknown_version = Invalidation::InitUnknownVersion(*it); + unknown_version.set_ack_handler(GetThisAsAckHandler()); + invalidations.Insert(unknown_version); + } + + DispatchInvalidations(invalidations); +} + +// If a handler is registered, emit right away. Otherwise, save it for later. +void SyncInvalidationListener::DispatchInvalidations( + const ObjectIdInvalidationMap& invalidations) { + DCHECK(CalledOnValidThread()); + + ObjectIdInvalidationMap to_save = invalidations; + ObjectIdInvalidationMap to_emit = + invalidations.GetSubsetWithObjectIds(registered_ids_); + + SaveInvalidations(to_save); + EmitSavedInvalidations(to_emit); +} + +void SyncInvalidationListener::SaveInvalidations( + const ObjectIdInvalidationMap& to_save) { + ObjectIdSet objects_to_save = to_save.GetObjectIds(); + for (ObjectIdSet::const_iterator it = objects_to_save.begin(); + it != objects_to_save.end(); ++it) { + UnackedInvalidationsMap::iterator lookup = + unacked_invalidations_map_.find(*it); + if (lookup == unacked_invalidations_map_.end()) { + lookup = unacked_invalidations_map_.insert( + std::make_pair(*it, UnackedInvalidationSet(*it))).first; + } + lookup->second.AddSet(to_save.ForObject(*it)); + } + + invalidation_state_tracker_.Call( + FROM_HERE, + &InvalidationStateTracker::SetSavedInvalidations, + unacked_invalidations_map_); +} + +void SyncInvalidationListener::EmitSavedInvalidations( + const ObjectIdInvalidationMap& to_emit) { + DVLOG(2) << "Emitting invalidations: " << to_emit.ToString(); + delegate_->OnInvalidate(to_emit); +} + +void SyncInvalidationListener::InformRegistrationStatus( + invalidation::InvalidationClient* client, + const invalidation::ObjectId& object_id, + InvalidationListener::RegistrationState new_state) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + DVLOG(1) << "InformRegistrationStatus: " + << ObjectIdToString(object_id) << " " << new_state; + + if (new_state != InvalidationListener::REGISTERED) { + // Let |registration_manager_| handle the registration backoff policy. + registration_manager_->MarkRegistrationLost(object_id); + } +} + +void SyncInvalidationListener::InformRegistrationFailure( + invalidation::InvalidationClient* client, + const invalidation::ObjectId& object_id, + bool is_transient, + const std::string& error_message) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + DVLOG(1) << "InformRegistrationFailure: " + << ObjectIdToString(object_id) + << "is_transient=" << is_transient + << ", message=" << error_message; + + if (is_transient) { + // We don't care about |unknown_hint|; we let + // |registration_manager_| handle the registration backoff policy. + registration_manager_->MarkRegistrationLost(object_id); + } else { + // Non-transient failures require an action to resolve. This could happen + // because: + // - the server doesn't yet recognize the data type, which could happen for + // brand-new data types. + // - the user has changed his password and hasn't updated it yet locally. + // Either way, block future registration attempts for |object_id|. However, + // we don't forget any saved invalidation state since we may use it once the + // error is addressed. + registration_manager_->DisableId(object_id); + } +} + +void SyncInvalidationListener::ReissueRegistrations( + invalidation::InvalidationClient* client, + const std::string& prefix, + int prefix_length) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + DVLOG(1) << "AllRegistrationsLost"; + registration_manager_->MarkAllRegistrationsLost(); +} + +void SyncInvalidationListener::InformError( + invalidation::InvalidationClient* client, + const invalidation::ErrorInfo& error_info) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(client, invalidation_client_.get()); + LOG(ERROR) << "Ticl error " << error_info.error_reason() << ": " + << error_info.error_message() + << " (transient = " << error_info.is_transient() << ")"; + if (error_info.error_reason() == invalidation::ErrorReason::AUTH_FAILURE) { + ticl_state_ = INVALIDATION_CREDENTIALS_REJECTED; + } else { + ticl_state_ = TRANSIENT_INVALIDATION_ERROR; + } + EmitStateChange(); +} + +void SyncInvalidationListener::Acknowledge( + const invalidation::ObjectId& id, + const syncer::AckHandle& handle) { + UnackedInvalidationsMap::iterator lookup = + unacked_invalidations_map_.find(id); + if (lookup == unacked_invalidations_map_.end()) { + DLOG(WARNING) << "Received acknowledgement for untracked object ID"; + return; + } + lookup->second.Acknowledge(handle); + invalidation_state_tracker_.Call( + FROM_HERE, + &InvalidationStateTracker::SetSavedInvalidations, + unacked_invalidations_map_); +} + +void SyncInvalidationListener::Drop( + const invalidation::ObjectId& id, + const syncer::AckHandle& handle) { + UnackedInvalidationsMap::iterator lookup = + unacked_invalidations_map_.find(id); + if (lookup == unacked_invalidations_map_.end()) { + DLOG(WARNING) << "Received drop for untracked object ID"; + return; + } + lookup->second.Drop(handle); + invalidation_state_tracker_.Call( + FROM_HERE, + &InvalidationStateTracker::SetSavedInvalidations, + unacked_invalidations_map_); +} + +void SyncInvalidationListener::WriteState(const std::string& state) { + DCHECK(CalledOnValidThread()); + DVLOG(1) << "WriteState"; + invalidation_state_tracker_.Call( + FROM_HERE, &InvalidationStateTracker::SetBootstrapData, state); +} + +void SyncInvalidationListener::DoRegistrationUpdate() { + DCHECK(CalledOnValidThread()); + const ObjectIdSet& unregistered_ids = + registration_manager_->UpdateRegisteredIds(registered_ids_); + for (ObjectIdSet::iterator it = unregistered_ids.begin(); + it != unregistered_ids.end(); ++it) { + unacked_invalidations_map_.erase(*it); + } + invalidation_state_tracker_.Call( + FROM_HERE, + &InvalidationStateTracker::SetSavedInvalidations, + unacked_invalidations_map_); + + ObjectIdInvalidationMap object_id_invalidation_map; + for (UnackedInvalidationsMap::iterator map_it = + unacked_invalidations_map_.begin(); + map_it != unacked_invalidations_map_.end(); ++map_it) { + if (registered_ids_.find(map_it->first) == registered_ids_.end()) { + continue; + } + map_it->second.ExportInvalidations( + GetThisAsAckHandler(), + &object_id_invalidation_map); + } + + // There's no need to run these through DispatchInvalidations(); they've + // already been saved to storage (that's where we found them) so all we need + // to do now is emit them. + EmitSavedInvalidations(object_id_invalidation_map); +} + +void SyncInvalidationListener::RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const { + DCHECK(CalledOnValidThread()); + sync_network_channel_->RequestDetailedStatus(callback); + callback.Run(*CollectDebugData()); +} + +scoped_ptr<base::DictionaryValue> +SyncInvalidationListener::CollectDebugData() const { + scoped_ptr<base::DictionaryValue> return_value(new base::DictionaryValue()); + return_value->SetString( + "SyncInvalidationListener.PushClientState", + std::string(InvalidatorStateToString(push_client_state_))); + return_value->SetString("SyncInvalidationListener.TiclState", + std::string(InvalidatorStateToString(ticl_state_))); + scoped_ptr<base::DictionaryValue> unacked_map(new base::DictionaryValue()); + for (UnackedInvalidationsMap::const_iterator it = + unacked_invalidations_map_.begin(); + it != unacked_invalidations_map_.end(); + ++it) { + unacked_map->Set((it->first).name(), (it->second).ToValue().release()); + } + return_value->Set("SyncInvalidationListener.UnackedInvalidationsMap", + unacked_map.release()); + return return_value.Pass(); +} + +void SyncInvalidationListener::StopForTest() { + DCHECK(CalledOnValidThread()); + Stop(); +} + +void SyncInvalidationListener::Stop() { + DCHECK(CalledOnValidThread()); + if (!invalidation_client_) { + return; + } + + registration_manager_.reset(); + sync_system_resources_.Stop(); + invalidation_client_->Stop(); + + invalidation_client_.reset(); + delegate_ = NULL; + + ticl_state_ = DEFAULT_INVALIDATION_ERROR; + push_client_state_ = DEFAULT_INVALIDATION_ERROR; +} + +InvalidatorState SyncInvalidationListener::GetState() const { + DCHECK(CalledOnValidThread()); + if (ticl_state_ == INVALIDATION_CREDENTIALS_REJECTED || + push_client_state_ == INVALIDATION_CREDENTIALS_REJECTED) { + // If either the ticl or the push client rejected our credentials, + // return INVALIDATION_CREDENTIALS_REJECTED. + return INVALIDATION_CREDENTIALS_REJECTED; + } + if (ticl_state_ == INVALIDATIONS_ENABLED && + push_client_state_ == INVALIDATIONS_ENABLED) { + // If the ticl is ready and the push client notifications are + // enabled, return INVALIDATIONS_ENABLED. + return INVALIDATIONS_ENABLED; + } + // Otherwise, we have a transient error. + return TRANSIENT_INVALIDATION_ERROR; +} + +void SyncInvalidationListener::EmitStateChange() { + DCHECK(CalledOnValidThread()); + delegate_->OnInvalidatorStateChange(GetState()); +} + +WeakHandle<AckHandler> SyncInvalidationListener::GetThisAsAckHandler() { + DCHECK(CalledOnValidThread()); + return WeakHandle<AckHandler>(weak_ptr_factory_.GetWeakPtr()); +} + +void SyncInvalidationListener::OnNetworkChannelStateChanged( + InvalidatorState invalidator_state) { + DCHECK(CalledOnValidThread()); + push_client_state_ = invalidator_state; + EmitStateChange(); +} + +} // namespace syncer diff --git a/sync/notifier/sync_invalidation_listener.h b/sync/notifier/sync_invalidation_listener.h new file mode 100644 index 0000000..93bc0a5 --- /dev/null +++ b/sync/notifier/sync_invalidation_listener.h @@ -0,0 +1,196 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// A simple wrapper around invalidation::InvalidationClient that +// handles all the startup/shutdown details and hookups. + +#ifndef SYNC_NOTIFIER_SYNC_INVALIDATION_LISTENER_H_ +#define SYNC_NOTIFIER_SYNC_INVALIDATION_LISTENER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "google/cacheinvalidation/include/invalidation-listener.h" +#include "sync/base/sync_export.h" +#include "sync/internal_api/public/util/weak_handle.h" +#include "sync/notifier/ack_handler.h" +#include "sync/notifier/invalidation_state_tracker.h" +#include "sync/notifier/invalidator_state.h" +#include "sync/notifier/state_writer.h" +#include "sync/notifier/sync_system_resources.h" +#include "sync/notifier/unacked_invalidation_set.h" + +namespace buzz { +class XmppTaskParentInterface; +} // namespace buzz + +namespace notifier { +class PushClient; +} // namespace notifier + +namespace syncer { + +class ObjectIdInvalidationMap; +class RegistrationManager; + +// SyncInvalidationListener is not thread-safe and lives on the sync +// thread. +class SYNC_EXPORT_PRIVATE SyncInvalidationListener + : public NON_EXPORTED_BASE(invalidation::InvalidationListener), + public StateWriter, + public SyncNetworkChannel::Observer, + public AckHandler, + public base::NonThreadSafe { + public: + typedef base::Callback<invalidation::InvalidationClient*( + invalidation::SystemResources*, + int, + const invalidation::string&, + const invalidation::string&, + invalidation::InvalidationListener*)> CreateInvalidationClientCallback; + + class SYNC_EXPORT_PRIVATE Delegate { + public: + virtual ~Delegate(); + + virtual void OnInvalidate( + const ObjectIdInvalidationMap& invalidations) = 0; + + virtual void OnInvalidatorStateChange(InvalidatorState state) = 0; + }; + + explicit SyncInvalidationListener( + scoped_ptr<SyncNetworkChannel> network_channel); + + // Calls Stop(). + virtual ~SyncInvalidationListener(); + + // Does not take ownership of |delegate| or |state_writer|. + // |invalidation_state_tracker| must be initialized. + void Start( + const CreateInvalidationClientCallback& + create_invalidation_client_callback, + const std::string& client_id, const std::string& client_info, + const std::string& invalidation_bootstrap_data, + const UnackedInvalidationsMap& initial_object_states, + const WeakHandle<InvalidationStateTracker>& invalidation_state_tracker, + Delegate* delegate); + + void UpdateCredentials(const std::string& email, const std::string& token); + + // Update the set of object IDs that we're interested in getting + // notifications for. May be called at any time. + void UpdateRegisteredIds(const ObjectIdSet& ids); + + // invalidation::InvalidationListener implementation. + virtual void Ready( + invalidation::InvalidationClient* client) OVERRIDE; + virtual void Invalidate( + invalidation::InvalidationClient* client, + const invalidation::Invalidation& invalidation, + const invalidation::AckHandle& ack_handle) OVERRIDE; + virtual void InvalidateUnknownVersion( + invalidation::InvalidationClient* client, + const invalidation::ObjectId& object_id, + const invalidation::AckHandle& ack_handle) OVERRIDE; + virtual void InvalidateAll( + invalidation::InvalidationClient* client, + const invalidation::AckHandle& ack_handle) OVERRIDE; + virtual void InformRegistrationStatus( + invalidation::InvalidationClient* client, + const invalidation::ObjectId& object_id, + invalidation::InvalidationListener::RegistrationState reg_state) OVERRIDE; + virtual void InformRegistrationFailure( + invalidation::InvalidationClient* client, + const invalidation::ObjectId& object_id, + bool is_transient, + const std::string& error_message) OVERRIDE; + virtual void ReissueRegistrations( + invalidation::InvalidationClient* client, + const std::string& prefix, + int prefix_length) OVERRIDE; + virtual void InformError( + invalidation::InvalidationClient* client, + const invalidation::ErrorInfo& error_info) OVERRIDE; + + // AckHandler implementation. + virtual void Acknowledge( + const invalidation::ObjectId& id, + const syncer::AckHandle& handle) OVERRIDE; + virtual void Drop( + const invalidation::ObjectId& id, + const syncer::AckHandle& handle) OVERRIDE; + + // StateWriter implementation. + virtual void WriteState(const std::string& state) OVERRIDE; + + // SyncNetworkChannel::Observer implementation. + virtual void OnNetworkChannelStateChanged( + InvalidatorState invalidator_state) OVERRIDE; + + void DoRegistrationUpdate(); + + void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) const; + + void StopForTest(); + + private: + void Stop(); + + InvalidatorState GetState() const; + + void EmitStateChange(); + + // Sends invalidations to their appropriate destination. + // + // If there are no observers registered for them, they will be saved for + // later. + // + // If there are observers registered, they will be saved (to make sure we + // don't drop them until they've been acted on) and emitted to the observers. + void DispatchInvalidations(const ObjectIdInvalidationMap& invalidations); + + // Saves invalidations. + // + // This call isn't synchronous so we can't guarantee these invalidations will + // be safely on disk by the end of the call, but it should ensure that the + // data makes it to disk eventually. + void SaveInvalidations(const ObjectIdInvalidationMap& to_save); + + // Emits previously saved invalidations to their registered observers. + void EmitSavedInvalidations(const ObjectIdInvalidationMap& to_emit); + + // Generate a Dictionary with all the debugging information. + scoped_ptr<base::DictionaryValue> CollectDebugData() const; + + WeakHandle<AckHandler> GetThisAsAckHandler(); + + scoped_ptr<SyncNetworkChannel> sync_network_channel_; + SyncSystemResources sync_system_resources_; + UnackedInvalidationsMap unacked_invalidations_map_; + WeakHandle<InvalidationStateTracker> invalidation_state_tracker_; + Delegate* delegate_; + scoped_ptr<invalidation::InvalidationClient> invalidation_client_; + scoped_ptr<RegistrationManager> registration_manager_; + // Stored to pass to |registration_manager_| on start. + ObjectIdSet registered_ids_; + + // The states of the ticl and the push client. + InvalidatorState ticl_state_; + InvalidatorState push_client_state_; + + base::WeakPtrFactory<SyncInvalidationListener> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(SyncInvalidationListener); +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_SYNC_INVALIDATION_LISTENER_H_ diff --git a/sync/notifier/sync_invalidation_listener_unittest.cc b/sync/notifier/sync_invalidation_listener_unittest.cc new file mode 100644 index 0000000..dbf5fd3 --- /dev/null +++ b/sync/notifier/sync_invalidation_listener_unittest.cc @@ -0,0 +1,1129 @@ +// Copyright 2014 The Chromium 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 <cstddef> +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/message_loop/message_loop.h" +#include "base/stl_util.h" +#include "google/cacheinvalidation/include/invalidation-client.h" +#include "google/cacheinvalidation/include/types.h" +#include "jingle/notifier/listener/fake_push_client.h" +#include "sync/internal_api/public/util/weak_handle.h" +#include "sync/notifier/dropped_invalidation_tracker.h" +#include "sync/notifier/fake_invalidation_state_tracker.h" +#include "sync/notifier/invalidation_util.h" +#include "sync/notifier/object_id_invalidation_map.h" +#include "sync/notifier/push_client_channel.h" +#include "sync/notifier/sync_invalidation_listener.h" +#include "sync/notifier/unacked_invalidation_set_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { + +namespace { + +using invalidation::AckHandle; +using invalidation::ObjectId; + +const char kClientId[] = "client_id"; +const char kClientInfo[] = "client_info"; + +const char kState[] = "state"; +const char kNewState[] = "new_state"; + +const char kPayload1[] = "payload1"; +const char kPayload2[] = "payload2"; + +const int64 kVersion1 = 1LL; +const int64 kVersion2 = 2LL; + +const int kChromeSyncSourceId = 1004; + +struct AckHandleLessThan { + bool operator()(const AckHandle& lhs, const AckHandle& rhs) const { + return lhs.handle_data() < rhs.handle_data(); + } +}; + +typedef std::set<AckHandle, AckHandleLessThan> AckHandleSet; + +// Fake invalidation::InvalidationClient implementation that keeps +// track of registered IDs and acked handles. +class FakeInvalidationClient : public invalidation::InvalidationClient { + public: + FakeInvalidationClient() : started_(false) {} + virtual ~FakeInvalidationClient() {} + + const ObjectIdSet& GetRegisteredIds() const { + return registered_ids_; + } + + void ClearAckedHandles() { + acked_handles_.clear(); + } + + bool IsAckedHandle(const AckHandle& ack_handle) const { + return (acked_handles_.find(ack_handle) != acked_handles_.end()); + } + + // invalidation::InvalidationClient implementation. + + virtual void Start() OVERRIDE { + started_ = true; + } + + virtual void Stop() OVERRIDE { + started_ = false; + } + + virtual void Register(const ObjectId& object_id) OVERRIDE { + if (!started_) { + ADD_FAILURE(); + return; + } + registered_ids_.insert(object_id); + } + + virtual void Register( + const invalidation::vector<ObjectId>& object_ids) OVERRIDE { + if (!started_) { + ADD_FAILURE(); + return; + } + registered_ids_.insert(object_ids.begin(), object_ids.end()); + } + + virtual void Unregister(const ObjectId& object_id) OVERRIDE { + if (!started_) { + ADD_FAILURE(); + return; + } + registered_ids_.erase(object_id); + } + + virtual void Unregister( + const invalidation::vector<ObjectId>& object_ids) OVERRIDE { + if (!started_) { + ADD_FAILURE(); + return; + } + for (invalidation::vector<ObjectId>::const_iterator + it = object_ids.begin(); it != object_ids.end(); ++it) { + registered_ids_.erase(*it); + } + } + + virtual void Acknowledge(const AckHandle& ack_handle) OVERRIDE { + if (!started_) { + ADD_FAILURE(); + return; + } + acked_handles_.insert(ack_handle); + } + + private: + bool started_; + ObjectIdSet registered_ids_; + AckHandleSet acked_handles_; +}; + +// Fake delegate tkat keeps track of invalidation counts, payloads, +// and state. +class FakeDelegate : public SyncInvalidationListener::Delegate { + public: + explicit FakeDelegate(SyncInvalidationListener* listener) + : state_(TRANSIENT_INVALIDATION_ERROR), + drop_handlers_deleter_(&drop_handlers_) {} + virtual ~FakeDelegate() {} + + size_t GetInvalidationCount(const ObjectId& id) const { + Map::const_iterator it = invalidations_.find(id); + if (it == invalidations_.end()) { + return 0; + } else { + return it->second.size(); + } + } + + int64 GetVersion(const ObjectId& id) const { + Map::const_iterator it = invalidations_.find(id); + if (it == invalidations_.end()) { + ADD_FAILURE() << "No invalidations for ID " << ObjectIdToString(id); + return 0; + } else { + return it->second.back().version(); + } + } + + std::string GetPayload(const ObjectId& id) const { + Map::const_iterator it = invalidations_.find(id); + if (it == invalidations_.end()) { + ADD_FAILURE() << "No invalidations for ID " << ObjectIdToString(id); + return 0; + } else { + return it->second.back().payload(); + } + } + + bool IsUnknownVersion(const ObjectId& id) const { + Map::const_iterator it = invalidations_.find(id); + if (it == invalidations_.end()) { + ADD_FAILURE() << "No invalidations for ID " << ObjectIdToString(id); + return false; + } else { + return it->second.back().is_unknown_version(); + } + } + + bool StartsWithUnknownVersion(const ObjectId& id) const { + Map::const_iterator it = invalidations_.find(id); + if (it == invalidations_.end()) { + ADD_FAILURE() << "No invalidations for ID " << ObjectIdToString(id); + return false; + } else { + return it->second.front().is_unknown_version(); + } + } + + InvalidatorState GetInvalidatorState() const { + return state_; + } + + DroppedInvalidationTracker* GetDropTrackerForObject(const ObjectId& id) { + DropHandlers::iterator it = drop_handlers_.find(id); + if (it == drop_handlers_.end()) { + drop_handlers_.insert( + std::make_pair(id, new DroppedInvalidationTracker(id))); + return drop_handlers_.find(id)->second; + } else { + return it->second; + } + } + + void AcknowledgeNthInvalidation(const ObjectId& id, size_t n) { + List& list = invalidations_[id]; + List::iterator it = list.begin() + n; + it->Acknowledge(); + } + + void AcknowledgeAll(const ObjectId& id) { + List& list = invalidations_[id]; + for (List::iterator it = list.begin(); it != list.end(); ++it) { + it->Acknowledge(); + } + } + + void DropNthInvalidation(const ObjectId& id, size_t n) { + DroppedInvalidationTracker* drop_tracker = GetDropTrackerForObject(id); + List& list = invalidations_[id]; + List::iterator it = list.begin() + n; + it->Drop(drop_tracker); + } + + void RecoverFromDropEvent(const ObjectId& id) { + DroppedInvalidationTracker* drop_tracker = GetDropTrackerForObject(id); + drop_tracker->RecordRecoveryFromDropEvent(); + } + + // SyncInvalidationListener::Delegate implementation. + virtual void OnInvalidate( + const ObjectIdInvalidationMap& invalidation_map) OVERRIDE { + ObjectIdSet ids = invalidation_map.GetObjectIds(); + for (ObjectIdSet::iterator it = ids.begin(); it != ids.end(); ++it) { + const SingleObjectInvalidationSet& incoming = + invalidation_map.ForObject(*it); + List& list = invalidations_[*it]; + list.insert(list.end(), incoming.begin(), incoming.end()); + } + } + + virtual void OnInvalidatorStateChange(InvalidatorState state) OVERRIDE { + state_ = state; + } + + private: + typedef std::vector<Invalidation> List; + typedef std::map<ObjectId, List, ObjectIdLessThan> Map; + typedef std::map<ObjectId, + DroppedInvalidationTracker*, + ObjectIdLessThan> DropHandlers; + + Map invalidations_; + InvalidatorState state_; + DropHandlers drop_handlers_; + STLValueDeleter<DropHandlers> drop_handlers_deleter_; +}; + +invalidation::InvalidationClient* CreateFakeInvalidationClient( + FakeInvalidationClient** fake_invalidation_client, + invalidation::SystemResources* resources, + int client_type, + const invalidation::string& client_name, + const invalidation::string& application_name, + invalidation::InvalidationListener* listener) { + *fake_invalidation_client = new FakeInvalidationClient(); + return *fake_invalidation_client; +} + +class SyncInvalidationListenerTest : public testing::Test { + protected: + SyncInvalidationListenerTest() + : kBookmarksId_(kChromeSyncSourceId, "BOOKMARK"), + kPreferencesId_(kChromeSyncSourceId, "PREFERENCE"), + kExtensionsId_(kChromeSyncSourceId, "EXTENSION"), + kAppsId_(kChromeSyncSourceId, "APP"), + fake_push_client_(new notifier::FakePushClient()), + fake_invalidation_client_(NULL), + listener_(scoped_ptr<SyncNetworkChannel>(new PushClientChannel( + scoped_ptr<notifier::PushClient>(fake_push_client_)))), + fake_delegate_(&listener_) {} + + virtual void SetUp() { + StartClient(); + + registered_ids_.insert(kBookmarksId_); + registered_ids_.insert(kPreferencesId_); + listener_.UpdateRegisteredIds(registered_ids_); + } + + virtual void TearDown() { + StopClient(); + } + + // Restart client without re-registering IDs. + void RestartClient() { + StopClient(); + StartClient(); + } + + void StartClient() { + fake_invalidation_client_ = NULL; + listener_.Start(base::Bind(&CreateFakeInvalidationClient, + &fake_invalidation_client_), + kClientId, kClientInfo, kState, + fake_tracker_.GetSavedInvalidations(), + MakeWeakHandle(fake_tracker_.AsWeakPtr()), + &fake_delegate_); + DCHECK(fake_invalidation_client_); + } + + void StopClient() { + // listener_.StopForTest() stops the invalidation scheduler, which + // deletes any pending tasks without running them. Some tasks + // "run and delete" another task, so they must be run in order to + // avoid leaking the inner task. listener_.StopForTest() does not + // schedule any tasks, so it's both necessary and sufficient to + // drain the task queue before calling it. + FlushPendingWrites(); + fake_invalidation_client_ = NULL; + listener_.StopForTest(); + } + + size_t GetInvalidationCount(const ObjectId& id) const { + return fake_delegate_.GetInvalidationCount(id); + } + + int64 GetVersion(const ObjectId& id) const { + return fake_delegate_.GetVersion(id); + } + + std::string GetPayload(const ObjectId& id) const { + return fake_delegate_.GetPayload(id); + } + + bool IsUnknownVersion(const ObjectId& id) const { + return fake_delegate_.IsUnknownVersion(id); + } + + bool StartsWithUnknownVersion(const ObjectId& id) const { + return fake_delegate_.StartsWithUnknownVersion(id); + } + + void AcknowledgeNthInvalidation(const ObjectId& id, size_t n) { + fake_delegate_.AcknowledgeNthInvalidation(id, n); + } + + void DropNthInvalidation(const ObjectId& id, size_t n) { + return fake_delegate_.DropNthInvalidation(id, n); + } + + void RecoverFromDropEvent(const ObjectId& id) { + return fake_delegate_.RecoverFromDropEvent(id); + } + + void AcknowledgeAll(const ObjectId& id) { + fake_delegate_.AcknowledgeAll(id); + } + + InvalidatorState GetInvalidatorState() const { + return fake_delegate_.GetInvalidatorState(); + } + + std::string GetInvalidatorClientId() const { + return fake_tracker_.GetInvalidatorClientId(); + } + + std::string GetBootstrapData() const { + return fake_tracker_.GetBootstrapData(); + } + + UnackedInvalidationsMap GetSavedInvalidations() { + // Allow any queued writes to go through first. + FlushPendingWrites(); + return fake_tracker_.GetSavedInvalidations(); + } + + SingleObjectInvalidationSet GetSavedInvalidationsForType(const ObjectId& id) { + const UnackedInvalidationsMap& saved_state = GetSavedInvalidations(); + UnackedInvalidationsMap::const_iterator it = + saved_state.find(kBookmarksId_); + if (it == saved_state.end()) { + ADD_FAILURE() << "No state saved for ID " << ObjectIdToString(id); + return SingleObjectInvalidationSet(); + } + ObjectIdInvalidationMap map; + it->second.ExportInvalidations(WeakHandle<AckHandler>(), &map); + if (map.Empty()) { + return SingleObjectInvalidationSet(); + } else { + return map.ForObject(id); + } + } + + ObjectIdSet GetRegisteredIds() const { + return fake_invalidation_client_->GetRegisteredIds(); + } + + // |payload| can be NULL. + void FireInvalidate(const ObjectId& object_id, + int64 version, const char* payload) { + invalidation::Invalidation inv; + if (payload) { + inv = invalidation::Invalidation(object_id, version, payload); + } else { + inv = invalidation::Invalidation(object_id, version); + } + const AckHandle ack_handle("fakedata"); + fake_invalidation_client_->ClearAckedHandles(); + listener_.Invalidate(fake_invalidation_client_, inv, ack_handle); + EXPECT_TRUE(fake_invalidation_client_->IsAckedHandle(ack_handle)); + } + + // |payload| can be NULL, but not |type_name|. + void FireInvalidateUnknownVersion(const ObjectId& object_id) { + const AckHandle ack_handle("fakedata_unknown"); + fake_invalidation_client_->ClearAckedHandles(); + listener_.InvalidateUnknownVersion(fake_invalidation_client_, + object_id, + ack_handle); + EXPECT_TRUE(fake_invalidation_client_->IsAckedHandle(ack_handle)); + } + + void FireInvalidateAll() { + const AckHandle ack_handle("fakedata_all"); + fake_invalidation_client_->ClearAckedHandles(); + listener_.InvalidateAll(fake_invalidation_client_, ack_handle); + EXPECT_TRUE(fake_invalidation_client_->IsAckedHandle(ack_handle)); + } + + void WriteState(const std::string& new_state) { + listener_.WriteState(new_state); + + // Pump message loop to trigger + // InvalidationStateTracker::WriteState(). + FlushPendingWrites(); + } + + void FlushPendingWrites() { + message_loop_.RunUntilIdle(); + } + + void EnableNotifications() { + fake_push_client_->EnableNotifications(); + } + + void DisableNotifications(notifier::NotificationsDisabledReason reason) { + fake_push_client_->DisableNotifications(reason); + } + + const ObjectId kBookmarksId_; + const ObjectId kPreferencesId_; + const ObjectId kExtensionsId_; + const ObjectId kAppsId_; + + ObjectIdSet registered_ids_; + + private: + base::MessageLoop message_loop_; + notifier::FakePushClient* const fake_push_client_; + + protected: + // A derrived test needs direct access to this. + FakeInvalidationStateTracker fake_tracker_; + + // Tests need to access these directly. + FakeInvalidationClient* fake_invalidation_client_; + SyncInvalidationListener listener_; + + private: + FakeDelegate fake_delegate_; +}; + +// Write a new state to the client. It should propagate to the +// tracker. +TEST_F(SyncInvalidationListenerTest, WriteState) { + WriteState(kNewState); + + EXPECT_EQ(kNewState, GetBootstrapData()); +} + +// Invalidation tests. + +// Fire an invalidation without a payload. It should be processed, +// the payload should remain empty, and the version should be updated. +TEST_F(SyncInvalidationListenerTest, InvalidateNoPayload) { + const ObjectId& id = kBookmarksId_; + + FireInvalidate(id, kVersion1, NULL); + + ASSERT_EQ(1U, GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + EXPECT_EQ(kVersion1, GetVersion(id)); + EXPECT_EQ("", GetPayload(id)); +} + +// Fire an invalidation with an empty payload. It should be +// processed, the payload should remain empty, and the version should +// be updated. +TEST_F(SyncInvalidationListenerTest, InvalidateEmptyPayload) { + const ObjectId& id = kBookmarksId_; + + FireInvalidate(id, kVersion1, ""); + + ASSERT_EQ(1U, GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + EXPECT_EQ(kVersion1, GetVersion(id)); + EXPECT_EQ("", GetPayload(id)); +} + +// Fire an invalidation with a payload. It should be processed, and +// both the payload and the version should be updated. +TEST_F(SyncInvalidationListenerTest, InvalidateWithPayload) { + const ObjectId& id = kPreferencesId_; + + FireInvalidate(id, kVersion1, kPayload1); + + ASSERT_EQ(1U, GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + EXPECT_EQ(kVersion1, GetVersion(id)); + EXPECT_EQ(kPayload1, GetPayload(id)); +} + +// Fire ten invalidations in a row. All should be received. +TEST_F(SyncInvalidationListenerTest, ManyInvalidations_NoDrop) { + const int kRepeatCount = 10; + const ObjectId& id = kPreferencesId_; + int64 initial_version = kVersion1; + for (int64 i = initial_version; i < initial_version + kRepeatCount; ++i) { + FireInvalidate(id, i, kPayload1); + } + ASSERT_EQ(static_cast<size_t>(kRepeatCount), GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + EXPECT_EQ(kPayload1, GetPayload(id)); + EXPECT_EQ(initial_version + kRepeatCount - 1, GetVersion(id)); +} + +// Fire an invalidation for an unregistered object ID with a payload. It should +// still be processed, and both the payload and the version should be updated. +TEST_F(SyncInvalidationListenerTest, InvalidateBeforeRegistration_Simple) { + const ObjectId kUnregisteredId(kChromeSyncSourceId, "unregistered"); + const ObjectId& id = kUnregisteredId; + ObjectIdSet ids; + ids.insert(id); + + EXPECT_EQ(0U, GetInvalidationCount(id)); + + FireInvalidate(id, kVersion1, kPayload1); + + ASSERT_EQ(0U, GetInvalidationCount(id)); + + EnableNotifications(); + listener_.Ready(fake_invalidation_client_); + listener_.UpdateRegisteredIds(ids); + + ASSERT_EQ(1U, GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + EXPECT_EQ(kVersion1, GetVersion(id)); + EXPECT_EQ(kPayload1, GetPayload(id)); +} + +// Fire ten invalidations before an object registers. Some invalidations will +// be dropped an replaced with an unknown version invalidation. +TEST_F(SyncInvalidationListenerTest, InvalidateBeforeRegistration_Drop) { + const int kRepeatCount = + UnackedInvalidationSet::kMaxBufferedInvalidations + 1; + const ObjectId kUnregisteredId(kChromeSyncSourceId, "unregistered"); + const ObjectId& id = kUnregisteredId; + ObjectIdSet ids; + ids.insert(id); + + EXPECT_EQ(0U, GetInvalidationCount(id)); + + int64 initial_version = kVersion1; + for (int64 i = initial_version; i < initial_version + kRepeatCount; ++i) { + FireInvalidate(id, i, kPayload1); + } + + EnableNotifications(); + listener_.Ready(fake_invalidation_client_); + listener_.UpdateRegisteredIds(ids); + + ASSERT_EQ(UnackedInvalidationSet::kMaxBufferedInvalidations, + GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + EXPECT_EQ(initial_version + kRepeatCount - 1, GetVersion(id)); + EXPECT_EQ(kPayload1, GetPayload(id)); + EXPECT_TRUE(StartsWithUnknownVersion(id)); +} + +// Fire an invalidation, then fire another one with a lower version. Both +// should be received. +TEST_F(SyncInvalidationListenerTest, InvalidateVersion) { + const ObjectId& id = kPreferencesId_; + + FireInvalidate(id, kVersion2, kPayload2); + + ASSERT_EQ(1U, GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + EXPECT_EQ(kVersion2, GetVersion(id)); + EXPECT_EQ(kPayload2, GetPayload(id)); + + FireInvalidate(id, kVersion1, kPayload1); + + ASSERT_EQ(2U, GetInvalidationCount(id)); + ASSERT_FALSE(IsUnknownVersion(id)); + + EXPECT_EQ(kVersion1, GetVersion(id)); + EXPECT_EQ(kPayload1, GetPayload(id)); +} + +// Fire an invalidation with an unknown version. +TEST_F(SyncInvalidationListenerTest, InvalidateUnknownVersion) { + const ObjectId& id = kBookmarksId_; + + FireInvalidateUnknownVersion(id); + + ASSERT_EQ(1U, GetInvalidationCount(id)); + EXPECT_TRUE(IsUnknownVersion(id)); +} + +// Fire an invalidation for all enabled IDs. +TEST_F(SyncInvalidationListenerTest, InvalidateAll) { + FireInvalidateAll(); + + for (ObjectIdSet::const_iterator it = registered_ids_.begin(); + it != registered_ids_.end(); ++it) { + ASSERT_EQ(1U, GetInvalidationCount(*it)); + EXPECT_TRUE(IsUnknownVersion(*it)); + } +} + +// Test a simple scenario for multiple IDs. +TEST_F(SyncInvalidationListenerTest, InvalidateMultipleIds) { + FireInvalidate(kBookmarksId_, 3, NULL); + + ASSERT_EQ(1U, GetInvalidationCount(kBookmarksId_)); + ASSERT_FALSE(IsUnknownVersion(kBookmarksId_)); + EXPECT_EQ(3, GetVersion(kBookmarksId_)); + EXPECT_EQ("", GetPayload(kBookmarksId_)); + + // kExtensionId is not registered, so the invalidation should not get through. + FireInvalidate(kExtensionsId_, 2, NULL); + ASSERT_EQ(0U, GetInvalidationCount(kExtensionsId_)); +} + +// Registration tests. + +// With IDs already registered, enable notifications then ready the +// client. The IDs should be registered only after the client is +// readied. +TEST_F(SyncInvalidationListenerTest, RegisterEnableReady) { + EXPECT_TRUE(GetRegisteredIds().empty()); + + EnableNotifications(); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); +} + +// With IDs already registered, ready the client then enable +// notifications. The IDs should be registered after the client is +// readied. +TEST_F(SyncInvalidationListenerTest, RegisterReadyEnable) { + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); + + EnableNotifications(); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); +} + +// Unregister the IDs, enable notifications, re-register the IDs, then +// ready the client. The IDs should be registered only after the +// client is readied. +TEST_F(SyncInvalidationListenerTest, EnableRegisterReady) { + listener_.UpdateRegisteredIds(ObjectIdSet()); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + EnableNotifications(); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.UpdateRegisteredIds(registered_ids_); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); +} + +// Unregister the IDs, enable notifications, ready the client, then +// re-register the IDs. The IDs should be registered only after the +// client is readied. +TEST_F(SyncInvalidationListenerTest, EnableReadyRegister) { + listener_.UpdateRegisteredIds(ObjectIdSet()); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + EnableNotifications(); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.UpdateRegisteredIds(registered_ids_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); +} + +// Unregister the IDs, ready the client, enable notifications, then +// re-register the IDs. The IDs should be registered only after the +// client is readied. +TEST_F(SyncInvalidationListenerTest, ReadyEnableRegister) { + listener_.UpdateRegisteredIds(ObjectIdSet()); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + EnableNotifications(); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.UpdateRegisteredIds(registered_ids_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); +} + +// Unregister the IDs, ready the client, re-register the IDs, then +// enable notifications. The IDs should be registered only after the +// client is readied. +// +// This test is important: see http://crbug.com/139424. +TEST_F(SyncInvalidationListenerTest, ReadyRegisterEnable) { + listener_.UpdateRegisteredIds(ObjectIdSet()); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.UpdateRegisteredIds(registered_ids_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); + + EnableNotifications(); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); +} + +// With IDs already registered, ready the client, restart the client, +// then re-ready it. The IDs should still be registered. +TEST_F(SyncInvalidationListenerTest, RegisterTypesPreserved) { + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); + + RestartClient(); + + EXPECT_TRUE(GetRegisteredIds().empty()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(registered_ids_, GetRegisteredIds()); +} + +// Make sure that state is correctly purged from the local invalidation state +// map cache when an ID is unregistered. +TEST_F(SyncInvalidationListenerTest, UnregisterCleansUpStateMapCache) { + const ObjectId& id = kBookmarksId_; + listener_.Ready(fake_invalidation_client_); + + EXPECT_TRUE(GetSavedInvalidations().empty()); + FireInvalidate(id, 1, "hello"); + EXPECT_EQ(1U, GetSavedInvalidations().size()); + EXPECT_TRUE(ContainsKey(GetSavedInvalidations(), id)); + FireInvalidate(kPreferencesId_, 2, "world"); + EXPECT_EQ(2U, GetSavedInvalidations().size()); + + EXPECT_TRUE(ContainsKey(GetSavedInvalidations(), id)); + EXPECT_TRUE(ContainsKey(GetSavedInvalidations(), kPreferencesId_)); + + ObjectIdSet ids; + ids.insert(id); + listener_.UpdateRegisteredIds(ids); + EXPECT_EQ(1U, GetSavedInvalidations().size()); + EXPECT_TRUE(ContainsKey(GetSavedInvalidations(), id)); +} + +TEST_F(SyncInvalidationListenerTest, DuplicateInvaldiations_Simple) { + const ObjectId& id = kBookmarksId_; + listener_.Ready(fake_invalidation_client_); + + // Send a stream of invalidations, including two copies of the second. + FireInvalidate(id, 1, "one"); + FireInvalidate(id, 2, "two"); + FireInvalidate(id, 3, "three"); + FireInvalidate(id, 2, "two"); + + // Expect that the duplicate was discarded. + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + EXPECT_EQ(3U, list.GetSize()); + SingleObjectInvalidationSet::const_iterator it = list.begin(); + EXPECT_EQ(1, it->version()); + it++; + EXPECT_EQ(2, it->version()); + it++; + EXPECT_EQ(3, it->version()); +} + +TEST_F(SyncInvalidationListenerTest, DuplicateInvalidations_NearBufferLimit) { + const size_t kPairsToSend = UnackedInvalidationSet::kMaxBufferedInvalidations; + const ObjectId& id = kBookmarksId_; + listener_.Ready(fake_invalidation_client_); + + // We will have enough buffer space in the state tracker for all these + // invalidations only if duplicates are ignored. + for (size_t i = 0; i < kPairsToSend; ++i) { + FireInvalidate(id, i, "payload"); + FireInvalidate(id, i, "payload"); + } + + // Expect that the state map ignored duplicates. + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + EXPECT_EQ(kPairsToSend, list.GetSize()); + EXPECT_FALSE(list.begin()->is_unknown_version()); + + // Expect that all invalidations (including duplicates) were emitted. + EXPECT_EQ(kPairsToSend*2, GetInvalidationCount(id)); + + // Acknowledge all invalidations to clear the internal state. + AcknowledgeAll(id); + EXPECT_TRUE(GetSavedInvalidationsForType(id).IsEmpty()); +} + +TEST_F(SyncInvalidationListenerTest, DuplicateInvalidations_UnknownVersion) { + const ObjectId& id = kBookmarksId_; + listener_.Ready(fake_invalidation_client_); + + FireInvalidateUnknownVersion(id); + FireInvalidateUnknownVersion(id); + + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + EXPECT_EQ(1U, list.GetSize()); + } + + // Acknowledge the second. There should be no effect on the stored list. + ASSERT_EQ(2U, GetInvalidationCount(id)); + AcknowledgeNthInvalidation(id, 1); + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + EXPECT_EQ(1U, list.GetSize()); + } + + // Acknowledge the first. This should remove the invalidation from the list. + ASSERT_EQ(2U, GetInvalidationCount(id)); + AcknowledgeNthInvalidation(id, 0); + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + EXPECT_EQ(0U, list.GetSize()); + } +} + +// Make sure that acknowledgements erase items from the local store. +TEST_F(SyncInvalidationListenerTest, AcknowledgementsCleanUpStateMapCache) { + const ObjectId& id = kBookmarksId_; + listener_.Ready(fake_invalidation_client_); + + EXPECT_TRUE(GetSavedInvalidations().empty()); + FireInvalidate(id, 10, "hello"); + FireInvalidate(id, 20, "world"); + FireInvalidateUnknownVersion(id); + + // Expect that all three invalidations have been saved to permanent storage. + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + ASSERT_EQ(3U, list.GetSize()); + EXPECT_TRUE(list.begin()->is_unknown_version()); + EXPECT_EQ(20, list.back().version()); + } + + // Acknowledge the second sent invaldiation (version 20) and verify it was + // removed from storage. + AcknowledgeNthInvalidation(id, 1); + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + ASSERT_EQ(2U, list.GetSize()); + EXPECT_TRUE(list.begin()->is_unknown_version()); + EXPECT_EQ(10, list.back().version()); + } + + // Acknowledge the last sent invalidation (unknown version) and verify it was + // removed from storage. + AcknowledgeNthInvalidation(id, 2); + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + ASSERT_EQ(1U, list.GetSize()); + EXPECT_FALSE(list.begin()->is_unknown_version()); + EXPECT_EQ(10, list.back().version()); + } +} + +// Make sure that drops erase items from the local store. +TEST_F(SyncInvalidationListenerTest, DropsCleanUpStateMapCache) { + const ObjectId& id = kBookmarksId_; + listener_.Ready(fake_invalidation_client_); + + EXPECT_TRUE(GetSavedInvalidations().empty()); + FireInvalidate(id, 10, "hello"); + FireInvalidate(id, 20, "world"); + FireInvalidateUnknownVersion(id); + + // Expect that all three invalidations have been saved to permanent storage. + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + ASSERT_EQ(3U, list.GetSize()); + EXPECT_TRUE(list.begin()->is_unknown_version()); + EXPECT_EQ(20, list.back().version()); + } + + // Drop the second sent invalidation (version 20) and verify it was removed + // from storage. Also verify we still have an unknown version invalidation. + DropNthInvalidation(id, 1); + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + ASSERT_EQ(2U, list.GetSize()); + EXPECT_TRUE(list.begin()->is_unknown_version()); + EXPECT_EQ(10, list.back().version()); + } + + // Drop the remaining invalidation. Verify an unknown version is all that + // remains. + DropNthInvalidation(id, 0); + { + SingleObjectInvalidationSet list = GetSavedInvalidationsForType(id); + ASSERT_EQ(1U, list.GetSize()); + EXPECT_TRUE(list.begin()->is_unknown_version()); + } + + // Announce that the delegate has recovered from the drop. Verify no + // invalidations remain saved. + RecoverFromDropEvent(id); + EXPECT_TRUE(GetSavedInvalidationsForType(id).IsEmpty()); + + RecoverFromDropEvent(id); +} + +// Without readying the client, disable notifications, then enable +// them. The listener should still think notifications are disabled. +TEST_F(SyncInvalidationListenerTest, EnableNotificationsNotReady) { + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, + GetInvalidatorState()); + + DisableNotifications( + notifier::TRANSIENT_NOTIFICATION_ERROR); + + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState()); + + DisableNotifications(notifier::NOTIFICATION_CREDENTIALS_REJECTED); + + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, GetInvalidatorState()); + + EnableNotifications(); + + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState()); +} + +// Enable notifications then Ready the invalidation client. The +// delegate should then be ready. +TEST_F(SyncInvalidationListenerTest, EnableNotificationsThenReady) { + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState()); + + EnableNotifications(); + + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState()); +} + +// Ready the invalidation client then enable notifications. The +// delegate should then be ready. +TEST_F(SyncInvalidationListenerTest, ReadyThenEnableNotifications) { + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(TRANSIENT_INVALIDATION_ERROR, GetInvalidatorState()); + + EnableNotifications(); + + EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState()); +} + +// Enable notifications and ready the client. Then disable +// notifications with an auth error and re-enable notifications. The +// delegate should go into an auth error mode and then back out. +TEST_F(SyncInvalidationListenerTest, PushClientAuthError) { + EnableNotifications(); + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState()); + + DisableNotifications( + notifier::NOTIFICATION_CREDENTIALS_REJECTED); + + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, GetInvalidatorState()); + + EnableNotifications(); + + EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState()); +} + +// Enable notifications and ready the client. Then simulate an auth +// error from the invalidation client. Simulate some notification +// events, then re-ready the client. The delegate should go into an +// auth error mode and come out of it only after the client is ready. +TEST_F(SyncInvalidationListenerTest, InvalidationClientAuthError) { + EnableNotifications(); + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState()); + + listener_.InformError( + fake_invalidation_client_, + invalidation::ErrorInfo( + invalidation::ErrorReason::AUTH_FAILURE, + false /* is_transient */, + "auth error", + invalidation::ErrorContext())); + + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, GetInvalidatorState()); + + DisableNotifications(notifier::TRANSIENT_NOTIFICATION_ERROR); + + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, GetInvalidatorState()); + + DisableNotifications(notifier::TRANSIENT_NOTIFICATION_ERROR); + + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, GetInvalidatorState()); + + EnableNotifications(); + + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, GetInvalidatorState()); + + listener_.Ready(fake_invalidation_client_); + + EXPECT_EQ(INVALIDATIONS_ENABLED, GetInvalidatorState()); +} + +// A variant of SyncInvalidationListenerTest that starts with some initial +// state. We make not attempt to abstract away the contents of this state. The +// tests that make use of this harness depend on its implementation details. +class SyncInvalidationListenerTest_WithInitialState + : public SyncInvalidationListenerTest { + public: + virtual void SetUp() { + UnackedInvalidationSet bm_state(kBookmarksId_); + UnackedInvalidationSet ext_state(kExtensionsId_); + + Invalidation bm_unknown = Invalidation::InitUnknownVersion(kBookmarksId_); + Invalidation bm_v100 = Invalidation::Init(kBookmarksId_, 100, "hundred"); + bm_state.Add(bm_unknown); + bm_state.Add(bm_v100); + + Invalidation ext_v10 = Invalidation::Init(kExtensionsId_, 10, "ten"); + Invalidation ext_v20 = Invalidation::Init(kExtensionsId_, 20, "twenty"); + ext_state.Add(ext_v10); + ext_state.Add(ext_v20); + + initial_state.insert(std::make_pair(kBookmarksId_, bm_state)); + initial_state.insert(std::make_pair(kExtensionsId_, ext_state)); + + fake_tracker_.SetSavedInvalidations(initial_state); + + SyncInvalidationListenerTest::SetUp(); + } + + UnackedInvalidationsMap initial_state; +}; + +// Verify that saved invalidations are forwarded when handlers register. +TEST_F(SyncInvalidationListenerTest_WithInitialState, + ReceiveSavedInvalidations) { + EnableNotifications(); + listener_.Ready(fake_invalidation_client_); + + EXPECT_THAT(initial_state, test_util::Eq(GetSavedInvalidations())); + + ASSERT_EQ(2U, GetInvalidationCount(kBookmarksId_)); + EXPECT_EQ(100, GetVersion(kBookmarksId_)); + + ASSERT_EQ(0U, GetInvalidationCount(kExtensionsId_)); + + FireInvalidate(kExtensionsId_, 30, "thirty"); + + ObjectIdSet ids = GetRegisteredIds(); + ids.insert(kExtensionsId_); + listener_.UpdateRegisteredIds(ids); + + ASSERT_EQ(3U, GetInvalidationCount(kExtensionsId_)); + EXPECT_EQ(30, GetVersion(kExtensionsId_)); +} + +} // namespace + +} // namespace syncer diff --git a/sync/notifier/sync_system_resources.cc b/sync/notifier/sync_system_resources.cc new file mode 100644 index 0000000..5df110d --- /dev/null +++ b/sync/notifier/sync_system_resources.cc @@ -0,0 +1,333 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/sync_system_resources.h" + +#include <cstdlib> +#include <cstring> +#include <string> + +#include "base/bind.h" +#include "base/logging.h" +#include "base/message_loop/message_loop.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "google/cacheinvalidation/deps/callback.h" +#include "google/cacheinvalidation/include/types.h" +#include "jingle/notifier/listener/push_client.h" +#include "sync/notifier/gcm_network_channel.h" +#include "sync/notifier/gcm_network_channel_delegate.h" +#include "sync/notifier/invalidation_util.h" +#include "sync/notifier/push_client_channel.h" + +namespace syncer { + +SyncLogger::SyncLogger() {} +SyncLogger::~SyncLogger() {} + +void SyncLogger::Log(LogLevel level, const char* file, int line, + const char* format, ...) { + logging::LogSeverity log_severity = -2; // VLOG(2) + bool emit_log = false; + switch (level) { + case FINE_LEVEL: + log_severity = -2; // VLOG(2) + emit_log = VLOG_IS_ON(2); + break; + case INFO_LEVEL: + log_severity = -1; // VLOG(1) + emit_log = VLOG_IS_ON(1); + break; + case WARNING_LEVEL: + log_severity = logging::LOG_WARNING; + emit_log = LOG_IS_ON(WARNING); + break; + case SEVERE_LEVEL: + log_severity = logging::LOG_ERROR; + emit_log = LOG_IS_ON(ERROR); + break; + } + if (emit_log) { + va_list ap; + va_start(ap, format); + std::string result; + base::StringAppendV(&result, format, ap); + logging::LogMessage(file, line, log_severity).stream() << result; + va_end(ap); + } +} + +void SyncLogger::SetSystemResources(invalidation::SystemResources* resources) { + // Do nothing. +} + +SyncInvalidationScheduler::SyncInvalidationScheduler() + : created_on_loop_(base::MessageLoop::current()), + is_started_(false), + is_stopped_(false), + weak_factory_(this) { + CHECK(created_on_loop_); +} + +SyncInvalidationScheduler::~SyncInvalidationScheduler() { + CHECK_EQ(created_on_loop_, base::MessageLoop::current()); + CHECK(is_stopped_); +} + +void SyncInvalidationScheduler::Start() { + CHECK_EQ(created_on_loop_, base::MessageLoop::current()); + CHECK(!is_started_); + is_started_ = true; + is_stopped_ = false; + weak_factory_.InvalidateWeakPtrs(); +} + +void SyncInvalidationScheduler::Stop() { + CHECK_EQ(created_on_loop_, base::MessageLoop::current()); + is_stopped_ = true; + is_started_ = false; + weak_factory_.InvalidateWeakPtrs(); + STLDeleteElements(&posted_tasks_); + posted_tasks_.clear(); +} + +void SyncInvalidationScheduler::Schedule(invalidation::TimeDelta delay, + invalidation::Closure* task) { + DCHECK(invalidation::IsCallbackRepeatable(task)); + CHECK_EQ(created_on_loop_, base::MessageLoop::current()); + + if (!is_started_) { + delete task; + return; + } + + posted_tasks_.insert(task); + base::MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&SyncInvalidationScheduler::RunPostedTask, + weak_factory_.GetWeakPtr(), task), + delay); +} + +bool SyncInvalidationScheduler::IsRunningOnThread() const { + return created_on_loop_ == base::MessageLoop::current(); +} + +invalidation::Time SyncInvalidationScheduler::GetCurrentTime() const { + CHECK_EQ(created_on_loop_, base::MessageLoop::current()); + return base::Time::Now(); +} + +void SyncInvalidationScheduler::SetSystemResources( + invalidation::SystemResources* resources) { + // Do nothing. +} + +void SyncInvalidationScheduler::RunPostedTask(invalidation::Closure* task) { + CHECK_EQ(created_on_loop_, base::MessageLoop::current()); + task->Run(); + posted_tasks_.erase(task); + delete task; +} + +SyncNetworkChannel::SyncNetworkChannel() + : invalidator_state_(DEFAULT_INVALIDATION_ERROR), + received_messages_count_(0) {} + +SyncNetworkChannel::~SyncNetworkChannel() { + STLDeleteElements(&network_status_receivers_); +} + +void SyncNetworkChannel::SetMessageReceiver( + invalidation::MessageCallback* incoming_receiver) { + incoming_receiver_.reset(incoming_receiver); +} + +void SyncNetworkChannel::AddNetworkStatusReceiver( + invalidation::NetworkStatusCallback* network_status_receiver) { + network_status_receiver->Run(invalidator_state_ == INVALIDATIONS_ENABLED); + network_status_receivers_.push_back(network_status_receiver); +} + +void SyncNetworkChannel::SetSystemResources( + invalidation::SystemResources* resources) { + // Do nothing. +} + +void SyncNetworkChannel::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void SyncNetworkChannel::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +scoped_ptr<SyncNetworkChannel> SyncNetworkChannel::CreatePushClientChannel( + const notifier::NotifierOptions& notifier_options) { + scoped_ptr<notifier::PushClient> push_client( + notifier::PushClient::CreateDefaultOnIOThread(notifier_options)); + return scoped_ptr<SyncNetworkChannel>( + new PushClientChannel(push_client.Pass())); +} + +scoped_ptr<SyncNetworkChannel> SyncNetworkChannel::CreateGCMNetworkChannel( + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + scoped_ptr<GCMNetworkChannelDelegate> delegate) { + return scoped_ptr<SyncNetworkChannel>(new GCMNetworkChannel( + request_context_getter, delegate.Pass())); +} + +void SyncNetworkChannel::NotifyStateChange(InvalidatorState invalidator_state) { + // Remember state for future NetworkStatusReceivers. + invalidator_state_ = invalidator_state; + // Notify NetworkStatusReceivers in cacheinvalidation. + for (NetworkStatusReceiverList::const_iterator it = + network_status_receivers_.begin(); + it != network_status_receivers_.end(); ++it) { + (*it)->Run(invalidator_state_ == INVALIDATIONS_ENABLED); + } + // Notify observers. + FOR_EACH_OBSERVER(Observer, observers_, + OnNetworkChannelStateChanged(invalidator_state_)); +} + +bool SyncNetworkChannel::DeliverIncomingMessage(const std::string& message) { + if (!incoming_receiver_) { + DLOG(ERROR) << "No receiver for incoming notification"; + return false; + } + received_messages_count_++; + incoming_receiver_->Run(message); + return true; +} + +int SyncNetworkChannel::GetReceivedMessagesCount() const { + return received_messages_count_; +} + +SyncStorage::SyncStorage(StateWriter* state_writer, + invalidation::Scheduler* scheduler) + : state_writer_(state_writer), + scheduler_(scheduler) { + DCHECK(state_writer_); + DCHECK(scheduler_); +} + +SyncStorage::~SyncStorage() {} + +void SyncStorage::WriteKey(const std::string& key, const std::string& value, + invalidation::WriteKeyCallback* done) { + CHECK(state_writer_); + // TODO(ghc): actually write key,value associations, and don't invoke the + // callback until the operation completes. + state_writer_->WriteState(value); + cached_state_ = value; + // According to the cache invalidation API folks, we can do this as + // long as we make sure to clear the persistent state that we start + // up the cache invalidation client with. However, we musn't do it + // right away, as we may be called under a lock that the callback + // uses. + scheduler_->Schedule( + invalidation::Scheduler::NoDelay(), + invalidation::NewPermanentCallback( + this, &SyncStorage::RunAndDeleteWriteKeyCallback, + done)); +} + +void SyncStorage::ReadKey(const std::string& key, + invalidation::ReadKeyCallback* done) { + DCHECK(scheduler_->IsRunningOnThread()) << "not running on scheduler thread"; + RunAndDeleteReadKeyCallback(done, cached_state_); +} + +void SyncStorage::DeleteKey(const std::string& key, + invalidation::DeleteKeyCallback* done) { + // TODO(ghc): Implement. + LOG(WARNING) << "ignoring call to DeleteKey(" << key << ", callback)"; +} + +void SyncStorage::ReadAllKeys(invalidation::ReadAllKeysCallback* done) { + // TODO(ghc): Implement. + LOG(WARNING) << "ignoring call to ReadAllKeys(callback)"; +} + +void SyncStorage::SetSystemResources( + invalidation::SystemResources* resources) { + // Do nothing. +} + +void SyncStorage::RunAndDeleteWriteKeyCallback( + invalidation::WriteKeyCallback* callback) { + callback->Run( + invalidation::Status(invalidation::Status::SUCCESS, std::string())); + delete callback; +} + +void SyncStorage::RunAndDeleteReadKeyCallback( + invalidation::ReadKeyCallback* callback, const std::string& value) { + callback->Run(std::make_pair( + invalidation::Status(invalidation::Status::SUCCESS, std::string()), + value)); + delete callback; +} + +SyncSystemResources::SyncSystemResources( + SyncNetworkChannel* sync_network_channel, + StateWriter* state_writer) + : is_started_(false), + logger_(new SyncLogger()), + internal_scheduler_(new SyncInvalidationScheduler()), + listener_scheduler_(new SyncInvalidationScheduler()), + storage_(new SyncStorage(state_writer, internal_scheduler_.get())), + sync_network_channel_(sync_network_channel) { +} + +SyncSystemResources::~SyncSystemResources() { + Stop(); +} + +void SyncSystemResources::Start() { + internal_scheduler_->Start(); + listener_scheduler_->Start(); + is_started_ = true; +} + +void SyncSystemResources::Stop() { + internal_scheduler_->Stop(); + listener_scheduler_->Stop(); +} + +bool SyncSystemResources::IsStarted() const { + return is_started_; +} + +void SyncSystemResources::set_platform(const std::string& platform) { + platform_ = platform; +} + +std::string SyncSystemResources::platform() const { + return platform_; +} + +SyncLogger* SyncSystemResources::logger() { + return logger_.get(); +} + +SyncStorage* SyncSystemResources::storage() { + return storage_.get(); +} + +SyncNetworkChannel* SyncSystemResources::network() { + return sync_network_channel_; +} + +SyncInvalidationScheduler* SyncSystemResources::internal_scheduler() { + return internal_scheduler_.get(); +} + +SyncInvalidationScheduler* SyncSystemResources::listener_scheduler() { + return listener_scheduler_.get(); +} + +} // namespace syncer diff --git a/sync/notifier/sync_system_resources.h b/sync/notifier/sync_system_resources.h new file mode 100644 index 0000000..ebddccf --- /dev/null +++ b/sync/notifier/sync_system_resources.h @@ -0,0 +1,241 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Simple system resources class that uses the current message loop +// for scheduling. Assumes the current message loop is already +// running. + +#ifndef SYNC_NOTIFIER_SYNC_SYSTEM_RESOURCES_H_ +#define SYNC_NOTIFIER_SYNC_SYSTEM_RESOURCES_H_ + +#include <set> +#include <string> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/threading/non_thread_safe.h" +#include "base/values.h" +#include "google/cacheinvalidation/include/system-resources.h" +#include "jingle/notifier/base/notifier_options.h" +#include "sync/base/sync_export.h" +#include "sync/notifier/invalidator_state.h" +#include "sync/notifier/state_writer.h" + +namespace syncer { + +class GCMNetworkChannelDelegate; + +class SyncLogger : public invalidation::Logger { + public: + SyncLogger(); + + virtual ~SyncLogger(); + + // invalidation::Logger implementation. + virtual void Log(LogLevel level, const char* file, int line, + const char* format, ...) OVERRIDE; + + virtual void SetSystemResources( + invalidation::SystemResources* resources) OVERRIDE; +}; + +class SyncInvalidationScheduler : public invalidation::Scheduler { + public: + SyncInvalidationScheduler(); + + virtual ~SyncInvalidationScheduler(); + + // Start and stop the scheduler. + void Start(); + void Stop(); + + // invalidation::Scheduler implementation. + virtual void Schedule(invalidation::TimeDelta delay, + invalidation::Closure* task) OVERRIDE; + + virtual bool IsRunningOnThread() const OVERRIDE; + + virtual invalidation::Time GetCurrentTime() const OVERRIDE; + + virtual void SetSystemResources( + invalidation::SystemResources* resources) OVERRIDE; + + private: + // Runs the task, deletes it, and removes it from |posted_tasks_|. + void RunPostedTask(invalidation::Closure* task); + + // Holds all posted tasks that have not yet been run. + std::set<invalidation::Closure*> posted_tasks_; + + const base::MessageLoop* created_on_loop_; + bool is_started_; + bool is_stopped_; + + base::WeakPtrFactory<SyncInvalidationScheduler> weak_factory_; +}; + +// SyncNetworkChannel implements common tasks needed to interact with +// invalidation library: +// - registering message and network status callbacks +// - notifying observers about network channel state change +// Implementation of particular network protocol should implement +// SendMessage and call NotifyStateChange and DeliverIncomingMessage. +class SYNC_EXPORT_PRIVATE SyncNetworkChannel + : public NON_EXPORTED_BASE(invalidation::NetworkChannel) { + public: + class Observer { + public: + // Called when network channel state changes. Possible states are: + // - INVALIDATIONS_ENABLED : connection is established and working + // - TRANSIENT_INVALIDATION_ERROR : no network, connection lost, etc. + // - INVALIDATION_CREDENTIALS_REJECTED : Issues with auth token + virtual void OnNetworkChannelStateChanged( + InvalidatorState invalidator_state) = 0; + }; + + SyncNetworkChannel(); + + virtual ~SyncNetworkChannel(); + + // invalidation::NetworkChannel implementation. + // SyncNetworkChannel doesn't implement SendMessage. It is responsibility of + // subclass to implement it. + virtual void SetMessageReceiver( + invalidation::MessageCallback* incoming_receiver) OVERRIDE; + virtual void AddNetworkStatusReceiver( + invalidation::NetworkStatusCallback* network_status_receiver) OVERRIDE; + virtual void SetSystemResources( + invalidation::SystemResources* resources) OVERRIDE; + + // Subclass should implement UpdateCredentials to pass new token to channel + // library. + virtual void UpdateCredentials(const std::string& email, + const std::string& token) = 0; + + // Return value from GetInvalidationClientType will be passed to + // invalidation::CreateInvalidationClient. Subclass should return one of the + // values from ipc::invalidation::ClientType enum from types.proto. + virtual int GetInvalidationClientType() = 0; + + // Subclass should implement RequestDetailedStatus to provide debugging + // information. + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) = 0; + + // Classes interested in network channel state changes should implement + // SyncNetworkChannel::Observer and register here. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Helper functions that know how to construct network channels from channel + // specific parameters. + static scoped_ptr<SyncNetworkChannel> CreatePushClientChannel( + const notifier::NotifierOptions& notifier_options); + static scoped_ptr<SyncNetworkChannel> CreateGCMNetworkChannel( + scoped_refptr<net::URLRequestContextGetter> request_context_getter, + scoped_ptr<GCMNetworkChannelDelegate> delegate); + + // Get the count of how many valid received messages were received. + int GetReceivedMessagesCount() const; + + protected: + // Subclass should notify about connection state through NotifyStateChange. + void NotifyStateChange(InvalidatorState invalidator_state); + // Subclass should call DeliverIncomingMessage for message to reach + // invalidations library. + bool DeliverIncomingMessage(const std::string& message); + + private: + typedef std::vector<invalidation::NetworkStatusCallback*> + NetworkStatusReceiverList; + + // Callbacks into invalidation library + scoped_ptr<invalidation::MessageCallback> incoming_receiver_; + NetworkStatusReceiverList network_status_receivers_; + + // Last channel state for new network status receivers. + InvalidatorState invalidator_state_; + + int received_messages_count_; + + ObserverList<Observer> observers_; +}; + +class SyncStorage : public invalidation::Storage { + public: + SyncStorage(StateWriter* state_writer, invalidation::Scheduler* scheduler); + + virtual ~SyncStorage(); + + void SetInitialState(const std::string& value) { + cached_state_ = value; + } + + // invalidation::Storage implementation. + virtual void WriteKey(const std::string& key, const std::string& value, + invalidation::WriteKeyCallback* done) OVERRIDE; + + virtual void ReadKey(const std::string& key, + invalidation::ReadKeyCallback* done) OVERRIDE; + + virtual void DeleteKey(const std::string& key, + invalidation::DeleteKeyCallback* done) OVERRIDE; + + virtual void ReadAllKeys( + invalidation::ReadAllKeysCallback* key_callback) OVERRIDE; + + virtual void SetSystemResources( + invalidation::SystemResources* resources) OVERRIDE; + + private: + // Runs the given storage callback with SUCCESS status and deletes it. + void RunAndDeleteWriteKeyCallback( + invalidation::WriteKeyCallback* callback); + + // Runs the given callback with the given value and deletes it. + void RunAndDeleteReadKeyCallback( + invalidation::ReadKeyCallback* callback, const std::string& value); + + StateWriter* state_writer_; + invalidation::Scheduler* scheduler_; + std::string cached_state_; +}; + +class SYNC_EXPORT_PRIVATE SyncSystemResources + : public NON_EXPORTED_BASE(invalidation::SystemResources) { + public: + SyncSystemResources(SyncNetworkChannel* sync_network_channel, + StateWriter* state_writer); + + virtual ~SyncSystemResources(); + + // invalidation::SystemResources implementation. + virtual void Start() OVERRIDE; + virtual void Stop() OVERRIDE; + virtual bool IsStarted() const OVERRIDE; + virtual void set_platform(const std::string& platform); + virtual std::string platform() const OVERRIDE; + virtual SyncLogger* logger() OVERRIDE; + virtual SyncStorage* storage() OVERRIDE; + virtual SyncNetworkChannel* network() OVERRIDE; + virtual SyncInvalidationScheduler* internal_scheduler() OVERRIDE; + virtual SyncInvalidationScheduler* listener_scheduler() OVERRIDE; + + private: + bool is_started_; + std::string platform_; + scoped_ptr<SyncLogger> logger_; + scoped_ptr<SyncInvalidationScheduler> internal_scheduler_; + scoped_ptr<SyncInvalidationScheduler> listener_scheduler_; + scoped_ptr<SyncStorage> storage_; + // sync_network_channel_ is owned by SyncInvalidationListener. + SyncNetworkChannel* sync_network_channel_; +}; + +} // namespace syncer + +#endif // SYNC_NOTIFIER_SYNC_SYSTEM_RESOURCES_H_ diff --git a/sync/notifier/sync_system_resources_unittest.cc b/sync/notifier/sync_system_resources_unittest.cc new file mode 100644 index 0000000..1cbf964 --- /dev/null +++ b/sync/notifier/sync_system_resources_unittest.cc @@ -0,0 +1,249 @@ +// Copyright 2014 The Chromium 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 "sync/notifier/sync_system_resources.h" + +#include <string> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/message_loop/message_loop.h" + +#include "google/cacheinvalidation/include/types.h" +#include "jingle/notifier/listener/fake_push_client.h" +#include "sync/notifier/push_client_channel.h" +#include "sync/notifier/state_writer.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace syncer { +namespace { + +using ::testing::_; +using ::testing::SaveArg; + +class MockStateWriter : public StateWriter { + public: + MOCK_METHOD1(WriteState, void(const std::string&)); +}; + +class MockClosure { + public: + MOCK_CONST_METHOD0(Run, void(void)); + base::Closure* CreateClosure() { + return new base::Closure( + base::Bind(&MockClosure::Run, base::Unretained(this))); + } +}; + +class MockStorageCallback { + public: + MOCK_CONST_METHOD1(Run, void(invalidation::Status)); + base::Callback<void(invalidation::Status)>* CreateCallback() { + return new base::Callback<void(invalidation::Status)>( + base::Bind(&MockStorageCallback::Run, base::Unretained(this))); + } +}; + +class SyncSystemResourcesTest : public testing::Test { + protected: + SyncSystemResourcesTest() + : push_client_channel_( + scoped_ptr<notifier::PushClient>(new notifier::FakePushClient())), + sync_system_resources_(&push_client_channel_, &mock_state_writer_) {} + + virtual ~SyncSystemResourcesTest() {} + + void ScheduleShouldNotRun() { + { + // Owned by ScheduleImmediately. + MockClosure mock_closure; + base::Closure* should_not_run = mock_closure.CreateClosure(); + EXPECT_CALL(mock_closure, Run()).Times(0); + sync_system_resources_.internal_scheduler()->Schedule( + invalidation::Scheduler::NoDelay(), should_not_run); + } + { + // Owned by ScheduleOnListenerThread. + MockClosure mock_closure; + base::Closure* should_not_run = mock_closure.CreateClosure(); + EXPECT_CALL(mock_closure, Run()).Times(0); + sync_system_resources_.listener_scheduler()->Schedule( + invalidation::Scheduler::NoDelay(), should_not_run); + } + { + // Owned by ScheduleWithDelay. + MockClosure mock_closure; + base::Closure* should_not_run = mock_closure.CreateClosure(); + EXPECT_CALL(mock_closure, Run()).Times(0); + sync_system_resources_.internal_scheduler()->Schedule( + invalidation::TimeDelta::FromSeconds(0), should_not_run); + } + } + + // Needed by |sync_system_resources_|. + base::MessageLoop message_loop_; + MockStateWriter mock_state_writer_; + PushClientChannel push_client_channel_; + SyncSystemResources sync_system_resources_; + + private: + DISALLOW_COPY_AND_ASSIGN(SyncSystemResourcesTest); +}; + +// Make sure current_time() doesn't crash or leak. +TEST_F(SyncSystemResourcesTest, CurrentTime) { + invalidation::Time current_time = + sync_system_resources_.internal_scheduler()->GetCurrentTime(); + DVLOG(1) << "current_time returned: " << current_time.ToInternalValue(); +} + +// Make sure Log() doesn't crash or leak. +TEST_F(SyncSystemResourcesTest, Log) { + sync_system_resources_.logger()->Log(SyncLogger::INFO_LEVEL, + __FILE__, __LINE__, "%s %d", + "test string", 5); +} + +TEST_F(SyncSystemResourcesTest, ScheduleBeforeStart) { + ScheduleShouldNotRun(); + sync_system_resources_.Start(); +} + +TEST_F(SyncSystemResourcesTest, ScheduleAfterStop) { + sync_system_resources_.Start(); + sync_system_resources_.Stop(); + ScheduleShouldNotRun(); +} + +TEST_F(SyncSystemResourcesTest, ScheduleAndStop) { + sync_system_resources_.Start(); + ScheduleShouldNotRun(); + sync_system_resources_.Stop(); +} + +TEST_F(SyncSystemResourcesTest, ScheduleAndDestroy) { + sync_system_resources_.Start(); + ScheduleShouldNotRun(); +} + +TEST_F(SyncSystemResourcesTest, ScheduleImmediately) { + sync_system_resources_.Start(); + MockClosure mock_closure; + EXPECT_CALL(mock_closure, Run()); + sync_system_resources_.internal_scheduler()->Schedule( + invalidation::Scheduler::NoDelay(), mock_closure.CreateClosure()); + message_loop_.RunUntilIdle(); +} + +TEST_F(SyncSystemResourcesTest, ScheduleOnListenerThread) { + sync_system_resources_.Start(); + MockClosure mock_closure; + EXPECT_CALL(mock_closure, Run()); + sync_system_resources_.listener_scheduler()->Schedule( + invalidation::Scheduler::NoDelay(), mock_closure.CreateClosure()); + EXPECT_TRUE( + sync_system_resources_.internal_scheduler()->IsRunningOnThread()); + message_loop_.RunUntilIdle(); +} + +TEST_F(SyncSystemResourcesTest, ScheduleWithZeroDelay) { + sync_system_resources_.Start(); + MockClosure mock_closure; + EXPECT_CALL(mock_closure, Run()); + sync_system_resources_.internal_scheduler()->Schedule( + invalidation::TimeDelta::FromSeconds(0), mock_closure.CreateClosure()); + message_loop_.RunUntilIdle(); +} + +// TODO(akalin): Figure out how to test with a non-zero delay. + +TEST_F(SyncSystemResourcesTest, WriteState) { + sync_system_resources_.Start(); + EXPECT_CALL(mock_state_writer_, WriteState(_)); + // Owned by WriteState. + MockStorageCallback mock_storage_callback; + invalidation::Status results(invalidation::Status::PERMANENT_FAILURE, + "fake-failure"); + EXPECT_CALL(mock_storage_callback, Run(_)) + .WillOnce(SaveArg<0>(&results)); + sync_system_resources_.storage()->WriteKey( + std::string(), "state", mock_storage_callback.CreateCallback()); + message_loop_.RunUntilIdle(); + EXPECT_EQ(invalidation::Status(invalidation::Status::SUCCESS, std::string()), + results); +} + +class TestSyncNetworkChannel : public SyncNetworkChannel { + public: + TestSyncNetworkChannel() {} + virtual ~TestSyncNetworkChannel() {} + + using SyncNetworkChannel::NotifyStateChange; + using SyncNetworkChannel::DeliverIncomingMessage; + + virtual void SendMessage(const std::string& message) OVERRIDE { + } + + virtual void UpdateCredentials(const std::string& email, + const std::string& token) OVERRIDE { + } + + virtual int GetInvalidationClientType() OVERRIDE { + return 0; + } + + virtual void RequestDetailedStatus( + base::Callback<void(const base::DictionaryValue&)> callback) OVERRIDE { + base::DictionaryValue value; + callback.Run(value); + } +}; + +class SyncNetworkChannelTest + : public testing::Test, + public SyncNetworkChannel::Observer { + protected: + SyncNetworkChannelTest() + : last_invalidator_state_(DEFAULT_INVALIDATION_ERROR), + connected_(false) { + network_channel_.AddObserver(this); + network_channel_.AddNetworkStatusReceiver( + invalidation::NewPermanentCallback( + this, &SyncNetworkChannelTest::OnNetworkStatusChange)); + } + + virtual ~SyncNetworkChannelTest() { + network_channel_.RemoveObserver(this); + } + + virtual void OnNetworkChannelStateChanged( + InvalidatorState invalidator_state) OVERRIDE { + last_invalidator_state_ = invalidator_state; + } + + void OnNetworkStatusChange(bool connected) { + connected_ = connected; + } + + TestSyncNetworkChannel network_channel_; + InvalidatorState last_invalidator_state_; + bool connected_; +}; + +// Simulate network channel state change. It should propagate to observer. +TEST_F(SyncNetworkChannelTest, OnNetworkChannelStateChanged) { + EXPECT_EQ(DEFAULT_INVALIDATION_ERROR, last_invalidator_state_); + EXPECT_FALSE(connected_); + network_channel_.NotifyStateChange(INVALIDATIONS_ENABLED); + EXPECT_EQ(INVALIDATIONS_ENABLED, last_invalidator_state_); + EXPECT_TRUE(connected_); + network_channel_.NotifyStateChange(INVALIDATION_CREDENTIALS_REJECTED); + EXPECT_EQ(INVALIDATION_CREDENTIALS_REJECTED, last_invalidator_state_); + EXPECT_FALSE(connected_); +} + +} // namespace +} // namespace syncer diff --git a/sync/notifier/unacked_invalidation_set.cc b/sync/notifier/unacked_invalidation_set.cc index d1d913c..6991fc0 100644 --- a/sync/notifier/unacked_invalidation_set.cc +++ b/sync/notifier/unacked_invalidation_set.cc @@ -7,6 +7,7 @@ #include "base/strings/string_number_conversions.h" #include "sync/internal_api/public/base/ack_handle.h" #include "sync/notifier/object_id_invalidation_map.h" +#include "sync/notifier/sync_invalidation_listener.h" namespace { diff --git a/sync/sync_internal_api.gypi b/sync/sync_internal_api.gypi index f0ae375..f248552 100644 --- a/sync/sync_internal_api.gypi +++ b/sync/sync_internal_api.gypi @@ -64,8 +64,6 @@ 'internal_api/public/base/enum_set.h', 'internal_api/public/base/invalidation.cc', 'internal_api/public/base/invalidation.h', - 'internal_api/public/base/invalidator_state.cc', - 'internal_api/public/base/invalidator_state.h', 'internal_api/public/base/model_type.h', 'internal_api/public/base/node_ordinal.cc', 'internal_api/public/base/node_ordinal.h', diff --git a/sync/sync_notifier.gypi b/sync/sync_notifier.gypi index 9c6dc38..f8e9d5c 100644 --- a/sync/sync_notifier.gypi +++ b/sync/sync_notifier.gypi @@ -28,13 +28,16 @@ 'notifier/dropped_invalidation_tracker.cc', 'notifier/dropped_invalidation_tracker.h', 'notifier/invalidation_handler.h', - 'notifier/invalidation_state_tracker.cc', 'notifier/invalidation_state_tracker.h', 'notifier/invalidation_util.cc', 'notifier/invalidation_util.h', 'notifier/unacked_invalidation_set.cc', 'notifier/unacked_invalidation_set.h', 'notifier/invalidator.h', + 'notifier/invalidator_registrar.cc', + 'notifier/invalidator_registrar.h', + 'notifier/invalidator_state.cc', + 'notifier/invalidator_state.h', 'notifier/mock_ack_handler.cc', 'notifier/mock_ack_handler.h', 'notifier/object_id_invalidation_map.cc', @@ -45,8 +48,24 @@ 'conditions': [ ['OS != "android"', { 'sources': [ + 'notifier/gcm_network_channel.cc', + 'notifier/gcm_network_channel.h', + 'notifier/gcm_network_channel_delegate.h', + 'notifier/invalidation_notifier.cc', + 'notifier/invalidation_notifier.h', + 'notifier/non_blocking_invalidator.cc', + 'notifier/non_blocking_invalidator.h', + 'notifier/p2p_invalidator.cc', + 'notifier/p2p_invalidator.h', + 'notifier/push_client_channel.cc', + 'notifier/push_client_channel.h', 'notifier/registration_manager.cc', 'notifier/registration_manager.h', + 'notifier/state_writer.h', + 'notifier/sync_invalidation_listener.cc', + 'notifier/sync_invalidation_listener.h', + 'notifier/sync_system_resources.cc', + 'notifier/sync_system_resources.h', ], }], ], diff --git a/sync/sync_tests.gypi b/sync/sync_tests.gypi index e92d6c4..cd4f933 100644 --- a/sync/sync_tests.gypi +++ b/sync/sync_tests.gypi @@ -159,6 +159,14 @@ 'sync', ], 'sources': [ + 'notifier/fake_invalidation_handler.cc', + 'notifier/fake_invalidation_handler.h', + 'notifier/fake_invalidation_state_tracker.cc', + 'notifier/fake_invalidation_state_tracker.h', + 'notifier/fake_invalidator.cc', + 'notifier/fake_invalidator.h', + 'notifier/invalidator_test_template.cc', + 'notifier/invalidator_test_template.h', 'notifier/unacked_invalidation_set_test_util.cc', 'notifier/unacked_invalidation_set_test_util.h', 'internal_api/public/base/object_id_invalidation_map_test_util.h', @@ -366,9 +374,18 @@ 'conditions': [ ['OS != "android"', { 'sources': [ + 'notifier/fake_invalidator_unittest.cc', + 'notifier/gcm_network_channel_unittest.cc', + 'notifier/invalidation_notifier_unittest.cc', + 'notifier/invalidator_registrar_unittest.cc', + 'notifier/non_blocking_invalidator_unittest.cc', 'notifier/object_id_invalidation_map_unittest.cc', + 'notifier/p2p_invalidator_unittest.cc', + 'notifier/push_client_channel_unittest.cc', 'notifier/registration_manager_unittest.cc', 'notifier/single_object_invalidation_set_unittest.cc', + 'notifier/sync_invalidation_listener_unittest.cc', + 'notifier/sync_system_resources_unittest.cc', 'notifier/unacked_invalidation_set_unittest.cc', ], }], diff --git a/sync/tools/DEPS b/sync/tools/DEPS index ac34026..322ec26 100644 --- a/sync/tools/DEPS +++ b/sync/tools/DEPS @@ -1,5 +1,4 @@ include_rules = [ - "+components/invalidation", "+jingle/notifier/base", "+net", "+sync/internal_api/public", diff --git a/sync/tools/sync_client.cc b/sync/tools/sync_client.cc index a5890cf..8edd6fa 100644 --- a/sync/tools/sync_client.cc +++ b/sync/tools/sync_client.cc @@ -20,7 +20,6 @@ #include "base/rand_util.h" #include "base/task_runner.h" #include "base/threading/thread.h" -#include "components/invalidation/non_blocking_invalidator.h" #include "jingle/notifier/base/notification_method.h" #include "jingle/notifier/base/notifier_options.h" #include "net/base/host_port_pair.h" @@ -42,6 +41,7 @@ #include "sync/internal_api/public/util/weak_handle.h" #include "sync/js/js_event_details.h" #include "sync/js/js_event_handler.h" +#include "sync/notifier/non_blocking_invalidator.h" #include "sync/test/fake_encryptor.h" #include "sync/tools/null_invalidation_state_tracker.h" diff --git a/sync/tools/sync_listen_notifications.cc b/sync/tools/sync_listen_notifications.cc index e924e53..6a9a505 100644 --- a/sync/tools/sync_listen_notifications.cc +++ b/sync/tools/sync_listen_notifications.cc @@ -15,7 +15,6 @@ #include "base/message_loop/message_loop.h" #include "base/rand_util.h" #include "base/threading/thread.h" -#include "components/invalidation/non_blocking_invalidator.h" #include "jingle/notifier/base/notification_method.h" #include "jingle/notifier/base/notifier_options.h" #include "net/base/host_port_pair.h" @@ -28,6 +27,7 @@ #include "sync/notifier/invalidation_state_tracker.h" #include "sync/notifier/invalidation_util.h" #include "sync/notifier/invalidator.h" +#include "sync/notifier/non_blocking_invalidator.h" #include "sync/notifier/object_id_invalidation_map.h" #include "sync/tools/null_invalidation_state_tracker.h" diff --git a/sync/tools/sync_tools.gyp b/sync/tools/sync_tools.gyp index 20ca8ec..7cc8352 100644 --- a/sync/tools/sync_tools.gyp +++ b/sync/tools/sync_tools.gyp @@ -39,7 +39,6 @@ ], 'dependencies': [ '../../base/base.gyp:base', - '../../components/components.gyp:invalidation', '../../jingle/jingle.gyp:notifier', '../../net/net.gyp:net', '../../net/net.gyp:net_test_support', @@ -60,7 +59,6 @@ ], 'dependencies': [ '../../base/base.gyp:base', - '../../components/components.gyp:invalidation', '../../jingle/jingle.gyp:notifier', '../../net/net.gyp:net', '../../net/net.gyp:net_test_support', |