PageRenderTime 53ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/src/rdb/connection.php

https://gitlab.com/bran921007/php-rql
PHP | 359 lines | 279 code | 64 blank | 16 comment | 92 complexity | cf1bbda1c584fd8faeadbb638934c12f MD5 | raw file
  1. <?php namespace r;
  2. require_once("util.php");
  3. require_once("datum.php");
  4. class Connection
  5. {
  6. public function __construct($host, $port = 28015, $db = null, $apiKey = null, $timeout = null) {
  7. if (!isset($host)) throw new RqlDriverError("No host given.");
  8. if (!isset($port)) throw new RqlDriverError("No port given.");
  9. if (isset($apiKey) && !is_string($apiKey)) throw new RqlDriverError("The API key must be a string.");
  10. $this->host = $host;
  11. $this->port = $port;
  12. if (!isset($apiKey))
  13. $apiKey = "";
  14. $this->apiKey = $apiKey;
  15. $this->timeout = null;
  16. if (isset($db))
  17. $this->useDb($db);
  18. if (isset($timeout))
  19. $this->setTimeout($timeout);
  20. $this->connect();
  21. }
  22. public function __destruct() {
  23. if ($this->isOpen())
  24. $this->close(false);
  25. }
  26. public function close($noreplyWait = true) {
  27. if (!$this->isOpen()) throw new RqlDriverError("Not connected.");
  28. if ($noreplyWait) {
  29. $this->noreplyWait();
  30. }
  31. fclose($this->socket);
  32. $this->socket = null;
  33. $this->activeTokens = null;
  34. }
  35. public function reconnect($noreplyWait = true) {
  36. if ($this->isOpen())
  37. $this->close($noreplyWait);
  38. $this->connect();
  39. }
  40. public function isOpen() {
  41. return isset($this->socket);
  42. }
  43. public function useDb($dbName) {
  44. if (!is_string($dbName)) throw new RqlDriverError("Database must be a string.");
  45. $this->defaultDb = new Db($dbName);
  46. }
  47. public function setTimeout($timeout) {
  48. if (!is_numeric($timeout)) throw new RqlDriverError("Timeout must be a number.");
  49. $this->applyTimeout($timeout);
  50. $this->timeout = $timeout;
  51. }
  52. public function noreplyWait() {
  53. if (!$this->isOpen()) throw new RqlDriverError("Not connected.");
  54. // Generate a token for the request
  55. $token = $this->generateToken();
  56. // Send the request
  57. $jsonQuery = array(pb\Query_QueryType::PB_NOREPLY_WAIT);
  58. $this->sendQuery($token, $jsonQuery);
  59. // Await the response
  60. $response = $this->receiveResponse($token);
  61. if ($response['t'] != pb\Response_ResponseType::PB_WAIT_COMPLETE) {
  62. throw new RqlDriverError("Unexpected response type to noreplyWait query.");
  63. }
  64. }
  65. public function _run(Query $query, $options, &$profile) {
  66. if (isset($options) && !is_array($options)) throw new RqlDriverError("Options must be an array.");
  67. if (!$this->isOpen()) throw new RqlDriverError("Not connected.");
  68. // Generate a token for the request
  69. $token = $this->generateToken();
  70. // Send the request
  71. $jsonTerm = $query->_getJSONTerm();
  72. $globalOptargs = array();
  73. if (isset($this->defaultDb)) {
  74. $globalOptargs['db'] = $this->defaultDb->_getJSONTerm();
  75. }
  76. if (isset($options)) {
  77. foreach ($options as $key => $value) {
  78. $globalOptargs[$key] = nativeToDatum($value)->_getJSONTerm();
  79. }
  80. }
  81. $jsonQuery = array(pb\Query_QueryType::PB_START, $jsonTerm, (Object)$globalOptargs);
  82. $this->sendQuery($token, $jsonQuery);
  83. if (isset($options) && isset($options['noreply']) && $options['noreply'] === true) {
  84. return null;
  85. }
  86. else {
  87. // Await the response
  88. $response = $this->receiveResponse($token, $query);
  89. if ($response['t'] == pb\Response_ResponseType::PB_SUCCESS_PARTIAL
  90. || $response['t'] == pb\Response_ResponseType::PB_SUCCESS_ATOM_FEED
  91. || $response['t'] == pb\Response_ResponseType::PB_SUCCESS_FEED) {
  92. $this->activeTokens[$token] = true;
  93. }
  94. if (isset($response['p'])) {
  95. $profile = decodedJSONToDatum($response['p']);
  96. }
  97. if ($response['t'] == pb\Response_ResponseType::PB_SUCCESS_ATOM)
  98. return $this->createDatumFromResponse($response);
  99. else
  100. return $this->createCursorFromResponse($response, $token);
  101. }
  102. }
  103. public function _continueQuery($token) {
  104. if (!$this->isOpen()) throw new RqlDriverError("Not connected.");
  105. if (!is_numeric($token)) throw new RqlDriverError("Token must be a number.");
  106. // Send the request
  107. $jsonQuery = array(pb\Query_QueryType::PB_CONTINUE);
  108. $this->sendQuery($token, $jsonQuery);
  109. // Await the response
  110. $response = $this->receiveResponse($token);
  111. if ($response['t'] != pb\Response_ResponseType::PB_SUCCESS_PARTIAL
  112. && $response['t'] != pb\Response_ResponseType::PB_SUCCESS_ATOM_FEED
  113. && $response['t'] != pb\Response_ResponseType::PB_SUCCESS_FEED) {
  114. unset($this->activeTokens[$token]);
  115. }
  116. return $response;
  117. }
  118. public function _stopQuery($token) {
  119. if (!$this->isOpen()) throw new RqlDriverError("Not connected.");
  120. if (!is_numeric($token)) throw new RqlDriverError("Token must be a number.");
  121. // Send the request
  122. $jsonQuery = array(pb\Query_QueryType::PB_STOP);
  123. $this->sendQuery($token, $jsonQuery);
  124. // Await the response (but don't check for errors. the stop response doesn't even have a type)
  125. $response = $this->receiveResponse($token, null, true);
  126. unset($this->activeTokens[$token]);
  127. return $response;
  128. }
  129. private function generateToken() {
  130. $tries = 0;
  131. $maxToken = 1 << 30;
  132. do {
  133. $token = \rand(0, $maxToken);
  134. $haveCollision = isset($this->activeTokens[$token]);
  135. } while ($haveCollision && $tries++ < 1024);
  136. if ($haveCollision) {
  137. throw new RqlDriverError("Unable to generate a unique token for the query.");
  138. }
  139. return $token;
  140. }
  141. private function receiveResponse($token, $query = null, $noChecks = false) {
  142. $responseHeader = $this->receiveStr(4 + 8);
  143. $responseHeader = unpack("Vtoken/Vtoken2/Vsize", $responseHeader);
  144. $responseToken = $responseHeader['token'];
  145. if ($responseHeader['token2'] != 0) {
  146. throw new RqlDriverError("Invalid response from server: Invalid token.");
  147. }
  148. $responseSize = $responseHeader['size'];
  149. $responseBuf = $this->receiveStr($responseSize);
  150. $response = json_decode($responseBuf);
  151. if (json_last_error() != JSON_ERROR_NONE) {
  152. throw new RqlDriverError("Unable to decode JSON response (error code " . json_last_error() . ")");
  153. }
  154. if (!is_object($response)) {
  155. throw new RqlDriverError("Invalid response from server: Not an object.");
  156. }
  157. $response = (array)$response;
  158. if (!$noChecks)
  159. $this->checkResponse($response, $responseToken, $token, $query);
  160. return $response;
  161. }
  162. private function checkResponse($response, $responseToken, $token, $query = null) {
  163. if (!isset($response['t'])) throw new RqlDriverError("Response message has no type.");
  164. if ($response['t'] == pb\Response_ResponseType::PB_CLIENT_ERROR) {
  165. throw new RqlDriverError("Server says PHP-RQL is buggy: " . $response['r'][0]);
  166. }
  167. if ($responseToken != $token) {
  168. throw new RqlDriverError("Received wrong token. Response does not match the request. Expected $token, received " . $responseToken);
  169. }
  170. if ($response['t'] == pb\Response_ResponseType::PB_COMPILE_ERROR) {
  171. $backtrace = null;
  172. if (isset($response['b']))
  173. $backtrace = Backtrace::_fromJSON($response['b']);
  174. throw new RqlUserError("Compile error: " . $response['r'][0], $query, $backtrace);
  175. }
  176. else if ($response['t'] == pb\Response_ResponseType::PB_RUNTIME_ERROR) {
  177. $backtrace = null;
  178. if (isset($response['b']))
  179. $backtrace = Backtrace::_fromJSON($response['b']);
  180. throw new RqlUserError("Runtime error: " . $response['r'][0], $query, $backtrace);
  181. }
  182. }
  183. private function createCursorFromResponse($response, $token) {
  184. return new Cursor($this, $response, $token);
  185. }
  186. private function createDatumFromResponse($response) {
  187. $datum = $response['r'][0];
  188. return decodedJSONToDatum($datum);
  189. }
  190. private function sendQuery($token, $json) {
  191. // PHP by default loses some precision when encoding floats, so we temporarily
  192. // bump up the `precision` option to avoid this.
  193. // The 17 assumes IEEE-754 double precision numbers.
  194. // Source: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
  195. // "The same argument applied to double precision shows that 17 decimal
  196. // digits are required to recover a double precision number."
  197. $previousPrecision = ini_set("precision", 17);
  198. $request = json_encode($json);
  199. if ($previousPrecision !== false) {
  200. ini_set("precision", $previousPrecision);
  201. }
  202. if ($request === false) throw new RqlDriverError("Failed to encode query as JSON: " . json_last_error());
  203. $requestSize = pack("V", strlen($request));
  204. $binaryToken = pack("V", $token) . pack("V", 0);
  205. $this->sendStr($binaryToken . $requestSize . $request);
  206. }
  207. private function applyTimeout($timeout) {
  208. if ($this->isOpen()) {
  209. if (!stream_set_timeout($this->socket, $timeout)) {
  210. throw new RqlDriverError("Could not set timeout");
  211. }
  212. }
  213. }
  214. private function connect() {
  215. if ($this->isOpen()) throw new RqlDriverError("Already connected");
  216. $this->socket = stream_socket_client("tcp://" . $this->host . ":" . $this->port, $errno, $errstr);
  217. if ($errno != 0 || $this->socket === false) {
  218. $this->socket = null;
  219. throw new RqlDriverError("Unable to connect: " . $errstr);
  220. }
  221. if ($this->timeout) {
  222. $this->applyTimeout($this->timeout);
  223. }
  224. $this->sendHandshake();
  225. $this->receiveHandshakeResponse();
  226. }
  227. private function sendHandshake() {
  228. if (!$this->isOpen()) throw new RqlDriverError("Not connected");
  229. $binaryVersion = pack("V", pb\VersionDummy_Version::PB_V0_3); // "V" is little endian, 32 bit unsigned integer
  230. $handshake = $binaryVersion;
  231. $binaryKeyLength = pack("V", strlen($this->apiKey));
  232. $handshake .= $binaryKeyLength . $this->apiKey;
  233. $binaryProtocol = pack("V", pb\VersionDummy_Protocol::PB_JSON);
  234. $handshake .= $binaryProtocol;
  235. $this->sendStr($handshake);
  236. }
  237. private function receiveHandshakeResponse() {
  238. if (!$this->isOpen()) throw new RqlDriverError("Not connected");
  239. $response = "";
  240. while (true) {
  241. $ch = stream_get_contents($this->socket, 1);
  242. if ($ch === false || strlen($ch) < 1) {
  243. $this->close(false);
  244. throw new RqlDriverError("Unable to read from socket during handshake. Disconnected.");
  245. }
  246. if ($ch === chr(0))
  247. break;
  248. else
  249. $response .= $ch;
  250. }
  251. if ($response != "SUCCESS") {
  252. $this->close(false);
  253. throw new RqlDriverError("Handshake failed: $response Disconnected.");
  254. }
  255. }
  256. private function sendStr($s) {
  257. $bytesWritten = 0;
  258. while ($bytesWritten < strlen($s)) {
  259. $result = fwrite($this->socket, substr($s, $bytesWritten));
  260. if ($result === false || $result === 0) {
  261. $metaData = stream_get_meta_data($this->socket);
  262. $this->close(false);
  263. if ($metaData['timed_out']) {
  264. throw new RqlDriverError("Timed out while writing to socket. Disconnected. Call setTimeout(seconds) on the connection to change the timeout.");
  265. }
  266. throw new RqlDriverError("Unable to write to socket. Disconnected.");
  267. }
  268. $bytesWritten += $result;
  269. }
  270. }
  271. private function receiveStr($length) {
  272. $s = "";
  273. while (strlen($s) < $length) {
  274. $partialS = stream_get_contents($this->socket, $length - strlen($s));
  275. if ($partialS === false) {
  276. $metaData = stream_get_meta_data($this->socket);
  277. $this->close(false);
  278. if ($metaData['timed_out']) {
  279. throw new RqlDriverError("Timed out while reading from socket. Disconnected. Call setTimeout(seconds) on the connection to change the timeout.");
  280. }
  281. throw new RqlDriverError("Unable to read from socket. Disconnected.");
  282. }
  283. $s = $s . $partialS;
  284. }
  285. return $s;
  286. }
  287. private $socket;
  288. private $host;
  289. private $port;
  290. private $defaultDb;
  291. private $apiKey;
  292. private $activeTokens;
  293. private $timeout;
  294. }
  295. ?>