ChainedFastBackend.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. <?php
  2. namespace Drupal\Core\Cache;
  3. /**
  4. * Defines a backend with a fast and a consistent backend chain.
  5. *
  6. * In order to mitigate a network roundtrip for each cache get operation, this
  7. * cache allows a fast backend to be put in front of a slow(er) backend.
  8. * Typically the fast backend will be something like APCu, and be bound to a
  9. * single web node, and will not require a network round trip to fetch a cache
  10. * item. The fast backend will also typically be inconsistent (will only see
  11. * changes from one web node). The slower backend will be something like Mysql,
  12. * Memcached or Redis, and will be used by all web nodes, thus making it
  13. * consistent, but also require a network round trip for each cache get.
  14. *
  15. * In addition to being useful for sites running on multiple web nodes, this
  16. * backend can also be useful for sites running on a single web node where the
  17. * fast backend (e.g., APCu) isn't shareable between the web and CLI processes.
  18. * Single-node configurations that don't have that limitation can just use the
  19. * fast cache backend directly.
  20. *
  21. * We always use the fast backend when reading (get()) entries from cache, but
  22. * check whether they were created before the last write (set()) to this
  23. * (chained) cache backend. Those cache entries that were created before the
  24. * last write are discarded, but we use their cache IDs to then read them from
  25. * the consistent (slower) cache backend instead; at the same time we update
  26. * the fast cache backend so that the next read will hit the faster backend
  27. * again. Hence we can guarantee that the cache entries we return are all
  28. * up-to-date, and maximally exploit the faster cache backend. This cache
  29. * backend uses and maintains a "last write timestamp" to determine which cache
  30. * entries should be discarded.
  31. *
  32. * Because this backend will mark all the cache entries in a bin as out-dated
  33. * for each write to a bin, it is best suited to bins with fewer changes.
  34. *
  35. * Note that this is designed specifically for combining a fast inconsistent
  36. * cache backend with a slower consistent cache back-end. To still function
  37. * correctly, it needs to do a consistency check (see the "last write timestamp"
  38. * logic). This contrasts with \Drupal\Core\Cache\BackendChain, which assumes
  39. * both chained cache backends are consistent, thus a consistency check being
  40. * pointless.
  41. *
  42. * @see \Drupal\Core\Cache\BackendChain
  43. *
  44. * @ingroup cache
  45. */
  46. class ChainedFastBackend implements CacheBackendInterface, CacheTagsInvalidatorInterface {
  47. /**
  48. * Cache key prefix for the bin-specific entry to track the last write.
  49. */
  50. const LAST_WRITE_TIMESTAMP_PREFIX = 'last_write_timestamp_';
  51. /**
  52. * @var string
  53. */
  54. protected $bin;
  55. /**
  56. * The consistent cache backend.
  57. *
  58. * @var \Drupal\Core\Cache\CacheBackendInterface
  59. */
  60. protected $consistentBackend;
  61. /**
  62. * The fast cache backend.
  63. *
  64. * @var \Drupal\Core\Cache\CacheBackendInterface
  65. */
  66. protected $fastBackend;
  67. /**
  68. * The time at which the last write to this cache bin happened.
  69. *
  70. * @var float
  71. */
  72. protected $lastWriteTimestamp;
  73. /**
  74. * Constructs a ChainedFastBackend object.
  75. *
  76. * @param \Drupal\Core\Cache\CacheBackendInterface $consistent_backend
  77. * The consistent cache backend.
  78. * @param \Drupal\Core\Cache\CacheBackendInterface $fast_backend
  79. * The fast cache backend.
  80. * @param string $bin
  81. * The cache bin for which the object is created.
  82. */
  83. public function __construct(CacheBackendInterface $consistent_backend, CacheBackendInterface $fast_backend, $bin) {
  84. $this->consistentBackend = $consistent_backend;
  85. $this->fastBackend = $fast_backend;
  86. $this->bin = 'cache_' . $bin;
  87. $this->lastWriteTimestamp = NULL;
  88. }
  89. /**
  90. * {@inheritdoc}
  91. */
  92. public function get($cid, $allow_invalid = FALSE) {
  93. $cids = [$cid];
  94. $cache = $this->getMultiple($cids, $allow_invalid);
  95. return reset($cache);
  96. }
  97. /**
  98. * {@inheritdoc}
  99. */
  100. public function getMultiple(&$cids, $allow_invalid = FALSE) {
  101. $cids_copy = $cids;
  102. $cache = [];
  103. // If we can determine the time at which the last write to the consistent
  104. // backend occurred (we might not be able to if it has been recently
  105. // flushed/restarted), then we can use that to validate items from the fast
  106. // backend, so try to get those first. Otherwise, we can't assume that
  107. // anything in the fast backend is valid, so don't even bother fetching
  108. // from there.
  109. $last_write_timestamp = $this->getLastWriteTimestamp();
  110. if ($last_write_timestamp) {
  111. // Items in the fast backend might be invalid based on their timestamp,
  112. // but we can't check the timestamp prior to getting the item, which
  113. // includes unserializing it. However, unserializing an invalid item can
  114. // throw an exception. For example, a __wakeup() implementation that
  115. // receives object properties containing references to code or data that
  116. // no longer exists in the application's current state.
  117. //
  118. // Unserializing invalid data, whether it throws an exception or not, is
  119. // a waste of time, but we only incur it while a cache invalidation has
  120. // not yet finished propagating to all the fast backend instances.
  121. //
  122. // Most cache backend implementations should not wrap their internal
  123. // get() implementations with a try/catch, because they have no reason to
  124. // assume that their data is invalid, and doing so would mask
  125. // unserialization errors of valid data. We do so here, only because the
  126. // fast backend is non-authoritative, and after discarding its
  127. // exceptions, we proceed to check the consistent (authoritative) backend
  128. // and allow exceptions from that to bubble up.
  129. try {
  130. $items = $this->fastBackend->getMultiple($cids, $allow_invalid);
  131. }
  132. catch (\Exception $e) {
  133. $cids = $cids_copy;
  134. $items = [];
  135. }
  136. // Even if items were successfully fetched from the fast backend, they
  137. // are potentially invalid if older than the last time the bin was
  138. // written to in the consistent backend, so only keep ones that aren't.
  139. foreach ($items as $item) {
  140. if ($item->created < $last_write_timestamp) {
  141. $cids[array_search($item->cid, $cids_copy)] = $item->cid;
  142. }
  143. else {
  144. $cache[$item->cid] = $item;
  145. }
  146. }
  147. }
  148. // If there were any cache entries that were not available in the fast
  149. // backend, retrieve them from the consistent backend and store them in the
  150. // fast one.
  151. if ($cids) {
  152. foreach ($this->consistentBackend->getMultiple($cids, $allow_invalid) as $item) {
  153. $cache[$item->cid] = $item;
  154. // Don't write the cache tags to the fast backend as any cache tag
  155. // invalidation results in an invalidation of the whole fast backend.
  156. $this->fastBackend->set($item->cid, $item->data, $item->expire);
  157. }
  158. }
  159. return $cache;
  160. }
  161. /**
  162. * {@inheritdoc}
  163. */
  164. public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
  165. $this->consistentBackend->set($cid, $data, $expire, $tags);
  166. $this->markAsOutdated();
  167. // Don't write the cache tags to the fast backend as any cache tag
  168. // invalidation results in an invalidation of the whole fast backend.
  169. $this->fastBackend->set($cid, $data, $expire);
  170. }
  171. /**
  172. * {@inheritdoc}
  173. */
  174. public function setMultiple(array $items) {
  175. $this->consistentBackend->setMultiple($items);
  176. $this->markAsOutdated();
  177. // Don't write the cache tags to the fast backend as any cache tag
  178. // invalidation results in an invalidation of the whole fast backend.
  179. foreach ($items as &$item) {
  180. unset($item['tags']);
  181. }
  182. $this->fastBackend->setMultiple($items);
  183. }
  184. /**
  185. * {@inheritdoc}
  186. */
  187. public function delete($cid) {
  188. $this->consistentBackend->deleteMultiple([$cid]);
  189. $this->markAsOutdated();
  190. }
  191. /**
  192. * {@inheritdoc}
  193. */
  194. public function deleteMultiple(array $cids) {
  195. $this->consistentBackend->deleteMultiple($cids);
  196. $this->markAsOutdated();
  197. }
  198. /**
  199. * {@inheritdoc}
  200. */
  201. public function deleteAll() {
  202. $this->consistentBackend->deleteAll();
  203. $this->markAsOutdated();
  204. }
  205. /**
  206. * {@inheritdoc}
  207. */
  208. public function invalidate($cid) {
  209. $this->invalidateMultiple([$cid]);
  210. }
  211. /**
  212. * {@inheritdoc}
  213. */
  214. public function invalidateMultiple(array $cids) {
  215. $this->consistentBackend->invalidateMultiple($cids);
  216. $this->markAsOutdated();
  217. }
  218. /**
  219. * {@inheritdoc}
  220. */
  221. public function invalidateTags(array $tags) {
  222. if ($this->consistentBackend instanceof CacheTagsInvalidatorInterface) {
  223. $this->consistentBackend->invalidateTags($tags);
  224. }
  225. $this->markAsOutdated();
  226. }
  227. /**
  228. * {@inheritdoc}
  229. */
  230. public function invalidateAll() {
  231. $this->consistentBackend->invalidateAll();
  232. $this->markAsOutdated();
  233. }
  234. /**
  235. * {@inheritdoc}
  236. */
  237. public function garbageCollection() {
  238. $this->consistentBackend->garbageCollection();
  239. $this->fastBackend->garbageCollection();
  240. }
  241. /**
  242. * {@inheritdoc}
  243. */
  244. public function removeBin() {
  245. $this->consistentBackend->removeBin();
  246. $this->fastBackend->removeBin();
  247. }
  248. /**
  249. * @todo Document in https://www.drupal.org/node/2311945.
  250. */
  251. public function reset() {
  252. $this->lastWriteTimestamp = NULL;
  253. }
  254. /**
  255. * Gets the last write timestamp.
  256. */
  257. protected function getLastWriteTimestamp() {
  258. if ($this->lastWriteTimestamp === NULL) {
  259. $cache = $this->consistentBackend->get(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin);
  260. $this->lastWriteTimestamp = $cache ? $cache->data : 0;
  261. }
  262. return $this->lastWriteTimestamp;
  263. }
  264. /**
  265. * Marks the fast cache bin as outdated because of a write.
  266. */
  267. protected function markAsOutdated() {
  268. // Clocks on a single server can drift. Multiple servers may have slightly
  269. // differing opinions about the current time. Given that, do not assume
  270. // 'now' on this server is always later than our stored timestamp.
  271. // Also add 1 millisecond, to ensure that caches written earlier in the same
  272. // millisecond are invalidated. It is possible that caches will be later in
  273. // the same millisecond and are then incorrectly invalidated, but that only
  274. // costs one additional roundtrip to the persistent cache.
  275. $now = round(microtime(TRUE) + .001, 3);
  276. if ($now > $this->getLastWriteTimestamp()) {
  277. $this->lastWriteTimestamp = $now;
  278. $this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp);
  279. }
  280. }
  281. }