Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(69)

Side by Side Diff: components/offline_pages/background/request_queue_store_sql.cc

Issue 2053163002: [Offline pages] Adding persistent request queue based on SQLite (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Addressing code review feedback from petewil Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 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/background/request_queue_store_sql.h"
6
7 #include "base/bind.h"
8 #include "base/files/file_path.h"
9 #include "base/files/file_util.h"
10 #include "base/location.h"
11 #include "base/logging.h"
12 #include "base/sequenced_task_runner.h"
13 #include "base/threading/thread_task_runner_handle.h"
14 #include "components/offline_pages/background/save_page_request.h"
15 #include "sql/connection.h"
16 #include "sql/statement.h"
17 #include "sql/transaction.h"
18
19 namespace offline_pages {
20
21 namespace {
22
23 // This is a macro instead of a const so that
24 // it can be used inline in other SQL statements below.
25 #define REQUEST_QUEUE_TABLE_NAME "request_queue_v1"
26
27 // New columns should be added at the end of the list in order to avoid
28 // complicated table upgrade.
29 const char kOfflinePagesColumns[] =
30 "(request_id INTEGER PRIMARY KEY NOT NULL,"
31 " creation_time INTEGER NOT NULL,"
32 " activation_time INTEGER NOT NULL DEFAULT 0,"
33 " last_attempt_time INTEGER NOT NULL DEFAULT 0,"
34 " attempt_count INTEGER NOT NULL,"
35 " url VARCHAR NOT NULL,"
36 " client_namespace VARCHAR NOT NULL,"
37 " client_id VARCHAR NOT NULL"
38 ")";
39
40 // Defines indices of the columns in a SELECT * FROM query. Should be kept in
41 // sync with above.
42 enum : int {
43 RQ_REQUEST_ID,
44 RQ_CREATION_TIME,
45 RQ_ACTIVATION_TIME,
46 RQ_LAST_ATTMEPT_TIME,
47 RQ_ATTEMPT_COUNT,
48 RQ_URL,
49 RQ_CLIENT_NAMESPACE,
50 RQ_CLIENT_ID,
51 };
52
53 enum class RequestExistsResult {
54 SQL_FAILED,
55 REQUEST_EXISTS,
56 REQUEST_DOES_NOT_EXIST,
57 };
58
59 // This is cloned from //content/browser/appcache/appcache_database.cc
60 struct TableInfo {
61 const char* table_name;
62 const char* columns;
63 };
64
65 const TableInfo kRequestQueueTable{REQUEST_QUEUE_TABLE_NAME,
66 kOfflinePagesColumns};
67
68 bool CreateTable(sql::Connection* db, const TableInfo& table_info) {
69 std::string sql("CREATE TABLE ");
70 sql += table_info.table_name;
71 sql += table_info.columns;
72 return db->Execute(sql.c_str());
Scott Hess - ex-Googler 2016/06/20 19:38:32 I think I mentioned that this approach is weird in
fgorski 2016/06/20 23:42:59 Hey, if there is a suggestion how to better write
73 }
74
75 bool CreateSchema(sql::Connection* db) {
76 // If you create a transaction but don't Commit() it is automatically
77 // rolled back by its destructor when it falls out of scope.
78 sql::Transaction transaction(db);
79 if (!transaction.Begin())
80 return false;
81
82 if (!db->DoesTableExist(kRequestQueueTable.table_name)) {
83 if (!CreateTable(db, kRequestQueueTable))
Scott Hess - ex-Googler 2016/06/20 19:38:33 "CREATE TABLE IF NOT EXISTS " should work the same
fgorski 2016/06/20 23:43:00 Done. I am keeping the CreateTable method separate
Scott Hess - ex-Googler 2016/06/22 18:31:45 Since you don't have a MetaTable instance or somet
fgorski 2016/06/22 23:32:16 The way we think of doing schema changes is pretty
84 return false;
85 }
86
87 // TODO(fgorski): Add indices here.
88 return transaction.Commit();
89 }
90
91 bool DeleteByRequestId(sql::Connection* db, int64_t request_id) {
92 static const char kSql[] =
Scott Hess - ex-Googler 2016/06/20 19:38:32 I don't think you need static here. The literal s
fgorski 2016/06/20 23:43:00 Done.
93 "DELETE FROM " REQUEST_QUEUE_TABLE_NAME " WHERE request_id=?";
94 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
95 statement.BindInt64(0, request_id);
96 LOG(ERROR) << "In DeleteRequestById: "
97 << request_id; // statement.GetSQLStatement();
Scott Hess - ex-Googler 2016/06/20 19:38:32 Drop the logging, or make it VLOG. Or just drop i
fgorski 2016/06/20 23:42:59 Done. This code is a leftover from debugging.
98 return statement.Run();
99 }
100
101 // Create a save page request from a SQL result. Expects complete rows with
102 // all columns present.
103 SavePageRequest MakeSavePageRequest(sql::Statement* statement) {
Scott Hess - ex-Googler 2016/06/20 19:38:32 Will const sql::Statement& work, here? My notion
fgorski 2016/06/20 23:43:00 Done.
104 int64_t id = statement->ColumnInt64(RQ_REQUEST_ID);
105 GURL url(statement->ColumnString(RQ_URL));
Scott Hess - ex-Googler 2016/06/20 19:38:33 Maybe const all of these. consting the GURL, Clie
fgorski 2016/06/20 23:42:59 Done.
106 ClientId client_id(statement->ColumnString(RQ_CLIENT_NAMESPACE),
107 statement->ColumnString(RQ_CLIENT_ID));
108 int64_t attempt_count = statement->ColumnInt64(RQ_ATTEMPT_COUNT);
109 base::Time creation_time =
110 base::Time::FromInternalValue(statement->ColumnInt64(RQ_CREATION_TIME));
111 base::Time activation_time =
112 base::Time::FromInternalValue(statement->ColumnInt64(RQ_ACTIVATION_TIME));
113
114 SavePageRequest request(id, url, client_id, creation_time, activation_time);
115 base::Time last_attempt_time = base::Time::FromInternalValue(
116 statement->ColumnInt64(RQ_LAST_ATTMEPT_TIME));
Scott Hess - ex-Googler 2016/06/20 19:38:32 Either push last_attempt_time up with the other ge
fgorski 2016/06/20 23:43:00 Unfortunately that would not work, because they ar
117 request.set_last_attempt_time(last_attempt_time);
118 request.set_attempt_count(attempt_count);
119
120 return request;
121 }
122
123 RequestExistsResult RequestExists(sql::Connection* db, int64_t request_id) {
124 const char kSql[] =
125 "SELECT COUNT(*) FROM " REQUEST_QUEUE_TABLE_NAME " WHERE request_id = ?";
126 sql::Statement statement(db->GetUniqueStatement(kSql));
127 statement.BindInt64(0, request_id);
128 if (!statement.Step()) {
129 LOG(ERROR) << "Failed to check if request exists: " << request_id
130 << ", SQL statement: " << statement.GetSQLStatement();
Scott Hess - ex-Googler 2016/06/20 19:38:32 Again, suggest dropping the log line. Note that i
fgorski 2016/06/20 23:43:00 Done. Again debugging -> I had hard time with gett
Scott Hess - ex-Googler 2016/06/22 18:31:45 OK. I've often thought it would be nice to have a
fgorski 2016/06/22 23:32:16 Acknowledged. It was just not clear to me how to q
Scott Hess - ex-Googler 2016/06/22 23:46:05 I would err on the side of unique over cached. Th
131 return RequestExistsResult::SQL_FAILED;
132 }
133 return statement.ColumnInt64(0) ? RequestExistsResult::REQUEST_EXISTS
134 : RequestExistsResult::REQUEST_DOES_NOT_EXIST;
135 }
136
137 RequestQueueStore::UpdateStatus InsertOrReplace(
138 sql::Connection* db,
139 const SavePageRequest& request) {
140 // In order to use the enums in the Bind* methods, keep the order of fields
141 // the same as in the definition/select query.
Scott Hess - ex-Googler 2016/06/20 19:38:32 Did I mention this is weird? It's like you're bui
fgorski 2016/06/20 23:43:00 OK. I'll gladly remove the comment. Is there a sug
142 const char kInsertSql[] =
143 "INSERT OR REPLACE INTO " REQUEST_QUEUE_TABLE_NAME
144 " (request_id, creation_time, activation_time, last_attempt_time, "
145 " attempt_count, url, client_namespace, client_id) "
146 " VALUES "
147 " (?, ?, ?, ?, ?, ?, ?, ?)";
148
149 // Checking if a request exists and adding/updating it will happen in a single
150 // transaction.
151 sql::Transaction transaction(db);
152 if (!transaction.Begin()) {
153 LOG(ERROR) << "Failed to start transaction on InsertOrReplace.";
Scott Hess - ex-Googler 2016/06/20 19:38:33 VLOG or drop the logging. I think the only way tr
fgorski 2016/06/20 23:43:00 Done.
154 return RequestQueueStore::UpdateStatus::FAILED;
155 }
156
157 RequestExistsResult exists = RequestExists(db, request.request_id());
158 if (exists == RequestExistsResult::SQL_FAILED) {
159 LOG(ERROR) << "Failed to check if request exists: " << request.request_id();
160 return RequestQueueStore::UpdateStatus::FAILED;
161 }
162
163 RequestQueueStore::UpdateStatus status =
164 exists == RequestExistsResult::REQUEST_EXISTS
165 ? RequestQueueStore::UpdateStatus::UPDATED
166 : RequestQueueStore::UpdateStatus::ADDED;
Scott Hess - ex-Googler 2016/06/20 19:38:33 Is tracking whether this was added or replaced rea
fgorski 2016/06/20 23:43:00 Yeah, I don't like it, but that was requested by a
Scott Hess - ex-Googler 2016/06/22 18:31:45 If someone is going to use it, fine. My main conc
fgorski 2016/06/22 23:32:16 We discussed it internally and decided against it
167
168 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kInsertSql));
169 statement.BindInt64(RQ_REQUEST_ID, request.request_id());
170 statement.BindString(RQ_URL, request.url().spec());
171 statement.BindString(RQ_CLIENT_NAMESPACE, request.client_id().name_space);
172 statement.BindString(RQ_CLIENT_ID, request.client_id().id);
173 statement.BindInt64(RQ_CREATION_TIME,
174 request.creation_time().ToInternalValue());
175 statement.BindInt64(RQ_ACTIVATION_TIME,
176 request.activation_time().ToInternalValue());
177 statement.BindInt64(RQ_LAST_ATTMEPT_TIME,
178 request.last_attempt_time().ToInternalValue());
179 statement.BindInt64(RQ_ATTEMPT_COUNT, request.attempt_count());
180
181 if (!statement.Run() || !transaction.Commit()) {
182 LOG(ERROR) << "Failed to add/update a request: " << request.request_id();
183 return RequestQueueStore::UpdateStatus::FAILED;
184 }
185
186 return status;
187 }
188
189 bool InitDatabase(sql::Connection* db, const base::FilePath& path) {
190 db->set_page_size(4096);
191 db->set_cache_size(500);
192 db->set_histogram_tag("BackgroundRequestQueue");
193 db->set_exclusive_locking();
194
195 base::File::Error err;
196 if (!base::CreateDirectoryAndGetError(path.DirName(), &err)) {
Scott Hess - ex-Googler 2016/06/20 19:38:33 Does this need an entire directory for two files (
fgorski 2016/06/20 23:43:00 We like things separated and don't mind having a d
Scott Hess - ex-Googler 2016/06/22 18:31:45 Acknowledged.
197 LOG(ERROR) << "Failed to create background request queue db directory: "
198 << base::File::ErrorToString(err);
199 return false;
200 }
201 if (!db->Open(path)) {
202 LOG(ERROR) << "Failed to open database";
203 return false;
204 }
205 db->Preload();
206
207 return CreateSchema(db);
208 }
209
210 } // anonymous namespace
211
212 RequestQueueStoreSQL::RequestQueueStoreSQL(
213 scoped_refptr<base::SequencedTaskRunner> background_task_runner,
214 const base::FilePath& path)
215 : background_task_runner_(std::move(background_task_runner)),
216 db_file_path_(path.AppendASCII("RequestQueue.db")),
217 weak_ptr_factory_(this) {
218 OpenConnection();
219 }
220
221 RequestQueueStoreSQL::~RequestQueueStoreSQL() {
222 if (db_.get() &&
223 !background_task_runner_->DeleteSoon(FROM_HERE, db_.release())) {
224 DLOG(WARNING) << "SQL database will not be deleted.";
225 }
226 }
227
228 // static
229 void RequestQueueStoreSQL::OpenConnectionSync(
230 sql::Connection* db,
231 scoped_refptr<base::SingleThreadTaskRunner> runner,
232 const base::FilePath& path,
233 const base::Callback<void(bool)>& callback) {
234 bool success = InitDatabase(db, path);
235 runner->PostTask(FROM_HERE, base::Bind(callback, success));
236 }
237
238 // static
239 void RequestQueueStoreSQL::GetRequestsSync(
240 sql::Connection* db,
241 scoped_refptr<base::SingleThreadTaskRunner> runner,
242 const GetRequestsCallback& callback) {
243 const char kSql[] = "SELECT * FROM " REQUEST_QUEUE_TABLE_NAME;
244
245 sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
246
247 std::vector<SavePageRequest> result;
248 while (statement.Step()) {
249 result.push_back(MakeSavePageRequest(&statement));
250 }
251
252 runner->PostTask(FROM_HERE,
253 base::Bind(callback, statement.Succeeded(), result));
254 }
255
256 // static
257 void RequestQueueStoreSQL::AddOrUpdateRequestSync(
258 sql::Connection* db,
259 scoped_refptr<base::SingleThreadTaskRunner> runner,
260 const SavePageRequest& request,
261 const UpdateCallback& callback) {
262 // TODO(fgorski): add UMA metrics here.
263 RequestQueueStore::UpdateStatus status = InsertOrReplace(db, request);
264 runner->PostTask(FROM_HERE, base::Bind(callback, status));
265 }
266
267 // static
268 void RequestQueueStoreSQL::RemoveRequestsSync(
269 sql::Connection* db,
270 scoped_refptr<base::SingleThreadTaskRunner> runner,
271 const std::vector<int64_t>& request_ids,
272 const RemoveCallback& callback) {
273 // TODO(fgorski): add UMA metrics here.
274
275 // If you create a transaction but don't Commit() it is automatically
276 // rolled back by its destructor when it falls out of scope.
277 sql::Transaction transaction(db);
278 if (!transaction.Begin()) {
279 runner->PostTask(FROM_HERE, base::Bind(callback, false, 0));
280 return;
Scott Hess - ex-Googler 2016/06/20 19:38:33 This boilerplate seems brittle. Would it make sen
fgorski 2016/06/20 23:42:59 Done.
281 }
282 int count = 0;
Scott Hess - ex-Googler 2016/06/20 19:38:33 I usually expect countable things to be size_t.
fgorski 2016/06/20 23:42:59 I played with the idea, however: a) GetLastChangeC
Scott Hess - ex-Googler 2016/06/22 18:31:45 OK. I think the generic ruling is something like
fgorski 2016/06/22 23:32:16 OK. Until we change the interface to be size_t I w
283 for (auto request_id : request_ids) {
284 RequestExistsResult exists = RequestExists(db, request_id);
285 if (exists == RequestExistsResult::SQL_FAILED) {
286 runner->PostTask(FROM_HERE, base::Bind(callback, false, 0));
287 return;
288 }
289 if (exists == RequestExistsResult::REQUEST_DOES_NOT_EXIST)
290 continue;
291
292 ++count;
293 if (!DeleteByRequestId(db, request_id)) {
294 runner->PostTask(FROM_HERE, base::Bind(callback, false, 0));
295 return;
296 }
Scott Hess - ex-Googler 2016/06/20 19:38:33 The contents of this loop seem overly complicated.
fgorski 2016/06/20 23:43:00 Done. Brilliant idea and it is on the connection n
297 }
298
299 if (!transaction.Commit()) {
300 runner->PostTask(FROM_HERE, base::Bind(callback, false, 0));
301 return;
302 }
303
304 runner->PostTask(FROM_HERE, base::Bind(callback, true, count));
305 }
306
307 // static
308 void RequestQueueStoreSQL::ResetSync(
309 std::unique_ptr<sql::Connection> db,
310 const base::FilePath& db_file_path,
311 scoped_refptr<base::SingleThreadTaskRunner> runner,
312 const ResetCallback& callback) {
313 // This method deletes the content of the whole store. It should be used with
314 // caution, e.g. when recovering from situation, in which the contents of the
Scott Hess - ex-Googler 2016/06/20 19:38:32 "when recovering from situation" doesn't really pa
fgorski 2016/06/20 23:42:59 I am not sure what exactly you didn't parse, but I
315 // store does not make sense and cannot be converted to reasonable requests.
316 db->Close();
317 bool success = base::DeleteFile(db_file_path, /* recursive */ false);
Scott Hess - ex-Googler 2016/06/20 19:38:33 Don't use DeleteFile(). Use sql::Connection::Dele
fgorski 2016/06/20 23:43:00 Done.
Scott Hess - ex-Googler 2016/06/22 18:31:44 Given the other point about this class managing a
fgorski 2016/06/22 23:32:16 The only thing I would be worried about is when th
318 runner->PostTask(FROM_HERE, base::Bind(callback, success));
319 }
320
321 void RequestQueueStoreSQL::GetRequests(const GetRequestsCallback& callback) {
322 DCHECK(db_.get());
323 if (!db_.get()) {
324 // Nothing to do, but post a callback instead of calling directly
325 // to preserve the async style behavior to prevent bugs.
326 base::ThreadTaskRunnerHandle::Get()->PostTask(
327 FROM_HERE, base::Bind(callback, false, std::vector<SavePageRequest>()));
328 return;
329 }
330
331 background_task_runner_->PostTask(
332 FROM_HERE, base::Bind(&RequestQueueStoreSQL::GetRequestsSync, db_.get(),
333 base::ThreadTaskRunnerHandle::Get(), callback));
334 }
335
336 void RequestQueueStoreSQL::AddOrUpdateRequest(const SavePageRequest& request,
337 const UpdateCallback& callback) {
338 DCHECK(db_.get());
339 if (!db_.get()) {
340 // Nothing to do, but post a callback instead of calling directly
341 // to preserve the async style behavior to prevent bugs.
342 base::ThreadTaskRunnerHandle::Get()->PostTask(
343 FROM_HERE, base::Bind(callback, UpdateStatus::FAILED));
344 return;
345 }
346
347 background_task_runner_->PostTask(
348 FROM_HERE,
349 base::Bind(&RequestQueueStoreSQL::AddOrUpdateRequestSync, db_.get(),
350 base::ThreadTaskRunnerHandle::Get(), request, callback));
351 }
352
353 void RequestQueueStoreSQL::RemoveRequests(
354 const std::vector<int64_t>& request_ids,
355 const RemoveCallback& callback) {
356 DCHECK(db_.get());
357 if (!db_.get()) {
358 // Nothing to do, but post a callback instead of calling directly
359 // to preserve the async style behavior to prevent bugs.
360 base::ThreadTaskRunnerHandle::Get()->PostTask(
361 FROM_HERE, base::Bind(callback, false, 0));
362 return;
363 }
364
365 if (request_ids.empty()) {
366 // Nothing to do, but post a callback instead of calling directly
367 // to preserve the async style behavior to prevent bugs.
Scott Hess - ex-Googler 2016/06/20 19:38:32 Should there be a DCHECK for this case? Also, IMH
fgorski 2016/06/20 23:43:00 No that is OK.
368 base::ThreadTaskRunnerHandle::Get()->PostTask(
369 FROM_HERE, base::Bind(callback, true, 0));
370 return;
371 }
372
373 background_task_runner_->PostTask(
374 FROM_HERE,
375 base::Bind(&RequestQueueStoreSQL::RemoveRequestsSync, db_.get(),
376 base::ThreadTaskRunnerHandle::Get(), request_ids, callback));
377 }
378
379 void RequestQueueStoreSQL::Reset(const ResetCallback& callback) {
380 DCHECK(db_.get());
381 if (!db_.get()) {
382 // Nothing to do, but post a callback instead of calling directly
383 // to preserve the async style behavior to prevent bugs.
384 base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
385 base::Bind(callback, false));
386 return;
387 }
388
389 background_task_runner_->PostTask(
390 FROM_HERE,
391 base::Bind(&RequestQueueStoreSQL::ResetSync, base::Passed(&db_),
392 db_file_path_, base::ThreadTaskRunnerHandle::Get(),
393 base::Bind(&RequestQueueStoreSQL::OnResetDone,
394 weak_ptr_factory_.GetWeakPtr(), callback)));
395 }
396
397 void RequestQueueStoreSQL::OpenConnection() {
398 DCHECK(!db_);
399 db_.reset(new sql::Connection());
400 background_task_runner_->PostTask(
401 FROM_HERE,
402 base::Bind(&RequestQueueStoreSQL::OpenConnectionSync, db_.get(),
403 base::ThreadTaskRunnerHandle::Get(), db_file_path_,
404 base::Bind(&RequestQueueStoreSQL::OnOpenConnectionDone,
405 weak_ptr_factory_.GetWeakPtr())));
406 }
407
408 void RequestQueueStoreSQL::OnOpenConnectionDone(bool success) {
409 DCHECK(db_.get());
410
411 // Unfortunately we were not able to open DB connection.
412 if (!success) {
413 LOG(ERROR) << "Database creation fialed.";
Scott Hess - ex-Googler 2016/06/20 19:38:33 "fialed", though if the logging were removed entir
fgorski 2016/06/20 23:43:00 Done.
414 db_.reset();
415 }
416
417 // If reset callback is set this opening followed a reset. Inform the caller
418 // about the success.
Scott Hess - ex-Googler 2016/06/20 19:38:33 If you bundled the background processing together
fgorski 2016/06/20 23:42:59 OK. I managed to get rid of reset_callback_.
419 if (!reset_callback_.is_null()) {
420 reset_callback_.Run(success);
421 reset_callback_.Reset();
422 }
423 }
424
425 void RequestQueueStoreSQL::OnResetDone(const ResetCallback& callback,
426 bool success) {
427 if (!success) {
428 callback.Run(false);
429 return;
430 }
431
432 DCHECK(reset_callback_.is_null());
433 reset_callback_ = callback;
434 OpenConnection();
435 }
436
437 } // namespace offline_pages
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698