cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
grpc_channel.c
Go to the documentation of this file.
1
27#define _POSIX_C_SOURCE 200809L
30#include <cjson/cJSON.h>
31#include <arpa/inet.h>
32#include <netdb.h>
33#include <pthread.h>
34#include <stdlib.h>
35#include <string.h>
36#include <strings.h>
37#include <time.h>
38#include <unistd.h>
39
40#define GRPC_CHANNEL_MAX_ADDRS 64
41#define GRPC_CHANNEL_DEFAULT_BUFFER_LIMIT (256u * 1024u)
42#define GRPC_CHANNEL_MAX_ATTEMPTS_CAP 5
43
44/* doc/connection-backoff.md parameters */
45#define GRPC_CHANNEL_CONN_BACKOFF_INITIAL_MS 1000.0
46#define GRPC_CHANNEL_CONN_BACKOFF_MULTIPLIER 1.6
47#define GRPC_CHANNEL_CONN_BACKOFF_MAX_MS 120000.0
48#define GRPC_CHANNEL_CONN_BACKOFF_JITTER 0.2
49
50typedef struct cwist_grpc_subchannel {
51 char *host; /* literal/numeric address to dial */
52 uint16_t port;
53 cwist_grpc_client *client; /* NULL when not connected */
54 int inflight; /* calls handed out */
55 uint64_t next_connect_at; /* monotonic ms; 0 = dialable now */
56 double backoff_ms; /* current connection backoff */
58
59typedef struct grpc_method_config {
60 char *service; /* "" matches every service */
61 char *method; /* NULL matches every method of the service */
62 int has_policy;
64 int has_wfr;
65 int wait_for_ready;
66 int has_timeout;
67 uint64_t timeout_ms;
68 struct grpc_method_config *next;
70
71struct cwist_grpc_channel {
72 pthread_mutex_t mu; /* subchannels, LB state, RNG */
73 pthread_mutex_t tmu; /* throttle token count */
75 size_t nsubs;
76 char *authority; /* :authority for every subchannel */
77 char *tls_name; /* SNI hostname (dns name, not the IP) */
78 cwist_grpc_client_options copt; /* base transport options */
80 size_t rr_next; /* round_robin cursor */
81 size_t pf_index; /* pick_first sticky index */
82 int has_policy;
84 int has_throttle;
86 double tokens;
87 uint64_t buffer_limit;
88 int wait_for_ready;
89 uint64_t rng;
90 grpc_method_config *mcs; /* JSON service config method configs */
91};
92
95 cwist_grpc_subchannel *sub; /* NULL for synthetic (pre-dispatch) results */
96 cwist_grpc_call *call; /* NULL for synthetic results */
97 uint32_t attempts; /* policy attempts (transparent excluded) */
98 int accounted; /* throttle end-of-call bookkeeping done */
99 uint32_t acct_mask; /* retryable mask for end-of-call accounting */
100 int acct_has_policy;
101 cwist_grpc_status_t syn_status; /* synthetic result (call == NULL) */
102 char *syn_message; /* owned */
103};
104
106static uint64_t channel_now_ms(void) {
107 struct timespec ts;
109 return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
110}
111
113static void channel_sleep_ms(uint64_t ms) {
114 if (!ms) return;
115 struct timespec ts = {(time_t)(ms / 1000), (long)((ms % 1000) * 1000000L)};
116 nanosleep(&ts, NULL);
117}
118
123static uint64_t channel_rand(cwist_grpc_channel *ch) {
124 uint64_t x = ch->rng;
125 x ^= x << 13;
126 x ^= x >> 7;
127 x ^= x << 17;
128 ch->rng = x;
129 return x;
130}
131
135static double channel_jitter(cwist_grpc_channel *ch) {
136 pthread_mutex_lock(&ch->mu);
137 double u = (double)(channel_rand(ch) >> 11) * (1.0 / 9007199254740992.0);
138 pthread_mutex_unlock(&ch->mu);
139 return 0.8 + 0.4 * u;
140}
141
142/* --- throttling (gRFC A6 retryThrottling) --- */
143
150static int channel_throttle_allows(cwist_grpc_channel *ch) {
151 if (!ch->has_throttle) return 1;
152 pthread_mutex_lock(&ch->tmu);
153 int ok = ch->tokens > ch->throttle.max_tokens / 2.0;
154 pthread_mutex_unlock(&ch->tmu);
155 return ok;
156}
157
159static void channel_throttle_failure(cwist_grpc_channel *ch) {
160 if (!ch->has_throttle) return;
161 pthread_mutex_lock(&ch->tmu);
162 ch->tokens -= 1.0;
163 if (ch->tokens < 0) ch->tokens = 0;
164 pthread_mutex_unlock(&ch->tmu);
165}
166
170static void channel_throttle_success(cwist_grpc_channel *ch) {
171 if (!ch->has_throttle) return;
172 pthread_mutex_lock(&ch->tmu);
173 ch->tokens += ch->throttle.token_ratio;
174 if (ch->tokens > ch->throttle.max_tokens) ch->tokens = ch->throttle.max_tokens;
175 pthread_mutex_unlock(&ch->tmu);
176}
177
182static void channel_account(cwist_grpc_channel *ch, cwist_grpc_status_t status, int has_policy,
183 uint32_t mask) {
184 if (status == CWIST_GRPC_OK) {
185 channel_throttle_success(ch);
186 } else if (has_policy && (mask & CWIST_GRPC_STATUS_BIT(status))) {
187 channel_throttle_failure(ch);
188 }
189}
190
191/* --- subchannels --- */
192
197static void channel_recycle_locked(cwist_grpc_channel *ch, cwist_grpc_subchannel *sub) {
198 (void)ch;
199 if (sub->client && cwist_grpc_client_dead(sub->client)) {
200 cwist_grpc_client_close(sub->client);
201 sub->client = NULL;
202 }
203}
204
216static int sub_ensure_connected(cwist_grpc_channel *ch, cwist_grpc_subchannel *sub) {
217 channel_recycle_locked(ch, sub);
218 if (sub->client) return 0;
219 uint64_t now = channel_now_ms();
220 if (sub->next_connect_at && now < sub->next_connect_at) return -1;
221 cwist_grpc_client_options o = ch->copt;
222 o.authority = ch->authority;
223 o.tls_server_name = ch->tls_name;
224 cwist_grpc_client *client = cwist_grpc_client_connect(sub->host, sub->port, &o);
225 if (!client) {
228 ((double)(channel_rand(ch) >> 11) * (1.0 / 9007199254740992.0));
229 sub->next_connect_at = now + (uint64_t)(sub->backoff_ms * jitter);
230 sub->backoff_ms *= GRPC_CHANNEL_CONN_BACKOFF_MULTIPLIER;
231 if (sub->backoff_ms > GRPC_CHANNEL_CONN_BACKOFF_MAX_MS)
232 sub->backoff_ms = GRPC_CHANNEL_CONN_BACKOFF_MAX_MS;
233 return -1;
234 }
235 /* The SETTINGS handshake completed: reset the backoff (connection-backoff.md). */
236 sub->client = client;
237 sub->backoff_ms = GRPC_CHANNEL_CONN_BACKOFF_INITIAL_MS;
238 sub->next_connect_at = 0;
239 return 0;
240}
241
250static cwist_grpc_subchannel *channel_pick_locked(cwist_grpc_channel *ch) {
251 if (ch->lb == CWIST_GRPC_LB_ROUND_ROBIN) {
252 for (size_t k = 0; k < ch->nsubs; k++) {
253 size_t i = (ch->rr_next + k) % ch->nsubs;
254 cwist_grpc_subchannel *s = &ch->subs[i];
255 if (s->client && !s->inflight && !cwist_grpc_client_dead(s->client)) {
256 ch->rr_next = (i + 1) % ch->nsubs;
257 return s;
258 }
259 }
260 return NULL;
261 }
262 /* pick_first: stick to the connected subchannel (doc/load-balancing.md). */
263 if (ch->pf_index < ch->nsubs) {
264 cwist_grpc_subchannel *s = &ch->subs[ch->pf_index];
265 if (s->client && !s->inflight && !cwist_grpc_client_dead(s->client)) return s;
266 }
267 return NULL;
268}
269
278static cwist_grpc_subchannel *channel_acquire(cwist_grpc_channel *ch) {
279 pthread_mutex_lock(&ch->mu);
280 cwist_grpc_subchannel *s = channel_pick_locked(ch);
281 if (s) {
282 s->inflight++;
283 pthread_mutex_unlock(&ch->mu);
284 return s;
285 }
286 if (ch->lb == CWIST_GRPC_LB_ROUND_ROBIN) {
287 for (size_t k = 0; k < ch->nsubs; k++) {
288 size_t i = (ch->rr_next + k) % ch->nsubs;
289 cwist_grpc_subchannel *c = &ch->subs[i];
290 if (c->inflight) continue;
291 if (sub_ensure_connected(ch, c) == 0) {
292 ch->rr_next = (i + 1) % ch->nsubs;
293 c->inflight++;
294 pthread_mutex_unlock(&ch->mu);
295 return c;
296 }
297 }
298 } else {
299 /* pick_first: dial the addresses one at a time, in order. */
300 for (size_t i = 0; i < ch->nsubs; i++) {
301 cwist_grpc_subchannel *c = &ch->subs[i];
302 if (c->inflight) continue;
303 if (sub_ensure_connected(ch, c) == 0) {
304 ch->pf_index = i;
305 c->inflight++;
306 pthread_mutex_unlock(&ch->mu);
307 return c;
308 }
309 }
310 }
311 pthread_mutex_unlock(&ch->mu);
312 return NULL;
313}
314
316static void channel_release(cwist_grpc_channel *ch, cwist_grpc_subchannel *sub) {
317 pthread_mutex_lock(&ch->mu);
318 if (sub->inflight > 0) sub->inflight--;
319 pthread_mutex_unlock(&ch->mu);
320}
321
323static void channel_recycle(cwist_grpc_channel *ch, cwist_grpc_subchannel *sub) {
324 pthread_mutex_lock(&ch->mu);
325 channel_recycle_locked(ch, sub);
326 pthread_mutex_unlock(&ch->mu);
327}
328
329/* --- method configs (JSON service config) --- */
330
336static void channel_split_method(const char *path, const char **service, size_t *service_len,
337 const char **method) {
338 const char *start = path;
339 if (*start == '/') start++;
340 const char *slash = strrchr(start, '/');
341 if (!slash) {
342 *service = start;
344 *method = NULL;
345 return;
346 }
347 *service = start;
349 *method = slash + 1;
350}
351
358static const grpc_method_config *channel_find_mc(cwist_grpc_channel *ch, const char *path) {
359 const char *service, *method;
360 size_t service_len;
361 channel_split_method(path, &service, &service_len, &method);
363 for (const grpc_method_config *mc = ch->mcs; mc; mc = mc->next) {
364 if (mc->service[0] == '\0') {
365 if (!wild_match) wild_match = mc;
366 continue;
367 }
368 if (strlen(mc->service) != service_len || memcmp(mc->service, service, service_len) != 0)
369 continue;
370 if (!mc->method) {
372 continue;
373 }
374 if (method && strcmp(mc->method, method) == 0) return mc; /* exact */
375 }
377}
378
380static void channel_free_mcs(cwist_grpc_channel *ch) {
381 grpc_method_config *mc = ch->mcs;
382 while (mc) {
383 grpc_method_config *next = mc->next;
384 cwist_free(mc->service);
385 cwist_free(mc->method);
386 cwist_free(mc);
387 mc = next;
388 }
389 ch->mcs = NULL;
390}
391
392/* --- synthetic (pre-dispatch) call results --- */
393
395static char *channel_strdup(const char *s) {
396 if (!s) return NULL;
397 size_t len = strlen(s) + 1;
398 char *copy = cwist_alloc(len);
399 if (copy) memcpy(copy, s, len);
400 return copy;
401}
402
411static cwist_grpc_channel_call *channel_synthetic(cwist_grpc_channel *ch,
412 cwist_grpc_status_t status, const char *message,
413 uint32_t attempts, int has_policy,
414 uint32_t mask) {
416 if (!cc) return NULL;
417 memset(cc, 0, sizeof(*cc));
418 cc->ch = ch;
419 cc->attempts = attempts;
420 cc->syn_status = status;
421 cc->syn_message = channel_strdup(message);
422 cc->acct_has_policy = has_policy;
423 cc->acct_mask = mask;
424 return cc;
425}
426
432static cwist_grpc_channel_call *channel_wrap(cwist_grpc_channel *ch, cwist_grpc_subchannel *sub,
433 cwist_grpc_call *call, uint32_t attempts,
434 int has_policy, uint32_t mask, int accounted) {
436 channel_synthetic(ch, CWIST_GRPC_OK, NULL, attempts, has_policy, mask);
437 if (!cc) {
438 if (call) cwist_grpc_call_destroy(call);
439 if (sub) channel_release(ch, sub);
440 return NULL;
441 }
442 cc->sub = sub;
443 cc->call = call;
444 cc->accounted = accounted;
445 return cc;
446}
447
448/* --- retry engine --- */
449
466cwist_grpc_channel_call *cwist_grpc_channel_call_start(cwist_grpc_channel *ch, const char *method,
467 const void *request, size_t request_len,
468 uint64_t timeout_ms) {
469 if (!ch || !method || (request_len && !request)) return NULL;
470
471 const cwist_grpc_retry_policy *policy = ch->has_policy ? &ch->policy : NULL;
472 int wfr = ch->wait_for_ready;
473 uint64_t eff_timeout = timeout_ms;
474 const grpc_method_config *mc = channel_find_mc(ch, method);
475 if (mc) {
476 if (mc->has_policy) policy = &mc->policy;
477 if (mc->has_wfr) wfr = mc->wait_for_ready;
478 if (mc->has_timeout && (eff_timeout == 0 || mc->timeout_ms < eff_timeout))
479 eff_timeout = mc->timeout_ms;
480 }
481 uint64_t deadline = eff_timeout ? channel_now_ms() + eff_timeout : 0;
482 int buffer_ok = request_len <= ch->buffer_limit;
483 uint32_t max_attempts = 1;
484 uint32_t mask = 0;
485 if (policy && buffer_ok) {
486 max_attempts = policy->max_attempts > GRPC_CHANNEL_MAX_ATTEMPTS_CAP
488 : policy->max_attempts;
489 if (max_attempts < 1) max_attempts = 1;
490 mask = policy->retryable_status_mask;
491 }
492 int has_policy = policy != NULL;
493
494 uint32_t policy_attempts = 0; /* counts against maxAttempts (A6) */
495 uint32_t wire_attempts = 0; /* grpc-previous-rpc-attempts header */
496 int case3_used = 0; /* one immediate transparent retry (A6) */
497 double backoff = policy ? (double)policy->initial_backoff_ms : 0.0;
498
499 for (;;) {
500 uint64_t now = channel_now_ms();
501 if (deadline && now >= deadline)
502 return channel_synthetic(ch, CWIST_GRPC_DEADLINE_EXCEEDED, "deadline exceeded",
503 policy_attempts, has_policy, mask);
504
505 cwist_grpc_subchannel *sub = channel_acquire(ch);
506 if (!sub) {
507 /* A6 failure case 1: the RPC fails at the load balancing step. */
508 if (wfr) {
509 channel_sleep_ms(20);
510 continue;
511 }
514 if (coded) channel_throttle_failure(ch); /* failed attempt (A6) */
515 if (has_policy && buffer_ok && coded && policy_attempts < max_attempts &&
516 channel_throttle_allows(ch)) {
517 uint64_t delay = (uint64_t)(backoff * channel_jitter(ch));
518 backoff *= policy->backoff_multiplier;
519 if (backoff > (double)policy->max_backoff_ms)
520 backoff = (double)policy->max_backoff_ms;
521 if (deadline) {
522 uint64_t left = deadline - channel_now_ms();
523 if (delay > left) delay = left;
524 }
525 channel_sleep_ms(delay);
526 continue;
527 }
529 channel_synthetic(ch, CWIST_GRPC_UNAVAILABLE, "no ready subchannel",
530 policy_attempts, has_policy, mask);
531 if (cc) cc->accounted = 1; /* per-attempt accounting already done */
532 return cc;
533 }
534
538 if (deadline) {
539 now = channel_now_ms();
540 remaining = now < deadline ? deadline - now : 1;
541 }
542 cwist_grpc_call *call = cwist_grpc_call_start_ex(sub->client, method, request, request_len,
544 if (!call) {
545 /* A6 case 2: the RPC never left the client. Transparent retry,
546 * uncounted, until the deadline passes. */
547 channel_recycle(ch, sub);
548 channel_release(ch, sub);
551 continue;
552 }
553
554 if (cwist_grpc_call_await_headers(call) == 0) {
555 /* Response-Headers arrived: the RPC is committed (A6). */
556 return channel_wrap(ch, sub, call, policy_attempts, has_policy, mask, 0);
557 }
558
560 if (status == CWIST_GRPC_OK) {
561 /* Trailers-Only OK: successful empty response, hand it over. */
563 channel_wrap(ch, sub, call, policy_attempts, has_policy, mask, 1);
564 if (cc) channel_account(ch, CWIST_GRPC_OK, has_policy, mask);
565 return cc;
566 }
567
568 if (cwist_grpc_call_refused(call) && !case3_used) {
569 /* A6 case 3: never seen by server application logic. One
570 * immediate transparent retry, uncounted. */
571 case3_used = 1;
573 channel_recycle(ch, sub);
574 channel_release(ch, sub);
576 continue;
577 }
578
579 int32_t pushback = 0;
581 int coded_retryable = (mask & CWIST_GRPC_STATUS_BIT(status)) != 0;
582 /* Throttle bookkeeping for this failed attempt (A6). */
583 if (coded_retryable || (has_pushback && pushback < 0)) channel_throttle_failure(ch);
584
585 if (has_pushback && pushback < 0) {
586 /* Pushback: do not retry. */
587 return channel_wrap(ch, sub, call, policy_attempts, has_policy, mask, 1);
588 }
589
590 if (!has_policy || !buffer_ok || !coded_retryable || policy_attempts >= max_attempts ||
591 !channel_throttle_allows(ch)) {
592 return channel_wrap(ch, sub, call, policy_attempts, has_policy, mask, 1);
593 }
594
595 uint64_t delay;
596 if (has_pushback) {
597 /* Retry after exactly the pushback delay; the backoff progression
598 * restarts from initialBackoff afterwards (A6). */
599 delay = (uint64_t)pushback;
601 } else {
602 delay = (uint64_t)(backoff * channel_jitter(ch));
603 backoff *= policy->backoff_multiplier;
604 if (backoff > (double)policy->max_backoff_ms) backoff = (double)policy->max_backoff_ms;
605 }
607 channel_release(ch, sub);
608 if (deadline) {
609 uint64_t left = deadline - channel_now_ms();
610 if (delay > left) delay = left;
611 }
612 channel_sleep_ms(delay);
613 }
614}
615
624int cwist_grpc_channel_call_recv(cwist_grpc_channel_call *cc, cwist_grpc_message *out) {
625 if (!cc || !out) return -1;
626 if (!cc->call) return 0; /* synthetic failure: no messages */
627 return cwist_grpc_call_recv(cc->call, out);
628}
629
638 const char **message) {
639 if (!cc) {
640 if (message) *message = NULL;
641 return CWIST_GRPC_INTERNAL;
642 }
643 cwist_grpc_status_t status;
644 if (cc->call) {
645 status = cwist_grpc_call_finish(cc->call, message);
646 } else {
647 status = cc->syn_status;
648 if (message) *message = cc->syn_message;
649 }
650 if (!cc->accounted) {
651 channel_account(cc->ch, status, cc->acct_has_policy, cc->acct_mask);
652 cc->accounted = 1;
653 }
654 return status;
655}
656
658void cwist_grpc_channel_call_cancel(cwist_grpc_channel_call *cc) {
659 if (cc && cc->call) cwist_grpc_call_cancel(cc->call);
660}
661
668void cwist_grpc_channel_call_destroy(cwist_grpc_channel_call *cc) {
669 if (!cc) return;
670 if (!cc->accounted) {
671 cwist_grpc_status_t status = cc->call ? cwist_grpc_call_status(cc->call) : cc->syn_status;
672 channel_account(cc->ch, status, cc->acct_has_policy, cc->acct_mask);
673 cc->accounted = 1;
674 }
675 if (cc->call) cwist_grpc_call_destroy(cc->call);
676 if (cc->sub) {
677 channel_recycle(cc->ch, cc->sub);
678 channel_release(cc->ch, cc->sub);
679 }
680 cwist_free(cc->syn_message);
681 cwist_free(cc);
682}
683
685uint32_t cwist_grpc_channel_call_attempts(const cwist_grpc_channel_call *cc) {
686 return cc ? cc->attempts : 0;
687}
688
700cwist_grpc_status_t cwist_grpc_channel_unary(cwist_grpc_channel *ch, const char *method,
701 const void *request, size_t request_len,
702 uint64_t timeout_ms, uint8_t **response,
703 size_t *response_len, char **status_message) {
704 if (response) *response = NULL;
705 if (response_len) *response_len = 0;
706 if (status_message) *status_message = NULL;
708 cwist_grpc_channel_call_start(ch, method, request, request_len, timeout_ms);
709 if (!cc) return CWIST_GRPC_INTERNAL;
710
711 uint8_t *copy = NULL;
712 size_t copy_len = 0;
714 if (cwist_grpc_channel_call_recv(cc, &msg) == 1) {
715 copy = cwist_alloc(msg.len ? msg.len : 1);
716 if (copy && msg.len) memcpy(copy, msg.data, msg.len);
717 copy_len = copy ? msg.len : 0;
718 while (cwist_grpc_channel_call_recv(cc, &msg) == 1); /* unary: ignore trailing messages */
719 }
720 const char *sm = NULL;
722 if (status_message && sm) *status_message = channel_strdup(sm);
724
725 if (status != CWIST_GRPC_OK) {
726 cwist_free(copy);
727 copy = NULL;
728 copy_len = 0;
729 }
730 if (response)
731 *response = copy;
732 else
733 cwist_free(copy);
735 return status;
736}
737
738/* --- name resolution (doc/naming.md) --- */
739
750static int channel_split_host_port(const char *s, size_t len, char *host, size_t host_cap,
751 uint16_t *port) {
752 *port = 443;
753 if (!s || !len) return -1;
754 if (s[0] == '[') {
755 const char *close = memchr(s, ']', len);
756 if (!close) return -1;
757 size_t hlen = (size_t)(close - s - 1);
758 if (!hlen || hlen >= host_cap) return -1;
759 memcpy(host, s + 1, hlen);
760 host[hlen] = '\0';
761 size_t used = (size_t)(close - s) + 1;
762 if (used < len) {
763 if (close[1] != ':' || used + 1 == len) return -1;
764 long p = strtol(close + 2, NULL, 10);
765 if (p <= 0 || p > 65535) return -1;
766 *port = (uint16_t)p;
767 }
768 return 0;
769 }
770 const char *first_colon = memchr(s, ':', len);
771 const char *last_colon = NULL;
772 for (size_t i = 0; i < len; i++)
773 if (s[i] == ':') last_colon = s + i;
775 /* several colons: bare IPv6 literal, no port */
776 if (len >= host_cap) return -1;
777 memcpy(host, s, len);
778 host[len] = '\0';
779 return 0;
780 }
781 if (last_colon) {
782 size_t hlen = (size_t)(last_colon - s);
783 if (!hlen || hlen >= host_cap) return -1;
784 long p = strtol(last_colon + 1, NULL, 10);
785 if (p <= 0 || p > 65535) return -1;
786 memcpy(host, s, hlen);
787 host[hlen] = '\0';
788 *port = (uint16_t)p;
789 return 0;
790 }
791 if (len >= host_cap) return -1;
792 memcpy(host, s, len);
793 host[len] = '\0';
794 return 0;
795}
796
802static int channel_add_addr(cwist_grpc_channel *ch, const char *host, uint16_t port) {
803 for (size_t i = 0; i < ch->nsubs; i++)
804 if (ch->subs[i].port == port && strcmp(ch->subs[i].host, host) == 0)
805 return 0; /* duplicate */
806 if (ch->nsubs >= GRPC_CHANNEL_MAX_ADDRS) return -1;
807 cwist_grpc_subchannel *sub = &ch->subs[ch->nsubs];
808 memset(sub, 0, sizeof(*sub));
809 sub->host = channel_strdup(host);
810 if (!sub->host) return -1;
811 sub->port = port;
812 sub->backoff_ms = GRPC_CHANNEL_CONN_BACKOFF_INITIAL_MS;
813 ch->nsubs++;
814 return 0;
815}
816
825static int channel_resolve_dns(cwist_grpc_channel *ch, const char *name) {
826 char host[256];
827 uint16_t port;
828 if (channel_split_host_port(name, strlen(name), host, sizeof(host), &port) != 0) return -1;
829 char port_str[8];
830 snprintf(port_str, sizeof(port_str), "%u", (unsigned)port);
831 struct addrinfo hints;
832 memset(&hints, 0, sizeof(hints));
833 hints.ai_family = AF_UNSPEC;
834 hints.ai_socktype = SOCK_STREAM;
835 struct addrinfo *res = NULL;
836 if (getaddrinfo(host, port_str, &hints, &res) != 0) return -1;
837 int rc = -1;
838 for (struct addrinfo *it = res; it; it = it->ai_next) {
840 if (getnameinfo(it->ai_addr, it->ai_addrlen, numeric, sizeof(numeric), NULL, 0,
841 NI_NUMERICHOST) != 0)
842 continue;
843 if (channel_add_addr(ch, numeric, port) == 0) rc = 0;
844 }
845 freeaddrinfo(res);
846 if (rc == 0 && !ch->tls_name) ch->tls_name = channel_strdup(host);
847 if (rc == 0 && !ch->authority) {
848 char buf[300];
849 snprintf(buf, sizeof(buf), "%s:%u", host, (unsigned)port);
850 ch->authority = channel_strdup(buf);
851 }
852 return rc;
853}
854
864static int channel_resolve_literal(cwist_grpc_channel *ch, const char *list, int v6) {
865 char *copy = channel_strdup(list);
866 if (!copy) return -1;
867 int rc = -1;
868 char *save = NULL;
869 for (char *tok = strtok_r(copy, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
870 char host[256];
871 uint16_t port;
872 if (channel_split_host_port(tok, strlen(tok), host, sizeof(host), &port) != 0) continue;
873 struct in_addr a4;
874 struct in6_addr a6;
875 if (v6 ? inet_pton(AF_INET6, host, &a6) != 1 : inet_pton(AF_INET, host, &a4) != 1) continue;
876 if (channel_add_addr(ch, host, port) == 0) rc = 0;
877 }
878 cwist_free(copy);
879 if (rc == 0 && !ch->tls_name && ch->nsubs) ch->tls_name = channel_strdup(ch->subs[0].host);
880 if (rc == 0 && !ch->authority && ch->nsubs) {
881 char buf[300];
882 snprintf(buf, sizeof(buf), "%s:%u", ch->subs[0].host, (unsigned)ch->subs[0].port);
883 ch->authority = channel_strdup(buf);
884 }
885 return rc;
886}
887
888/* --- channel lifecycle --- */
889
902 const cwist_grpc_channel_options *options) {
903 if (!target || !*target) return NULL;
904 cwist_grpc_channel *ch = cwist_alloc(sizeof(*ch));
905 if (!ch) return NULL;
906 memset(ch, 0, sizeof(*ch));
907 pthread_mutex_init(&ch->mu, NULL);
908 pthread_mutex_init(&ch->tmu, NULL);
909 ch->subs = cwist_alloc(sizeof(*ch->subs) * GRPC_CHANNEL_MAX_ADDRS);
910 if (!ch->subs) {
911 cwist_free(ch);
912 return NULL;
913 }
914 ch->lb = options ? options->lb_policy : CWIST_GRPC_LB_PICK_FIRST;
915 if (options && options->retry_policy) {
916 ch->policy = *options->retry_policy;
917 ch->has_policy = 1;
918 }
919 if (options && options->retry_throttle) {
920 ch->throttle = *options->retry_throttle;
921 ch->has_throttle = 1;
922 ch->tokens = ch->throttle.max_tokens; /* starts at maxTokens (A6) */
923 }
924 ch->buffer_limit = options && options->per_rpc_buffer_limit ? options->per_rpc_buffer_limit
926 ch->wait_for_ready = options ? options->wait_for_ready : 0;
927 if (options) {
928 ch->copt.use_tls = options->use_tls;
929 ch->copt.verify_peer = options->verify_peer;
930 ch->copt.connect_timeout_ms = options->connect_timeout_ms;
931 if (options->authority) ch->authority = channel_strdup(options->authority);
932 }
933 ch->rng = (uint64_t)channel_now_ms() ^ ((uintptr_t)ch >> 4) ^ ((uint64_t)getpid() << 32);
934 if (!ch->rng) ch->rng = 0x9e3779b97f4a7c15ULL;
935
936 int rc;
937 if (strncmp(target, "dns:", 4) == 0) {
938 const char *name = target + 4;
939 if (name[0] == '/' && name[1] == '/') {
940 /* dns://authority/name — the default resolver ignores the
941 * authority, like the gRPC C-core dns resolver. */
942 name += 2;
943 const char *slash = strchr(name, '/');
944 if (!slash) goto fail;
945 name = slash + 1;
946 }
947 rc = channel_resolve_dns(ch, name);
948 } else if (strncmp(target, "ipv4:", 5) == 0) {
949 rc = channel_resolve_literal(ch, target + 5, 0);
950 } else if (strncmp(target, "ipv6:", 5) == 0) {
951 rc = channel_resolve_literal(ch, target + 5, 1);
952 } else if (strncmp(target, "unix:", 5) == 0 || strncmp(target, "unix-abstract:", 14) == 0 ||
953 strncmp(target, "vsock:", 6) == 0) {
954 goto fail; /* transports the client does not implement */
955 } else {
956 rc = channel_resolve_dns(ch, target); /* no scheme = dns (naming.md) */
957 }
958 if (rc != 0 || ch->nsubs == 0) goto fail;
959 if (!ch->authority) goto fail;
960
961 /* round_robin connects to every address up front and keeps watching
962 * them (load-balancing.md); pick_first stays IDLE until the first RPC. */
963 if (ch->lb == CWIST_GRPC_LB_ROUND_ROBIN) {
964 pthread_mutex_lock(&ch->mu);
965 for (size_t i = 0; i < ch->nsubs; i++) (void)sub_ensure_connected(ch, &ch->subs[i]);
966 pthread_mutex_unlock(&ch->mu);
967 }
968 return ch;
969
970fail:
972 return NULL;
973}
974
979void cwist_grpc_channel_close(cwist_grpc_channel *ch) {
980 if (!ch) return;
981 /* Calls handed out must be destroyed before closing the channel. */
982 for (size_t i = 0; i < ch->nsubs; i++) {
983 if (ch->subs[i].client) cwist_grpc_client_close(ch->subs[i].client);
984 cwist_free(ch->subs[i].host);
985 }
986 cwist_free(ch->subs);
987 channel_free_mcs(ch);
988 cwist_free(ch->authority);
989 cwist_free(ch->tls_name);
990 pthread_mutex_destroy(&ch->mu);
991 pthread_mutex_destroy(&ch->tmu);
992 cwist_free(ch);
993}
994
1002 if (!ch) return CWIST_GRPC_CHANNEL_SHUTDOWN;
1003 uint64_t now = channel_now_ms();
1004 int any_ready = 0, any_dialable = 0;
1005 pthread_mutex_lock(&ch->mu);
1006 for (size_t i = 0; i < ch->nsubs; i++) {
1007 cwist_grpc_subchannel *s = &ch->subs[i];
1008 if (s->client && !cwist_grpc_client_dead(s->client)) {
1009 any_ready = 1;
1010 break;
1011 }
1012 if (!s->next_connect_at || now >= s->next_connect_at) any_dialable = 1;
1013 }
1014 pthread_mutex_unlock(&ch->mu);
1015 /* load-balancing.md aggregation: READY if any subchannel is READY,
1016 * else IDLE while any subchannel may dial, else TRANSIENT_FAILURE. */
1020}
1021
1022/* --- JSON service config (doc/service_config.md subset, gRFC A6) --- */
1023
1024static const char *const grpc_status_names[] = {
1025 "OK", "CANCELLED", "UNKNOWN", "INVALID_ARGUMENT", "DEADLINE_EXCEEDED",
1026 "NOT_FOUND", "ALREADY_EXISTS", "PERMISSION_DENIED", "RESOURCE_EXHAUSTED", "FAILED_PRECONDITION",
1027 "ABORTED", "OUT_OF_RANGE", "UNIMPLEMENTED", "INTERNAL", "UNAVAILABLE",
1028 "DATA_LOSS", "UNAUTHENTICATED"};
1029
1036static int grpc_status_from_json(const cJSON *item, uint32_t *bit) {
1037 if (cJSON_IsNumber(item)) {
1038 int code = (int)item->valuedouble;
1039 if (code < 0 || code > 16) return -1;
1041 return 0;
1042 }
1043 if (cJSON_IsString(item)) {
1044 for (int i = 0; i <= 16; i++) {
1045 if (strcasecmp(item->valuestring, grpc_status_names[i]) == 0) {
1047 return 0;
1048 }
1049 }
1050 }
1051 return -1;
1052}
1053
1062static int grpc_json_duration_ms(const cJSON *item, uint64_t *out_ms) {
1063 if (!cJSON_IsString(item)) return -1;
1064 const char *s = item->valuestring;
1065 size_t len = strlen(s);
1066 if (len < 2 || s[len - 1] != 's') return -1;
1067 char *end = NULL;
1068 double v = strtod(s, &end);
1069 if (end != s + len - 1 || v <= 0) return -1;
1070 *out_ms = (uint64_t)(v * 1000.0 + 0.5);
1071 if (*out_ms == 0) *out_ms = 1;
1072 return 0;
1073}
1074
1086static int grpc_json_retry_policy(const cJSON *obj, cwist_grpc_retry_policy *out) {
1087 const cJSON *max_attempts = cJSON_GetObjectItemCaseSensitive(obj, "maxAttempts");
1088 const cJSON *initial = cJSON_GetObjectItemCaseSensitive(obj, "initialBackoff");
1089 const cJSON *max = cJSON_GetObjectItemCaseSensitive(obj, "maxBackoff");
1090 const cJSON *mult = cJSON_GetObjectItemCaseSensitive(obj, "backoffMultiplier");
1091 const cJSON *codes = cJSON_GetObjectItemCaseSensitive(obj, "retryableStatusCodes");
1092 if (!cJSON_IsNumber(max_attempts) || max_attempts->valuedouble < 2 ||
1093 max_attempts->valuedouble != (double)(int)max_attempts->valuedouble)
1094 return -1;
1096 if (grpc_json_duration_ms(initial, &initial_ms) != 0 ||
1097 grpc_json_duration_ms(max, &max_ms) != 0)
1098 return -1;
1099 if (!cJSON_IsNumber(mult) || mult->valuedouble <= 0) return -1;
1100 if (!cJSON_IsArray(codes) || cJSON_GetArraySize((cJSON *)codes) == 0) return -1;
1101 uint32_t mask = 0;
1102 const cJSON *code;
1104 uint32_t bit;
1105 if (grpc_status_from_json(code, &bit) != 0) return -1;
1106 mask |= bit;
1107 }
1108 out->max_attempts = (uint32_t)max_attempts->valuedouble;
1110 out->max_attempts = GRPC_CHANNEL_MAX_ATTEMPTS_CAP; /* A6: no validation error */
1112 out->max_backoff_ms = max_ms;
1113 out->backoff_multiplier = mult->valuedouble;
1115 return 0;
1116}
1117
1119static void grpc_json_free_mc_list(grpc_method_config *list) {
1120 while (list) {
1121 grpc_method_config *next = list->next;
1122 cwist_free(list->service);
1123 cwist_free(list->method);
1125 list = next;
1126 }
1127}
1128
1140int cwist_grpc_channel_apply_service_config_json(cwist_grpc_channel *ch, const char *json) {
1141 if (!ch || !json) return -1;
1142 cJSON *root = cJSON_Parse(json);
1143 if (!cJSON_IsObject(root)) {
1145 return -1;
1146 }
1147
1148 int lb_set = 0;
1149 cwist_grpc_lb_policy lb = ch->lb;
1150 int throttle_set = 0;
1152 grpc_method_config *mcs = NULL, **mcs_tail = &mcs;
1153
1154 /* loadBalancingConfig: [{"round_robin":{}}, {"pick_first":{}}] — the
1155 * first supported policy wins (service_config.md). */
1156 const cJSON *lbc = cJSON_GetObjectItemCaseSensitive(root, "loadBalancingConfig");
1157 if (cJSON_IsArray(lbc)) {
1158 const cJSON *entry;
1159 cJSON_ArrayForEach(entry, lbc) {
1160 if (!cJSON_IsObject(entry)) continue;
1161 if (cJSON_GetObjectItemCaseSensitive(entry, "round_robin")) {
1163 lb_set = 1;
1164 break;
1165 }
1166 if (cJSON_GetObjectItemCaseSensitive(entry, "pick_first")) {
1168 lb_set = 1;
1169 break;
1170 }
1171 /* unsupported policy: try the next entry */
1172 }
1173 }
1174 if (!lb_set) {
1175 /* Deprecated loadBalancingPolicy string form. */
1176 const cJSON *lbp = cJSON_GetObjectItemCaseSensitive(root, "loadBalancingPolicy");
1177 if (cJSON_IsString(lbp)) {
1178 if (strcasecmp(lbp->valuestring, "ROUND_ROBIN") == 0) {
1180 lb_set = 1;
1181 } else if (strcasecmp(lbp->valuestring, "PICK_FIRST") == 0) {
1183 lb_set = 1;
1184 }
1185 }
1186 }
1187
1188 const cJSON *rt = cJSON_GetObjectItemCaseSensitive(root, "retryThrottling");
1189 if (rt) {
1190 const cJSON *max_tokens = cJSON_GetObjectItemCaseSensitive(rt, "maxTokens");
1191 const cJSON *ratio = cJSON_GetObjectItemCaseSensitive(rt, "tokenRatio");
1192 if (!cJSON_IsNumber(max_tokens) || max_tokens->valuedouble <= 0 ||
1193 max_tokens->valuedouble > 1000 || !cJSON_IsNumber(ratio) || ratio->valuedouble <= 0)
1194 goto invalid;
1195 throttle.max_tokens = max_tokens->valuedouble;
1196 /* A6: decimal places beyond 3 are ignored */
1197 throttle.token_ratio = ((double)(int)(ratio->valuedouble * 1000.0)) / 1000.0;
1198 throttle_set = 1;
1199 }
1200
1201 const cJSON *mc_array = cJSON_GetObjectItemCaseSensitive(root, "methodConfig");
1202 if (mc_array && !cJSON_IsArray(mc_array)) goto invalid;
1203 if (cJSON_IsArray(mc_array)) {
1204 const cJSON *entry;
1206 if (!cJSON_IsObject(entry)) goto invalid;
1207 const cJSON *names = cJSON_GetObjectItemCaseSensitive(entry, "name");
1208 if (!cJSON_IsArray(names) || cJSON_GetArraySize((cJSON *)names) == 0) goto invalid;
1209
1210 int has_policy = 0;
1212 const cJSON *rp = cJSON_GetObjectItemCaseSensitive(entry, "retryPolicy");
1213 const cJSON *hp = cJSON_GetObjectItemCaseSensitive(entry, "hedgingPolicy");
1214 if (rp && hp) goto invalid; /* A6: one policy per RPC, never both */
1215 if (rp) {
1216 if (grpc_json_retry_policy(rp, &policy) != 0) goto invalid;
1217 has_policy = 1;
1218 }
1219 /* hedgingPolicy is not implemented (as in gRPC C-core): the
1220 * entry simply carries no retry policy. */
1221
1222 int has_wfr = 0, wfr = 0;
1223 const cJSON *wfr_item = cJSON_GetObjectItemCaseSensitive(entry, "waitForReady");
1224 if (wfr_item) {
1225 if (!cJSON_IsBool(wfr_item)) goto invalid;
1226 has_wfr = 1;
1228 }
1229 int has_timeout = 0;
1230 uint64_t timeout_ms = 0;
1231 const cJSON *timeout = cJSON_GetObjectItemCaseSensitive(entry, "timeout");
1232 if (timeout) {
1233 if (grpc_json_duration_ms(timeout, &timeout_ms) != 0) goto invalid;
1234 has_timeout = 1;
1235 }
1236
1237 /* One methodConfig entry per name; first match wins at lookup,
1238 * so list order is preserved. */
1239 const cJSON *name;
1240 cJSON_ArrayForEach(name, names) {
1241 if (!cJSON_IsObject(name)) goto invalid;
1242 const cJSON *service = cJSON_GetObjectItemCaseSensitive(name, "service");
1243 const cJSON *method = cJSON_GetObjectItemCaseSensitive(name, "method");
1244 if (!cJSON_IsString(service)) goto invalid;
1245 if (method && !cJSON_IsString(method)) goto invalid;
1246 grpc_method_config *mc = cwist_alloc(sizeof(*mc));
1247 if (!mc) goto invalid;
1248 memset(mc, 0, sizeof(*mc));
1249 mc->service = channel_strdup(service->valuestring);
1250 mc->method = method ? channel_strdup(method->valuestring) : NULL;
1251 if (!mc->service || (method && !mc->method)) {
1252 cwist_free(mc->service);
1253 cwist_free(mc->method);
1254 cwist_free(mc);
1255 goto invalid;
1256 }
1257 mc->has_policy = has_policy;
1258 mc->policy = policy;
1259 mc->has_wfr = has_wfr;
1260 mc->wait_for_ready = wfr;
1261 mc->has_timeout = has_timeout;
1262 mc->timeout_ms = timeout_ms;
1263 *mcs_tail = mc;
1264 mcs_tail = &mc->next;
1265 }
1266 }
1267 }
1268
1269 /* Everything validated: swap it in. */
1270 pthread_mutex_lock(&ch->mu);
1271 if (lb_set) ch->lb = lb;
1272 channel_free_mcs(ch);
1273 ch->mcs = mcs;
1274 if (ch->lb == CWIST_GRPC_LB_ROUND_ROBIN) {
1275 /* round_robin watches every subchannel (load-balancing.md): dial
1276 * them all so the READY set rotates strictly. */
1277 for (size_t i = 0; i < ch->nsubs; i++) (void)sub_ensure_connected(ch, &ch->subs[i]);
1278 }
1279 pthread_mutex_unlock(&ch->mu);
1280 if (throttle_set) {
1281 pthread_mutex_lock(&ch->tmu);
1282 int had = ch->has_throttle;
1283 ch->throttle = throttle;
1284 ch->has_throttle = 1;
1285 if (!had) ch->tokens = throttle.max_tokens;
1286 pthread_mutex_unlock(&ch->tmu);
1287 }
1289 return 0;
1290
1291invalid:
1292 grpc_json_free_mc_list(mcs);
1294 return -1;
1295}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
@ CWIST_GRPC_OK
Definition grpc.h:22
@ CWIST_GRPC_UNAVAILABLE
Definition grpc.h:36
@ CWIST_GRPC_DEADLINE_EXCEEDED
Definition grpc.h:26
@ CWIST_GRPC_INTERNAL
Definition grpc.h:35
enum cwist_grpc_status cwist_grpc_status_t
void cwist_grpc_channel_call_destroy(cwist_grpc_channel_call *cc)
Destroy a channel call and release its resources.
void cwist_grpc_channel_close(cwist_grpc_channel *ch)
Close a channel and free all its resources.
cwist_grpc_channel_state cwist_grpc_channel_get_state(cwist_grpc_channel *ch)
Aggregate connectivity state across subchannels (load-balancing.md).
cwist_grpc_channel * cwist_grpc_channel_connect(const char *target, const cwist_grpc_channel_options *options)
Create a channel for target and resolve its backend addresses.
uint32_t cwist_grpc_channel_call_attempts(const cwist_grpc_channel_call *cc)
Number of policy attempts a channel call used (transparent retries excluded).
struct cwist_grpc_subchannel cwist_grpc_subchannel
void cwist_grpc_channel_call_cancel(cwist_grpc_channel_call *cc)
Cancel the underlying attempt call, if it reached the transport.
#define GRPC_CHANNEL_DEFAULT_BUFFER_LIMIT
cwist_grpc_status_t cwist_grpc_channel_unary(cwist_grpc_channel *ch, const char *method, const void *request, size_t request_len, uint64_t timeout_ms, uint8_t **response, size_t *response_len, char **status_message)
Convenience blocking unary RPC: start, read one response, finish.
#define GRPC_CHANNEL_MAX_ATTEMPTS_CAP
#define GRPC_CHANNEL_CONN_BACKOFF_MAX_MS
struct grpc_method_config grpc_method_config
#define GRPC_CHANNEL_CONN_BACKOFF_INITIAL_MS
cwist_grpc_channel_call * cwist_grpc_channel_call_start(cwist_grpc_channel *ch, const char *method, const void *request, size_t request_len, uint64_t timeout_ms)
Start a unary RPC through the channel, running the gRFC A6 retry engine transparently across attempts...
#define GRPC_CHANNEL_CONN_BACKOFF_MULTIPLIER
cwist_grpc_status_t cwist_grpc_channel_call_finish(cwist_grpc_channel_call *cc, const char **message)
Finish a channel call and return its final gRPC status.
#define GRPC_CHANNEL_MAX_ADDRS
int cwist_grpc_channel_call_recv(cwist_grpc_channel_call *cc, cwist_grpc_message *out)
Receive the next response message of a channel call.
#define GRPC_CHANNEL_CONN_BACKOFF_JITTER
int cwist_grpc_channel_apply_service_config_json(cwist_grpc_channel *ch, const char *json)
Apply a JSON service config (doc/service_config.md subset, gRFC A6).
gRPC client channel: name resolution, client-side load balancing, and gRFC A6 retry policy on top of ...
cwist_grpc_channel_state
@ CWIST_GRPC_CHANNEL_IDLE
@ CWIST_GRPC_CHANNEL_SHUTDOWN
@ CWIST_GRPC_CHANNEL_READY
@ CWIST_GRPC_CHANNEL_TRANSIENT_FAILURE
#define CWIST_GRPC_STATUS_BIT(code)
struct cwist_grpc_channel_call cwist_grpc_channel_call
struct cwist_grpc_channel cwist_grpc_channel
cwist_grpc_lb_policy
@ CWIST_GRPC_LB_ROUND_ROBIN
@ CWIST_GRPC_LB_PICK_FIRST
struct cwist_grpc_client cwist_grpc_client
Definition grpc_client.h:19
void cwist_grpc_client_close(cwist_grpc_client *client)
Close the client: send GOAWAY, tear down TLS and the socket, free all state.
cwist_grpc_client * cwist_grpc_client_connect(const char *host, uint16_t port, const cwist_grpc_client_options *options)
Connect to a gRPC server and complete the HTTP/2 handshake.
int cwist_grpc_call_recv(cwist_grpc_call *call, cwist_grpc_message *out)
Receive the next response message of a call.
int cwist_grpc_call_await_headers(cwist_grpc_call *call)
Block until response headers arrive or the stream ends.
int cwist_grpc_client_dead(cwist_grpc_client *client)
Whether the client connection is unusable.
int cwist_grpc_call_refused(const cwist_grpc_call *call)
Whether the call failed before commit in a retryable way (gRFC A6 case 3: REFUSED_STREAM before heade...
void cwist_grpc_call_cancel(cwist_grpc_call *call)
Cancel a call: send RST_STREAM(CANCEL), mark it failed with CWIST_GRPC_CANCELLED, and end its stream.
int cwist_grpc_call_retry_pushback_ms(const cwist_grpc_call *call, int32_t *out_ms)
Report the server-provided retry pushback delay, if any.
void cwist_grpc_call_destroy(cwist_grpc_call *call)
Destroy a call: cancel it if still open, release it as the client's active call, and free all queued ...
struct cwist_grpc_call cwist_grpc_call
Definition grpc_client.h:20
cwist_grpc_status_t cwist_grpc_call_finish(cwist_grpc_call *call, const char **message)
Drain the call to completion and return its final status.
cwist_grpc_status_t cwist_grpc_call_status(const cwist_grpc_call *call)
Current gRPC status code of a call.
cwist_grpc_call * cwist_grpc_call_start_ex(cwist_grpc_client *client, const char *method, const void *request, size_t request_len, uint64_t timeout_ms, uint32_t previous_attempts)
Start a unary call, with a gRFC A6 retry-attempt marker.
size_t cap
Definition otel.c:56
const cwist_grpc_retry_throttle * retry_throttle
cwist_grpc_lb_policy lb_policy
const cwist_grpc_retry_policy * retry_policy