blob: 7bc4f1a1fa7f72978ab0b436cb08085eb4882f40 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_BUDGETER_H_
#define CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_BUDGETER_H_
#include <memory>
#include <optional>
#include <set>
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/timer/timer.h"
#include "content/browser/private_aggregation/private_aggregation_budget_key.h"
#include "content/common/content_export.h"
#include "content/public/browser/private_aggregation_data_model.h"
#include "content/public/browser/storage_partition.h"
#include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h"
namespace base {
class UpdateableSequencedTaskRunner;
} // namespace base
namespace content {
class PrivateAggregationBudgetStorage;
// UI thread class that provides an interface for querying and updating the
// budget used by each key, i.e. the sum of contributions, by interacting with
// the storage layer. This class is responsible for owning the storage class.
class CONTENT_EXPORT PrivateAggregationBudgeter {
public:
// Public for testing
enum class StorageStatus {
// The database initialization process hasn't started yet.
kPendingInitialization,
// The database is in the process of being initialized.
kInitializing,
// The database initialization did not succeed.
kInitializationFailed,
// The database successfully initialized and can be used.
kOpen,
};
// The result of a request to consume some budget. All results other than
// `kApproved` enumerate different reasons the request was rejected.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// LINT.IfChange(RequestResult)
enum class RequestResult {
kApproved = 0,
kInsufficientSmallerScopeBudget = 1,
kInsufficientLargerScopeBudget = 2,
kRequestedMoreThanTotalBudget = 3,
kTooManyPendingCalls = 4,
kStorageInitializationFailed = 5,
kBadValuesOnDisk = 6,
kMaxValue = kBadValuesOnDisk,
};
// LINT.ThenChange(//content/browser/private_aggregation/private_aggregation_budgeter.cc:ComputeOverallRequestResult)
// For a single contribution, whether the budgeter approved its budget usage
// (including provisionally) or denied it.
enum class ResultForContribution {
kApproved,
kDenied,
};
// Note that if the limit has been reached and there is no space for this
// report, the entire report will be dropped with a fatal error.
enum class PendingReportLimitResult {
// Indicates the limit has not been reached.
kNotAtLimit,
// Indicates the limit has been reached with this report, i.e. the report
// can still be processed.
kAtLimit
};
// Used to ensure the budget cannot be modified between calls to
// `InspectBudgetAndLock()` and `ConsumeBudget()`. Only one instance of this
// object can exist and is vended to a caller when the budgeter is 'locked'.
// This avoids the available budget changing between calls for the same report
// and will allow for certain optimizations.
class CONTENT_EXPORT Lock {
public:
// Move only
Lock(Lock&& other) = default;
Lock& operator=(Lock&& other) = default;
Lock(const Lock&) = delete;
Lock& operator=(const Lock&) = delete;
static Lock CreateForTesting() { return Lock(); }
private:
friend PrivateAggregationBudgeter;
Lock() = default;
};
struct CONTENT_EXPORT BudgetQueryResult {
BudgetQueryResult(
RequestResult overall_result,
std::vector<ResultForContribution> result_for_each_contribution);
BudgetQueryResult(BudgetQueryResult&& other);
BudgetQueryResult& operator=(BudgetQueryResult&& other);
~BudgetQueryResult();
// The result for the entire call, considering all contributions together.
// (That is, `kApproved` if all contributions were approved,
// `kInsufficientSmallerScopeBudget` if some were denied due to an
// insufficient smaller scope budget, etc.)
RequestResult overall_result;
// Empty if a fatal error (i.e. `kTooManyPendingCalls`,
// `kStorageInitializationFailed` or `kBadValuesOnDisk`) occurred.
std::vector<ResultForContribution> result_for_each_contribution;
};
struct CONTENT_EXPORT InspectBudgetCallResult {
InspectBudgetCallResult(
BudgetQueryResult query_result,
std::optional<Lock> lock,
PendingReportLimitResult pending_report_limit_result);
InspectBudgetCallResult(InspectBudgetCallResult&& other);
InspectBudgetCallResult& operator=(InspectBudgetCallResult&& other);
~InspectBudgetCallResult();
BudgetQueryResult query_result;
// Populated iff a fatal error did not occur (even if provisional budget was
// denied for all contributions).
std::optional<Lock> lock;
PendingReportLimitResult pending_report_limit_result;
};
// Represents the validity status of the stored budget data for the provided
// site and API retrieved during a `ConsumeBudget()` call. In case multiple
// statuses apply, the first one encountered/detected will be used.
//
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class BudgetValidityStatus {
kValid = 0,
kValidAndEmpty = 1,
kValidButContainsStaleWindow = 2,
kContainsTimestampInFuture = 3,
kContainsValueExceedingLimit = 4,
kContainsTimestampNotRoundedToMinute = 5,
kSpansMoreThanADay = 6,
kContainsNonPositiveValue = 7,
kMaxValue = kContainsNonPositiveValue,
};
// Represents the two different types of budgets, which differ on the duration
// of time that they apply to and what the allowable budget for that time is.
enum class BudgetScope {
// Scope is per-site per-API per-10 min
kSmallerScope,
// Scope is per-site per-API per-day
kLargerScope,
};
// Encapsulates constants that differ for the two scopes, allowing them to be
// passed around more easily.
struct CONTENT_EXPORT BudgetScopeValues {
BudgetScope budget_scope;
// Maximum budget allowed to be claimed for this scope.
int max_budget_per_scope;
// The total length of time that per-site per-API budgets are enforced
// against in this scope. (Note that there are 10 time windows per
// `kBudgetSmallerScopeDuration` and 1440 time windows per
// `kBudgetLargerScopeDuration`.)
base::TimeDelta budget_scope_duration;
};
static constexpr BudgetScopeValues kSmallerScopeValues = {
BudgetScope::kSmallerScope, /*max_budget_per_scope=*/65536,
/*budget_scope_duration=*/base::Minutes(10)};
static constexpr BudgetScopeValues kLargerScopeValues = {
BudgetScope::kLargerScope, /*max_budget_per_scope=*/1048576,
/*budget_scope_duration=*/base::Days(1)};
static_assert(kSmallerScopeValues.budget_scope_duration %
PrivateAggregationBudgetKey::TimeWindow::kDuration ==
base::TimeDelta());
static_assert(kLargerScopeValues.budget_scope_duration %
PrivateAggregationBudgetKey::TimeWindow::kDuration ==
base::TimeDelta());
// The minimum time that needs to pass between `CleanUpStaleData()` calls to
// avoid unnecessary computation.
static constexpr base::TimeDelta kMinStaleDataCleanUpGap = base::Minutes(5);
// To avoid unbounded memory growth, limit the number of pending calls during
// initialization. Data clearing calls can be posted even if it would exceed
// this limit.
static constexpr int kMaxPendingCalls = 1000;
// `db_task_runner` should not be nullptr.
PrivateAggregationBudgeter(
scoped_refptr<base::UpdateableSequencedTaskRunner> db_task_runner,
bool exclusively_run_in_memory,
base::FilePath path_to_db_dir);
PrivateAggregationBudgeter(const PrivateAggregationBudgeter& other) = delete;
PrivateAggregationBudgeter& operator=(
const PrivateAggregationBudgeter& other) = delete;
virtual ~PrivateAggregationBudgeter();
// Attempts to consume `budget` for `budget_key`. The callback `on_done` is
// then run with the appropriate `RequestResult`.
//
// The attempt is rejected if it would cause a contribution budget to be
// exceeded, i.e. if the site's per-10 min per-API budget would exceed
// `kSmallerScopeValues.max_budget_per_scope` and/or if the site's daily
// per-API budget would exceed `kLargerScopeValues.max_budget_per_scope` (for
// the 10-min and 24-hour period, respectively, ending at the *end* of
// `budget_key.time_window`, see the budget scope durations above and
// `PrivateAggregationBudgetKey` for more detail). The attempt is also
// rejected if the database is closed. If the database is initializing, this
// query is queued until the initialization is complete. Otherwise, the budget
// use is recorded and the attempt is successful. May clean up stale budget
// storage. Note that this call assumes that budget time windows are
// non-decreasing. In very rare cases, a network time update could allow
// budget to be used slightly early. Virtual for testing.
// `minimum_value_for_metrics` is the minimum value for any of the histogram
// contributions summed in `budget`; it is only used for metrics. `budget`
// must be positive.
//
// Note: can only be used if `kPrivateAggregationApiErrorReporting` is
// disabled.
virtual void ConsumeBudget(int budget,
const PrivateAggregationBudgetKey& budget_key,
int minimum_value_for_metrics,
base::OnceCallback<void(RequestResult)> on_done);
// Overload to allow minimum_value_for_metrics to have a default of 0 without
// compiler complaints about the function being virtual. Used mainly to
// simplify testing.
void ConsumeBudget(int budget,
const PrivateAggregationBudgetKey& budget_key,
base::OnceCallback<void(RequestResult)> on_done);
// Queries whether there is sufficient budget for each of the `contributions`
// without consuming any. The `result_callback` is then run with the
// appropriate `InspectBudgetCallResult`, containing the (hypothetical) result
// for each contribution, the `Lock` (unless a fatal error occurred) and
// whether the pending report limit was reached. The callback may be run
// either synchronously or asynchronously.
//
// Even though this call does not consume budget, the result for each
// contribution is computed as if any earlier items in `contributions` that
// were approved did indeed consume their budget. See `ConsumeBudget()` below
// for more detail on budget definitions and time update considerations.
//
// The attempt is also rejected if the database is closed. If the database is
// initializing or locked, this query is queued until the initialization is
// complete / lock is released. Otherwise, the query is successful.
//
// Note: can only be used if `kPrivateAggregationApiErrorReporting` is
// enabled.
virtual void InspectBudgetAndLock(
const std::vector<blink::mojom::AggregatableReportHistogramContribution>&
contributions,
const PrivateAggregationBudgetKey& budget_key,
base::OnceCallback<void(InspectBudgetCallResult)> result_callback);
// Attempts to consume budget for each of the `contributions`. The
// `result_callback` is then run with the result for each contribution. A
// `Lock` obtained from an earlier call to `InspectBudgetAndLock()` must be
// provided. The callback may be run either synchronously or asynchronously.
//
// Each contribution's attempt is rejected if it would cause a contribution
// budget to be exceeded, i.e. if the site's per-10 min per-API budget would
// exceed `kSmallerScopeValues.max_budget_per_scope` and/or if the site's
// daily per-API budget would exceed `kLargerScopeValues.max_budget_per_scope`
// (for the 10-min and 24-hour period, respectively, ending at the *end* of
// `budget_key.time_window`, see the budget scope durations above and
// `PrivateAggregationBudgetKey` for more detail). Otherwise, the attempt is
// successful (although see additional error cases below).
//
// The result for each contribution takes into account any budget used by
// earlier `contributions`.
//
// May clean up stale budget storage. Note that this call assumes that budget
// time windows are non-decreasing. In very rare cases, a network time update
// could allow budget to be used slightly early.
//
// Note: can only be used if `kPrivateAggregationApiErrorReporting` is
// enabled.
virtual void ConsumeBudget(
Lock lock,
const std::vector<blink::mojom::AggregatableReportHistogramContribution>&
contributions,
const PrivateAggregationBudgetKey& budget_key,
base::OnceCallback<void(BudgetQueryResult)> result_callback);
// Deletes all data in storage for any budgets that could have been set
// between `delete_begin` and `delete_end` time (inclusive). Note that the
// discrete time windows used may lead to more data being deleted than
// strictly necessary. Null times are treated as unbounded lower or upper
// range. If `!filter.is_null()`, budget entries without an origin that
// matches the `filter` are retained (i.e. not cleared). Note that budgets are
// scoped per-site, not per-origin. So, the budget storage keeps track of any
// reporting origins used in the last day and will delete that corresponding
// site's data if the `filter` matches any of those origins.
virtual void ClearData(base::Time delete_begin,
base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
base::OnceClosure done);
// Runs `callback` with all reporting origins as DataKeys for the Browsing
// Data Model. Partial data will still be returned in the event of an error.
virtual void GetAllDataKeys(
base::OnceCallback<void(std::set<PrivateAggregationDataModel::DataKey>)>
callback);
// Deletes all data in storage for storage keys matching the provided
// reporting origin in the data key.
virtual void DeleteByDataKey(const PrivateAggregationDataModel::DataKey& key,
base::OnceClosure callback);
// Combines the results from sequential queries to `InspectBudgetAndLock()`
// and `ConsumeBudget()`.
static RequestResult CombineRequestResults(
RequestResult inspect_budget_result,
RequestResult consume_budget_result);
protected:
// Should only be used for testing/mocking to avoid creating the underlying
// storage.
PrivateAggregationBudgeter();
// Called when storage is initialized. Iff initialization failed, `storage`
// will be nullptr. Virtual for testing.
virtual void OnStorageDoneInitializing(
std::unique_ptr<PrivateAggregationBudgetStorage> storage);
StorageStatus storage_status_ = StorageStatus::kPendingInitialization;
private:
// Begins initializing storage asynchronously. Repeat calls are no-ops.
// Registers a callback to `OnStorageDoneInitializing()`.
//
// We initialize storage lazily to keep startup code fast. This laziness also
// avoids unnecessary work when storage is not needed. So, rather than eagerly
// initializing storage in the constructor, the first call to `ConsumeBudget`,
// `ClearData()`, or `GetAllDataKeys()` will call this method.
void EnsureStorageInitializationBegun();
bool IsBudgeterLocked() const;
Lock VendLock();
void ConsumeBudgetImpl(int additional_budget,
const PrivateAggregationBudgetKey& budget_key,
int minimum_value_for_metrics,
base::OnceCallback<void(RequestResult)> on_done);
void InspectBudgetAndLockImpl(
const std::vector<blink::mojom::AggregatableReportHistogramContribution>&
contributions,
const PrivateAggregationBudgetKey& budget_key,
PendingReportLimitResult pending_report_limit_result,
base::OnceCallback<void(InspectBudgetCallResult)> result_callback);
BudgetQueryResult QueryBudget(
const std::vector<blink::mojom::AggregatableReportHistogramContribution>&
contributions,
const PrivateAggregationBudgetKey& budget_key,
bool consume_budget,
int minimum_value_for_metrics = 0);
void ClearDataImpl(base::Time delete_begin,
base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
base::OnceClosure done);
void GetAllDataKeysImpl(
base::OnceCallback<void(std::set<PrivateAggregationDataModel::DataKey>)>
callback);
void OnUserVisibleTaskStarted();
void OnUserVisibleTaskComplete();
void ProcessAllPendingCalls();
bool DidStorageInitializationSucceed() const;
// Deletes any budgeting data that is too old to affect current or future
// calls to the API.
void CleanUpStaleData();
// Runs `CleanUpStaleData()` unless it was run too recently, when it will be
// run after waiting for `kMinStaleDataCleanUpGap` to pass between calls.
void CleanUpStaleDataSoon();
// While the storage initializes, queues calls (e.g. to `ConsumeBudget()`) in
// the order the calls are received. Should be empty after storage is
// initialized. The size is limited to `kMaxPendingCalls` except that
// `ClearData()` can store additional tasks beyond that limit.
std::vector<base::OnceClosure> pending_calls_;
bool process_all_pending_calls_in_progress_ = false;
// The task runner for all private aggregation storage operations. Updateable
// to allow for priority to be temporarily increased to `USER_VISIBLE` when a
// clear data task is queued or running. Otherwise `BEST_EFFORT` is used.
scoped_refptr<base::UpdateableSequencedTaskRunner> db_task_runner_;
// How many user visible storage tasks are queued or running currently, i.e.
// have been posted but the reply has not been run.
int num_pending_user_visible_tasks_ = 0;
// Whether `storage_` should not write to disk.
bool exclusively_run_in_memory_ = false;
// Directory where `storage_` should search for its database. Do not use once
// storage initialization has begun as it will be in an unspecified state.
base::FilePath path_to_db_dir_;
// `nullptr` until initialization is complete or if initialization failed.
// Otherwise, owned by this class until destruction. Iff present,
// `storage_status_` should be `kOpen`.
std::unique_ptr<PrivateAggregationBudgetStorage> storage_;
// Timer used to defer calls to `CleanUpStaleData()` until
// `kMinStaleDataCleanUpGap` has passed since the last call.
base::OneShotTimer clean_up_stale_data_timer_;
// The last time `CleanUpStaleData()` was called, or `base::TimeTicks::Min()`
// if never called.
base::TimeTicks last_clean_up_time_ = base::TimeTicks::Min();
// Holds a closure that will shut down the initializing storage until
// initialization is complete. After then, it is null.
base::OnceClosure shutdown_initializing_storage_;
// When the budgeter is locked, `locked_timer_` is populated with a timer
// tracking the elapsed time since the `Lock` object was vended to the manager
// processing the corresponding report. See `Lock` above. Otherwise, this is
// std::nullopt.
std::optional<base::ElapsedTimer> locked_timer_;
base::WeakPtrFactory<PrivateAggregationBudgeter> weak_factory_{this};
};
} // namespace content
#endif // CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_BUDGETER_H_