diff options
-rw-r--r-- | chrome/common/net/cookie_monster_sqlite.cc | 75 | ||||
-rw-r--r-- | chrome/common/net/cookie_monster_sqlite.h | 2 | ||||
-rw-r--r-- | net/base/cookie_monster.cc | 51 | ||||
-rw-r--r-- | net/base/cookie_monster.h | 33 | ||||
-rw-r--r-- | net/base/cookie_monster_unittest.cc | 43 |
5 files changed, 174 insertions, 30 deletions
diff --git a/chrome/common/net/cookie_monster_sqlite.cc b/chrome/common/net/cookie_monster_sqlite.cc index e4e4a79..11bb002 100644 --- a/chrome/common/net/cookie_monster_sqlite.cc +++ b/chrome/common/net/cookie_monster_sqlite.cc @@ -37,12 +37,17 @@ class SQLitePersistentCookieStore::Backend DCHECK(num_pending_ == 0 && pending_.empty()); } - // Batch a cookie add + // Batch a cookie addition. void AddCookie(const std::string& key, const net::CookieMonster::CanonicalCookie& cc); - // Batch a cookie delete + + // Batch a cookie access time update. + void UpdateCookieAccessTime(const net::CookieMonster::CanonicalCookie& cc); + + // Batch a cookie deletion. void DeleteCookie(const net::CookieMonster::CanonicalCookie& cc); - // Commit and pending operations and close the database, must be called + + // Commit any pending operations and close the database. This must be called // before the object is destructed. void Close(); @@ -51,6 +56,7 @@ class SQLitePersistentCookieStore::Backend public: typedef enum { COOKIE_ADD, + COOKIE_UPDATEACCESS, COOKIE_DELETE, } OperationType; @@ -97,6 +103,11 @@ void SQLitePersistentCookieStore::Backend::AddCookie( BatchOperation(PendingOperation::COOKIE_ADD, key, cc); } +void SQLitePersistentCookieStore::Backend::UpdateCookieAccessTime( + const net::CookieMonster::CanonicalCookie& cc) { + BatchOperation(PendingOperation::COOKIE_UPDATEACCESS, std::string(), cc); +} + void SQLitePersistentCookieStore::Backend::DeleteCookie( const net::CookieMonster::CanonicalCookie& cc) { BatchOperation(PendingOperation::COOKIE_DELETE, std::string(), cc); @@ -150,13 +161,21 @@ void SQLitePersistentCookieStore::Backend::Commit() { SQLITE_UNIQUE_STATEMENT(add_smt, *cache_, "INSERT INTO cookies (creation_utc, host_key, name, value, path, " - "expires_utc, secure, httponly) " - "VALUES (?,?,?,?,?,?,?,?)"); + "expires_utc, secure, httponly, last_access_utc) " + "VALUES (?,?,?,?,?,?,?,?,?)"); if (!add_smt.is_valid()) { NOTREACHED(); return; } + SQLITE_UNIQUE_STATEMENT(update_access_smt, *cache_, + "UPDATE cookies SET last_access_utc=? " + "WHERE creation_utc=?"); + if (!update_access_smt.is_valid()) { + NOTREACHED(); + return; + } + SQLITE_UNIQUE_STATEMENT(del_smt, *cache_, "DELETE FROM cookies WHERE creation_utc=?"); if (!del_smt.is_valid()) { @@ -181,11 +200,23 @@ void SQLitePersistentCookieStore::Backend::Commit() { add_smt->bind_int64(5, po->cc().ExpiryDate().ToInternalValue()); add_smt->bind_int(6, po->cc().IsSecure()); add_smt->bind_int(7, po->cc().IsHttpOnly()); + add_smt->bind_int64(8, po->cc().LastAccessDate().ToInternalValue()); if (add_smt->step() != SQLITE_DONE) { NOTREACHED() << "Could not add a cookie to the DB."; } break; + case PendingOperation::COOKIE_UPDATEACCESS: + update_access_smt->reset(); + update_access_smt->bind_int64(0, + po->cc().LastAccessDate().ToInternalValue()); + update_access_smt->bind_int64(1, + po->cc().CreationDate().ToInternalValue()); + if (update_access_smt->step() != SQLITE_DONE) { + NOTREACHED() << "Could not update cookie last access time in the DB."; + } + break; + case PendingOperation::COOKIE_DELETE: del_smt->reset(); del_smt->bind_int64(0, po->cc().CreationDate().ToInternalValue()); @@ -241,8 +272,8 @@ SQLitePersistentCookieStore::~SQLitePersistentCookieStore() { } // Version number of the database. -static const int kCurrentVersionNumber = 2; -static const int kCompatibleVersionNumber = 2; +static const int kCurrentVersionNumber = 3; +static const int kCompatibleVersionNumber = 3; namespace { @@ -258,7 +289,8 @@ bool InitTable(sqlite3* db) { // We only store persistent, so we know it expires "expires_utc INTEGER NOT NULL," "secure INTEGER NOT NULL," - "httponly INTEGER NOT NULL)", + "httponly INTEGER NOT NULL," + "last_access_utc INTEGER NOT NULL)", NULL, NULL, NULL) != SQLITE_OK) return false; } @@ -292,7 +324,7 @@ bool SQLitePersistentCookieStore::Load( SQLStatement smt; if (smt.prepare(db, "SELECT creation_utc, host_key, name, value, path, expires_utc, secure, " - "httponly FROM cookies") != SQLITE_OK) { + "httponly, last_access_utc FROM cookies") != SQLITE_OK) { NOTREACHED() << "select statement prep failed"; sqlite3_close(db); return false; @@ -319,6 +351,7 @@ bool SQLitePersistentCookieStore::Load( smt.column_int(6) != 0, // secure smt.column_int(7) != 0, // httponly Time::FromInternalValue(smt.column_int64(0)), // creation_utc + Time::FromInternalValue(smt.column_int64(8)), // last_access_utc true, // has_expires Time::FromInternalValue(smt.column_int64(5)))); // expires_utc // Memory allocation failed. @@ -350,6 +383,24 @@ bool SQLitePersistentCookieStore::EnsureDatabaseVersion(sqlite3* db) { } int cur_version = meta_table_.GetVersionNumber(); + if (cur_version == 2) { + SQLTransaction transaction(db); + transaction.Begin(); + if ((sqlite3_exec(db, + "ALTER TABLE cookies ADD COLUMN last_access_utc " + "INTEGER DEFAULT 0", NULL, NULL, NULL) != SQLITE_OK) || + (sqlite3_exec(db, + "UPDATE cookies SET last_access_utc = creation_utc", + NULL, NULL, NULL) != SQLITE_OK)) { + LOG(WARNING) << "Unable to update cookie database to version 3."; + return false; + } + ++cur_version; + meta_table_.SetVersionNumber(cur_version); + meta_table_.SetCompatibleVersionNumber( + std::min(cur_version, kCompatibleVersionNumber)); + transaction.Commit(); + } // Put future migration cases here. @@ -368,6 +419,12 @@ void SQLitePersistentCookieStore::AddCookie( backend_->AddCookie(key, cc); } +void SQLitePersistentCookieStore::UpdateCookieAccessTime( + const net::CookieMonster::CanonicalCookie& cc) { + if (backend_.get()) + backend_->UpdateCookieAccessTime(cc); +} + void SQLitePersistentCookieStore::DeleteCookie( const net::CookieMonster::CanonicalCookie& cc) { if (backend_.get()) diff --git a/chrome/common/net/cookie_monster_sqlite.h b/chrome/common/net/cookie_monster_sqlite.h index 7607915..14da41c 100644 --- a/chrome/common/net/cookie_monster_sqlite.h +++ b/chrome/common/net/cookie_monster_sqlite.h @@ -28,6 +28,8 @@ class SQLitePersistentCookieStore virtual void AddCookie(const std::string&, const net::CookieMonster::CanonicalCookie&); + virtual void UpdateCookieAccessTime( + const net::CookieMonster::CanonicalCookie&); virtual void DeleteCookie(const net::CookieMonster::CanonicalCookie&); private: diff --git a/net/base/cookie_monster.cc b/net/base/cookie_monster.cc index 7a56831..987bfb8 100644 --- a/net/base/cookie_monster.cc +++ b/net/base/cookie_monster.cc @@ -68,6 +68,10 @@ using base::TimeDelta; namespace net { +// Default minimum delay after updating a cookie's LastAccessDate before we +// will update it again. +static const int kDefaultAccessUpdateThresholdSeconds = 60; + // static bool CookieMonster::enable_file_scheme_ = false; @@ -78,12 +82,16 @@ void CookieMonster::EnableFileScheme() { CookieMonster::CookieMonster() : initialized_(false), - store_(NULL) { + store_(NULL), + last_access_threshold_( + TimeDelta::FromSeconds(kDefaultAccessUpdateThresholdSeconds)) { } CookieMonster::CookieMonster(PersistentCookieStore* store) : initialized_(false), - store_(store) { + store_(store), + last_access_threshold_( + TimeDelta::FromSeconds(kDefaultAccessUpdateThresholdSeconds)) { } CookieMonster::~CookieMonster() { @@ -398,8 +406,8 @@ bool CookieMonster::SetCookieWithCreationTime(const GURL& url, cc.reset(new CanonicalCookie(pc.Name(), pc.Value(), cookie_path, pc.IsSecure(), pc.IsHttpOnly(), - creation_time, !cookie_expires.is_null(), - cookie_expires)); + creation_time, creation_time, + !cookie_expires.is_null(), cookie_expires)); if (!cc.get()) { COOKIE_DLOG(WARNING) << "Failed to allocate CanonicalCookie"; @@ -440,6 +448,20 @@ void CookieMonster::InternalInsertCookie(const std::string& key, cookies_.insert(CookieMap::value_type(key, cc)); } +void CookieMonster::InternalUpdateCookieAccessTime(CanonicalCookie* cc) { + // Based off the Mozilla code. When a cookie has been accessed recently, + // don't bother updating its access time again. This reduces the number of + // updates we do during pageload, which in turn reduces the chance our storage + // backend will hit its batch thresholds and be forced to update. + const Time current = Time::Now(); + if ((current - cc->LastAccessDate()) < last_access_threshold_) + return; + + cc->SetLastAccessDate(current); + if (cc->IsPersistent() && store_) + store_->UpdateCookieAccessTime(*cc); +} + void CookieMonster::InternalDeleteCookie(CookieMap::iterator it, bool sync_to_store) { CanonicalCookie* cc = it->second; @@ -507,10 +529,15 @@ int CookieMonster::GarbageCollect(const Time& current, return num_deleted; } -// TODO we should be sorting by last access time, however, right now -// we're not saving an access time, so we're sorting by creation time. -static bool OldestCookieSorter(const CookieMonster::CookieMap::iterator& it1, - const CookieMonster::CookieMap::iterator& it2) { +static bool LRUCookieSorter(const CookieMonster::CookieMap::iterator& it1, + const CookieMonster::CookieMap::iterator& it2) { + // Cookies accessed less recently should be deleted first. + if (it1->second->LastAccessDate() != it2->second->LastAccessDate()) + return it1->second->LastAccessDate() < it2->second->LastAccessDate(); + + // In rare cases we might have two cookies with identical last access times. + // To preserve the stability of the sort, in these cases prefer to delete + // older cookies over newer ones. CreationDate() is guaranteed to be unique. return it1->second->CreationDate() < it2->second->CreationDate(); } @@ -522,7 +549,7 @@ int CookieMonster::GarbageCollectRange(const Time& current, std::vector<CookieMap::iterator> cookie_its; int num_deleted = GarbageCollectExpired(current, itpair, &cookie_its); - // If the range still has too many cookies, delete the oldest. + // If the range still has too many cookies, delete the least recently used. if (cookie_its.size() > num_max) { COOKIE_DLOG(INFO) << "GarbageCollectRange() Deep Garbage Collect."; // Purge down to (|num_max| - |num_purge|) total cookies. @@ -530,7 +557,7 @@ int CookieMonster::GarbageCollectRange(const Time& current, num_purge += cookie_its.size() - num_max; std::partial_sort(cookie_its.begin(), cookie_its.begin() + num_purge, - cookie_its.end(), OldestCookieSorter); + cookie_its.end(), LRUCookieSorter); for (size_t i = 0; i < num_purge; ++i) InternalDeleteCookie(cookie_its[i], true); @@ -763,7 +790,9 @@ void CookieMonster::FindCookiesForKey( if (!cc->IsOnPath(url.path())) continue; - // Congratulations Charlie, you passed the test! + // Add this cookie to the set of matching cookies. Since we're reading the + // cookie, update its last access time. + InternalUpdateCookieAccessTime(cc); cookies->push_back(cc); } } diff --git a/net/base/cookie_monster.h b/net/base/cookie_monster.h index 6db774a..68d8c3b 100644 --- a/net/base/cookie_monster.h +++ b/net/base/cookie_monster.h @@ -30,9 +30,6 @@ namespace net { // // TODO(deanm) Implement CookieMonster, the cookie database. // - Verify that our domain enforcement and non-dotted handling is correct -// - Currently garbage collection is done on oldest CreationUTC, Mozilla -// purges cookies on last access time, which would require adding and -// keeping track of access times on a CanonicalCookie class CookieMonster { public: class ParsedCookie; @@ -68,6 +65,15 @@ class CookieMonster { // existence. CookieMonster(PersistentCookieStore* store); +#ifdef UNIT_TEST + CookieMonster::CookieMonster(int last_access_threshold_seconds) + : initialized_(false), + store_(NULL), + last_access_threshold_( + base::TimeDelta::FromSeconds(last_access_threshold_seconds)) { + } +#endif + ~CookieMonster(); // Parse the string with the cookie time (very forgivingly). @@ -90,7 +96,8 @@ class CookieMonster { // It will _not_ return httponly cookies, see GetCookiesWithOptions std::string GetCookies(const GURL& url); std::string GetCookiesWithOptions(const GURL& url, CookieOptions options); - // Returns all the cookies, for use in management UI, etc. + // Returns all the cookies, for use in management UI, etc. This does not mark + // the cookies as having been accessed. CookieList GetAllCookies(); // Delete all of the cookies. @@ -149,6 +156,8 @@ class CookieMonster { CanonicalCookie* cc, bool sync_to_store); + void InternalUpdateCookieAccessTime(CanonicalCookie* cc); + void InternalDeleteCookie(CookieMap::iterator it, bool sync_to_store); // If the number of cookies for host |key|, or globally, are over preset @@ -161,7 +170,8 @@ class CookieMonster { // Deletes all expired cookies in |itpair|; // then, if the number of remaining cookies is greater than |num_max|, - // collects the oldest cookies until (|num_max| - |num_purge|) cookies remain. + // collects the least recently accessed cookies until + // (|num_max| - |num_purge|) cookies remain. // // Returns the number of cookies deleted. int GarbageCollectRange(const base::Time& current, @@ -191,6 +201,10 @@ class CookieMonster { base::Time CurrentTime(); base::Time last_time_seen_; + // Minimum delay after updating a cookie's LastAccessDate before we will + // update it again. + const base::TimeDelta last_access_threshold_; + // Lock for thread-safety Lock lock_; @@ -261,12 +275,14 @@ class CookieMonster::CanonicalCookie { bool secure, bool httponly, const base::Time& creation, + const base::Time& last_access, bool has_expires, const base::Time& expires) : name_(name), value_(value), path_(path), creation_date_(creation), + last_access_date_(last_access), expiry_date_(expires), has_expires_(has_expires), secure_(secure), @@ -288,6 +304,7 @@ class CookieMonster::CanonicalCookie { const std::string& Value() const { return value_; } const std::string& Path() const { return path_; } const base::Time& CreationDate() const { return creation_date_; } + const base::Time& LastAccessDate() const { return last_access_date_; } bool DoesExpire() const { return has_expires_; } bool IsPersistent() const { return DoesExpire(); } const base::Time& ExpiryDate() const { return expiry_date_; } @@ -306,6 +323,10 @@ class CookieMonster::CanonicalCookie { return name_ == ecc.Name() && path_ == ecc.Path(); } + void SetLastAccessDate(const base::Time& date) { + last_access_date_ = date; + } + bool IsOnPath(const std::string& url_path) const; std::string DebugString() const; @@ -314,6 +335,7 @@ class CookieMonster::CanonicalCookie { std::string value_; std::string path_; base::Time creation_date_; + base::Time last_access_date_; base::Time expiry_date_; bool has_expires_; bool secure_; @@ -329,6 +351,7 @@ class CookieMonster::PersistentCookieStore { virtual bool Load(std::vector<CookieMonster::KeyedCanonicalCookie>*) = 0; virtual void AddCookie(const std::string&, const CanonicalCookie&) = 0; + virtual void UpdateCookieAccessTime(const CanonicalCookie&) = 0; virtual void DeleteCookie(const CanonicalCookie&) = 0; protected: diff --git a/net/base/cookie_monster_unittest.cc b/net/base/cookie_monster_unittest.cc index e9a5b00..7b6f0ad 100644 --- a/net/base/cookie_monster_unittest.cc +++ b/net/base/cookie_monster_unittest.cc @@ -718,6 +718,31 @@ TEST(CookieMonsterTest, TestSecure) { EXPECT_EQ("D=E; A=B", cm.GetCookies(url_google_secure)); } +static Time GetFirstCookieAccessDate(net::CookieMonster* cm) { + const net::CookieMonster::CookieList all_cookies(cm->GetAllCookies()); + return all_cookies.front().second.LastAccessDate(); +} + +static const int kLastAccessThresholdSeconds = 1; + +TEST(CookieMonsterTest, TestLastAccess) { + GURL url_google(kUrlGoogle); + net::CookieMonster cm(kLastAccessThresholdSeconds); + + EXPECT_TRUE(cm.SetCookie(url_google, "A=B")); + const Time last_access_date(GetFirstCookieAccessDate(&cm)); + + // Reading the cookie again immediately shouldn't update the access date, + // since we're inside the threshold. + EXPECT_EQ("A=B", cm.GetCookies(url_google)); + EXPECT_TRUE(last_access_date == GetFirstCookieAccessDate(&cm)); + + // Reading after a short wait should update the access date. + Sleep(1500); + EXPECT_EQ("A=B", cm.GetCookies(url_google)); + EXPECT_FALSE(last_access_date == GetFirstCookieAccessDate(&cm)); +} + static int CountInString(const std::string& str, char c) { int count = 0; for (std::string::const_iterator it = str.begin(); @@ -744,23 +769,31 @@ TEST(CookieMonsterTest, TestHostGarbageCollection) { } TEST(CookieMonsterTest, TestTotalGarbageCollection) { - net::CookieMonster cm; + net::CookieMonster cm(kLastAccessThresholdSeconds); // Add a bunch of cookies on a bunch of host, some should get purged. + const GURL sticky_cookie("http://a0000.izzle"); for (int i = 0; i < 2000; ++i) { GURL url(StringPrintf("http://a%04d.izzle", i)); EXPECT_TRUE(cm.SetCookie(url, "a=b")); EXPECT_EQ("a=b", cm.GetCookies(url)); + + // Keep touching the first cookie to ensure it's not purged (since it will + // always have the most recent access time). + if (!(i % 500)) { + Sleep(1500); // Ensure the timestamps will be different enough to update. + EXPECT_EQ("a=b", cm.GetCookies(sticky_cookie)); + } } // Check that cookies that still exist. for (int i = 0; i < 2000; ++i) { GURL url(StringPrintf("http://a%04d.izzle", i)); - if (i < 900) { - // Cookies should have gotten purged. - EXPECT_TRUE(cm.GetCookies(url).empty()); - } else if (i > 1100) { + if ((i == 0) || (i > 1101)) { // Cookies should still be around. EXPECT_FALSE(cm.GetCookies(url).empty()); + } else if (i < 901) { + // Cookies should have gotten purged. + EXPECT_TRUE(cm.GetCookies(url).empty()); } } } |