diff --git a/api/ice_transport_interface.h b/api/ice_transport_interface.h index cd6ada8e01..2ec41aaa69 100644 --- a/api/ice_transport_interface.h +++ b/api/ice_transport_interface.h @@ -99,8 +99,10 @@ struct IceTransportInit final { // constructed and used. // // 2. If the field trial is enabled - // - then an active ICE controller factory must be supplied and is used. - // - the legacy ICE controller factory is not used in this case. + // a. If an active ICE controller factory is supplied, it is used and + // the legacy ICE controller factory is not used. + // b. If not, a default active ICE controller is used, wrapping over the + // supplied or the default legacy ICE controller. void set_active_ice_controller_factory( cricket::ActiveIceControllerFactoryInterface* active_ice_controller_factory) { diff --git a/p2p/BUILD.gn b/p2p/BUILD.gn index 10093048b4..85ac605b1f 100644 --- a/p2p/BUILD.gn +++ b/p2p/BUILD.gn @@ -81,6 +81,8 @@ rtc_library("rtc_p2p") { "base/turn_port.cc", "base/turn_port.h", "base/udp_port.h", + "base/wrapping_active_ice_controller.cc", + "base/wrapping_active_ice_controller.h", "client/basic_port_allocator.cc", "client/basic_port_allocator.h", "client/relay_port_factory_interface.h", @@ -201,6 +203,7 @@ if (rtc_include_tests) { "base/fake_packet_transport.h", "base/mock_active_ice_controller.h", "base/mock_async_resolver.h", + "base/mock_ice_agent.h", "base/mock_ice_controller.h", "base/mock_ice_transport.h", "base/test_stun_server.cc", @@ -260,6 +263,7 @@ if (rtc_include_tests) { "base/transport_description_unittest.cc", "base/turn_port_unittest.cc", "base/turn_server_unittest.cc", + "base/wrapping_active_ice_controller_unittest.cc", "client/basic_port_allocator_unittest.cc", ] deps = [ diff --git a/p2p/base/mock_ice_agent.h b/p2p/base/mock_ice_agent.h new file mode 100644 index 0000000000..e4100ecd7a --- /dev/null +++ b/p2p/base/mock_ice_agent.h @@ -0,0 +1,50 @@ +/* + * Copyright 2018 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef P2P_BASE_MOCK_ICE_AGENT_H_ +#define P2P_BASE_MOCK_ICE_AGENT_H_ + +#include + +#include "p2p/base/connection.h" +#include "p2p/base/ice_agent_interface.h" +#include "p2p/base/ice_switch_reason.h" +#include "p2p/base/transport_description.h" +#include "test/gmock.h" + +namespace cricket { + +class MockIceAgent : public IceAgentInterface { + public: + ~MockIceAgent() override = default; + + MOCK_METHOD(int64_t, GetLastPingSentMs, (), (override, const)); + MOCK_METHOD(IceRole, GetIceRole, (), (override, const)); + MOCK_METHOD(void, OnStartedPinging, (), (override)); + MOCK_METHOD(void, UpdateConnectionStates, (), (override)); + MOCK_METHOD(void, UpdateState, (), (override)); + MOCK_METHOD(void, + ForgetLearnedStateForConnections, + (std::vector), + (override)); + MOCK_METHOD(void, SendPingRequest, (const Connection*), (override)); + MOCK_METHOD(void, + SwitchSelectedConnection, + (const Connection*, IceSwitchReason), + (override)); + MOCK_METHOD(bool, + PruneConnections, + (std::vector), + (override)); +}; + +} // namespace cricket + +#endif // P2P_BASE_MOCK_ICE_AGENT_H_ diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc index 2b0e906f09..0a0d8c8b1d 100644 --- a/p2p/base/p2p_transport_channel.cc +++ b/p2p/base/p2p_transport_channel.cc @@ -33,6 +33,7 @@ #include "p2p/base/connection.h" #include "p2p/base/connection_info.h" #include "p2p/base/port.h" +#include "p2p/base/wrapping_active_ice_controller.h" #include "rtc_base/checks.h" #include "rtc_base/crc32.h" #include "rtc_base/experiments/struct_parameters_parser.h" @@ -2472,10 +2473,15 @@ P2PTransportChannel::IceControllerAdapter::IceControllerAdapter( P2PTransportChannel* transport) : transport_(transport) { if (UseActiveIceControllerFieldTrialEnabled(field_trials)) { - RTC_DCHECK(active_ice_controller_factory); - ActiveIceControllerFactoryArgs active_args{args, - /* ice_agent= */ transport}; - active_ice_controller_ = active_ice_controller_factory->Create(active_args); + if (active_ice_controller_factory) { + ActiveIceControllerFactoryArgs active_args{args, + /* ice_agent= */ transport}; + active_ice_controller_ = + active_ice_controller_factory->Create(active_args); + } else { + active_ice_controller_ = std::make_unique( + /* ice_agent= */ transport, ice_controller_factory, args); + } } else { if (ice_controller_factory != nullptr) { legacy_ice_controller_ = ice_controller_factory->Create(args); diff --git a/p2p/base/wrapping_active_ice_controller.cc b/p2p/base/wrapping_active_ice_controller.cc new file mode 100644 index 0000000000..c6659217fc --- /dev/null +++ b/p2p/base/wrapping_active_ice_controller.cc @@ -0,0 +1,253 @@ +/* + * Copyright 2022 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "p2p/base/wrapping_active_ice_controller.h" + +#include +#include +#include + +#include "api/sequence_checker.h" +#include "api/task_queue/pending_task_safety_flag.h" +#include "api/units/time_delta.h" +#include "p2p/base/basic_ice_controller.h" +#include "p2p/base/connection.h" +#include "p2p/base/ice_agent_interface.h" +#include "p2p/base/ice_controller_interface.h" +#include "p2p/base/ice_switch_reason.h" +#include "p2p/base/ice_transport_internal.h" +#include "p2p/base/transport_description.h" +#include "rtc_base/logging.h" +#include "rtc_base/thread.h" +#include "rtc_base/time_utils.h" + +namespace { +using ::webrtc::SafeTask; +using ::webrtc::TimeDelta; +} // unnamed namespace + +namespace cricket { + +WrappingActiveIceController::WrappingActiveIceController( + IceAgentInterface* ice_agent, + std::unique_ptr wrapped) + : network_thread_(rtc::Thread::Current()), + wrapped_(std::move(wrapped)), + agent_(*ice_agent) { + RTC_DCHECK(ice_agent != nullptr); +} + +WrappingActiveIceController::WrappingActiveIceController( + IceAgentInterface* ice_agent, + IceControllerFactoryInterface* wrapped_factory, + const IceControllerFactoryArgs& wrapped_factory_args) + : network_thread_(rtc::Thread::Current()), agent_(*ice_agent) { + RTC_DCHECK(ice_agent != nullptr); + if (wrapped_factory) { + wrapped_ = wrapped_factory->Create(wrapped_factory_args); + } else { + wrapped_ = std::make_unique(wrapped_factory_args); + } +} + +WrappingActiveIceController::~WrappingActiveIceController() {} + +void WrappingActiveIceController::SetIceConfig(const IceConfig& config) { + RTC_DCHECK_RUN_ON(network_thread_); + wrapped_->SetIceConfig(config); +} + +bool WrappingActiveIceController::GetUseCandidateAttribute( + const Connection* connection, + NominationMode mode, + IceMode remote_ice_mode) const { + RTC_DCHECK_RUN_ON(network_thread_); + return wrapped_->GetUseCandidateAttr(connection, mode, remote_ice_mode); +} + +void WrappingActiveIceController::OnConnectionAdded( + const Connection* connection) { + RTC_DCHECK_RUN_ON(network_thread_); + wrapped_->AddConnection(connection); +} + +void WrappingActiveIceController::OnConnectionPinged( + const Connection* connection) { + RTC_DCHECK_RUN_ON(network_thread_); + wrapped_->MarkConnectionPinged(connection); +} + +void WrappingActiveIceController::OnConnectionUpdated( + const Connection* connection) { + RTC_LOG(LS_VERBOSE) << "Connection report for " << connection->ToString(); + // Do nothing. Native ICE controllers have direct access to Connection, so no + // need to update connection state separately. +} + +void WrappingActiveIceController::OnConnectionSwitched( + const Connection* connection) { + RTC_DCHECK_RUN_ON(network_thread_); + selected_connection_ = connection; + wrapped_->SetSelectedConnection(connection); +} + +void WrappingActiveIceController::OnConnectionDestroyed( + const Connection* connection) { + RTC_DCHECK_RUN_ON(network_thread_); + wrapped_->OnConnectionDestroyed(connection); +} + +void WrappingActiveIceController::MaybeStartPinging() { + RTC_DCHECK_RUN_ON(network_thread_); + if (started_pinging_) { + return; + } + + if (wrapped_->HasPingableConnection()) { + network_thread_->PostTask( + SafeTask(task_safety_.flag(), [this]() { SelectAndPingConnection(); })); + agent_.OnStartedPinging(); + started_pinging_ = true; + } +} + +void WrappingActiveIceController::SelectAndPingConnection() { + RTC_DCHECK_RUN_ON(network_thread_); + agent_.UpdateConnectionStates(); + + IceControllerInterface::PingResult result = + wrapped_->SelectConnectionToPing(agent_.GetLastPingSentMs()); + HandlePingResult(result); +} + +void WrappingActiveIceController::HandlePingResult( + IceControllerInterface::PingResult result) { + RTC_DCHECK_RUN_ON(network_thread_); + + if (result.connection.has_value()) { + agent_.SendPingRequest(result.connection.value()); + } + + network_thread_->PostDelayedTask( + SafeTask(task_safety_.flag(), [this]() { SelectAndPingConnection(); }), + TimeDelta::Millis(result.recheck_delay_ms)); +} + +void WrappingActiveIceController::OnSortAndSwitchRequest( + IceSwitchReason reason) { + RTC_DCHECK_RUN_ON(network_thread_); + if (!sort_pending_) { + network_thread_->PostTask(SafeTask(task_safety_.flag(), [this, reason]() { + SortAndSwitchToBestConnection(reason); + })); + sort_pending_ = true; + } +} + +void WrappingActiveIceController::OnImmediateSortAndSwitchRequest( + IceSwitchReason reason) { + RTC_DCHECK_RUN_ON(network_thread_); + SortAndSwitchToBestConnection(reason); +} + +void WrappingActiveIceController::SortAndSwitchToBestConnection( + IceSwitchReason reason) { + RTC_DCHECK_RUN_ON(network_thread_); + + // Make sure the connection states are up-to-date since this affects how they + // will be sorted. + agent_.UpdateConnectionStates(); + + // Any changes after this point will require a re-sort. + sort_pending_ = false; + + IceControllerInterface::SwitchResult result = + wrapped_->SortAndSwitchConnection(reason); + HandleSwitchResult(reason, result); + UpdateStateOnConnectionsResorted(); +} + +bool WrappingActiveIceController::OnImmediateSwitchRequest( + IceSwitchReason reason, + const Connection* selected) { + RTC_DCHECK_RUN_ON(network_thread_); + IceControllerInterface::SwitchResult result = + wrapped_->ShouldSwitchConnection(reason, selected); + HandleSwitchResult(reason, result); + return result.connection.has_value(); +} + +void WrappingActiveIceController::HandleSwitchResult( + IceSwitchReason reason_for_switch, + IceControllerInterface::SwitchResult result) { + RTC_DCHECK_RUN_ON(network_thread_); + if (result.connection.has_value()) { + RTC_LOG(LS_INFO) << "Switching selected connection due to: " + << IceSwitchReasonToString(reason_for_switch); + agent_.SwitchSelectedConnection(result.connection.value(), + reason_for_switch); + } + + if (result.recheck_event.has_value()) { + // If we do not switch to the connection because it missed the receiving + // threshold, the new connection is in a better receiving state than the + // currently selected connection. So we need to re-check whether it needs + // to be switched at a later time. + network_thread_->PostDelayedTask( + SafeTask(task_safety_.flag(), + [this, recheck_reason = result.recheck_event->reason]() { + SortAndSwitchToBestConnection(recheck_reason); + }), + TimeDelta::Millis(result.recheck_event->recheck_delay_ms)); + } + + agent_.ForgetLearnedStateForConnections( + result.connections_to_forget_state_on); +} + +void WrappingActiveIceController::UpdateStateOnConnectionsResorted() { + RTC_DCHECK_RUN_ON(network_thread_); + PruneConnections(); + + // Update the internal state of the ICE agentl. + agent_.UpdateState(); + + // Also possibly start pinging. + // We could start pinging if: + // * The first connection was created. + // * ICE credentials were provided. + // * A TCP connection became connected. + MaybeStartPinging(); +} + +void WrappingActiveIceController::PruneConnections() { + RTC_DCHECK_RUN_ON(network_thread_); + + // The controlled side can prune only if the selected connection has been + // nominated because otherwise it may prune the connection that will be + // selected by the controlling side. + // TODO(honghaiz): This is not enough to prevent a connection from being + // pruned too early because with aggressive nomination, the controlling side + // will nominate every connection until it becomes writable. + if (agent_.GetIceRole() == ICEROLE_CONTROLLING || + (selected_connection_ && selected_connection_->nominated())) { + std::vector connections_to_prune = + wrapped_->PruneConnections(); + agent_.PruneConnections(connections_to_prune); + } +} + +// Only for unit tests +const Connection* WrappingActiveIceController::FindNextPingableConnection() { + RTC_DCHECK_RUN_ON(network_thread_); + return wrapped_->FindNextPingableConnection(); +} + +} // namespace cricket diff --git a/p2p/base/wrapping_active_ice_controller.h b/p2p/base/wrapping_active_ice_controller.h new file mode 100644 index 0000000000..449c0f0ee1 --- /dev/null +++ b/p2p/base/wrapping_active_ice_controller.h @@ -0,0 +1,97 @@ +/* + * Copyright 2022 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_ +#define P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_ + +#include + +#include "absl/types/optional.h" +#include "api/task_queue/pending_task_safety_flag.h" +#include "p2p/base/active_ice_controller_interface.h" +#include "p2p/base/connection.h" +#include "p2p/base/ice_agent_interface.h" +#include "p2p/base/ice_controller_factory_interface.h" +#include "p2p/base/ice_controller_interface.h" +#include "p2p/base/ice_switch_reason.h" +#include "p2p/base/ice_transport_internal.h" +#include "p2p/base/transport_description.h" +#include "rtc_base/thread.h" +#include "rtc_base/thread_annotations.h" + +namespace cricket { + +// WrappingActiveIceController provides the functionality of a legacy passive +// ICE controller but packaged as an active ICE Controller. +class WrappingActiveIceController : public ActiveIceControllerInterface { + public: + // Constructs an active ICE controller wrapping an already constructed legacy + // ICE controller. Does not take ownership of the ICE agent, which must + // already exist and outlive the ICE controller. + WrappingActiveIceController(IceAgentInterface* ice_agent, + std::unique_ptr wrapped); + // Constructs an active ICE controller that wraps over a legacy ICE + // controller. The legacy ICE controller is constructed through a factory, if + // one is supplied. If not, a default BasicIceController is wrapped instead. + // Does not take ownership of the ICE agent, which must already exist and + // outlive the ICE controller. + WrappingActiveIceController( + IceAgentInterface* ice_agent, + IceControllerFactoryInterface* wrapped_factory, + const IceControllerFactoryArgs& wrapped_factory_args); + virtual ~WrappingActiveIceController(); + + void SetIceConfig(const IceConfig& config) override; + bool GetUseCandidateAttribute(const Connection* connection, + NominationMode mode, + IceMode remote_ice_mode) const override; + + void OnConnectionAdded(const Connection* connection) override; + void OnConnectionPinged(const Connection* connection) override; + void OnConnectionUpdated(const Connection* connection) override; + void OnConnectionSwitched(const Connection* connection) override; + void OnConnectionDestroyed(const Connection* connection) override; + + void OnSortAndSwitchRequest(IceSwitchReason reason) override; + void OnImmediateSortAndSwitchRequest(IceSwitchReason reason) override; + bool OnImmediateSwitchRequest(IceSwitchReason reason, + const Connection* selected) override; + + // Only for unit tests + const Connection* FindNextPingableConnection() override; + + private: + void MaybeStartPinging(); + void SelectAndPingConnection(); + void HandlePingResult(IceControllerInterface::PingResult result); + + void SortAndSwitchToBestConnection(IceSwitchReason reason); + void HandleSwitchResult(IceSwitchReason reason_for_switch, + IceControllerInterface::SwitchResult result); + void UpdateStateOnConnectionsResorted(); + + void PruneConnections(); + + rtc::Thread* const network_thread_; + webrtc::ScopedTaskSafety task_safety_; + + bool started_pinging_ RTC_GUARDED_BY(network_thread_) = false; + bool sort_pending_ RTC_GUARDED_BY(network_thread_) = false; + const Connection* selected_connection_ RTC_GUARDED_BY(network_thread_) = + nullptr; + + std::unique_ptr wrapped_ + RTC_GUARDED_BY(network_thread_); + IceAgentInterface& agent_ RTC_GUARDED_BY(network_thread_); +}; + +} // namespace cricket + +#endif // P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_ diff --git a/p2p/base/wrapping_active_ice_controller_unittest.cc b/p2p/base/wrapping_active_ice_controller_unittest.cc new file mode 100644 index 0000000000..19f9b95a57 --- /dev/null +++ b/p2p/base/wrapping_active_ice_controller_unittest.cc @@ -0,0 +1,311 @@ +/* + * Copyright 2009 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "p2p/base/wrapping_active_ice_controller.h" + +#include +#include +#include + +#include "p2p/base/connection.h" +#include "p2p/base/mock_ice_agent.h" +#include "p2p/base/mock_ice_controller.h" +#include "rtc_base/fake_clock.h" +#include "rtc_base/gunit.h" +#include "rtc_base/thread.h" + +namespace { + +using ::cricket::Connection; +using ::cricket::IceConfig; +using ::cricket::IceControllerFactoryArgs; +using ::cricket::IceControllerInterface; +using ::cricket::IceMode; +using ::cricket::IceRecheckEvent; +using ::cricket::IceSwitchReason; +using ::cricket::MockIceAgent; +using ::cricket::MockIceController; +using ::cricket::MockIceControllerFactory; +using ::cricket::NominationMode; +using ::cricket::WrappingActiveIceController; + +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Ref; +using ::testing::Return; +using ::testing::Sequence; + +using ::rtc::AutoThread; +using ::rtc::Event; +using ::rtc::ScopedFakeClock; +using ::webrtc::TimeDelta; + +using NiceMockIceController = NiceMock; + +static const Connection* kConnection = + reinterpret_cast(0xabcd); +static const Connection* kConnectionTwo = + reinterpret_cast(0xbcde); +static const Connection* kConnectionThree = + reinterpret_cast(0xcdef); + +static const std::vector kEmptyConnsList = + std::vector(); + +static const TimeDelta kTick = TimeDelta::Millis(1); + +TEST(WrappingActiveIceControllerTest, CreateLegacyIceControllerFromFactory) { + MockIceAgent agent; + IceControllerFactoryArgs args; + MockIceControllerFactory legacy_controller_factory; + EXPECT_CALL(legacy_controller_factory, RecordIceControllerCreated()).Times(1); + WrappingActiveIceController controller(&agent, &legacy_controller_factory, + args); +} + +TEST(WrappingActiveIceControllerTest, PassthroughIceControllerInterface) { + MockIceAgent agent; + std::unique_ptr will_move = + std::make_unique(IceControllerFactoryArgs{}); + MockIceController* wrapped = will_move.get(); + WrappingActiveIceController controller(&agent, std::move(will_move)); + + IceConfig config{}; + EXPECT_CALL(*wrapped, SetIceConfig(Ref(config))); + controller.SetIceConfig(config); + + EXPECT_CALL(*wrapped, + GetUseCandidateAttr(kConnection, NominationMode::AGGRESSIVE, + IceMode::ICEMODE_LITE)) + .WillOnce(Return(true)); + EXPECT_TRUE(controller.GetUseCandidateAttribute( + kConnection, NominationMode::AGGRESSIVE, IceMode::ICEMODE_LITE)); + + EXPECT_CALL(*wrapped, AddConnection(kConnection)); + controller.OnConnectionAdded(kConnection); + + EXPECT_CALL(*wrapped, OnConnectionDestroyed(kConnection)); + controller.OnConnectionDestroyed(kConnection); + + EXPECT_CALL(*wrapped, SetSelectedConnection(kConnection)); + controller.OnConnectionSwitched(kConnection); + + EXPECT_CALL(*wrapped, MarkConnectionPinged(kConnection)); + controller.OnConnectionPinged(kConnection); + + EXPECT_CALL(*wrapped, FindNextPingableConnection()) + .WillOnce(Return(kConnection)); + EXPECT_EQ(controller.FindNextPingableConnection(), kConnection); +} + +TEST(WrappingActiveIceControllerTest, HandlesImmediateSwitchRequest) { + AutoThread main; + ScopedFakeClock clock; + NiceMock agent; + std::unique_ptr will_move = + std::make_unique(IceControllerFactoryArgs{}); + NiceMockIceController* wrapped = will_move.get(); + WrappingActiveIceController controller(&agent, std::move(will_move)); + + IceSwitchReason reason = IceSwitchReason::NOMINATION_ON_CONTROLLED_SIDE; + std::vector conns_to_forget{kConnectionTwo}; + int recheck_delay_ms = 10; + IceControllerInterface::SwitchResult switch_result{ + kConnection, + IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK, + recheck_delay_ms), + conns_to_forget}; + + // ICE controller should switch to given connection immediately. + Sequence check_then_switch; + EXPECT_CALL(*wrapped, ShouldSwitchConnection(reason, kConnection)) + .InSequence(check_then_switch) + .WillOnce(Return(switch_result)); + EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason)) + .InSequence(check_then_switch); + EXPECT_CALL(agent, ForgetLearnedStateForConnections(conns_to_forget)); + + EXPECT_TRUE(controller.OnImmediateSwitchRequest(reason, kConnection)); + + // No rechecks before recheck delay. + clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms - 1)); + + // ICE controller should recheck for best connection after the recheck delay. + Sequence recheck_sort; + EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(recheck_sort); + EXPECT_CALL(*wrapped, + SortAndSwitchConnection(IceSwitchReason::ICE_CONTROLLER_RECHECK)) + .InSequence(recheck_sort) + .WillOnce(Return(IceControllerInterface::SwitchResult{})); + EXPECT_CALL(agent, ForgetLearnedStateForConnections(kEmptyConnsList)); + + clock.AdvanceTime(kTick); +} + +TEST(WrappingActiveIceControllerTest, HandlesImmediateSortAndSwitchRequest) { + AutoThread main; + ScopedFakeClock clock; + NiceMock agent; + std::unique_ptr will_move = + std::make_unique(IceControllerFactoryArgs{}); + NiceMockIceController* wrapped = will_move.get(); + WrappingActiveIceController controller(&agent, std::move(will_move)); + + IceSwitchReason reason = IceSwitchReason::NEW_CONNECTION_FROM_LOCAL_CANDIDATE; + std::vector conns_to_forget{kConnectionTwo}; + std::vector conns_to_prune{kConnectionThree}; + int recheck_delay_ms = 10; + IceControllerInterface::SwitchResult switch_result{ + kConnection, + IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK, + recheck_delay_ms), + conns_to_forget}; + + Sequence sort_and_switch; + EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(sort_and_switch); + EXPECT_CALL(*wrapped, SortAndSwitchConnection(reason)) + .InSequence(sort_and_switch) + .WillOnce(Return(switch_result)); + EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason)) + .InSequence(sort_and_switch); + EXPECT_CALL(*wrapped, PruneConnections()) + .InSequence(sort_and_switch) + .WillOnce(Return(conns_to_prune)); + EXPECT_CALL(agent, PruneConnections(conns_to_prune)) + .InSequence(sort_and_switch); + + controller.OnImmediateSortAndSwitchRequest(reason); + + // No rechecks before recheck delay. + clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms - 1)); + + // ICE controller should recheck for best connection after the recheck delay. + Sequence recheck_sort; + EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(recheck_sort); + EXPECT_CALL(*wrapped, + SortAndSwitchConnection(IceSwitchReason::ICE_CONTROLLER_RECHECK)) + .InSequence(recheck_sort) + .WillOnce(Return(IceControllerInterface::SwitchResult{})); + EXPECT_CALL(*wrapped, PruneConnections()) + .InSequence(recheck_sort) + .WillOnce(Return(kEmptyConnsList)); + EXPECT_CALL(agent, PruneConnections(kEmptyConnsList)) + .InSequence(recheck_sort); + + clock.AdvanceTime(kTick); +} + +TEST(WrappingActiveIceControllerTest, HandlesSortAndSwitchRequest) { + AutoThread main; + ScopedFakeClock clock; + + // Block the main task queue until ready. + Event init; + TimeDelta init_delay = TimeDelta::Millis(10); + main.PostTask([&init, &init_delay] { init.Wait(init_delay); }); + + NiceMock agent; + std::unique_ptr will_move = + std::make_unique(IceControllerFactoryArgs{}); + NiceMockIceController* wrapped = will_move.get(); + WrappingActiveIceController controller(&agent, std::move(will_move)); + + IceSwitchReason reason = IceSwitchReason::NETWORK_PREFERENCE_CHANGE; + + // No action should occur immediately + EXPECT_CALL(agent, UpdateConnectionStates()).Times(0); + EXPECT_CALL(*wrapped, SortAndSwitchConnection(_)).Times(0); + EXPECT_CALL(agent, SwitchSelectedConnection(_, _)).Times(0); + + controller.OnSortAndSwitchRequest(reason); + + std::vector conns_to_forget{kConnectionTwo}; + int recheck_delay_ms = 10; + IceControllerInterface::SwitchResult switch_result{ + kConnection, + IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK, + recheck_delay_ms), + conns_to_forget}; + + // Sort and switch should take place as the subsequent task. + Sequence sort_and_switch; + EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(sort_and_switch); + EXPECT_CALL(*wrapped, SortAndSwitchConnection(reason)) + .InSequence(sort_and_switch) + .WillOnce(Return(switch_result)); + EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason)) + .InSequence(sort_and_switch); + + // Unblock the init task. + clock.AdvanceTime(init_delay); +} + +TEST(WrappingActiveIceControllerTest, StartPingingAfterSortAndSwitch) { + AutoThread main; + ScopedFakeClock clock; + + // Block the main task queue until ready. + Event init; + TimeDelta init_delay = TimeDelta::Millis(10); + main.PostTask([&init, &init_delay] { init.Wait(init_delay); }); + + NiceMock agent; + std::unique_ptr will_move = + std::make_unique(IceControllerFactoryArgs{}); + NiceMockIceController* wrapped = will_move.get(); + WrappingActiveIceController controller(&agent, std::move(will_move)); + + // Pinging does not start automatically, unless triggered through a sort. + EXPECT_CALL(*wrapped, HasPingableConnection()).Times(0); + EXPECT_CALL(*wrapped, SelectConnectionToPing(_)).Times(0); + EXPECT_CALL(agent, OnStartedPinging()).Times(0); + + controller.OnSortAndSwitchRequest(IceSwitchReason::DATA_RECEIVED); + + // Pinging does not start if no pingable connection. + EXPECT_CALL(*wrapped, HasPingableConnection()).WillOnce(Return(false)); + EXPECT_CALL(*wrapped, SelectConnectionToPing(_)).Times(0); + EXPECT_CALL(agent, OnStartedPinging()).Times(0); + + // Unblock the init task. + clock.AdvanceTime(init_delay); + + int recheck_delay_ms = 10; + IceControllerInterface::PingResult ping_result(kConnection, recheck_delay_ms); + + // Pinging starts when there is a pingable connection. + Sequence start_pinging; + EXPECT_CALL(*wrapped, HasPingableConnection()) + .InSequence(start_pinging) + .WillOnce(Return(true)); + EXPECT_CALL(agent, OnStartedPinging()).InSequence(start_pinging); + EXPECT_CALL(agent, GetLastPingSentMs()) + .InSequence(start_pinging) + .WillOnce(Return(123)); + EXPECT_CALL(*wrapped, SelectConnectionToPing(123)) + .InSequence(start_pinging) + .WillOnce(Return(ping_result)); + EXPECT_CALL(agent, SendPingRequest(kConnection)).InSequence(start_pinging); + + controller.OnSortAndSwitchRequest(IceSwitchReason::DATA_RECEIVED); + clock.AdvanceTime(kTick); + + // ICE controller should recheck and ping after the recheck delay. + // No ping should be sent if no connection selected to ping. + EXPECT_CALL(agent, GetLastPingSentMs()).WillOnce(Return(456)); + EXPECT_CALL(*wrapped, SelectConnectionToPing(456)) + .WillOnce(Return(IceControllerInterface::PingResult( + /* connection= */ nullptr, recheck_delay_ms))); + EXPECT_CALL(agent, SendPingRequest(kConnection)).Times(0); + + clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms)); +} + +} // namespace