1 // Copyright 2017 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.
4
5 #include "components/offline_pages/core/prefetch/tasks/stale_entry_finalizer_task.h"
6
7 #include <array>
8
9 #include "base/bind.h"
10 #include "base/logging.h"
11 #include "base/metrics/histogram_functions.h"
12 #include "base/time/time.h"
13 #include "components/offline_pages/core/offline_clock.h"
14 #include "components/offline_pages/core/offline_store_utils.h"
15 #include "components/offline_pages/core/prefetch/prefetch_dispatcher.h"
16 #include "components/offline_pages/core/prefetch/prefetch_downloader.h"
17 #include "components/offline_pages/core/prefetch/prefetch_types.h"
18 #include "components/offline_pages/core/prefetch/store/prefetch_store.h"
19 #include "sql/database.h"
20 #include "sql/statement.h"
21 #include "sql/transaction.h"
22
23 namespace offline_pages {
24
25 using Result = StaleEntryFinalizerTask::Result;
26
27 namespace {
28
29 // Maximum amount of time into the future an item can has its freshness time set
30 // to after which it will be finalized (or deleted if in the zombie state).
31 constexpr base::TimeDelta kFutureItemTimeLimit = base::TimeDelta::FromDays(1);
32
33 // Expiration time delay for items entering the zombie state, after which they
34 // are permanently deleted.
35 constexpr base::TimeDelta kZombieItemLifetime = base::TimeDelta::FromDays(7);
36
37 // If this time changes, we need to update the desciption in histograms.xml
38 // for OfflinePages.Prefetching.StuckItemState.
39 const int kStuckTimeLimitInDays = 7;
40
FreshnessPeriodForState(PrefetchItemState state)41 const base::TimeDelta FreshnessPeriodForState(PrefetchItemState state) {
42 switch (state) {
43 // Bucket 1.
44 case PrefetchItemState::NEW_REQUEST:
45 return base::TimeDelta::FromDays(1);
46 // Bucket 2.
47 case PrefetchItemState::AWAITING_GCM:
48 case PrefetchItemState::RECEIVED_GCM:
49 case PrefetchItemState::RECEIVED_BUNDLE:
50 return base::TimeDelta::FromDays(1);
51 // Bucket 3.
52 case PrefetchItemState::DOWNLOADING:
53 case PrefetchItemState::IMPORTING:
54 return kPrefetchDownloadLifetime;
55 // The following states do not expire based on per bucket freshness so they
56 // are not expected to be passed into this function.
57 case PrefetchItemState::SENT_GENERATE_PAGE_BUNDLE:
58 case PrefetchItemState::SENT_GET_OPERATION:
59 case PrefetchItemState::DOWNLOADED:
60 case PrefetchItemState::FINISHED:
61 case PrefetchItemState::ZOMBIE:
62 NOTREACHED();
63 }
64 return base::TimeDelta::FromDays(1);
65 }
66
ErrorCodeForState(PrefetchItemState state)67 PrefetchItemErrorCode ErrorCodeForState(PrefetchItemState state) {
68 switch (state) {
69 // Valid values.
70 case PrefetchItemState::NEW_REQUEST:
71 return PrefetchItemErrorCode::STALE_AT_NEW_REQUEST;
72 case PrefetchItemState::AWAITING_GCM:
73 return PrefetchItemErrorCode::STALE_AT_AWAITING_GCM;
74 case PrefetchItemState::RECEIVED_GCM:
75 return PrefetchItemErrorCode::STALE_AT_RECEIVED_GCM;
76 case PrefetchItemState::RECEIVED_BUNDLE:
77 return PrefetchItemErrorCode::STALE_AT_RECEIVED_BUNDLE;
78 case PrefetchItemState::DOWNLOADING:
79 return PrefetchItemErrorCode::STALE_AT_DOWNLOADING;
80 case PrefetchItemState::IMPORTING:
81 return PrefetchItemErrorCode::STALE_AT_IMPORTING;
82 // The following states do not expire based on per bucket freshness so they
83 // are not expected to be passed into this function.
84 case PrefetchItemState::SENT_GENERATE_PAGE_BUNDLE:
85 case PrefetchItemState::SENT_GET_OPERATION:
86 case PrefetchItemState::DOWNLOADED:
87 case PrefetchItemState::FINISHED:
88 case PrefetchItemState::ZOMBIE:
89 NOTREACHED();
90 }
91 return PrefetchItemErrorCode::STALE_AT_UNKNOWN;
92 }
93
FinalizeStaleItems(PrefetchItemState state,base::Time now,sql::Database * db)94 bool FinalizeStaleItems(PrefetchItemState state,
95 base::Time now,
96 sql::Database* db) {
97 static const char kSql[] =
98 "UPDATE prefetch_items SET state = ?, error_code = ?"
99 " WHERE state = ? AND freshness_time < ?";
100 const int64_t earliest_fresh_db_time =
101 store_utils::ToDatabaseTime(now - FreshnessPeriodForState(state));
102 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
103 statement.BindInt(0, static_cast<int>(PrefetchItemState::FINISHED));
104 statement.BindInt(1, static_cast<int>(ErrorCodeForState(state)));
105 statement.BindInt(2, static_cast<int>(state));
106 statement.BindInt64(3, earliest_fresh_db_time);
107
108 return statement.Run();
109 }
110
MoreWorkInQueue(sql::Database * db)111 bool MoreWorkInQueue(sql::Database* db) {
112 static const char kSql[] =
113 "SELECT COUNT(*) FROM prefetch_items"
114 " WHERE state NOT IN (?, ?)";
115 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
116 statement.BindInt(0, static_cast<int>(PrefetchItemState::ZOMBIE));
117 statement.BindInt(1, static_cast<int>(PrefetchItemState::AWAITING_GCM));
118
119 // In event of failure, assume more work exists.
120 if (!statement.Step())
121 return true;
122
123 return statement.ColumnInt(0) > 0;
124 }
125
126 // If the user shifted the clock backwards too far, our items will stay around
127 // for a very long time. Don't allow that so we don't waste resources with
128 // potentially outdated content.
FinalizeFutureItems(PrefetchItemState state,base::Time now,sql::Database * db)129 bool FinalizeFutureItems(PrefetchItemState state,
130 base::Time now,
131 sql::Database* db) {
132 static const char kSql[] =
133 "UPDATE prefetch_items SET state = ?, error_code = ?"
134 " WHERE state = ? AND freshness_time > ?";
135 const int64_t future_fresh_db_time_limit =
136 store_utils::ToDatabaseTime(now + kFutureItemTimeLimit);
137 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
138 statement.BindInt(0, static_cast<int>(PrefetchItemState::FINISHED));
139 statement.BindInt(
140 1, static_cast<int>(
141 PrefetchItemErrorCode::MAXIMUM_CLOCK_BACKWARD_SKEW_EXCEEDED));
142 statement.BindInt(2, static_cast<int>(state));
143 statement.BindInt64(3, future_fresh_db_time_limit);
144
145 return statement.Run();
146 }
147
DeleteExpiredAndFutureZombies(base::Time now,sql::Database * db)148 bool DeleteExpiredAndFutureZombies(base::Time now, sql::Database* db) {
149 static const char kSql[] =
150 "DELETE FROM prefetch_items"
151 " WHERE state = ? "
152 " AND (freshness_time < ? OR freshness_time > ?)";
153 const int64_t earliest_zombie_db_time =
154 store_utils::ToDatabaseTime(now - kZombieItemLifetime);
155 const int64_t future_zombie_db_time =
156 store_utils::ToDatabaseTime(now + kFutureItemTimeLimit);
157 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
158 statement.BindInt(0, static_cast<int>(PrefetchItemState::ZOMBIE));
159 statement.BindInt64(1, earliest_zombie_db_time);
160 statement.BindInt64(2, future_zombie_db_time);
161 return statement.Run();
162 }
163
164 // If there is a bug in our code, an item might be stuck in the queue waiting
165 // on an event that didn't happen. If so, finalize that item and report it.
ReportAndFinalizeStuckItems(base::Time now,sql::Database * db)166 void ReportAndFinalizeStuckItems(base::Time now, sql::Database* db) {
167 const int64_t earliest_valid_creation_time = store_utils::ToDatabaseTime(
168 now - base::TimeDelta::FromDays(kStuckTimeLimitInDays));
169 // Report.
170 {
171 static constexpr char kSql[] =
172 "SELECT state FROM prefetch_items"
173 " WHERE creation_time < ?"
174 " AND state NOT IN (?, ?)"; // (ZOMBIE, FINISHED);
175 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
176 statement.BindInt64(0, earliest_valid_creation_time);
177 statement.BindInt64(1, static_cast<int>(PrefetchItemState::FINISHED));
178 statement.BindInt64(2, static_cast<int>(PrefetchItemState::ZOMBIE));
179
180 while (statement.Step()) {
181 int state_int = statement.ColumnInt(0);
182 if (ToPrefetchItemState(state_int)) { // Only report valid enum values.
183 base::UmaHistogramSparse("OfflinePages.Prefetching.StuckItemState",
184 state_int);
185 }
186 }
187 }
188 // Finalize.
189 {
190 static constexpr char kSql[] =
191 "UPDATE prefetch_items SET state = ?, error_code = ?"
192 " WHERE creation_time < ?"
193 " AND state NOT IN (?, ?)"; // (ZOMBIE, FINISHED)
194 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
195 statement.BindInt64(0, static_cast<int>(PrefetchItemState::FINISHED));
196 statement.BindInt64(1, static_cast<int>(PrefetchItemErrorCode::STUCK));
197 statement.BindInt64(2, earliest_valid_creation_time);
198 statement.BindInt64(3, static_cast<int>(PrefetchItemState::FINISHED));
199 statement.BindInt64(4, static_cast<int>(PrefetchItemState::ZOMBIE));
200
201 statement.Run();
202 }
203 }
204
FinalizeStaleEntriesSync(sql::Database * db)205 Result FinalizeStaleEntriesSync(sql::Database* db) {
206 sql::Transaction transaction(db);
207 if (!transaction.Begin())
208 return Result::NO_MORE_WORK;
209
210 // Only the following states are supposed to expire based on per bucket
211 // freshness.
212 static constexpr std::array<PrefetchItemState, 6> expirable_states = {{
213 // Bucket 1.
214 PrefetchItemState::NEW_REQUEST,
215 // Bucket 2.
216 PrefetchItemState::AWAITING_GCM, PrefetchItemState::RECEIVED_GCM,
217 PrefetchItemState::RECEIVED_BUNDLE,
218 // Bucket 3.
219 PrefetchItemState::DOWNLOADING, PrefetchItemState::IMPORTING,
220 }};
221 base::Time now = OfflineTimeNow();
222 for (PrefetchItemState state : expirable_states) {
223 if (!FinalizeStaleItems(state, now, db))
224 return Result::NO_MORE_WORK;
225
226 if (!FinalizeFutureItems(state, now, db))
227 return Result::NO_MORE_WORK;
228 }
229
230 if (!DeleteExpiredAndFutureZombies(now, db))
231 return Result::NO_MORE_WORK;
232
233 // Items could also be stuck in a non-expirable state due to a bug, report
234 // them. This should always be the last step, coming after the regular
235 // freshness maintenance steps above are done.
236 ReportAndFinalizeStuckItems(now, db);
237
238 Result result = Result::MORE_WORK_NEEDED;
239 if (!MoreWorkInQueue(db))
240 result = Result::NO_MORE_WORK;
241
242 // If all FinalizeStaleItems calls succeeded the transaction is committed.
243 return transaction.Commit() ? result : Result::NO_MORE_WORK;
244 }
245
246 } // namespace
247
StaleEntryFinalizerTask(PrefetchDispatcher * prefetch_dispatcher,PrefetchStore * prefetch_store)248 StaleEntryFinalizerTask::StaleEntryFinalizerTask(
249 PrefetchDispatcher* prefetch_dispatcher,
250 PrefetchStore* prefetch_store)
251 : prefetch_dispatcher_(prefetch_dispatcher),
252 prefetch_store_(prefetch_store) {
253 DCHECK(prefetch_dispatcher_);
254 DCHECK(prefetch_store_);
255 }
256
~StaleEntryFinalizerTask()257 StaleEntryFinalizerTask::~StaleEntryFinalizerTask() {}
258
Run()259 void StaleEntryFinalizerTask::Run() {
260 prefetch_store_->Execute(base::BindOnce(&FinalizeStaleEntriesSync),
261 base::BindOnce(&StaleEntryFinalizerTask::OnFinished,
262 weak_ptr_factory_.GetWeakPtr()),
263 Result::NO_MORE_WORK);
264 }
265
OnFinished(Result result)266 void StaleEntryFinalizerTask::OnFinished(Result result) {
267 final_status_ = result;
268 if (final_status_ == Result::MORE_WORK_NEEDED)
269 prefetch_dispatcher_->EnsureTaskScheduled();
270 DVLOG(1) << "Finalization task "
271 << (result == Result::NO_MORE_WORK ? "not " : "")
272 << "scheduling background processing.";
273 TaskComplete();
274 }
275
276 } // namespace offline_pages
277