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