0

update_client: Introduce a helper function to make timeout callbacks.

This wrapper guarantees that the callback will eventually be called even
if the recipient of the callback forgets about it.

Bug: 402773563
Change-Id: I48d23681d8b570972537d40c56d03591edea4e85
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4282299
Auto-Submit: Joshua Pawlicki <waffles@chromium.org>
Commit-Queue: Joshua Pawlicki <waffles@chromium.org>
Reviewed-by: Sorin Jianu <sorin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1431756}
This commit is contained in:
Joshua Pawlicki
2025-03-12 14:26:13 -07:00
committed by Chromium LUCI CQ
parent 463e0d9dde
commit d25bb2f9eb
3 changed files with 141 additions and 0 deletions

@ -136,6 +136,7 @@ static_library("update_client") {
"task_traits.h",
"task_update.cc",
"task_update.h",
"timed_callback.h",
"unpacker.cc",
"unpacker.h",
"unzipper.h",
@ -296,6 +297,7 @@ source_set("unit_tests") {
"protocol_serializer_json_unittest.cc",
"protocol_serializer_unittest.cc",
"request_sender_unittest.cc",
"timed_callback_unittest.cc",
"unpacker_unittest.cc",
"update_checker_unittest.cc",
"update_client_unittest.cc",

@ -0,0 +1,77 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_UPDATE_CLIENT_TIMED_CALLBACK_H_
#define COMPONENTS_UPDATE_CLIENT_TIMED_CALLBACK_H_
#include <memory>
#include <utility>
#include "base/cancelable_callback.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
namespace update_client {
// MakeTimedCallback posts a task to run `callback` after `timeout` with
// `timeout_args` on the current sequence, and returns a new callback to the
// caller that also calls `callback`. The returned callback must only be run on
// the current sequence.
//
// If the timeout occurs before the returned callback is
// run, the returned callback does nothing. It is safe to discard the returned
// callback in this case, or to call it.
//
// If the returned callback is run before the timeout, the timeout will do
// nothing.
//
// In either case, `callback` is guaranteed to run exactly once.
//
// For example, given:
// void HandleDecryptedContents(std::optional<std::string> plaintext);
// decryptAsync(ciphertext, base::BindOnce(&HandleDecryptedContents));
//
// It's possible to add a 30-second timeout:
// void HandleDecryptedContents(std::optional<std::string> plaintext);
// decryptAsync(
// cipher,
// MakeTimedCallback(
// base::BindOnce(&HandleDecryptedContents),
// base::Seconds(30),
// std::nullopt));
//
template <typename ReturnType, typename... Args>
base::OnceCallback<ReturnType(Args...)> MakeTimedCallback(
base::OnceCallback<ReturnType(Args...)> callback,
base::TimeDelta timeout,
Args... timeout_args) {
auto wrapper =
std::make_unique<base::CancelableOnceCallback<ReturnType(Args...)>>(
std::move(callback));
// Both callbacks must be constructed before either has a chance to run.
// `callback_1` will be run with the `timeout_args` after `timeout`.
// When either callback is run, they invalidate CancelableCallback's internal
// weak pointers, and the other callback becomes a no-op.
base::OnceCallback<ReturnType()> callback_1 =
base::BindOnce(wrapper->callback(), std::move(timeout_args)...);
base::OnceCallback<ReturnType(Args...)> callback_2 = wrapper->callback();
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](std::unique_ptr<base::CancelableOnceCallback<ReturnType(Args...)>>
/* wrapper */, // This callback takes ownership of `wrapper`.
base::OnceCallback<ReturnType()> callback_1) {
std::move(callback_1).Run();
},
std::move(wrapper), std::move(callback_1)),
timeout);
return callback_2;
}
} // namespace update_client
#endif // COMPONENTS_UPDATE_CLIENT_TIMED_CALLBACK_H_

@ -0,0 +1,62 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/update_client/timed_callback.h"
#include <utility>
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace update_client {
namespace {
TEST(TimedCallbackTest, CallNoTimeout) {
base::test::TaskEnvironment task_environment;
base::MockOnceClosure mock;
EXPECT_CALL(mock, Run());
std::move(MakeTimedCallback(mock.Get(), base::Seconds(55))).Run();
}
TEST(TimedCallbackTest, TimeoutThenCall) {
base::test::TaskEnvironment task_environment;
base::MockOnceCallback<void(int)> mock;
base::RunLoop loop;
EXPECT_CALL(mock, Run(1));
base::OnceCallback<void(int)> callback = MakeTimedCallback(
base::BindOnce(mock.Get().Then(loop.QuitClosure())), base::Seconds(1), 1);
loop.Run();
std::move(callback).Run(2); // Should have no effect.
}
TEST(TimedCallbackTest, CallThenTimeout) {
base::test::TaskEnvironment task_environment;
base::MockOnceClosure mock;
base::RunLoop loop;
EXPECT_CALL(mock, Run());
MakeTimedCallback(mock.Get(), base::Seconds(0)).Run();
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, loop.QuitClosure(), base::Milliseconds(10));
loop.Run();
}
TEST(TimedCallbackTest, MoveOnlyParameter) {
base::test::TaskEnvironment task_environment;
base::MockOnceClosure mock;
EXPECT_CALL(mock, Run());
std::move(MakeTimedCallback(base::BindOnce([](base::OnceClosure closure) {
std::move(closure).Run();
}),
base::Seconds(55), base::BindOnce([] {})))
.Run(mock.Get());
}
} // namespace
} // namespace update_client