CouchbaseBucketCache.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\Common\Cache;
  4. use Couchbase\Bucket;
  5. use Couchbase\Document;
  6. use Couchbase\Exception;
  7. use function phpversion;
  8. use function serialize;
  9. use function sprintf;
  10. use function substr;
  11. use function time;
  12. use function unserialize;
  13. use function version_compare;
  14. /**
  15. * Couchbase ^2.3.0 cache provider.
  16. */
  17. final class CouchbaseBucketCache extends CacheProvider
  18. {
  19. private const MINIMUM_VERSION = '2.3.0';
  20. private const KEY_NOT_FOUND = 13;
  21. private const MAX_KEY_LENGTH = 250;
  22. private const THIRTY_DAYS_IN_SECONDS = 2592000;
  23. /** @var Bucket */
  24. private $bucket;
  25. public function __construct(Bucket $bucket)
  26. {
  27. if (version_compare(phpversion('couchbase'), self::MINIMUM_VERSION) < 0) {
  28. // Manager is required to flush cache and pull stats.
  29. throw new \RuntimeException(sprintf('ext-couchbase:^%s is required.', self::MINIMUM_VERSION));
  30. }
  31. $this->bucket = $bucket;
  32. }
  33. /**
  34. * {@inheritdoc}
  35. */
  36. protected function doFetch($id)
  37. {
  38. $id = $this->normalizeKey($id);
  39. try {
  40. $document = $this->bucket->get($id);
  41. } catch (Exception $e) {
  42. return false;
  43. }
  44. if ($document instanceof Document && $document->value !== false) {
  45. return unserialize($document->value);
  46. }
  47. return false;
  48. }
  49. /**
  50. * {@inheritdoc}
  51. */
  52. protected function doContains($id)
  53. {
  54. $id = $this->normalizeKey($id);
  55. try {
  56. $document = $this->bucket->get($id);
  57. } catch (Exception $e) {
  58. return false;
  59. }
  60. if ($document instanceof Document) {
  61. return ! $document->error;
  62. }
  63. return false;
  64. }
  65. /**
  66. * {@inheritdoc}
  67. */
  68. protected function doSave($id, $data, $lifeTime = 0)
  69. {
  70. $id = $this->normalizeKey($id);
  71. $lifeTime = $this->normalizeExpiry($lifeTime);
  72. try {
  73. $encoded = serialize($data);
  74. $document = $this->bucket->upsert($id, $encoded, [
  75. 'expiry' => (int) $lifeTime,
  76. ]);
  77. } catch (Exception $e) {
  78. return false;
  79. }
  80. if ($document instanceof Document) {
  81. return ! $document->error;
  82. }
  83. return false;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. */
  88. protected function doDelete($id)
  89. {
  90. $id = $this->normalizeKey($id);
  91. try {
  92. $document = $this->bucket->remove($id);
  93. } catch (Exception $e) {
  94. return $e->getCode() === self::KEY_NOT_FOUND;
  95. }
  96. if ($document instanceof Document) {
  97. return ! $document->error;
  98. }
  99. return false;
  100. }
  101. /**
  102. * {@inheritdoc}
  103. */
  104. protected function doFlush()
  105. {
  106. $manager = $this->bucket->manager();
  107. // Flush does not return with success or failure, and must be enabled per bucket on the server.
  108. // Store a marker item so that we will know if it was successful.
  109. $this->doSave(__METHOD__, true, 60);
  110. $manager->flush();
  111. if ($this->doContains(__METHOD__)) {
  112. $this->doDelete(__METHOD__);
  113. return false;
  114. }
  115. return true;
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. protected function doGetStats()
  121. {
  122. $manager = $this->bucket->manager();
  123. $stats = $manager->info();
  124. $nodes = $stats['nodes'];
  125. $node = $nodes[0];
  126. $interestingStats = $node['interestingStats'];
  127. return [
  128. Cache::STATS_HITS => $interestingStats['get_hits'],
  129. Cache::STATS_MISSES => $interestingStats['cmd_get'] - $interestingStats['get_hits'],
  130. Cache::STATS_UPTIME => $node['uptime'],
  131. Cache::STATS_MEMORY_USAGE => $interestingStats['mem_used'],
  132. Cache::STATS_MEMORY_AVAILABLE => $node['memoryFree'],
  133. ];
  134. }
  135. private function normalizeKey(string $id) : string
  136. {
  137. $normalized = substr($id, 0, self::MAX_KEY_LENGTH);
  138. if ($normalized === false) {
  139. return $id;
  140. }
  141. return $normalized;
  142. }
  143. /**
  144. * Expiry treated as a unix timestamp instead of an offset if expiry is greater than 30 days.
  145. * @src https://developer.couchbase.com/documentation/server/4.1/developer-guide/expiry.html
  146. */
  147. private function normalizeExpiry(int $expiry) : int
  148. {
  149. if ($expiry > self::THIRTY_DAYS_IN_SECONDS) {
  150. return time() + $expiry;
  151. }
  152. return $expiry;
  153. }
  154. }