1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "extensions/browser/quota_service.h"
7 #include "base/message_loop/message_loop.h"
8 #include "base/stl_util.h"
9 #include "extensions/browser/extension_function.h"
10 #include "extensions/common/error_utils.h"
14 // If the browser stays open long enough, we reset state once a day.
15 // Whatever this value is, it should be an order of magnitude longer than
16 // the longest interval in any of the QuotaLimitHeuristics in use.
17 const int kPurgeIntervalInDays
= 1;
19 const char kOverQuotaError
[] = "This request exceeds the * quota.";
23 namespace extensions
{
25 QuotaService::QuotaService() {
26 if (base::MessageLoop::current() != NULL
) { // Null in unit tests.
27 purge_timer_
.Start(FROM_HERE
,
28 base::TimeDelta::FromDays(kPurgeIntervalInDays
),
30 &QuotaService::Purge
);
34 QuotaService::~QuotaService() {
35 DCHECK(CalledOnValidThread());
40 std::string
QuotaService::Assess(const std::string
& extension_id
,
41 ExtensionFunction
* function
,
42 const base::ListValue
* args
,
43 const base::TimeTicks
& event_time
) {
44 DCHECK(CalledOnValidThread());
46 if (function
->ShouldSkipQuotaLimiting())
49 // Lookup function list for extension.
50 FunctionHeuristicsMap
& functions
= function_heuristics_
[extension_id
];
52 // Lookup heuristics for function, create if necessary.
53 QuotaLimitHeuristics
& heuristics
= functions
[function
->name()];
54 if (heuristics
.empty())
55 function
->GetQuotaLimitHeuristics(&heuristics
);
57 if (heuristics
.empty())
58 return std::string(); // No heuristic implies no limit.
60 ViolationErrorMap::iterator violation_error
=
61 violation_errors_
.find(extension_id
);
62 if (violation_error
!= violation_errors_
.end())
63 return violation_error
->second
; // Repeat offender.
65 QuotaLimitHeuristic
* failed_heuristic
= NULL
;
66 for (QuotaLimitHeuristics::iterator heuristic
= heuristics
.begin();
67 heuristic
!= heuristics
.end();
69 // Apply heuristic to each item (bucket).
70 if (!(*heuristic
)->ApplyToArgs(args
, event_time
)) {
71 failed_heuristic
= *heuristic
;
76 if (!failed_heuristic
)
79 std::string error
= failed_heuristic
->GetError();
80 DCHECK_GT(error
.length(), 0u);
82 PurgeFunctionHeuristicsMap(&functions
);
83 function_heuristics_
.erase(extension_id
);
84 violation_errors_
[extension_id
] = error
;
88 void QuotaService::PurgeFunctionHeuristicsMap(FunctionHeuristicsMap
* map
) {
89 FunctionHeuristicsMap::iterator heuristics
= map
->begin();
90 while (heuristics
!= map
->end()) {
91 STLDeleteElements(&heuristics
->second
);
92 map
->erase(heuristics
++);
96 void QuotaService::Purge() {
97 DCHECK(CalledOnValidThread());
98 std::map
<std::string
, FunctionHeuristicsMap
>::iterator it
=
99 function_heuristics_
.begin();
100 for (; it
!= function_heuristics_
.end(); function_heuristics_
.erase(it
++))
101 PurgeFunctionHeuristicsMap(&it
->second
);
104 void QuotaLimitHeuristic::Bucket::Reset(const Config
& config
,
105 const base::TimeTicks
& start
) {
106 num_tokens_
= config
.refill_token_count
;
107 expiration_
= start
+ config
.refill_interval
;
110 void QuotaLimitHeuristic::SingletonBucketMapper::GetBucketsForArgs(
111 const base::ListValue
* args
,
112 BucketList
* buckets
) {
113 buckets
->push_back(&bucket_
);
116 QuotaLimitHeuristic::QuotaLimitHeuristic(const Config
& config
,
118 const std::string
& name
)
119 : config_(config
), bucket_mapper_(map
), name_(name
) {}
121 QuotaLimitHeuristic::~QuotaLimitHeuristic() {}
123 bool QuotaLimitHeuristic::ApplyToArgs(const base::ListValue
* args
,
124 const base::TimeTicks
& event_time
) {
126 bucket_mapper_
->GetBucketsForArgs(args
, &buckets
);
127 for (BucketList::iterator i
= buckets
.begin(); i
!= buckets
.end(); ++i
) {
128 if ((*i
)->expiration().is_null()) // A brand new bucket.
129 (*i
)->Reset(config_
, event_time
);
130 if (!Apply(*i
, event_time
))
131 return false; // It only takes one to spoil it for everyone.
136 std::string
QuotaLimitHeuristic::GetError() const {
137 return extensions::ErrorUtils::FormatErrorMessage(kOverQuotaError
, name_
);
140 QuotaService::SustainedLimit::SustainedLimit(const base::TimeDelta
& sustain
,
141 const Config
& config
,
143 const std::string
& name
)
144 : QuotaLimitHeuristic(config
, map
, name
),
145 repeat_exhaustion_allowance_(sustain
.InSeconds() /
146 config
.refill_interval
.InSeconds()),
147 num_available_repeat_exhaustions_(repeat_exhaustion_allowance_
) {}
149 bool QuotaService::TimedLimit::Apply(Bucket
* bucket
,
150 const base::TimeTicks
& event_time
) {
151 if (event_time
> bucket
->expiration())
152 bucket
->Reset(config(), event_time
);
154 return bucket
->DeductToken();
157 bool QuotaService::SustainedLimit::Apply(Bucket
* bucket
,
158 const base::TimeTicks
& event_time
) {
159 if (event_time
> bucket
->expiration()) {
160 // We reset state for this item and start over again if this request breaks
161 // the bad cycle that was previously being tracked. This occurs if the
162 // state in the bucket expired recently (it has been long enough since the
163 // event that we don't care about the last event), but the bucket still has
164 // tokens (so pressure was not sustained over that time), OR we are more
165 // than 1 full refill interval away from the last event (so even if we used
166 // up all the tokens in the last bucket, nothing happened in the entire
167 // next refill interval, so it doesn't matter).
168 if (bucket
->has_tokens() ||
169 event_time
> bucket
->expiration() + config().refill_interval
) {
170 bucket
->Reset(config(), event_time
);
171 num_available_repeat_exhaustions_
= repeat_exhaustion_allowance_
;
172 } else if (--num_available_repeat_exhaustions_
> 0) {
173 // The last interval was saturated with requests, and this is the first
174 // event in the next interval. If this happens
175 // repeat_exhaustion_allowance_ times, it's a violation. Reset the bucket
176 // state to start timing from the end of the last interval (and we'll
177 // deduct the token below) so we can detect this each time it happens.
178 bucket
->Reset(config(), bucket
->expiration());
180 // No allowances left; this request is a violation.
185 // We can go negative since we check has_tokens when we get to *next* bucket,
186 // and for the small interval all that matters is whether we used up all the
187 // tokens (which is true if num_tokens_ <= 0).
188 bucket
->DeductToken();
192 } // namespace extensions