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 "content/browser/indexed_db/indexed_db_tombstone_sweeper.h"
6 
7 #include <string>
8 
9 #include "base/metrics/histogram_functions.h"
10 #include "base/rand_util.h"
11 #include "base/sequenced_task_runner.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/threading/sequenced_task_runner_handle.h"
14 #include "base/time/tick_clock.h"
15 #include "components/services/storage/indexed_db/scopes/varint_coding.h"
16 #include "content/browser/indexed_db/indexed_db_backing_store.h"
17 #include "third_party/blink/public/common/indexeddb/indexeddb_key.h"
18 #include "third_party/blink/public/common/indexeddb/indexeddb_metadata.h"
19 #include "third_party/leveldatabase/env_chromium.h"
20 #include "third_party/leveldatabase/src/include/leveldb/db.h"
21 #include "third_party/leveldatabase/src/include/leveldb/iterator.h"
22 
23 namespace content {
24 namespace {
25 
26 using StopReason = IndexedDBPreCloseTaskQueue::StopReason;
27 using blink::IndexedDBDatabaseMetadata;
28 using blink::IndexedDBIndexMetadata;
29 using blink::IndexedDBKey;
30 using blink::IndexedDBObjectStoreMetadata;
31 
32 }  // namespace
33 
34 template <typename T>
WrappingIterator()35 WrappingIterator<T>::WrappingIterator() {}
36 
37 template <typename T>
WrappingIterator(const T * container,size_t start_position)38 WrappingIterator<T>::WrappingIterator(const T* container,
39                                       size_t start_position) {
40   container_ = container;
41   valid_ = true;
42   iterations_done_ = 0;
43   DCHECK_LT(start_position, container_->size());
44   inner_ = container_->begin();
45   std::advance(inner_, start_position);
46   DCHECK(inner_ != container_->end());
47 }
48 
49 template <typename T>
~WrappingIterator()50 WrappingIterator<T>::~WrappingIterator() {}
51 
52 template <typename T>
Next()53 void WrappingIterator<T>::Next() {
54   DCHECK(valid_);
55   iterations_done_++;
56   if (iterations_done_ >= container_->size()) {
57     valid_ = false;
58     return;
59   }
60   inner_++;
61   if (inner_ == container_->end()) {
62     inner_ = container_->begin();
63   }
64 }
65 
66 template <typename T>
Value() const67 const typename T::value_type& WrappingIterator<T>::Value() const {
68   CHECK(valid_);
69   return *inner_;
70 }
71 
IndexedDBTombstoneSweeper(int round_iterations,int max_iterations,leveldb::DB * database)72 IndexedDBTombstoneSweeper::IndexedDBTombstoneSweeper(int round_iterations,
73                                                      int max_iterations,
74                                                      leveldb::DB* database)
75     : IndexedDBPreCloseTaskQueue::PreCloseTask(database),
76       max_round_iterations_(round_iterations),
77       max_iterations_(max_iterations) {
78   sweep_state_.start_database_seed = static_cast<size_t>(base::RandUint64());
79   sweep_state_.start_object_store_seed =
80       static_cast<size_t>(base::RandUint64());
81   sweep_state_.start_index_seed = static_cast<size_t>(base::RandUint64());
82 }
83 
~IndexedDBTombstoneSweeper()84 IndexedDBTombstoneSweeper::~IndexedDBTombstoneSweeper() {}
85 
RequiresMetadata() const86 bool IndexedDBTombstoneSweeper::RequiresMetadata() const {
87   return true;
88 }
89 
SetMetadata(const std::vector<IndexedDBDatabaseMetadata> * metadata)90 void IndexedDBTombstoneSweeper::SetMetadata(
91     const std::vector<IndexedDBDatabaseMetadata>* metadata) {
92   database_metadata_ = metadata;
93   total_indices_ = 0;
94   for (const auto& db : *metadata) {
95     for (const auto& os_pair : db.object_stores) {
96       total_indices_ += os_pair.second.indexes.size();
97     }
98   }
99 }
100 
101 IndexedDBTombstoneSweeper::SweepState::SweepState() = default;
102 
103 IndexedDBTombstoneSweeper::SweepState::~SweepState() = default;
104 
Stop(StopReason reason)105 void IndexedDBTombstoneSweeper::Stop(StopReason reason) {
106   leveldb::Status s;
107   RecordUMAStats(reason, base::nullopt, s);
108 }
109 
RunRound()110 bool IndexedDBTombstoneSweeper::RunRound() {
111   DCHECK(database_metadata_);
112 
113   if (database_metadata_->empty())
114     return true;
115 
116   if (!start_time_) {
117     start_time_ = clock_for_testing_ ? clock_for_testing_->NowTicks()
118                                      : base::TimeTicks::Now();
119   }
120 
121   leveldb::Status s;
122   Status status = DoSweep(&s);
123 
124   if (status != Status::DONE_ERROR) {
125     s = FlushDeletions();
126     if (!s.ok())
127       status = Status::DONE_ERROR;
128   }
129 
130   if (status == Status::SWEEPING)
131     return false;
132 
133   RecordUMAStats(base::nullopt, status, s);
134   return true;
135 }
136 
RecordUMAStats(base::Optional<StopReason> stop_reason,base::Optional<IndexedDBTombstoneSweeper::Status> status,const leveldb::Status & leveldb_error)137 void IndexedDBTombstoneSweeper::RecordUMAStats(
138     base::Optional<StopReason> stop_reason,
139     base::Optional<IndexedDBTombstoneSweeper::Status> status,
140     const leveldb::Status& leveldb_error) {
141   DCHECK(stop_reason || status);
142   DCHECK(!stop_reason || !status);
143 
144   // Metadata error statistics are recorded in the PreCloseTaskList.
145   if (stop_reason && stop_reason == StopReason::METADATA_ERROR)
146     return;
147 
148   std::string uma_count_label =
149       "WebCore.IndexedDB.TombstoneSweeper.NumDeletedTombstones.";
150   std::string uma_size_label =
151       "WebCore.IndexedDB.TombstoneSweeper.DeletedTombstonesSize.";
152 
153   if (stop_reason) {
154     switch (stop_reason.value()) {
155       case StopReason::NEW_CONNECTION:
156         uma_count_label.append("ConnectionOpened");
157         uma_size_label.append("ConnectionOpened");
158         break;
159       case StopReason::TIMEOUT:
160         uma_count_label.append("TimeoutReached");
161         uma_size_label.append("TimeoutReached");
162         break;
163       case StopReason::METADATA_ERROR:
164         NOTREACHED();
165         break;
166     }
167   } else if (status) {
168     switch (status.value()) {
169       case Status::DONE_REACHED_MAX:
170         uma_count_label.append("MaxIterations");
171         uma_size_label.append("MaxIterations");
172         break;
173       case Status::DONE_ERROR:
174         base::UmaHistogramEnumeration(
175             "WebCore.IndexedDB.TombstoneSweeper.SweepError",
176             leveldb_env::GetLevelDBStatusUMAValue(leveldb_error),
177             leveldb_env::LEVELDB_STATUS_MAX);
178         uma_count_label.append("SweepError");
179         uma_size_label.append("SweepError");
180         break;
181       case Status::DONE_COMPLETE:
182         uma_count_label.append("Complete");
183         uma_size_label.append("Complete");
184         break;
185       case Status::SWEEPING:
186         NOTREACHED();
187         break;
188     }
189   } else {
190     NOTREACHED();
191   }
192 
193   // Some stats are only recorded for completed runs.
194   if (status && status.value() == Status::DONE_COMPLETE) {
195     if (start_time_) {
196       base::TimeDelta total_time =
197           (clock_for_testing_ ? clock_for_testing_->NowTicks()
198                               : base::TimeTicks::Now()) -
199           start_time_.value();
200 
201       base::UmaHistogramTimes(
202           "WebCore.IndexedDB.TombstoneSweeper.DeletionTotalTime.Complete",
203           total_time);
204       if (metrics_.seen_tombstones > 0) {
205         // Only record deletion time if we do a deletion.
206         base::UmaHistogramTimes(
207             "WebCore.IndexedDB.TombstoneSweeper.DeletionCommitTime."
208             "Complete",
209             total_deletion_time_);
210       }
211     }
212   }
213 
214   base::HistogramBase* count_histogram = base::Histogram::FactoryGet(
215       uma_count_label, 1, 1'000'000, 50,
216       base::HistogramBase::kUmaTargetedHistogramFlag);
217   // Range of 1 byte to 100 MB.
218   base::HistogramBase* size_histogram = base::Histogram::FactoryGet(
219       uma_size_label, 1, 100'000'000, 50,
220       base::HistogramBase::kUmaTargetedHistogramFlag);
221 
222   if (count_histogram)
223     count_histogram->Add(metrics_.seen_tombstones);
224   if (size_histogram)
225     size_histogram->Add(metrics_.seen_tombstones_size);
226 
227   // We put our max at 20 instead of 100 to reduce the number of buckets.
228   if (total_indices_ > 0) {
229     static const int kIndexPercentageBucketCount = 20;
230     base::UmaHistogramExactLinear(
231         "WebCore.IndexedDB.TombstoneSweeper.IndexScanPercent",
232         indices_scanned_ * kIndexPercentageBucketCount / total_indices_,
233         kIndexPercentageBucketCount + 1);
234   }
235 }
236 
FlushDeletions()237 leveldb::Status IndexedDBTombstoneSweeper::FlushDeletions() {
238   if (!has_writes_)
239     return leveldb::Status::OK();
240   base::TimeTicks start = base::TimeTicks::Now();
241 
242   leveldb::Status status =
243       database()->Write(leveldb::WriteOptions(), &round_deletion_batch_);
244   round_deletion_batch_.Clear();
245   has_writes_ = false;
246 
247   if (!status.ok()) {
248     base::UmaHistogramEnumeration(
249         "WebCore.IndexedDB.TombstoneSweeper.DeletionWriteError",
250         leveldb_env::GetLevelDBStatusUMAValue(status),
251         leveldb_env::LEVELDB_STATUS_MAX);
252     return status;
253   }
254 
255   base::TimeDelta diff = base::TimeTicks::Now() - start;
256   total_deletion_time_ += diff;
257   return status;
258 }
259 
ShouldContinueIteration(IndexedDBTombstoneSweeper::Status * sweep_status,leveldb::Status * leveldb_status,int * round_iterations)260 bool IndexedDBTombstoneSweeper::ShouldContinueIteration(
261     IndexedDBTombstoneSweeper::Status* sweep_status,
262     leveldb::Status* leveldb_status,
263     int* round_iterations) {
264   ++num_iterations_;
265   ++(*round_iterations);
266 
267   if (!iterator_->Valid()) {
268     *leveldb_status = iterator_->status();
269     if (!leveldb_status->ok()) {
270       *sweep_status = Status::DONE_ERROR;
271       return false;
272     }
273     *sweep_status = Status::SWEEPING;
274     return true;
275   }
276   if (*round_iterations >= max_round_iterations_) {
277     *sweep_status = Status::SWEEPING;
278     return false;
279   }
280   if (num_iterations_ >= max_iterations_) {
281     *sweep_status = Status::DONE_REACHED_MAX;
282     return false;
283   }
284   return true;
285 }
286 
DoSweep(leveldb::Status * leveldb_status)287 IndexedDBTombstoneSweeper::Status IndexedDBTombstoneSweeper::DoSweep(
288     leveldb::Status* leveldb_status) {
289   int round_iterations = 0;
290   Status sweep_status;
291   if (database_metadata_->empty())
292     return Status::DONE_COMPLETE;
293 
294   if (!iterator_) {
295     leveldb::ReadOptions iterator_options;
296     iterator_options.fill_cache = false;
297     iterator_options.verify_checksums = true;
298     iterator_.reset(database()->NewIterator(iterator_options));
299   }
300 
301   if (!sweep_state_.database_it) {
302     size_t start_database_idx = static_cast<size_t>(
303         sweep_state_.start_database_seed % database_metadata_->size());
304     sweep_state_.database_it = WrappingIterator<DatabaseMetadataVector>(
305         database_metadata_, start_database_idx);
306   }
307   // Loop conditions facilitate starting at random index.
308   for (; sweep_state_.database_it.value().IsValid();
309        sweep_state_.database_it.value().Next()) {
310     const IndexedDBDatabaseMetadata& database =
311         sweep_state_.database_it.value().Value();
312     if (database.object_stores.empty())
313       continue;
314 
315     if (!sweep_state_.object_store_it) {
316       size_t start_object_store_idx = static_cast<size_t>(
317           sweep_state_.start_object_store_seed % database.object_stores.size());
318       sweep_state_.object_store_it = WrappingIterator<ObjectStoreMetadataMap>(
319           &database.object_stores, start_object_store_idx);
320     }
321     // Loop conditions facilitate starting at random index.
322     for (; sweep_state_.object_store_it.value().IsValid();
323          sweep_state_.object_store_it.value().Next()) {
324       const IndexedDBObjectStoreMetadata& object_store =
325           sweep_state_.object_store_it.value().Value().second;
326 
327       if (object_store.indexes.empty())
328         continue;
329 
330       if (!sweep_state_.index_it) {
331         size_t start_index_idx = static_cast<size_t>(
332             sweep_state_.start_index_seed % object_store.indexes.size());
333         sweep_state_.index_it = WrappingIterator<IndexMetadataMap>(
334             &object_store.indexes, start_index_idx);
335       }
336       // Loop conditions facilitate starting at random index.
337       for (; sweep_state_.index_it.value().IsValid();
338            sweep_state_.index_it.value().Next()) {
339         const IndexedDBIndexMetadata& index =
340             sweep_state_.index_it.value().Value().second;
341 
342         bool can_continue =
343             IterateIndex(database.id, object_store.id, index, &sweep_status,
344                          leveldb_status, &round_iterations);
345         if (!can_continue)
346           return sweep_status;
347       }
348       sweep_state_.index_it = base::nullopt;
349     }
350     sweep_state_.object_store_it = base::nullopt;
351   }
352   return Status::DONE_COMPLETE;
353 }
354 
IterateIndex(int64_t database_id,int64_t object_store_id,const IndexedDBIndexMetadata & index,IndexedDBTombstoneSweeper::Status * sweep_status,leveldb::Status * leveldb_status,int * round_iterations)355 bool IndexedDBTombstoneSweeper::IterateIndex(
356     int64_t database_id,
357     int64_t object_store_id,
358     const IndexedDBIndexMetadata& index,
359     IndexedDBTombstoneSweeper::Status* sweep_status,
360     leveldb::Status* leveldb_status,
361     int* round_iterations) {
362   // If the sweeper exited early from an index scan, continue where it left off.
363   if (sweep_state_.index_it_key) {
364     iterator_->Seek(sweep_state_.index_it_key.value().Encode());
365     if (!ShouldContinueIteration(sweep_status, leveldb_status,
366                                  round_iterations)) {
367       return false;
368     }
369     // Start at the first unvisited value.
370     iterator_->Next();
371     if (!ShouldContinueIteration(sweep_status, leveldb_status,
372                                  round_iterations)) {
373       return false;
374     }
375   } else {
376     iterator_->Seek(
377         IndexDataKey::EncodeMinKey(database_id, object_store_id, index.id));
378     if (!ShouldContinueIteration(sweep_status, leveldb_status,
379                                  round_iterations)) {
380       return false;
381     }
382   }
383 
384   while (iterator_->Valid()) {
385     leveldb::Slice key_slice = iterator_->key();
386     base::StringPiece index_key_str = leveldb_env::MakeStringPiece(key_slice);
387     size_t key_size = index_key_str.size();
388     base::StringPiece index_value_str =
389         leveldb_env::MakeStringPiece(iterator_->value());
390     size_t value_size = index_value_str.size();
391     // See if we've reached the end of the current index or all indexes.
392     sweep_state_.index_it_key.emplace(IndexDataKey());
393     if (!IndexDataKey::Decode(&index_key_str,
394                               &sweep_state_.index_it_key.value()) ||
395         sweep_state_.index_it_key.value().IndexId() != index.id) {
396       break;
397     }
398 
399     size_t entry_size = key_size + value_size;
400 
401     int64_t index_data_version;
402     std::unique_ptr<IndexedDBKey> primary_key;
403 
404     if (!DecodeVarInt(&index_value_str, &index_data_version)) {
405       ++metrics_.num_invalid_index_values;
406       iterator_->Next();
407       if (!ShouldContinueIteration(sweep_status, leveldb_status,
408                                    round_iterations)) {
409         return false;
410       }
411       continue;
412     }
413     std::string encoded_primary_key = index_value_str.as_string();
414     std::string exists_key = ExistsEntryKey::Encode(
415         database_id, object_store_id, encoded_primary_key);
416 
417     std::string exists_value;
418     leveldb::Status s =
419         database()->Get(leveldb::ReadOptions(), exists_key, &exists_value);
420     if (!s.ok()) {
421       ++metrics_.num_errors_reading_exists_table;
422       iterator_->Next();
423       if (!ShouldContinueIteration(sweep_status, leveldb_status,
424                                    round_iterations)) {
425         return false;
426       }
427       continue;
428     }
429     base::StringPiece exists_value_piece(exists_value);
430     int64_t decoded_exists_version;
431     if (!DecodeInt(&exists_value_piece, &decoded_exists_version) ||
432         !exists_value_piece.empty()) {
433       ++metrics_.num_invalid_exists_values;
434       iterator_->Next();
435       if (!ShouldContinueIteration(sweep_status, leveldb_status,
436                                    round_iterations)) {
437         return false;
438       }
439       continue;
440     }
441 
442     if (decoded_exists_version != index_data_version) {
443       has_writes_ = true;
444       round_deletion_batch_.Delete(key_slice);
445       ++metrics_.seen_tombstones;
446       metrics_.seen_tombstones_size += entry_size;
447     }
448 
449     iterator_->Next();
450     if (!ShouldContinueIteration(sweep_status, leveldb_status,
451                                  round_iterations)) {
452       return false;
453     }
454   }
455   ++indices_scanned_;
456   sweep_state_.index_it_key = base::nullopt;
457   return true;
458 }
459 
460 }  // namespace content
461