/lib/Jamm/Memory/Shm/MultiAccess.php

https://bitbucket.org/jamm/memory · PHP · 270 lines · 233 code · 21 blank · 16 comment · 46 complexity · 6959c42ddd7b0344639aa585f6bb73e7 MD5 · raw file

  1. <?php
  2. namespace Jamm\Memory\Shm;
  3. use Jamm\Memory\IKeyLocker;
  4. class MultiAccess implements IMutex
  5. {
  6. protected $read_q_key = 1;
  7. protected $write_q_key = 2;
  8. protected $writers_mutex_key = 3;
  9. protected $err_log = array();
  10. protected $read_q;
  11. protected $write_q;
  12. protected $writers_mutex;
  13. protected $writers_count = 0;
  14. protected $readers_count = 0;
  15. protected $mutex_acquired = false;
  16. protected $max_wait_time = 0.05;
  17. const readers = 1;
  18. const writers = 2;
  19. public function get_access_read(&$auto_unlocker_reference)
  20. {
  21. if (($this->writers_count <= 0) && ($this->readers_count <= 0)) //if it is nested call - access should be given only once
  22. {
  23. if (!$this->wait(self::writers)) return false; //if somebody are writing here now - we will wait
  24. $sent = $this->increment(self::readers); //increment count of readers - writers will be waiting for us while we read.
  25. if (!$sent) return false;
  26. }
  27. $auto_unlocker_reference = new \Jamm\Memory\KeyAutoUnlocker(array($this, 'release_access_read'));
  28. $this->readers_count++;
  29. return true;
  30. }
  31. public function release_access_read(IKeyLocker $autoUnlocker = NULL)
  32. {
  33. if (!empty($autoUnlocker)) $autoUnlocker->revoke();
  34. if ($this->readers_count > 0) $this->readers_count--;
  35. if ($this->readers_count <= 0) $this->decrement(self::readers); //tell to writers, that we have read and they can write now
  36. return true;
  37. }
  38. public function get_access_write(&$auto_unlocker_reference)
  39. {
  40. if ($this->writers_count <= 0) //if we are writers already - don't need to lock semaphore again
  41. {
  42. if ($this->readers_count > 0) //if we got reader's access and want to write - release our reader's access and got new access - writer's (else process will wait itself for a while)
  43. {
  44. $this->readers_count = 0;
  45. $this->release_access_read($auto_unlocker_reference);
  46. }
  47. //acquire mutex for writing
  48. if (!$this->acquire_writers_mutex()) return false;
  49. //only 1 writer can send message to writers queue, so if in queue more than 1 message - it's somebody's error, and we will fix it now:
  50. $this->clean_queue(self::writers);
  51. //tell to readers, that they should wait while we will write
  52. //this action should be made before writer will wait for readers
  53. $sent = $this->increment(self::writers); //after this command, readers will wait, until we will leave the queue
  54. if (!$sent)
  55. {
  56. $this->release_writers_mutex();
  57. return false;
  58. }
  59. //but if readers has come before us - wait, until they finish
  60. if (!$this->wait(self::readers))
  61. {
  62. $this->decrement(self::writers);
  63. $this->release_writers_mutex();
  64. return false;
  65. }
  66. }
  67. $auto_unlocker_reference = new \Jamm\Memory\KeyAutoUnlocker(array($this, 'release_access_write'));
  68. $this->writers_count++;
  69. //and now we can write :)
  70. return true;
  71. }
  72. public function release_access_write(IKeyLocker $autoUnlocker = NULL)
  73. {
  74. if (!empty($autoUnlocker)) $autoUnlocker->revoke();
  75. if ($this->writers_count > 0) $this->writers_count--;
  76. if ($this->writers_count <= 0)
  77. {
  78. $this->decrement(self::writers); //tell to readers, that they can read now
  79. $this->release_writers_mutex();
  80. }
  81. return true;
  82. }
  83. /**
  84. * Returned value of this function should not be ignored.
  85. * 'False' means that access should not be granted.
  86. * @return bool
  87. */
  88. protected function acquire_writers_mutex()
  89. {
  90. if (!$this->mutex_acquired)
  91. {
  92. if (empty($this->writers_mutex)) $this->writers_mutex = sem_get($this->writers_mutex_key, 1, 0777, 1);
  93. if (empty($this->writers_mutex)) return false;
  94. ignore_user_abort(true); //don't hang with semaphore, please :)
  95. set_time_limit(30);
  96. if (!sem_acquire($this->writers_mutex))
  97. {
  98. $this->err_log[] = 'Can not acquire writers mutex';
  99. return false;
  100. }
  101. $this->mutex_acquired = true;
  102. }
  103. return true;
  104. }
  105. protected function release_writers_mutex()
  106. {
  107. if (!empty($this->writers_mutex))
  108. {
  109. if (sem_release($this->writers_mutex))
  110. {
  111. unset($this->writers_mutex);
  112. $this->mutex_acquired = false;
  113. return true;
  114. }
  115. else return false;
  116. }
  117. return true;
  118. }
  119. protected function clean_queue($type = self::writers)
  120. {
  121. $q = $this->select_q($type);
  122. $stat = msg_stat_queue($q);
  123. if ($stat['msg_qnum'] > 0)
  124. {
  125. for ($i = $stat['msg_qnum']; $i > 0; $i--)
  126. {
  127. msg_receive($q, $type, $t, 100, $msg, false, MSG_IPC_NOWAIT+MSG_NOERROR, $err);
  128. }
  129. }
  130. }
  131. protected function increment($type = self::readers)
  132. {
  133. $q = $this->select_q($type);
  134. if (empty($q)) return false;
  135. $sent = msg_send($q, $type, $type, false, false, $err);
  136. if ($sent==false)
  137. {
  138. $counter = $this->get_counter($type);
  139. $this->err_log[] = 'Message was not sent to queue '.($type==self::readers ? 'readers '.$this->read_q_key
  140. : 'writers '.$this->write_q_key)
  141. .' counter: '.$counter.', error: '.$err;
  142. return false;
  143. }
  144. return true;
  145. }
  146. protected function decrement($type = self::readers)
  147. {
  148. $q = $this->select_q($type);
  149. if (empty($q)) return false;
  150. $recieve = msg_receive($q, $type, $t, 100, $msg, false, MSG_IPC_NOWAIT+MSG_NOERROR, $err);
  151. if ($recieve===false)
  152. {
  153. $counter = $this->get_counter($type);
  154. if ($counter > 0)
  155. {
  156. $this->err_log[] = 'Message was not recieved from queue '.($type==self::readers
  157. ? 'readers '.$this->read_q_key : 'writers '.$this->write_q_key)
  158. .' counter: '.$counter.', error: '.$err;
  159. return false;
  160. }
  161. }
  162. return true;
  163. }
  164. public function get_counter($type = self::writers)
  165. {
  166. $q = $this->select_q($type);
  167. $stat = msg_stat_queue($q);
  168. return $stat['msg_qnum'];
  169. }
  170. /**
  171. * Wait for queue. If this function has returned 'false', access should not be granted.
  172. * @param int $type
  173. * @return bool
  174. */
  175. protected function wait($type = self::writers)
  176. {
  177. $q = $this->select_q($type);
  178. if (empty($q)) return false;
  179. $stat = msg_stat_queue($q);
  180. if ($stat['msg_qnum'] > 0)
  181. {
  182. $starttime = microtime(true);
  183. do
  184. {
  185. $stat = msg_stat_queue($q);
  186. if (empty($stat)) break;
  187. if ($stat['msg_qnum'] <= 0) break;
  188. if ((microtime(true)-$starttime) > $this->max_wait_time) return false;
  189. } while ($stat['msg_qnum'] > 0);
  190. }
  191. return true;
  192. }
  193. protected function select_q($type)
  194. {
  195. if ($type==self::readers)
  196. {
  197. if (empty($this->read_q)) $this->read_q = msg_get_queue($this->read_q_key, 0777);
  198. $q = $this->read_q;
  199. }
  200. else
  201. {
  202. if (empty($this->write_q)) $this->write_q = msg_get_queue($this->write_q_key, 0777);
  203. $q = $this->write_q;
  204. }
  205. return $q;
  206. }
  207. public function __construct($id = '')
  208. {
  209. if (!empty($id))
  210. {
  211. if (is_string($id))
  212. {
  213. if (is_file($id) || is_dir($id)) $id = ftok($id, 'I');
  214. else $id = intval($id);
  215. }
  216. $this->read_q_key += $id*10;
  217. $this->write_q_key += $id*10;
  218. $this->writers_mutex_key += $id*10;
  219. }
  220. }
  221. public function __destruct()
  222. {
  223. if ($this->writers_count > 0)
  224. {
  225. $this->err_log[] = 'writers count = '.$this->writers_count;
  226. $this->writers_count = 0;
  227. $this->release_access_write();
  228. }
  229. $this->release_writers_mutex();
  230. if ($this->readers_count > 0)
  231. {
  232. $this->err_log[] = 'readers count = '.$this->readers_count;
  233. $this->readers_count = 0;
  234. $this->release_access_read();
  235. }
  236. if (!empty($this->err_log))
  237. {
  238. trigger_error('MultiAccess errors '.implode("\n", $this->err_log), E_USER_NOTICE);
  239. }
  240. }
  241. public function getReadQKey()
  242. { return $this->read_q_key; }
  243. public function getWriteQKey()
  244. { return $this->write_q_key; }
  245. public function getErrLog()
  246. { return $this->err_log; }
  247. public function setMaxWaitTime($max_wait_time)
  248. { $this->max_wait_time = floatval($max_wait_time); }
  249. }