Currently, the CookieMonster cookie store is initialized by reading in the cookies from the backing store in a large lump on first access. This is bad for several reasons:
I believe that these requirements imply:
Notes on synchronous vs. asynchronous interfaces to the CookieMonster
(See Issue 68657)
Generally, services on the IO thread provide asynchronous interfaces with callbacks, to avoid the IO thread becoming a bottleneck in providing services for other threads. For all requests after the initial one, the CookieMonster follows this rule and does not block; all information required for all CookieMonster APIs is in memory. However, the first call to the CookieMonster triggers loading in of the entire persistent backing store, done on-thread, which means that the IO thread blocks on disk operations on the first request to a CookieMonster with a backing store.
Giving the CookieMonster an asynchronous interface is a large task, not because of the internal structure of the Cookie, but because of the large number of callers, all of whom would need to be adapted to the new interface. Issue <xxx> tracks this task.
In an ideal world, the current design would not be dependent on async CookieMonster interfaces. However, a key part of this design is breaking up the loading of the backing store into indepedent pieces, using Task objects, which are spawned off asynchronously in the backing in the absence of incoming reuqests to laod the backing store database. Combining this with a synchronous interface for those reuqests that need backing store loads would mean doing one of:
None of these options is particularly appealing. Thus it's preferred that this work be done after the CookieMonster has been converted over to having an async interface. The rest of this design assumes such an interface.
[Note that it would be relatively easy to layer an asynchronous interface (if not behavior) on top of the current synchronous interface; the async trigger would wrap the sync call and a PostTask(self) for the callback. This could allow incremental changes to the various callers. When all callers had been converted, the sync interface could be removed and the async interface made to behave in a non-blocking manner. ]
High Level Design
The cookie monster will get an additional data structure, indexed by domain key (currently eTLD+1) indicating whether or not a particular domain key's set of values has been loaded. Incoming requests (either set of get) will trigger the loading of the domain key(s) required for those requests if they have not yet been loaded. In these cases, the incoming request will block (as it currently does if the backing store hasn't been loaded). All actual database accesses will occur on the DB thread.
At CookieMonster construction, a background operation will be invoked to load the entire cookie store. This will run on the database thread, and will break down into a series of tasks (T) marks each separate task:
These are broken up into indiviudal tasks both to avoid freezing the DB thread, and to allow loads of specific keys triggered by particular web page access to "jump the queue" and get pulled in earlier.
Routine Flow Outline
The primary routines and data structures involved in this design are sketched out below.
There are two basic "ropes" (since they're potentially cross-thread) of routine invocation: the initial spawn of loading of all of the cookies, and the load-on-demand triggerred by specific requests to the cookie monster.
The initial spawn occurs from the CookieMonster constructor -> SQLitePersistentCookieStore::SpawnFullLoad -> SQLitePersistentCookieStore::BackingStore::SpawnFullLoad. Once the rope has reached the DB thread, a full list of all the keys in the SQLite DB is loaded and stored in BackingStore. Then SpawnFullLoad does a self-dispatch (to the DB thread) of a ChainLoadKeyCookies task for the first key in the list. ChainLoadKeyCookies will call LoadKeyCookies (which will load in the cookies associated with that key from the backing store and notify the CookieMonster of that load) and will then self-dispatch another instance of ChainLoadKeyCookies. When the last entry in the key list is loaded, ChainLoadKeyCookies will return without any action. This will (incrementally, allowing other tasks to run on the DB thread) load all the cookies.
If a request comes into the CookieMonster while this chain is executing, it will check all_loaded_ and keys_loaded_ to detemrine that the relevant data for the request has not been loaded (if the relevant data has already been loaded, it will execute the in-memory lookup inline as current and call the callback with the information). It will then register the callback and operation information into the key->callback mapping. If that is the first registration of that key, it will call SQLitePersistentCookieStore::LoadKeyCookies, which will dispatch an invocation to the DB thread of SQLitePersistentCookieStore::BackingStore::LoadKeyCookies. If LoadKeyCookies determines that the key requested has already been loaded (race with above chain), it will simply exit.
The tricky part of this design is making sure that there are no races that result in dangling requests. The invariants that guarantee that this doesn't happen are:
So it's possible for a LoadKeyCookies() dispatch to not result in a CompleteLoad() being sent back, but only if there's already one in transit. Thus, if a callback is registered in the keys->callback table, it will be executed with maximum latency round trip to DB thread + queue wait time + load time.
Object Lifetime Relationships
[Will Chan take note :-}.]
Both CookieMonster and PersistentCookieStore are RefCountedThreadSafe objects. The CookieMonster keeps a reference to the PersistentCookieStore in its store_ pointer; this guarantees that the store lives as long as the Monster does.
Both SpawnFullLoad and LoadKeyCookies will take as arguments scoped_refptrs on the CookieMonster which the PersistentCookieStore (or its backing store) will use for callbacks. These references will guarantee that the CookieMonster stays alive for the duration of the callback.
If the CookieMonster needs to be torn down, it will call an abort() routine on the PersistentCookieStore, which will break the chain load and release the scoped_refptr<> held on the CookieMonster by that chain load.