1+ <?php
2+
3+ namespace Devloops \Typesence ;
4+
5+ use Exception ;
6+ use Http \Client \Exception as HttpClientException ;
7+ use Http \Client \Exception \HttpException ;
8+ use Http \Client \HttpClient ;
9+ use Psr \Http \Message \StreamInterface ;
10+ use Psr \Log \LoggerInterface ;
11+ use Typesense \Exceptions \HTTPStatus0Error ;
12+ use Devloops \Typesence \Lib \Configuration ;
13+ use Devloops \Typesence \Lib \Node ;
14+ use Devloops \Typesence \Exceptions \ServerError ;
15+ use Devloops \Typesence \Exceptions \ObjectNotFound ;
16+ use Devloops \Typesence \Exceptions \RequestMalformed ;
17+ use Devloops \Typesence \Exceptions \ServiceUnavailable ;
18+ use Devloops \Typesence \Exceptions \RequestUnauthorized ;
19+ use Devloops \Typesence \Exceptions \ObjectAlreadyExists ;
20+ use Devloops \Typesence \Exceptions \ObjectUnprocessable ;
21+ use Devloops \Typesence \Exceptions \TypesenseClientError ;
22+
23+ /**
24+ * Class ApiCall
25+ *
26+ * @package \Typesense
27+ * @date 4/5/20
28+ * @author Abdullah Al-Faqeir <abdullah@devloops.net>
29+ */
30+ class ApiCallv4
31+ {
32+
33+ private const API_KEY_HEADER_NAME = 'X-TYPESENSE-API-KEY ' ;
34+
35+ /**
36+ * @var \GuzzleHttp\Client
37+ */
38+ private $ client ;
39+
40+ /**
41+ * @var \Devloops\Typesence\Lib\Configuration
42+ */
43+ private Configuration $ config ;
44+
45+ /**
46+ * @var array|Node[]
47+ */
48+ private static array $ nodes ;
49+
50+ /**
51+ * @var Node|null
52+ */
53+ private static ?Node $ nearestNode ;
54+
55+ /**
56+ * @var int
57+ */
58+ private int $ nodeIndex ;
59+
60+ /**
61+ * @var LoggerInterface|null
62+ */
63+ public ?LoggerInterface $ logger ;
64+
65+ /**
66+ * ApiCall constructor.
67+ *
68+ * @param \Devloops\Typesence\Lib\Configuration $config
69+ */
70+ public function __construct (Configuration $ config )
71+ {
72+ $ this ->config = $ config ;
73+ $ this ->logger = null ;
74+ $ this ->client = new \GuzzleHttp \Client ();
75+ static ::$ nodes = $ this ->config ->getNodes ();
76+ static ::$ nearestNode = $ this ->config ->getNearestNode ();
77+ $ this ->nodeIndex = 0 ;
78+ $ this ->initializeNodes ();
79+ }
80+
81+ /**
82+ * Initialize Nodes
83+ */
84+ private function initializeNodes (): void
85+ {
86+ if (static ::$ nearestNode !== null ) {
87+ $ this ->setNodeHealthCheck (static ::$ nearestNode , true );
88+ }
89+
90+ foreach (static ::$ nodes as &$ node ) {
91+ $ this ->setNodeHealthCheck ($ node , true );
92+ }
93+ }
94+
95+ /**
96+ * @param string $endPoint
97+ * @param array $params
98+ * @param bool $asJson
99+ *
100+ * @return string|array
101+ * @throws TypesenseClientError
102+ * @throws Exception|HttpClientException
103+ */
104+ public function get (string $ endPoint , array $ params , bool $ asJson = true )
105+ {
106+ return $ this ->makeRequest ('get ' , $ endPoint , $ asJson , [
107+ 'query ' => $ params ?? [],
108+ ]);
109+ }
110+
111+ /**
112+ * @param string $endPoint
113+ * @param mixed $body
114+ *
115+ * @param bool $asJson
116+ * @param array $queryParameters
117+ *
118+ * @return array|string
119+ * @throws TypesenseClientError
120+ * @throws HttpClientException
121+ */
122+ public function post (string $ endPoint , $ body , bool $ asJson = true , array $ queryParameters = [])
123+ {
124+ return $ this ->makeRequest ('post ' , $ endPoint , $ asJson , [
125+ 'data ' => $ body ?? [],
126+ 'query ' => $ queryParameters ?? []
127+ ]);
128+ }
129+
130+ /**
131+ * @param string $endPoint
132+ * @param array $body
133+ *
134+ * @param bool $asJson
135+ * @param array $queryParameters
136+ *
137+ * @return array
138+ * @throws TypesenseClientError|HttpClientException
139+ */
140+ public function put (string $ endPoint , array $ body , bool $ asJson = true , array $ queryParameters = []): array
141+ {
142+ return $ this ->makeRequest ('put ' , $ endPoint , $ asJson , [
143+ 'data ' => $ body ?? [],
144+ 'query ' => $ queryParameters ?? []
145+ ]);
146+ }
147+
148+ /**
149+ * @param string $endPoint
150+ * @param array $body
151+ *
152+ * @param bool $asJson
153+ * @param array $queryParameters
154+ *
155+ * @return array
156+ * @throws TypesenseClientError|HttpClientException
157+ */
158+ public function patch (string $ endPoint , array $ body , bool $ asJson = true , array $ queryParameters = []): array
159+ {
160+ return $ this ->makeRequest ('patch ' , $ endPoint , $ asJson , [
161+ 'data ' => $ body ?? [],
162+ 'query ' => $ queryParameters ?? []
163+ ]);
164+ }
165+
166+ /**
167+ * @param string $endPoint
168+ *
169+ * @param bool $asJson
170+ * @param array $queryParameters
171+ *
172+ * @return array
173+ * @throws TypesenseClientError|HttpClientException
174+ */
175+ public function delete (string $ endPoint , bool $ asJson = true , array $ queryParameters = []): array
176+ {
177+ return $ this ->makeRequest ('delete ' , $ endPoint , $ asJson , [
178+ 'query ' => $ queryParameters ?? []
179+ ]);
180+ }
181+
182+ /**
183+ * Makes the actual http request, along with retries
184+ *
185+ * @param string $method
186+ * @param string $endPoint
187+ * @param bool $asJson
188+ * @param array $options
189+ *
190+ * @return string|array
191+ * @throws TypesenseClientError|HttpClientException
192+ * @throws Exception
193+ */
194+ private function makeRequest (string $ method , string $ endPoint , bool $ asJson , array $ options )
195+ {
196+ $ numRetries = 0 ;
197+ $ lastException = null ;
198+ while ($ numRetries < $ this ->config ->getNumRetries () + 1 ) {
199+ $ numRetries ++;
200+ $ node = $ this ->getNode ();
201+
202+ try {
203+ $ url = $ node ->url () . $ endPoint ;
204+ $ reqOp = $ this ->getRequestOptions ();
205+ if (isset ($ options ['data ' ])) {
206+ if (is_string ($ options ['data ' ]) || $ options ['data ' ] instanceof StreamInterface) {
207+ $ reqOp ['body ' ] = $ options ['data ' ];
208+ } else {
209+ $ reqOp ['body ' ] = \json_encode ($ options ['data ' ]);
210+ }
211+ }
212+
213+ if (isset ($ options ['query ' ])) {
214+ foreach ($ options ['query ' ] as $ key => $ value ) :
215+ if (is_bool ($ value )) {
216+ $ options ['query ' ][$ key ] = ($ value ) ? 'true ' : 'false ' ;
217+ }
218+ endforeach ;
219+ $ reqOp ['query ' ] = http_build_query ($ options ['query ' ]);
220+ }
221+
222+ $ response = $ this ->client ->request (
223+ \strtoupper ($ method ),
224+ $ url . '? ' . ($ reqOp ['query ' ] ?? '' ),
225+ $ reqOp
226+ );
227+
228+ $ statusCode = $ response ->getStatusCode ();
229+ if (0 < $ statusCode && $ statusCode < 500 ) {
230+ $ this ->setNodeHealthCheck ($ node , true );
231+ }
232+
233+ if (!(200 <= $ statusCode && $ statusCode < 300 )) {
234+ $ errorMessage = json_decode ($ response ->getBody ()
235+ ->getContents (), true , 512 , JSON_THROW_ON_ERROR )['message ' ] ?? 'API error. ' ;
236+ throw $ this ->getException ($ statusCode )
237+ ->setMessage ($ errorMessage );
238+ }
239+
240+ return $ asJson ? json_decode ($ response ->getBody ()
241+ ->getContents (), true , 512 , JSON_THROW_ON_ERROR ) : $ response ->getBody ()
242+ ->getContents ();
243+ } catch (HttpException $ exception ) {
244+ if (
245+ $ exception ->getResponse ()
246+ ->getStatusCode () === 408
247+ ) {
248+ continue ;
249+ }
250+ $ this ->setNodeHealthCheck ($ node , false );
251+ throw $ this ->getException ($ exception ->getResponse ()
252+ ->getStatusCode ())
253+ ->setMessage ($ exception ->getMessage ());
254+ } catch (TypesenseClientError | HttpClientException $ exception ) {
255+ $ this ->setNodeHealthCheck ($ node , false );
256+ throw $ exception ;
257+ } catch (Exception $ exception ) {
258+ $ this ->setNodeHealthCheck ($ node , false );
259+ $ lastException = $ exception ;
260+ sleep ($ this ->config ->getRetryIntervalSeconds ());
261+ }
262+ }
263+
264+ if ($ lastException ) {
265+ throw $ lastException ;
266+ }
267+ }
268+
269+ /**
270+ * @return array
271+ */
272+ private function getRequestOptions (): array
273+ {
274+ return [
275+ 'headers ' => [
276+ static ::API_KEY_HEADER_NAME => $ this ->config ->getApiKey (),
277+ ]
278+ ];
279+ }
280+
281+ /**
282+ * @param Node $node
283+ *
284+ * @return bool
285+ */
286+ private function nodeDueForHealthCheck (Node $ node ): bool
287+ {
288+ $ currentTimestamp = time ();
289+ return ($ currentTimestamp - $ node ->getLastAccessTs ()) > $ this ->config ->getHealthCheckIntervalSeconds ();
290+ }
291+
292+ /**
293+ * @param Node $node
294+ * @param bool $isHealthy
295+ */
296+ public function setNodeHealthCheck (Node $ node , bool $ isHealthy ): void
297+ {
298+ $ node ->setHealthy ($ isHealthy );
299+ $ node ->setLastAccessTs (time ());
300+ }
301+
302+ /**
303+ * Returns a healthy host from the pool in a round-robin fashion
304+ * Might return an unhealthy host periodically to check for recovery.
305+ *
306+ * @return Node
307+ */
308+ public function getNode (): Lib \Node
309+ {
310+ if (static ::$ nearestNode !== null ) {
311+ if (static ::$ nearestNode ->isHealthy () || $ this ->nodeDueForHealthCheck (static ::$ nearestNode )) {
312+ return static ::$ nearestNode ;
313+ }
314+ }
315+ $ i = 0 ;
316+ while ($ i < count (static ::$ nodes )) {
317+ $ i ++;
318+ $ node = static ::$ nodes [$ this ->nodeIndex ];
319+ $ this ->nodeIndex = ($ this ->nodeIndex + 1 ) % count (static ::$ nodes );
320+ if ($ node ->isHealthy () || $ this ->nodeDueForHealthCheck ($ node )) {
321+ return $ node ;
322+ }
323+ }
324+
325+ /**
326+ * None of the nodes are marked healthy, but some of them could have become healthy since last health check.
327+ * So we will just return the next node.
328+ */
329+ return static ::$ nodes [$ this ->nodeIndex ];
330+ }
331+
332+ /**
333+ * @param int $httpCode
334+ *
335+ * @return TypesenseClientError
336+ */
337+ public function getException (int $ httpCode ): TypesenseClientError
338+ {
339+ switch ($ httpCode ) {
340+ case 0 :
341+ return new HTTPStatus0Error ();
342+ case 400 :
343+ return new RequestMalformed ();
344+ case 401 :
345+ return new RequestUnauthorized ();
346+ case 404 :
347+ return new ObjectNotFound ();
348+ case 409 :
349+ return new ObjectAlreadyExists ();
350+ case 422 :
351+ return new ObjectUnprocessable ();
352+ case 500 :
353+ return new ServerError ();
354+ case 503 :
355+ return new ServiceUnavailable ();
356+ default :
357+ return new TypesenseClientError ();
358+ }
359+ }
360+
361+ /**
362+ * @return LoggerInterface
363+ */
364+ public function getLogger ()
365+ {
366+ return $ this ->logger ;
367+ }
368+ }
0 commit comments