OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 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 // Package dscache provides a transparent cache for RawDatastore which is |
| 6 // backed by Memcache. |
| 7 // |
| 8 // Inspiration |
| 9 // |
| 10 // Although this is not a port of any particular implementation, it takes |
| 11 // inspiration from these fine libraries: |
| 12 // - https://cloud.google.com/appengine/docs/python/ndb/ |
| 13 // - https://github.com/qedus/nds |
| 14 // - https://github.com/mjibson/goon |
| 15 // |
| 16 // Algorithm |
| 17 // |
| 18 // Memcache contains cache entries for single datastore entities. The memcache |
| 19 // key looks like |
| 20 // |
| 21 // "gae:" | vers | ":" | shard | ":" | Base64_std_nopad(SHA1(rawdatastore.Key)
) |
| 22 // |
| 23 // Where: |
| 24 // - vers is an ascii-hex-encoded number (currently 1). |
| 25 // - shard is a zero-based ascii-hex-encoded number (depends on dscache.shards
). |
| 26 // - SHA1 has been chosen as unlikely (p == 1e-18) to collide, given dedicated |
| 27 // memcache sizes of up to 170 Exabytes (assuming an average entry size of |
| 28 // 100KB including the memcache key). This is clearly overkill, but MD5 |
| 29 // could start showing collisions at this probability in as small as a 26GB |
| 30 // cache (and also MD5 sucks). |
| 31 // |
| 32 // The memcache value is a compression byte, indicating the scheme (See |
| 33 // CompressionType), followed by the encoded (and possibly compressed) value. |
| 34 // Encoding is done with rawdatastore.PropertyMap.Write(). The memcache value |
| 35 // may also be the empty byte sequence, indicating that this entity is deleted. |
| 36 // |
| 37 // The memcache entry may also have a 'flags' value set to one of the following: |
| 38 // - 0 "entity" (cached value) |
| 39 // - 1 "lock" (someone is mutating this entry) |
| 40 // |
| 41 // Algorithm - Put and Delete |
| 42 // |
| 43 // On a Put (or Delete), the memcache value written with a LockTimeSeconds |
| 44 // expiration (default 31 seconds), and a memcache flag value of 0x1 (indicating |
| 45 // that it's a put-locked key). |
| 46 // |
| 47 // The datastore operation will then occur. Assuming success, Put will then |
| 48 // delete all of the memcache locks (Not using CompareAndSwap). |
| 49 // |
| 50 // Algorithm - Get |
| 51 // |
| 52 // On a Get, "Add" a lock for it (which only does something if there's no entry |
| 53 // in memcache yet) with a nonce value. We immediately Get the memcache entries |
| 54 // back (for CAS purposes later). |
| 55 // |
| 56 // If it doesn't exist (unlikely since we just Add'd it) or if it's flag is |
| 57 // "lock" and the Value != the nonce we put there, go hit the datastore without |
| 58 // trying to update memcache. |
| 59 // |
| 60 // If its flag is "entity", decode the object and return it. If the Value is |
| 61 // the empty byte sequence, return ErrNoSuchEntity. |
| 62 // |
| 63 // If its flag is "lock" and the Value equals the nonce, go get it from the |
| 64 // datastore. If that's successful, then encode the value to bytes, and CAS |
| 65 // the object to memcache. The CAS will succeed if nothing else touched the |
| 66 // memcache in the meantime (like a Put, a memcache expiration/eviction, etc.). |
| 67 // |
| 68 // Algorithm - Transactions |
| 69 // |
| 70 // In a transaction, all Put memcache operations are held until the very end of |
| 71 // the transaction. Right before the transaction is committed, all accumulated |
| 72 // Put keys are locked. If the transaction is sucessfully committed (err == |
| 73 // nil), then all the locks will be deleted. |
| 74 // |
| 75 // The assumption here is that get operations apply all outstanding |
| 76 // transactions before they return data (https://cloud.google.com/appengine/docs
/go/datastore/#Go_Datastore_writes_and_data_visibility), |
| 77 // and so it is safe to purge all the locks if the transaction is known-good. |
| 78 // |
| 79 // If the transaction succeeds, but RunInTransaction returns an error (which can |
| 80 // happen), or if the transaction fails, then the lock entries time out |
| 81 // naturally. This will mean 31-ish seconds of direct datastore access, but it's |
| 82 // the more-correct thing to do. |
| 83 // |
| 84 // Cache control |
| 85 // |
| 86 // An entity may expose the following metadata (see |
| 87 // rawdatastore.PropertyLoadSaver.GetMeta) to control the behavior of its cache. |
| 88 // |
| 89 // - "dscache.enable,<true|false>" - whether or not this entity should be |
| 90 // cached at all. If ommitted, dscache defaults to true. |
| 91 // - "dscache.expiration,#seconds" - the number of seconds of persistance to |
| 92 // use when this item is cached. 0 is infinite. If omitted, defaults to 0. |
| 93 // |
| 94 // In addition, the application may set a function ShardsForKey(key) which |
| 95 // returns the number of shards to use for a given datastore key. |
| 96 // |
| 97 // Caveats |
| 98 // |
| 99 // A couple things to note that may differ from other appengine datastore |
| 100 // caching libraries (like goon, nds, or ndb). |
| 101 // |
| 102 // - It does NOT provide in-memory ("per-request") caching. |
| 103 // - It's tolerant of memcache failures (but will give potentially |
| 104 // inconsistent results if memcache is non-operational). Using a transaction |
| 105 // bypasses the cache logic, which will present a consistent view of the dat
a. |
| 106 // - Queries do not interact with the cache at all. |
| 107 // - Negative lookups (e.g. ErrNoSuchEntity) are cached. |
| 108 // - Allows sharding hot memcache entries as recommended by |
| 109 // https://cloud.google.com/appengine/articles/best-practices-for-app-engine
-memcache#distribute-load . |
| 110 package dscache |
OLD | NEW |