PageRenderTime 50ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/examples/PHP/mdbroker.php

https://github.com/plouj/zguide
PHP | 342 lines | 213 code | 36 blank | 93 comment | 55 complexity | e4aa2c3eb72f0f1808ae0d4f8f415800 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. */
  93. public function purge_workers() {
  94. foreach($this->waiting as $id => $worker) {
  95. if(microtime(true) < $worker->expiry) {
  96. continue; // Worker is alive, we're done here
  97. }
  98. if($this->verbose) {
  99. printf("I: deleting expired worker: %s %s",
  100. $worker->identity, PHP_EOL);
  101. }
  102. $this->worker_delete($worker);
  103. }
  104. }
  105. /**
  106. * Locate or create new service entry
  107. *
  108. * @param string $name
  109. * @return stdClass
  110. */
  111. public function service_require($name) {
  112. $service = isset($this->services[$name]) ? $this->services[$name] : NULL;
  113. if($service == NULL) {
  114. $service = new stdClass();
  115. $service->name = $name;
  116. $service->requests = array();
  117. $service->waiting = array();
  118. $this->services[$name] = $service;
  119. }
  120. return $service;
  121. }
  122. /**
  123. * Dispatch requests to waiting workers as possible
  124. *
  125. * @param type $service
  126. * @param type $msg
  127. */
  128. public function service_dispatch($service, $msg) {
  129. if($msg) {
  130. $service->requests[] = $msg;
  131. }
  132. $this->purge_workers();
  133. while(count($service->waiting) && count($service->requests)) {
  134. $worker = array_shift($service->waiting);
  135. $msg = array_shift($service->requests);
  136. $this->worker_send($worker, MDPW_REQUEST, NULL, $msg);
  137. }
  138. }
  139. /**
  140. * Handle internal service according to 8/MMI specification
  141. *
  142. * @param string $frame
  143. * @param Zmsg $msg
  144. */
  145. public function service_internal($frame, $msg) {
  146. if($frame == "mmi.service") {
  147. $name = $msg->last();
  148. $service = $this->services[$name];
  149. $return_code = $service && $service->workers ? "200" : "404";
  150. } else {
  151. $return_code = "501";
  152. }
  153. $msg->set_last($return_code);
  154. // Remove & save client return envelope and insert the
  155. // protocol header and service name, then rewrap envelope
  156. $client = $msg->unwrap();
  157. $msg->push($frame);
  158. $msg->push(MDPC_CLIENT);
  159. $msg->wrap($client, "");
  160. $msg->set_socket($this->socket)->send();
  161. }
  162. /**
  163. * Creates worker if necessary
  164. *
  165. * @param string $address
  166. * @return stdClass
  167. */
  168. public function worker_require($address) {
  169. $worker = isset($this->workers[$address]) ? $this->workers[$address] : NULL;
  170. if($worker == NULL) {
  171. $worker = new stdClass();
  172. $worker->identity = $address;
  173. $worker->address = $address;
  174. if($this->verbose) {
  175. printf("I: registering new worker: %s %s", $address, PHP_EOL);
  176. }
  177. $this->workers[$address] = $worker;
  178. }
  179. return $worker;
  180. }
  181. /**
  182. * Remove a worker
  183. *
  184. * @param stdClass $worker
  185. * @param boolean $disconnect
  186. */
  187. public function worker_delete($worker, $disconnect = false) {
  188. if($disconnect) {
  189. $this->worker_send($worker, MDPW_DISCONNECT, NULL, NULL);
  190. }
  191. if(isset($worker->service)) {
  192. worker_remove_from_array($worker, $worker->service->waiting);
  193. $worker->service->workers--;
  194. }
  195. worker_remove_from_array($worker, $this->waiting);
  196. unset($this->workers[$worker->identity]);
  197. }
  198. private function worker_remove_from_array($worker, &$array) {
  199. $index = array_search($worker, $array);
  200. if ($index !== false) {
  201. unset($array[$index]);
  202. }
  203. }
  204. /**
  205. * Process message sent to us by a worker
  206. *
  207. * @param string $sender
  208. * @param Zmsg $msg
  209. */
  210. public function worker_process($sender, $msg) {
  211. $command = $msg->pop();
  212. $worker_ready = isset($this->workers[$sender]);
  213. $worker = $this->worker_require($sender);
  214. if($command == MDPW_READY) {
  215. if($worker_ready) {
  216. $this->worker_delete($worker, true); // Not first command in session
  217. } else if(strlen($sender) >= 4 // Reserved service name
  218. && substr($sender, 0, 4) == 'mmi.') {
  219. $this->worker_delete($worker, true);
  220. } else {
  221. // Attach worker to service and mark as idle
  222. $service_frame = $msg->pop();
  223. $worker->service = $this->service_require($service_frame);
  224. $worker->service->workers++;
  225. $this->worker_waiting($worker);
  226. }
  227. } else if($command == MDPW_REPLY) {
  228. if($worker_ready) {
  229. // Remove & save client return envelope and insert the
  230. // protocol header and service name, then rewrap envelope.
  231. $client = $msg->unwrap();
  232. $msg->push($worker->service->name);
  233. $msg->push(MDPC_CLIENT);
  234. $msg->wrap($client, "");
  235. $msg->set_socket($this->socket)->send();
  236. $this->worker_waiting($worker);
  237. } else {
  238. $this->worker_delete($worker, true);
  239. }
  240. } else if($command == MDPW_HEARTBEAT) {
  241. if($worker_ready) {
  242. $worker->expiry = microtime(true) + HEARTBEAT_EXPIRY;
  243. } else {
  244. $this->worker_delete($worker, true);
  245. }
  246. } else if($command == MDPW_DISCONNECT) {
  247. $this->worker_delete($worker, true);
  248. } else {
  249. echo "E: invalid input message", PHP_EOL, $msg->__toString();
  250. }
  251. }
  252. /**
  253. * Send message to worker
  254. *
  255. * @param stdClass $worker
  256. * @param string $command
  257. * @param mixed $option
  258. * @param Zmsg $msg
  259. */
  260. public function worker_send($worker, $command, $option, $msg) {
  261. $msg = $msg ? $msg : new Zmsg();
  262. // Stack protocol envelope to start of message
  263. if($option) {
  264. $msg->push($option);
  265. }
  266. $msg->push($command);
  267. $msg->push(MDPW_WORKER);
  268. // Stack routing envelope to start of message
  269. $msg->wrap($worker->address, "");
  270. if($this->verbose) {
  271. printf("I: sending %s to worker %s",
  272. $command, PHP_EOL);
  273. echo $msg->__toString();
  274. }
  275. $msg->set_socket($this->socket)->send();
  276. }
  277. /**
  278. * This worker is now waiting for work
  279. *
  280. * @param stdClass $worker
  281. */
  282. public function worker_waiting($worker) {
  283. // Queue to broker and service waiting lists
  284. $this->waiting[] = $worker;
  285. $worker->service->waiting[] = $worker;
  286. $worker->expiry = microtime(true) + (HEARTBEAT_EXPIRY/1000);
  287. $this->service_dispatch($worker->service, NULL);
  288. }
  289. /**
  290. * Process a request coming from a client
  291. *
  292. * @param string $sender
  293. * @param Zmsg $msg
  294. */
  295. public function client_process($sender, $msg) {
  296. $service_frame = $msg->pop();
  297. $service = $this->service_require($service_frame);
  298. // Set reply return address to client sender
  299. $msg->wrap($sender, "");
  300. if(substr($service_frame, 0, 4) == 'mmi.') {
  301. $this->service_internal($service_frame, $msg);
  302. } else {
  303. $this->service_dispatch($service, $msg);
  304. }
  305. }
  306. }