RiakCache.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <?php
  2. /*
  3. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14. *
  15. * This software consists of voluntary contributions made by many individuals
  16. * and is licensed under the MIT license. For more information, see
  17. * <http://www.doctrine-project.org>.
  18. */
  19. namespace Doctrine\Common\Cache;
  20. use Riak\Bucket;
  21. use Riak\Connection;
  22. use Riak\Input;
  23. use Riak\Exception;
  24. use Riak\Object;
  25. /**
  26. * Riak cache provider.
  27. *
  28. * @link www.doctrine-project.org
  29. * @since 1.1
  30. * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
  31. */
  32. class RiakCache extends CacheProvider
  33. {
  34. const EXPIRES_HEADER = 'X-Riak-Meta-Expires';
  35. /**
  36. * @var \Riak\Bucket
  37. */
  38. private $bucket;
  39. /**
  40. * Sets the riak bucket instance to use.
  41. *
  42. * @param \Riak\Bucket $bucket
  43. */
  44. public function __construct(Bucket $bucket)
  45. {
  46. $this->bucket = $bucket;
  47. }
  48. /**
  49. * {@inheritdoc}
  50. */
  51. protected function doFetch($id)
  52. {
  53. try {
  54. $response = $this->bucket->get(urlencode($id));
  55. // No objects found
  56. if ( ! $response->hasObject()) {
  57. return false;
  58. }
  59. // Check for attempted siblings
  60. $object = ($response->hasSiblings())
  61. ? $this->resolveConflict($id, $response->getVClock(), $response->getObjectList())
  62. : $response->getFirstObject();
  63. // Check for expired object
  64. if ($this->isExpired($object)) {
  65. $this->bucket->delete($object);
  66. return false;
  67. }
  68. return unserialize($object->getContent());
  69. } catch (Exception\RiakException $e) {
  70. // Covers:
  71. // - Riak\ConnectionException
  72. // - Riak\CommunicationException
  73. // - Riak\UnexpectedResponseException
  74. // - Riak\NotFoundException
  75. }
  76. return false;
  77. }
  78. /**
  79. * {@inheritdoc}
  80. */
  81. protected function doContains($id)
  82. {
  83. try {
  84. // We only need the HEAD, not the entire object
  85. $input = new Input\GetInput();
  86. $input->setReturnHead(true);
  87. $response = $this->bucket->get(urlencode($id), $input);
  88. // No objects found
  89. if ( ! $response->hasObject()) {
  90. return false;
  91. }
  92. $object = $response->getFirstObject();
  93. // Check for expired object
  94. if ($this->isExpired($object)) {
  95. $this->bucket->delete($object);
  96. return false;
  97. }
  98. return true;
  99. } catch (Exception\RiakException $e) {
  100. // Do nothing
  101. }
  102. return false;
  103. }
  104. /**
  105. * {@inheritdoc}
  106. */
  107. protected function doSave($id, $data, $lifeTime = 0)
  108. {
  109. try {
  110. $object = new Object(urlencode($id));
  111. $object->setContent(serialize($data));
  112. if ($lifeTime > 0) {
  113. $object->addMetadata(self::EXPIRES_HEADER, (string) (time() + $lifeTime));
  114. }
  115. $this->bucket->put($object);
  116. return true;
  117. } catch (Exception\RiakException $e) {
  118. // Do nothing
  119. }
  120. return false;
  121. }
  122. /**
  123. * {@inheritdoc}
  124. */
  125. protected function doDelete($id)
  126. {
  127. try {
  128. $this->bucket->delete(urlencode($id));
  129. return true;
  130. } catch (Exception\BadArgumentsException $e) {
  131. // Key did not exist on cluster already
  132. } catch (Exception\RiakException $e) {
  133. // Covers:
  134. // - Riak\Exception\ConnectionException
  135. // - Riak\Exception\CommunicationException
  136. // - Riak\Exception\UnexpectedResponseException
  137. }
  138. return false;
  139. }
  140. /**
  141. * {@inheritdoc}
  142. */
  143. protected function doFlush()
  144. {
  145. try {
  146. $keyList = $this->bucket->getKeyList();
  147. foreach ($keyList as $key) {
  148. $this->bucket->delete($key);
  149. }
  150. return true;
  151. } catch (Exception\RiakException $e) {
  152. // Do nothing
  153. }
  154. return false;
  155. }
  156. /**
  157. * {@inheritdoc}
  158. */
  159. protected function doGetStats()
  160. {
  161. // Only exposed through HTTP stats API, not Protocol Buffers API
  162. return null;
  163. }
  164. /**
  165. * Check if a given Riak Object have expired.
  166. *
  167. * @param \Riak\Object $object
  168. *
  169. * @return boolean
  170. */
  171. private function isExpired(Object $object)
  172. {
  173. $metadataMap = $object->getMetadataMap();
  174. return isset($metadataMap[self::EXPIRES_HEADER])
  175. && $metadataMap[self::EXPIRES_HEADER] < time();
  176. }
  177. /**
  178. * On-read conflict resolution. Applied approach here is last write wins.
  179. * Specific needs may override this method to apply alternate conflict resolutions.
  180. *
  181. * {@internal Riak does not attempt to resolve a write conflict, and store
  182. * it as sibling of conflicted one. By following this approach, it is up to
  183. * the next read to resolve the conflict. When this happens, your fetched
  184. * object will have a list of siblings (read as a list of objects).
  185. * In our specific case, we do not care about the intermediate ones since
  186. * they are all the same read from storage, and we do apply a last sibling
  187. * (last write) wins logic.
  188. * If by any means our resolution generates another conflict, it'll up to
  189. * next read to properly solve it.}
  190. *
  191. * @param string $id
  192. * @param string $vClock
  193. * @param array $objectList
  194. *
  195. * @return \Riak\Object
  196. */
  197. protected function resolveConflict($id, $vClock, array $objectList)
  198. {
  199. // Our approach here is last-write wins
  200. $winner = $objectList[count($objectList)];
  201. $putInput = new Input\PutInput();
  202. $putInput->setVClock($vClock);
  203. $mergedObject = new Object(urlencode($id));
  204. $mergedObject->setContent($winner->getContent());
  205. $this->bucket->put($mergedObject, $putInput);
  206. return $mergedObject;
  207. }
  208. }