1 // Copyright (c) 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.
4 
5 #include "content/browser/indexed_db/indexed_db_transaction.h"
6 
7 #include <utility>
8 #include <vector>
9 
10 #include "base/bind.h"
11 #include "base/location.h"
12 #include "base/logging.h"
13 #include "base/metrics/histogram_functions.h"
14 #include "base/metrics/histogram_macros.h"
15 #include "base/stl_util.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "base/threading/sequenced_task_runner_handle.h"
18 #include "content/browser/indexed_db/indexed_db_backing_store.h"
19 #include "content/browser/indexed_db/indexed_db_cursor.h"
20 #include "content/browser/indexed_db/indexed_db_database.h"
21 #include "content/browser/indexed_db/indexed_db_database_callbacks.h"
22 #include "content/browser/indexed_db/indexed_db_tracing.h"
23 #include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
24 #include "third_party/leveldatabase/env_chromium.h"
25 
26 namespace content {
27 
28 namespace {
29 
30 const int64_t kInactivityTimeoutPeriodSeconds = 60;
31 
32 // Used for UMA metrics - do not change values.
33 enum UmaIDBException {
34   UmaIDBExceptionUnknownError = 0,
35   UmaIDBExceptionConstraintError = 1,
36   UmaIDBExceptionDataError = 2,
37   UmaIDBExceptionVersionError = 3,
38   UmaIDBExceptionAbortError = 4,
39   UmaIDBExceptionQuotaError = 5,
40   UmaIDBExceptionTimeoutError = 6,
41   UmaIDBExceptionExclusiveMaxValue = 7
42 };
43 
44 // Used for UMA metrics - do not change mappings.
ExceptionCodeToUmaEnum(blink::mojom::IDBException code)45 UmaIDBException ExceptionCodeToUmaEnum(blink::mojom::IDBException code) {
46   switch (code) {
47     case blink::mojom::IDBException::kUnknownError:
48       return UmaIDBExceptionUnknownError;
49     case blink::mojom::IDBException::kConstraintError:
50       return UmaIDBExceptionConstraintError;
51     case blink::mojom::IDBException::kDataError:
52       return UmaIDBExceptionDataError;
53     case blink::mojom::IDBException::kVersionError:
54       return UmaIDBExceptionVersionError;
55     case blink::mojom::IDBException::kAbortError:
56       return UmaIDBExceptionAbortError;
57     case blink::mojom::IDBException::kQuotaError:
58       return UmaIDBExceptionQuotaError;
59     case blink::mojom::IDBException::kTimeoutError:
60       return UmaIDBExceptionTimeoutError;
61     default:
62       NOTREACHED();
63   }
64   return UmaIDBExceptionUnknownError;
65 }
66 
67 }  // namespace
68 
69 IndexedDBTransaction::TaskQueue::TaskQueue() = default;
70 IndexedDBTransaction::TaskQueue::~TaskQueue() = default;
71 
clear()72 void IndexedDBTransaction::TaskQueue::clear() {
73   while (!queue_.empty())
74     queue_.pop();
75 }
76 
pop()77 IndexedDBTransaction::Operation IndexedDBTransaction::TaskQueue::pop() {
78   DCHECK(!queue_.empty());
79   Operation task = std::move(queue_.front());
80   queue_.pop();
81   return task;
82 }
83 
84 IndexedDBTransaction::TaskStack::TaskStack() = default;
85 IndexedDBTransaction::TaskStack::~TaskStack() = default;
86 
clear()87 void IndexedDBTransaction::TaskStack::clear() {
88   while (!stack_.empty())
89     stack_.pop();
90 }
91 
pop()92 IndexedDBTransaction::AbortOperation IndexedDBTransaction::TaskStack::pop() {
93   DCHECK(!stack_.empty());
94   AbortOperation task = std::move(stack_.top());
95   stack_.pop();
96   return task;
97 }
98 
IndexedDBTransaction(int64_t id,IndexedDBConnection * connection,const std::set<int64_t> & object_store_ids,blink::mojom::IDBTransactionMode mode,TasksAvailableCallback tasks_available_callback,TearDownCallback tear_down_callback,IndexedDBBackingStore::Transaction * backing_store_transaction)99 IndexedDBTransaction::IndexedDBTransaction(
100     int64_t id,
101     IndexedDBConnection* connection,
102     const std::set<int64_t>& object_store_ids,
103     blink::mojom::IDBTransactionMode mode,
104     TasksAvailableCallback tasks_available_callback,
105     TearDownCallback tear_down_callback,
106     IndexedDBBackingStore::Transaction* backing_store_transaction)
107     : id_(id),
108       object_store_ids_(object_store_ids),
109       mode_(mode),
110       connection_(connection->GetWeakPtr()),
111       run_tasks_callback_(std::move(tasks_available_callback)),
112       tear_down_callback_(std::move(tear_down_callback)),
113       transaction_(backing_store_transaction) {
114   IDB_ASYNC_TRACE_BEGIN("IndexedDBTransaction::lifetime", this);
115   callbacks_ = connection_->callbacks();
116   database_ = connection_->database();
117   if (database_)
118     database_->TransactionCreated();
119 
120   diagnostics_.tasks_scheduled = 0;
121   diagnostics_.tasks_completed = 0;
122   diagnostics_.creation_time = base::Time::Now();
123 }
124 
~IndexedDBTransaction()125 IndexedDBTransaction::~IndexedDBTransaction() {
126   IDB_ASYNC_TRACE_END("IndexedDBTransaction::lifetime", this);
127   // It shouldn't be possible for this object to get deleted until it's either
128   // complete or aborted.
129   DCHECK_EQ(state_, FINISHED);
130   DCHECK(preemptive_task_queue_.empty());
131   DCHECK_EQ(pending_preemptive_events_, 0);
132   DCHECK(task_queue_.empty());
133   DCHECK(abort_task_stack_.empty());
134   DCHECK(!processing_event_queue_);
135 }
136 
SetCommitFlag()137 void IndexedDBTransaction::SetCommitFlag() {
138   is_commit_pending_ = true;
139   run_tasks_callback_.Run();
140 }
141 
ScheduleTask(blink::mojom::IDBTaskType type,Operation task)142 void IndexedDBTransaction::ScheduleTask(blink::mojom::IDBTaskType type,
143                                         Operation task) {
144   if (state_ == FINISHED)
145     return;
146 
147   timeout_timer_.Stop();
148   used_ = true;
149   if (type == blink::mojom::IDBTaskType::Normal) {
150     task_queue_.push(std::move(task));
151     ++diagnostics_.tasks_scheduled;
152   } else {
153     preemptive_task_queue_.push(std::move(task));
154   }
155   if (state() == STARTED)
156     run_tasks_callback_.Run();
157 }
158 
ScheduleAbortTask(AbortOperation abort_task)159 void IndexedDBTransaction::ScheduleAbortTask(AbortOperation abort_task) {
160   DCHECK_NE(FINISHED, state_);
161   DCHECK(used_);
162   abort_task_stack_.push(std::move(abort_task));
163 }
164 
Abort(const IndexedDBDatabaseError & error)165 leveldb::Status IndexedDBTransaction::Abort(
166     const IndexedDBDatabaseError& error) {
167   if (state_ == FINISHED)
168     return leveldb::Status::OK();
169 
170   base::UmaHistogramEnumeration("WebCore.IndexedDB.TransactionAbortReason",
171                                 ExceptionCodeToUmaEnum(error.code()),
172                                 UmaIDBExceptionExclusiveMaxValue);
173 
174   aborted_ = true;
175   timeout_timer_.Stop();
176 
177   state_ = FINISHED;
178 
179   if (backing_store_transaction_begun_) {
180     leveldb::Status status = transaction_->Rollback();
181     if (!status.ok())
182       return status;
183   }
184 
185   // Run the abort tasks, if any.
186   while (!abort_task_stack_.empty())
187     abort_task_stack_.pop().Run();
188 
189   preemptive_task_queue_.clear();
190   pending_preemptive_events_ = 0;
191 
192   // Backing store resources (held via cursors) must be released
193   // before script callbacks are fired, as the script callbacks may
194   // release references and allow the backing store itself to be
195   // released, and order is critical.
196   CloseOpenCursorBindings();
197 
198   // Open cursors have to be deleted before we clear the task queue.
199   // If we clear the task queue and closures exist in it that refer
200   // to callbacks associated with the cursor mojo bindings, the callback
201   // deletion will fail due to a mojo assert.  |CloseOpenCursorBindings()|
202   // above will clear the binding, which also deletes the owned
203   // |IndexedDBCursor| objects.  After that, we can safely clear the
204   // task queue.
205   task_queue_.clear();
206 
207   transaction_->Reset();
208 
209   // Transactions must also be marked as completed before the
210   // front-end is notified, as the transaction completion unblocks
211   // operations like closing connections.
212   locks_receiver_.locks.clear();
213   locks_receiver_.AbortLockRequest();
214 
215   if (callbacks_.get())
216     callbacks_->OnAbort(*this, error);
217 
218   if (database_)
219     database_->TransactionFinished(mode_, false);
220   run_tasks_callback_.Run();
221   return leveldb::Status::OK();
222 }
223 
224 // static
CommitPhaseTwoProxy(IndexedDBTransaction * transaction)225 leveldb::Status IndexedDBTransaction::CommitPhaseTwoProxy(
226     IndexedDBTransaction* transaction) {
227   return transaction->CommitPhaseTwo();
228 }
229 
IsTaskQueueEmpty() const230 bool IndexedDBTransaction::IsTaskQueueEmpty() const {
231   return preemptive_task_queue_.empty() && task_queue_.empty();
232 }
233 
HasPendingTasks() const234 bool IndexedDBTransaction::HasPendingTasks() const {
235   return pending_preemptive_events_ || !IsTaskQueueEmpty();
236 }
237 
RegisterOpenCursor(IndexedDBCursor * cursor)238 void IndexedDBTransaction::RegisterOpenCursor(IndexedDBCursor* cursor) {
239   open_cursors_.insert(cursor);
240 }
241 
UnregisterOpenCursor(IndexedDBCursor * cursor)242 void IndexedDBTransaction::UnregisterOpenCursor(IndexedDBCursor* cursor) {
243   open_cursors_.erase(cursor);
244 }
245 
Start()246 void IndexedDBTransaction::Start() {
247   // The transaction has the potential to be aborted after the Start() task was
248   // posted.
249   if (state_ == FINISHED) {
250     DCHECK(locks_receiver_.locks.empty());
251     return;
252   }
253   DCHECK_EQ(CREATED, state_);
254   state_ = STARTED;
255   DCHECK(!locks_receiver_.locks.empty());
256   diagnostics_.start_time = base::Time::Now();
257   run_tasks_callback_.Run();
258 }
259 
EnsureBackingStoreTransactionBegun()260 void IndexedDBTransaction::EnsureBackingStoreTransactionBegun() {
261   if (!backing_store_transaction_begun_) {
262     transaction_->Begin(std::move(locks_receiver_.locks));
263     backing_store_transaction_begun_ = true;
264   }
265 }
266 
BlobWriteComplete(BlobWriteResult result)267 leveldb::Status IndexedDBTransaction::BlobWriteComplete(
268     BlobWriteResult result) {
269   IDB_TRACE("IndexedDBTransaction::BlobWriteComplete");
270   if (state_ == FINISHED)  // aborted
271     return leveldb::Status::OK();
272   DCHECK_EQ(state_, COMMITTING);
273 
274   switch (result) {
275     case BlobWriteResult::kFailure: {
276       leveldb::Status status = Abort(IndexedDBDatabaseError(
277           blink::mojom::IDBException::kDataError, "Failed to write blobs."));
278       if (!status.ok())
279         tear_down_callback_.Run(status);
280       // The result is ignored.
281       return leveldb::Status::OK();
282     }
283     case BlobWriteResult::kRunPhaseTwoAsync:
284       ScheduleTask(base::BindOnce(&CommitPhaseTwoProxy));
285       run_tasks_callback_.Run();
286       return leveldb::Status::OK();
287     case BlobWriteResult::kRunPhaseTwoAndReturnResult: {
288       return CommitPhaseTwo();
289     }
290   }
291   NOTREACHED();
292 }
293 
Commit()294 leveldb::Status IndexedDBTransaction::Commit() {
295   IDB_TRACE1("IndexedDBTransaction::Commit", "txn.id", id());
296 
297   timeout_timer_.Stop();
298 
299   // In multiprocess ports, front-end may have requested a commit but
300   // an abort has already been initiated asynchronously by the
301   // back-end.
302   if (state_ == FINISHED)
303     return leveldb::Status::OK();
304   DCHECK_NE(state_, COMMITTING);
305 
306   is_commit_pending_ = true;
307 
308   // Front-end has requested a commit, but this transaction is blocked by
309   // other transactions. The commit will be initiated when the transaction
310   // coordinator unblocks this transaction.
311   if (state_ != STARTED)
312     return leveldb::Status::OK();
313 
314   // Front-end has requested a commit, but there may be tasks like
315   // create_index which are considered synchronous by the front-end
316   // but are processed asynchronously.
317   if (HasPendingTasks())
318     return leveldb::Status::OK();
319 
320   // If a transaction is being committed but it has sent more errors to the
321   // front end than have been handled at this point, the transaction should be
322   // aborted as it is unknown whether or not any errors unaccounted for will be
323   // properly handled.
324   if (num_errors_sent_ != num_errors_handled_) {
325     is_commit_pending_ = false;
326     return Abort(
327         IndexedDBDatabaseError(blink::mojom::IDBException::kUnknownError));
328   }
329 
330   state_ = COMMITTING;
331 
332   leveldb::Status s;
333   if (!used_) {
334     s = CommitPhaseTwo();
335   } else {
336     // CommitPhaseOne will call the callback synchronously if there are no blobs
337     // to write.
338     s = transaction_->CommitPhaseOne(base::BindOnce(
339         [](base::WeakPtr<IndexedDBTransaction> transaction,
340            BlobWriteResult result) {
341           if (!transaction)
342             return leveldb::Status::OK();
343           return transaction->BlobWriteComplete(result);
344         },
345         ptr_factory_.GetWeakPtr()));
346   }
347 
348   return s;
349 }
350 
CommitPhaseTwo()351 leveldb::Status IndexedDBTransaction::CommitPhaseTwo() {
352   // Abort may have been called just as the blob write completed.
353   if (state_ == FINISHED)
354     return leveldb::Status::OK();
355 
356   DCHECK_EQ(state_, COMMITTING);
357 
358   state_ = FINISHED;
359 
360   leveldb::Status s;
361   bool committed;
362   if (!used_) {
363     committed = true;
364   } else {
365     base::TimeDelta active_time = base::Time::Now() - diagnostics_.start_time;
366     uint64_t size_kb = transaction_->GetTransactionSize() / 1024;
367     // All histograms record 1KB to 1GB.
368     switch (mode_) {
369       case blink::mojom::IDBTransactionMode::ReadOnly:
370         UMA_HISTOGRAM_MEDIUM_TIMES(
371             "WebCore.IndexedDB.Transaction.ReadOnly.TimeActive", active_time);
372         UMA_HISTOGRAM_COUNTS_1M(
373             "WebCore.IndexedDB.Transaction.ReadOnly.SizeOnCommit2", size_kb);
374         break;
375       case blink::mojom::IDBTransactionMode::ReadWrite:
376         UMA_HISTOGRAM_MEDIUM_TIMES(
377             "WebCore.IndexedDB.Transaction.ReadWrite.TimeActive", active_time);
378         UMA_HISTOGRAM_COUNTS_1M(
379             "WebCore.IndexedDB.Transaction.ReadWrite.SizeOnCommit2", size_kb);
380         break;
381       case blink::mojom::IDBTransactionMode::VersionChange:
382         UMA_HISTOGRAM_MEDIUM_TIMES(
383             "WebCore.IndexedDB.Transaction.VersionChange.TimeActive",
384             active_time);
385         UMA_HISTOGRAM_COUNTS_1M(
386             "WebCore.IndexedDB.Transaction.VersionChange.SizeOnCommit2",
387             size_kb);
388         break;
389       default:
390         NOTREACHED();
391     }
392 
393     s = transaction_->CommitPhaseTwo();
394     committed = s.ok();
395   }
396 
397   // Backing store resources (held via cursors) must be released
398   // before script callbacks are fired, as the script callbacks may
399   // release references and allow the backing store itself to be
400   // released, and order is critical.
401   CloseOpenCursors();
402   transaction_->Reset();
403 
404   // Transactions must also be marked as completed before the
405   // front-end is notified, as the transaction completion unblocks
406   // operations like closing connections.
407   locks_receiver_.locks.clear();
408 
409   if (committed) {
410     abort_task_stack_.clear();
411 
412     // |observations_callback_| must be called before OnComplete to ensure
413     // consistency of callbacks at renderer.
414     if (!connection_changes_map_.empty()) {
415       if (database_)
416         database_->SendObservations(std::move(connection_changes_map_));
417       connection_changes_map_.clear();
418     }
419     {
420       IDB_TRACE1(
421           "IndexedDBTransaction::CommitPhaseTwo.TransactionCompleteCallbacks",
422           "txn.id", id());
423       callbacks_->OnComplete(*this);
424     }
425     if (!pending_observers_.empty() && connection_)
426       connection_->ActivatePendingObservers(std::move(pending_observers_));
427     if (database_)
428       database_->TransactionFinished(mode_, true);
429     return s;
430   } else {
431     while (!abort_task_stack_.empty())
432       abort_task_stack_.pop().Run();
433 
434     IndexedDBDatabaseError error;
435     if (leveldb_env::IndicatesDiskFull(s)) {
436       error = IndexedDBDatabaseError(
437           blink::mojom::IDBException::kQuotaError,
438           "Encountered disk full while committing transaction.");
439     } else {
440       error = IndexedDBDatabaseError(blink::mojom::IDBException::kUnknownError,
441                                      "Internal error committing transaction.");
442     }
443     callbacks_->OnAbort(*this, error);
444     if (database_)
445       database_->TransactionFinished(mode_, false);
446   }
447   return s;
448 }
449 
450 std::tuple<IndexedDBTransaction::RunTasksResult, leveldb::Status>
RunTasks()451 IndexedDBTransaction::RunTasks() {
452   IDB_TRACE1("IndexedDBTransaction::RunTasks", "txn.id", id());
453 
454   DCHECK(!processing_event_queue_);
455 
456   // May have been aborted.
457   if (aborted_)
458     return std::make_tuple(RunTasksResult::kAborted, leveldb::Status::OK());
459   if (IsTaskQueueEmpty() && !is_commit_pending_)
460     return std::make_tuple(RunTasksResult::kNotFinished, leveldb::Status::OK());
461 
462   processing_event_queue_ = true;
463 
464   if (!backing_store_transaction_begun_) {
465     transaction_->Begin(std::move(locks_receiver_.locks));
466     backing_store_transaction_begun_ = true;
467   }
468 
469   bool run_preemptive_queue =
470       !preemptive_task_queue_.empty() || pending_preemptive_events_ != 0;
471   TaskQueue* task_queue =
472       run_preemptive_queue ? &preemptive_task_queue_ : &task_queue_;
473   while (!task_queue->empty() && state_ != FINISHED) {
474     DCHECK(state_ == STARTED || state_ == COMMITTING) << state_;
475     Operation task(task_queue->pop());
476     leveldb::Status result = std::move(task).Run(this);
477     if (!run_preemptive_queue) {
478       DCHECK(diagnostics_.tasks_completed < diagnostics_.tasks_scheduled);
479       ++diagnostics_.tasks_completed;
480     }
481     if (!result.ok()) {
482       processing_event_queue_ = false;
483       return std::make_tuple(
484           RunTasksResult::kError,
485           result
486       );
487     }
488 
489     run_preemptive_queue =
490         !preemptive_task_queue_.empty() || pending_preemptive_events_ != 0;
491     // Event itself may change which queue should be processed next.
492     task_queue = run_preemptive_queue ? &preemptive_task_queue_ : &task_queue_;
493   }
494 
495   // If there are no pending tasks, we haven't already committed/aborted,
496   // and the front-end requested a commit, it is now safe to do so.
497   if (!HasPendingTasks() && state_ == STARTED && is_commit_pending_) {
498     processing_event_queue_ = false;
499     // This can delete |this|.
500     leveldb::Status result = Commit();
501     if (!result.ok())
502       return std::make_tuple(RunTasksResult::kError, result);
503   }
504 
505   // The transaction may have been aborted while processing tasks.
506   if (state_ == FINISHED) {
507     processing_event_queue_ = false;
508     return std::make_tuple(aborted_ ? RunTasksResult::kAborted : RunTasksResult::kCommitted,
509             leveldb::Status::OK());
510   }
511 
512   DCHECK(state_ == STARTED || state_ == COMMITTING) << state_;
513 
514   // Otherwise, start a timer in case the front-end gets wedged and
515   // never requests further activity. Read-only transactions don't
516   // block other transactions, so don't time those out.
517   if (!HasPendingTasks() &&
518       mode_ != blink::mojom::IDBTransactionMode::ReadOnly &&
519       state_ == STARTED) {
520     timeout_timer_.Start(FROM_HERE, GetInactivityTimeout(),
521                          base::BindOnce(&IndexedDBTransaction::Timeout,
522                                         ptr_factory_.GetWeakPtr()));
523   }
524   processing_event_queue_ = false;
525   return std::make_tuple(RunTasksResult::kNotFinished, leveldb::Status::OK());
526 }
527 
GetInactivityTimeout() const528 base::TimeDelta IndexedDBTransaction::GetInactivityTimeout() const {
529   return base::TimeDelta::FromSeconds(kInactivityTimeoutPeriodSeconds);
530 }
531 
Timeout()532 void IndexedDBTransaction::Timeout() {
533   leveldb::Status result = Abort(IndexedDBDatabaseError(
534       blink::mojom::IDBException::kTimeoutError,
535       base::ASCIIToUTF16("Transaction timed out due to inactivity.")));
536   if (!result.ok())
537     tear_down_callback_.Run(result);
538 }
539 
CloseOpenCursorBindings()540 void IndexedDBTransaction::CloseOpenCursorBindings() {
541   IDB_TRACE1("IndexedDBTransaction::CloseOpenCursorBindings", "txn.id", id());
542   std::vector<IndexedDBCursor*> cursor_ptrs(open_cursors_.begin(),
543                                             open_cursors_.end());
544   for (auto* cursor_ptr : cursor_ptrs)
545     cursor_ptr->RemoveBinding();
546 }
547 
CloseOpenCursors()548 void IndexedDBTransaction::CloseOpenCursors() {
549   IDB_TRACE1("IndexedDBTransaction::CloseOpenCursors", "txn.id", id());
550 
551   // IndexedDBCursor::Close() indirectly mutates |open_cursors_|, when it calls
552   // IndexedDBTransaction::UnregisterOpenCursor().
553   std::set<IndexedDBCursor*> open_cursors = std::move(open_cursors_);
554   open_cursors_.clear();
555   for (auto* cursor : open_cursors)
556     cursor->Close();
557 }
558 
AddPendingObserver(int32_t observer_id,const IndexedDBObserver::Options & options)559 void IndexedDBTransaction::AddPendingObserver(
560     int32_t observer_id,
561     const IndexedDBObserver::Options& options) {
562   DCHECK_NE(mode(), blink::mojom::IDBTransactionMode::VersionChange);
563   pending_observers_.push_back(std::make_unique<IndexedDBObserver>(
564       observer_id, object_store_ids_, options));
565 }
566 
RemovePendingObservers(const std::vector<int32_t> & pending_observer_ids)567 void IndexedDBTransaction::RemovePendingObservers(
568     const std::vector<int32_t>& pending_observer_ids) {
569   const auto& it = std::remove_if(
570       pending_observers_.begin(), pending_observers_.end(),
571       [&pending_observer_ids](const std::unique_ptr<IndexedDBObserver>& o) {
572         return base::Contains(pending_observer_ids, o->id());
573       });
574   if (it != pending_observers_.end())
575     pending_observers_.erase(it, pending_observers_.end());
576 }
577 
AddObservation(int32_t connection_id,blink::mojom::IDBObservationPtr observation)578 void IndexedDBTransaction::AddObservation(
579     int32_t connection_id,
580     blink::mojom::IDBObservationPtr observation) {
581   auto it = connection_changes_map_.find(connection_id);
582   if (it == connection_changes_map_.end()) {
583     it = connection_changes_map_
584              .emplace(connection_id, blink::mojom::IDBObserverChanges::New())
585              .first;
586   }
587   it->second->observations.push_back(std::move(observation));
588 }
589 
590 blink::mojom::IDBObserverChangesPtr*
GetPendingChangesForConnection(int32_t connection_id)591 IndexedDBTransaction::GetPendingChangesForConnection(int32_t connection_id) {
592   auto it = connection_changes_map_.find(connection_id);
593   if (it != connection_changes_map_.end())
594     return &it->second;
595   return nullptr;
596 }
597 
598 }  // namespace content
599