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