PageRenderTime 43ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/examples/PHP/mdbroker.php

https://github.com/pjkundert/zguide
PHP | 343 lines | 213 code | 36 blank | 94 comment | 55 complexity | 537837a4ab6a30bd453e49a8eaf5397b MD5 | raw file
  1. <?php
  2. /*
  3. * Majordomo Protocol broker
  4. * A minimal implementation of http://rfc.zeromq.org/spec:7 and spec:8
  5. * @author Ian Barber <ian(dot)barber(at)gmail(dot)com>
  6. */
  7. include_once "zmsg.php";
  8. include_once "mdp.php";
  9. // We'd normally pull these from config data
  10. define("HEARTBEAT_LIVENESS", 3); // 3-5 is reasonable
  11. define("HEARTBEAT_INTERVAL", 2500); // msecs
  12. define("HEARTBEAT_EXPIRY", HEARTBEAT_INTERVAL * HEARTBEAT_LIVENESS);
  13. /* Main broker work happens here */
  14. $verbose = $_SERVER['argc'] > 1 && $_SERVER['argv'][1] == '-v';
  15. $broker = new Mdbroker($verbose);
  16. $broker->bind("tcp://*:5555");
  17. $broker->listen();
  18. class Mdbroker {
  19. private $ctx; // Our context
  20. private $socket; // Socket for clients & workers
  21. private $endpoint; // Broker binds to this endpoint
  22. private $services = array(); // Hash of known services
  23. private $workers = array(); // Hash of known workers
  24. private $waiting = array(); // List of waiting workers
  25. private $verbose = false; // Print activity to stdout
  26. // Heartbeat management
  27. private $heartbeat_at; // When to send HEARTBEAT
  28. /**
  29. * Constructor
  30. *
  31. * @param boolean $verbose
  32. */
  33. public function __construct($verbose = false) {
  34. $this->ctx = new ZMQContext();
  35. $this->socket = new ZMQSocket($this->ctx, ZMQ::SOCKET_XREP);
  36. $this->verbose = $verbose;
  37. $this->heartbeat_at = microtime(true) + (HEARTBEAT_INTERVAL/1000);
  38. }
  39. /**
  40. * Bind broker to endpoint, can call this multiple time
  41. * We use a single socket for both clients and workers.
  42. *
  43. * @param string $endpoint
  44. */
  45. public function bind($endpoint) {
  46. $this->socket->bind($endpoint);
  47. if($this->verbose) {
  48. printf("I: MDP broker/0.1.1 is active at %s %s", $endpoint, PHP_EOL);
  49. }
  50. }
  51. /**
  52. * This is the main listen and process loop
  53. */
  54. public function listen() {
  55. $read = $write = array();
  56. // Get and process messages forever or until interrupted
  57. while(true) {
  58. $poll = new ZMQPoll();
  59. $poll->add($this->socket, ZMQ::POLL_IN);
  60. $events = $poll->poll($read, $write, HEARTBEAT_INTERVAL * ZMQ_POLL_MSEC);
  61. // Process next input message, if any
  62. if($events) {
  63. $zmsg = new Zmsg($this->socket);
  64. $zmsg->recv();
  65. if($this->verbose) {
  66. echo "I: received message:", PHP_EOL, $zmsg->__toString();
  67. }
  68. $sender = $zmsg->pop();
  69. $empty = $zmsg->pop();
  70. $header = $zmsg->pop();
  71. if($header == MDPC_CLIENT) {
  72. $this->client_process($sender, $zmsg);
  73. } else if($header == MDPW_WORKER) {
  74. $this->worker_process($sender, $zmsg);
  75. } else {
  76. echo "E: invalid message", PHP_EOL, $zmsg->__toString();
  77. }
  78. }
  79. // Disconnect and delete any expired workers
  80. // Send heartbeats to idle workers if needed
  81. if(microtime(true) > $this->heartbeat_at) {
  82. $this->purge_workers();
  83. foreach($this->workers as $worker) {
  84. $this->worker_send($worker, MDPW_HEARTBEAT, NULL, NULL);
  85. }
  86. $this->heartbeat_at = microtime(true) + (HEARTBEAT_INTERVAL/1000);
  87. }
  88. }
  89. }
  90. /**
  91. * Delete any idle workers that haven't pinged us in a while.
  92. * We know that workers are ordered from oldest to most recent.
  93. */
  94. public function purge_workers() {
  95. foreach($this->waiting as $id => $worker) {
  96. if(microtime(true) < $worker->expiry) {
  97. break; // Worker is alive, we're done here
  98. }
  99. if($this->verbose) {
  100. printf("I: deleting expired worker: %s %s",
  101. $worker->identity, PHP_EOL);
  102. }
  103. $this->worker_delete($worker);
  104. }
  105. }
  106. /**
  107. * Locate or create new service entry
  108. *
  109. * @param string $name
  110. * @return stdClass
  111. */
  112. public function service_require($name) {
  113. $service = isset($this->services[$name]) ? $this->services[$name] : NULL;
  114. if($service == NULL) {
  115. $service = new stdClass();
  116. $service->name = $name;
  117. $service->requests = array();
  118. $service->waiting = array();
  119. $this->services[$name] = $service;
  120. }
  121. return $service;
  122. }
  123. /**
  124. * Dispatch requests to waiting workers as possible
  125. *
  126. * @param type $service
  127. * @param type $msg
  128. */
  129. public function service_dispatch($service, $msg) {
  130. if($msg) {
  131. $service->requests[] = $msg;
  132. }
  133. $this->purge_workers();
  134. while(count($service->waiting) && count($service->requests)) {
  135. $worker = array_shift($service->waiting);
  136. $msg = array_shift($service->requests);
  137. $this->worker_send($worker, MDPW_REQUEST, NULL, $msg);
  138. }
  139. }
  140. /**
  141. * Handle internal service according to 8/MMI specification
  142. *
  143. * @param string $frame
  144. * @param Zmsg $msg
  145. */
  146. public function service_internal($frame, $msg) {
  147. if($frame == "mmi.service") {
  148. $name = $msg->last();
  149. $service = $this->services[$name];
  150. $return_code = $service && $service->workers ? "200" : "404";
  151. } else {
  152. $return_code = "501";
  153. }
  154. $msg->set_last($return_code);
  155. // Remove & save client return envelope and insert the
  156. // protocol header and service name, then rewrap envelope
  157. $client = $msg->unwrap();
  158. $msg->push($frame);
  159. $msg->push(MDPC_CLIENT);
  160. $msg->wrap($client, "");
  161. $msg->set_socket($this->socket)->send();
  162. }
  163. /**
  164. * Creates worker if necessary
  165. *
  166. * @param string $address
  167. * @return stdClass
  168. */
  169. public function worker_require($address) {
  170. $worker = isset($this->workers[$address]) ? $this->workers[$address] : NULL;
  171. if($worker == NULL) {
  172. $worker = new stdClass();
  173. $worker->identity = $address;
  174. $worker->address = $address;
  175. if($this->verbose) {
  176. printf("I: registering new worker: %s %s", $address, PHP_EOL);
  177. }
  178. $this->workers[$address] = $worker;
  179. }
  180. return $worker;
  181. }
  182. /**
  183. * Remove a worker
  184. *
  185. * @param stdClass $worker
  186. * @param boolean $disconnect
  187. */
  188. public function worker_delete($worker, $disconnect = false) {
  189. if($disconnect) {
  190. $this->worker_send($worker, MDPW_DISCONNECT, NULL, NULL);
  191. }
  192. if(isset($worker->service)) {
  193. worker_remove_from_array($worker, $worker->service->waiting);
  194. $worker->service->workers--;
  195. }
  196. worker_remove_from_array($worker, $this->waiting);
  197. unset($this->workers[$worker->identity]);
  198. }
  199. private function worker_remove_from_array($worker, &$array) {
  200. $index = array_search($worker, $array);
  201. if ($index !== false) {
  202. unset($array[$index]);
  203. }
  204. }
  205. /**
  206. * Process message sent to us by a worker
  207. *
  208. * @param string $sender
  209. * @param Zmsg $msg
  210. */
  211. public function worker_process($sender, $msg) {
  212. $command = $msg->pop();
  213. $worker_ready = isset($this->workers[$sender]);
  214. $worker = $this->worker_require($sender);
  215. if($command == MDPW_READY) {
  216. if($worker_ready) {
  217. $this->worker_delete($worker, true); // Not first command in session
  218. } else if(strlen($sender) >= 4 // Reserved service name
  219. && substr($sender, 0, 4) == 'mmi.') {
  220. $this->worker_delete($worker, true);
  221. } else {
  222. // Attach worker to service and mark as idle
  223. $service_frame = $msg->pop();
  224. $worker->service = $this->service_require($service_frame);
  225. $worker->service->workers++;
  226. $this->worker_waiting($worker);
  227. }
  228. } else if($command == MDPW_REPLY) {
  229. if($worker_ready) {
  230. // Remove & save client return envelope and insert the
  231. // protocol header and service name, then rewrap envelope.
  232. $client = $msg->unwrap();
  233. $msg->push($worker->service->name);
  234. $msg->push(MDPC_CLIENT);
  235. $msg->wrap($client, "");
  236. $msg->set_socket($this->socket)->send();
  237. $this->worker_waiting($worker);
  238. } else {
  239. $this->worker_delete($worker, true);
  240. }
  241. } else if($command == MDPW_HEARTBEAT) {
  242. if($worker_ready) {
  243. $worker->expiry = microtime(true) + HEARTBEAT_EXPIRY;
  244. } else {
  245. $this->worker_delete($worker, true);
  246. }
  247. } else if($command == MDPW_DISCONNECT) {
  248. $this->worker_delete($worker, true);
  249. } else {
  250. echo "E: invalid input message", PHP_EOL, $msg->__toString();
  251. }
  252. }
  253. /**
  254. * Send message to worker
  255. *
  256. * @param stdClass $worker
  257. * @param string $command
  258. * @param mixed $option
  259. * @param Zmsg $msg
  260. */
  261. public function worker_send($worker, $command, $option, $msg) {
  262. $msg = $msg ? $msg : new Zmsg();
  263. // Stack protocol envelope to start of message
  264. if($option) {
  265. $msg->push($option);
  266. }
  267. $msg->push($command);
  268. $msg->push(MDPW_WORKER);
  269. // Stack routing envelope to start of message
  270. $msg->wrap($worker->address, "");
  271. if($this->verbose) {
  272. printf("I: sending %s to worker %s",
  273. $command, PHP_EOL);
  274. echo $msg->__toString();
  275. }
  276. $msg->set_socket($this->socket)->send();
  277. }
  278. /**
  279. * This worker is now waiting for work
  280. *
  281. * @param stdClass $worker
  282. */
  283. public function worker_waiting($worker) {
  284. // Queue to broker and service waiting lists
  285. $this->waiting[] = $worker;
  286. $worker->service->waiting[] = $worker;
  287. $worker->expiry = microtime(true) + (HEARTBEAT_EXPIRY/1000);
  288. $this->service_dispatch($worker->service, NULL);
  289. }
  290. /**
  291. * Process a request coming from a client
  292. *
  293. * @param string $sender
  294. * @param Zmsg $msg
  295. */
  296. public function client_process($sender, $msg) {
  297. $service_frame = $msg->pop();
  298. $service = $this->service_require($service_frame);
  299. // Set reply return address to client sender
  300. $msg->wrap($sender, "");
  301. if(substr($service_frame, 0, 4) == 'mmi.') {
  302. $this->service_internal($service_frame, $msg);
  303. } else {
  304. $this->service_dispatch($service, $msg);
  305. }
  306. }
  307. }