1 /*
2 * nghttp2 - HTTP/2 C Library
3 *
4 * Copyright (c) 2016 Tatsuhiro Tsujikawa
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining
7 * a copy of this software and associated documentation files (the
8 * "Software"), to deal in the Software without restriction, including
9 * without limitation the rights to use, copy, modify, merge, publish,
10 * distribute, sublicense, and/or sell copies of the Software, and to
11 * permit persons to whom the Software is furnished to do so, subject to
12 * the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be
15 * included in all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 */
25 #include "shrpx_live_check.h"
26 #include "shrpx_worker.h"
27 #include "shrpx_connect_blocker.h"
28 #include "shrpx_tls.h"
29 #include "shrpx_log.h"
30
31 namespace shrpx {
32
33 namespace {
34 constexpr size_t MAX_BUFFER_SIZE = 4_k;
35 } // namespace
36
37 namespace {
readcb(struct ev_loop * loop,ev_io * w,int revents)38 void readcb(struct ev_loop *loop, ev_io *w, int revents) {
39 int rv;
40 auto conn = static_cast<Connection *>(w->data);
41 auto live_check = static_cast<LiveCheck *>(conn->data);
42
43 rv = live_check->do_read();
44 if (rv != 0) {
45 live_check->on_failure();
46 return;
47 }
48 }
49 } // namespace
50
51 namespace {
writecb(struct ev_loop * loop,ev_io * w,int revents)52 void writecb(struct ev_loop *loop, ev_io *w, int revents) {
53 int rv;
54 auto conn = static_cast<Connection *>(w->data);
55 auto live_check = static_cast<LiveCheck *>(conn->data);
56
57 rv = live_check->do_write();
58 if (rv != 0) {
59 live_check->on_failure();
60 return;
61 }
62 }
63 } // namespace
64
65 namespace {
timeoutcb(struct ev_loop * loop,ev_timer * w,int revents)66 void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) {
67 auto conn = static_cast<Connection *>(w->data);
68 auto live_check = static_cast<LiveCheck *>(conn->data);
69
70 if (w == &conn->rt && !conn->expired_rt()) {
71 return;
72 }
73
74 live_check->on_failure();
75 }
76 } // namespace
77
78 namespace {
backoff_timeoutcb(struct ev_loop * loop,ev_timer * w,int revents)79 void backoff_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) {
80 int rv;
81 auto live_check = static_cast<LiveCheck *>(w->data);
82
83 rv = live_check->initiate_connection();
84 if (rv != 0) {
85 live_check->on_failure();
86 return;
87 }
88 }
89 } // namespace
90
91 namespace {
settings_timeout_cb(struct ev_loop * loop,ev_timer * w,int revents)92 void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) {
93 auto live_check = static_cast<LiveCheck *>(w->data);
94
95 if (LOG_ENABLED(INFO)) {
96 LOG(INFO) << "SETTINGS timeout";
97 }
98
99 live_check->on_failure();
100 }
101 } // namespace
102
LiveCheck(struct ev_loop * loop,SSL_CTX * ssl_ctx,Worker * worker,DownstreamAddr * addr,std::mt19937 & gen)103 LiveCheck::LiveCheck(struct ev_loop *loop, SSL_CTX *ssl_ctx, Worker *worker,
104 DownstreamAddr *addr, std::mt19937 &gen)
105 : conn_(loop, -1, nullptr, worker->get_mcpool(),
106 worker->get_downstream_config()->timeout.write,
107 worker->get_downstream_config()->timeout.read, {}, {}, writecb,
108 readcb, timeoutcb, this, get_config()->tls.dyn_rec.warmup_threshold,
109 get_config()->tls.dyn_rec.idle_timeout, Proto::NONE),
110 wb_(worker->get_mcpool()),
111 gen_(gen),
112 read_(&LiveCheck::noop),
113 write_(&LiveCheck::noop),
114 worker_(worker),
115 ssl_ctx_(ssl_ctx),
116 addr_(addr),
117 session_(nullptr),
118 raddr_(nullptr),
119 success_count_(0),
120 fail_count_(0),
121 settings_ack_received_(false),
122 session_closing_(false) {
123 ev_timer_init(&backoff_timer_, backoff_timeoutcb, 0., 0.);
124 backoff_timer_.data = this;
125
126 // SETTINGS ACK must be received in a short timeout. Otherwise, we
127 // assume that connection is broken.
128 ev_timer_init(&settings_timer_, settings_timeout_cb, 0., 0.);
129 settings_timer_.data = this;
130 }
131
~LiveCheck()132 LiveCheck::~LiveCheck() {
133 disconnect();
134
135 ev_timer_stop(conn_.loop, &backoff_timer_);
136 }
137
disconnect()138 void LiveCheck::disconnect() {
139 if (dns_query_) {
140 auto dns_tracker = worker_->get_dns_tracker();
141
142 dns_tracker->cancel(dns_query_.get());
143 }
144
145 dns_query_.reset();
146 // We can reuse resolved_addr_
147 raddr_ = nullptr;
148
149 conn_.rlimit.stopw();
150 conn_.wlimit.stopw();
151
152 ev_timer_stop(conn_.loop, &settings_timer_);
153
154 read_ = write_ = &LiveCheck::noop;
155
156 conn_.disconnect();
157
158 nghttp2_session_del(session_);
159 session_ = nullptr;
160
161 settings_ack_received_ = false;
162 session_closing_ = false;
163
164 wb_.reset();
165 }
166
167 // Use the similar backoff algorithm described in
168 // https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md
169 namespace {
170 constexpr size_t MAX_BACKOFF_EXP = 10;
171 constexpr auto MULTIPLIER = 1.6;
172 constexpr auto JITTER = 0.2;
173 } // namespace
174
schedule()175 void LiveCheck::schedule() {
176 auto base_backoff =
177 util::int_pow(MULTIPLIER, std::min(fail_count_, MAX_BACKOFF_EXP));
178 auto dist = std::uniform_real_distribution<>(-JITTER * base_backoff,
179 JITTER * base_backoff);
180
181 auto &downstreamconf = *get_config()->conn.downstream;
182
183 auto backoff =
184 std::min(downstreamconf.timeout.max_backoff, base_backoff + dist(gen_));
185
186 ev_timer_set(&backoff_timer_, backoff, 0.);
187 ev_timer_start(conn_.loop, &backoff_timer_);
188 }
189
do_read()190 int LiveCheck::do_read() { return read_(*this); }
191
do_write()192 int LiveCheck::do_write() { return write_(*this); }
193
initiate_connection()194 int LiveCheck::initiate_connection() {
195 int rv;
196
197 auto worker_blocker = worker_->get_connect_blocker();
198 if (worker_blocker->blocked()) {
199 if (LOG_ENABLED(INFO)) {
200 LOG(INFO) << "Worker wide backend connection was blocked temporarily";
201 }
202 return -1;
203 }
204
205 if (!dns_query_ && addr_->tls) {
206 assert(ssl_ctx_);
207
208 auto ssl = tls::create_ssl(ssl_ctx_);
209 if (!ssl) {
210 return -1;
211 }
212
213 switch (addr_->proto) {
214 case Proto::HTTP1:
215 tls::setup_downstream_http1_alpn(ssl);
216 break;
217 case Proto::HTTP2:
218 tls::setup_downstream_http2_alpn(ssl);
219 break;
220 default:
221 assert(0);
222 }
223
224 conn_.set_ssl(ssl);
225 conn_.tls.client_session_cache = &addr_->tls_session_cache;
226 }
227
228 if (addr_->dns) {
229 if (!dns_query_) {
230 auto dns_query = std::make_unique<DNSQuery>(
231 addr_->host, [this](DNSResolverStatus status, const Address *result) {
232 int rv;
233
234 if (status == DNSResolverStatus::OK) {
235 *this->resolved_addr_ = *result;
236 }
237 rv = this->initiate_connection();
238 if (rv != 0) {
239 this->on_failure();
240 }
241 });
242 auto dns_tracker = worker_->get_dns_tracker();
243
244 if (!resolved_addr_) {
245 resolved_addr_ = std::make_unique<Address>();
246 }
247
248 switch (dns_tracker->resolve(resolved_addr_.get(), dns_query.get())) {
249 case DNSResolverStatus::ERROR:
250 return -1;
251 case DNSResolverStatus::RUNNING:
252 dns_query_ = std::move(dns_query);
253 return 0;
254 case DNSResolverStatus::OK:
255 break;
256 default:
257 assert(0);
258 }
259 } else {
260 switch (dns_query_->status) {
261 case DNSResolverStatus::ERROR:
262 dns_query_.reset();
263 return -1;
264 case DNSResolverStatus::OK:
265 dns_query_.reset();
266 break;
267 default:
268 assert(0);
269 }
270 }
271
272 util::set_port(*resolved_addr_, addr_->port);
273 raddr_ = resolved_addr_.get();
274 } else {
275 raddr_ = &addr_->addr;
276 }
277
278 conn_.fd = util::create_nonblock_socket(raddr_->su.storage.ss_family);
279
280 if (conn_.fd == -1) {
281 auto error = errno;
282 LOG(WARN) << "socket() failed; addr=" << util::to_numeric_addr(raddr_)
283 << ", errno=" << error;
284 return -1;
285 }
286
287 rv = connect(conn_.fd, &raddr_->su.sa, raddr_->len);
288 if (rv != 0 && errno != EINPROGRESS) {
289 auto error = errno;
290 LOG(WARN) << "connect() failed; addr=" << util::to_numeric_addr(raddr_)
291 << ", errno=" << error;
292
293 close(conn_.fd);
294 conn_.fd = -1;
295
296 return -1;
297 }
298
299 if (addr_->tls) {
300 auto sni_name =
301 addr_->sni.empty() ? StringRef{addr_->host} : StringRef{addr_->sni};
302 if (!util::numeric_host(sni_name.c_str())) {
303 SSL_set_tlsext_host_name(conn_.tls.ssl, sni_name.c_str());
304 }
305
306 auto session = tls::reuse_tls_session(addr_->tls_session_cache);
307 if (session) {
308 SSL_set_session(conn_.tls.ssl, session);
309 SSL_SESSION_free(session);
310 }
311
312 conn_.prepare_client_handshake();
313 }
314
315 write_ = &LiveCheck::connected;
316
317 ev_io_set(&conn_.wev, conn_.fd, EV_WRITE);
318 ev_io_set(&conn_.rev, conn_.fd, EV_READ);
319
320 conn_.wlimit.startw();
321
322 auto &downstreamconf = *get_config()->conn.downstream;
323
324 conn_.wt.repeat = downstreamconf.timeout.connect;
325 ev_timer_again(conn_.loop, &conn_.wt);
326
327 return 0;
328 }
329
connected()330 int LiveCheck::connected() {
331 auto sock_error = util::get_socket_error(conn_.fd);
332 if (sock_error != 0) {
333 if (LOG_ENABLED(INFO)) {
334 LOG(INFO) << "Backend connect failed; addr="
335 << util::to_numeric_addr(raddr_) << ": errno=" << sock_error;
336 }
337
338 return -1;
339 }
340
341 if (LOG_ENABLED(INFO)) {
342 LOG(INFO) << "Connection established";
343 }
344
345 auto &downstreamconf = *get_config()->conn.downstream;
346
347 // Reset timeout for write. Previously, we set timeout for connect.
348 conn_.wt.repeat = downstreamconf.timeout.write;
349 ev_timer_again(conn_.loop, &conn_.wt);
350
351 conn_.rlimit.startw();
352 conn_.again_rt();
353
354 if (conn_.tls.ssl) {
355 read_ = &LiveCheck::tls_handshake;
356 write_ = &LiveCheck::tls_handshake;
357
358 return do_write();
359 }
360
361 if (addr_->proto == Proto::HTTP2) {
362 // For HTTP/2, we try to read SETTINGS ACK from server to make
363 // sure it is really alive, and serving HTTP/2.
364 read_ = &LiveCheck::read_clear;
365 write_ = &LiveCheck::write_clear;
366
367 if (connection_made() != 0) {
368 return -1;
369 }
370
371 return 0;
372 }
373
374 on_success();
375
376 return 0;
377 }
378
tls_handshake()379 int LiveCheck::tls_handshake() {
380 conn_.last_read = std::chrono::steady_clock::now();
381
382 ERR_clear_error();
383
384 auto rv = conn_.tls_handshake();
385
386 if (rv == SHRPX_ERR_INPROGRESS) {
387 return 0;
388 }
389
390 if (rv < 0) {
391 return rv;
392 }
393
394 if (LOG_ENABLED(INFO)) {
395 LOG(INFO) << "SSL/TLS handshake completed";
396 }
397
398 if (!get_config()->tls.insecure &&
399 tls::check_cert(conn_.tls.ssl, addr_, raddr_) != 0) {
400 return -1;
401 }
402
403 // Check negotiated ALPN
404
405 const unsigned char *next_proto = nullptr;
406 unsigned int next_proto_len = 0;
407
408 #ifndef OPENSSL_NO_NEXTPROTONEG
409 SSL_get0_next_proto_negotiated(conn_.tls.ssl, &next_proto, &next_proto_len);
410 #endif // !OPENSSL_NO_NEXTPROTONEG
411 #if OPENSSL_VERSION_NUMBER >= 0x10002000L
412 if (next_proto == nullptr) {
413 SSL_get0_alpn_selected(conn_.tls.ssl, &next_proto, &next_proto_len);
414 }
415 #endif // OPENSSL_VERSION_NUMBER >= 0x10002000L
416
417 auto proto = StringRef{next_proto, next_proto_len};
418
419 switch (addr_->proto) {
420 case Proto::HTTP1:
421 if (proto.empty() || proto == StringRef::from_lit("http/1.1")) {
422 break;
423 }
424 return -1;
425 case Proto::HTTP2:
426 if (util::check_h2_is_selected(proto)) {
427 // For HTTP/2, we try to read SETTINGS ACK from server to make
428 // sure it is really alive, and serving HTTP/2.
429 read_ = &LiveCheck::read_tls;
430 write_ = &LiveCheck::write_tls;
431
432 if (connection_made() != 0) {
433 return -1;
434 }
435
436 return 0;
437 }
438 return -1;
439 default:
440 break;
441 }
442
443 on_success();
444
445 return 0;
446 }
447
read_tls()448 int LiveCheck::read_tls() {
449 conn_.last_read = std::chrono::steady_clock::now();
450
451 std::array<uint8_t, 4_k> buf;
452
453 ERR_clear_error();
454
455 for (;;) {
456 auto nread = conn_.read_tls(buf.data(), buf.size());
457
458 if (nread == 0) {
459 return 0;
460 }
461
462 if (nread < 0) {
463 return nread;
464 }
465
466 if (on_read(buf.data(), nread) != 0) {
467 return -1;
468 }
469 }
470 }
471
write_tls()472 int LiveCheck::write_tls() {
473 conn_.last_read = std::chrono::steady_clock::now();
474
475 ERR_clear_error();
476
477 struct iovec iov;
478
479 for (;;) {
480 if (wb_.rleft() > 0) {
481 auto iovcnt = wb_.riovec(&iov, 1);
482 if (iovcnt != 1) {
483 assert(0);
484 return -1;
485 }
486 auto nwrite = conn_.write_tls(iov.iov_base, iov.iov_len);
487
488 if (nwrite == 0) {
489 return 0;
490 }
491
492 if (nwrite < 0) {
493 return nwrite;
494 }
495
496 wb_.drain(nwrite);
497
498 continue;
499 }
500
501 if (on_write() != 0) {
502 return -1;
503 }
504
505 if (wb_.rleft() == 0) {
506 conn_.start_tls_write_idle();
507 break;
508 }
509 }
510
511 conn_.wlimit.stopw();
512 ev_timer_stop(conn_.loop, &conn_.wt);
513
514 if (settings_ack_received_) {
515 on_success();
516 }
517
518 return 0;
519 }
520
read_clear()521 int LiveCheck::read_clear() {
522 conn_.last_read = std::chrono::steady_clock::now();
523
524 std::array<uint8_t, 4_k> buf;
525
526 for (;;) {
527 auto nread = conn_.read_clear(buf.data(), buf.size());
528
529 if (nread == 0) {
530 return 0;
531 }
532
533 if (nread < 0) {
534 return nread;
535 }
536
537 if (on_read(buf.data(), nread) != 0) {
538 return -1;
539 }
540 }
541 }
542
write_clear()543 int LiveCheck::write_clear() {
544 conn_.last_read = std::chrono::steady_clock::now();
545
546 struct iovec iov;
547
548 for (;;) {
549 if (wb_.rleft() > 0) {
550 auto iovcnt = wb_.riovec(&iov, 1);
551 if (iovcnt != 1) {
552 assert(0);
553 return -1;
554 }
555 auto nwrite = conn_.write_clear(iov.iov_base, iov.iov_len);
556
557 if (nwrite == 0) {
558 return 0;
559 }
560
561 if (nwrite < 0) {
562 return nwrite;
563 }
564
565 wb_.drain(nwrite);
566
567 continue;
568 }
569
570 if (on_write() != 0) {
571 return -1;
572 }
573
574 if (wb_.rleft() == 0) {
575 break;
576 }
577 }
578
579 conn_.wlimit.stopw();
580 ev_timer_stop(conn_.loop, &conn_.wt);
581
582 if (settings_ack_received_) {
583 on_success();
584 }
585
586 return 0;
587 }
588
on_read(const uint8_t * data,size_t len)589 int LiveCheck::on_read(const uint8_t *data, size_t len) {
590 ssize_t rv;
591
592 rv = nghttp2_session_mem_recv(session_, data, len);
593 if (rv < 0) {
594 LOG(ERROR) << "nghttp2_session_mem_recv() returned error: "
595 << nghttp2_strerror(rv);
596 return -1;
597 }
598
599 if (settings_ack_received_ && !session_closing_) {
600 session_closing_ = true;
601 rv = nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR);
602 if (rv != 0) {
603 return -1;
604 }
605 }
606
607 if (nghttp2_session_want_read(session_) == 0 &&
608 nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) {
609 if (LOG_ENABLED(INFO)) {
610 LOG(INFO) << "No more read/write for this session";
611 }
612
613 // If we have SETTINGS ACK already, we treat this success.
614 if (settings_ack_received_) {
615 return 0;
616 }
617
618 return -1;
619 }
620
621 signal_write();
622
623 return 0;
624 }
625
on_write()626 int LiveCheck::on_write() {
627 for (;;) {
628 const uint8_t *data;
629 auto datalen = nghttp2_session_mem_send(session_, &data);
630
631 if (datalen < 0) {
632 LOG(ERROR) << "nghttp2_session_mem_send() returned error: "
633 << nghttp2_strerror(datalen);
634 return -1;
635 }
636 if (datalen == 0) {
637 break;
638 }
639 wb_.append(data, datalen);
640
641 if (wb_.rleft() >= MAX_BUFFER_SIZE) {
642 break;
643 }
644 }
645
646 if (nghttp2_session_want_read(session_) == 0 &&
647 nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) {
648 if (LOG_ENABLED(INFO)) {
649 LOG(INFO) << "No more read/write for this session";
650 }
651
652 if (settings_ack_received_) {
653 return 0;
654 }
655
656 return -1;
657 }
658
659 return 0;
660 }
661
on_failure()662 void LiveCheck::on_failure() {
663 ++fail_count_;
664
665 if (LOG_ENABLED(INFO)) {
666 LOG(INFO) << "Liveness check for " << addr_->host << ":" << addr_->port
667 << " failed " << fail_count_ << " time(s) in a row";
668 }
669
670 disconnect();
671
672 schedule();
673 }
674
on_success()675 void LiveCheck::on_success() {
676 ++success_count_;
677 fail_count_ = 0;
678
679 if (LOG_ENABLED(INFO)) {
680 LOG(INFO) << "Liveness check for " << addr_->host << ":" << addr_->port
681 << " succeeded " << success_count_ << " time(s) in a row";
682 }
683
684 if (success_count_ < addr_->rise) {
685 disconnect();
686
687 schedule();
688
689 return;
690 }
691
692 LOG(NOTICE) << util::to_numeric_addr(&addr_->addr) << " is considered online";
693
694 addr_->connect_blocker->online();
695
696 success_count_ = 0;
697 fail_count_ = 0;
698
699 disconnect();
700 }
701
noop()702 int LiveCheck::noop() { return 0; }
703
start_settings_timer()704 void LiveCheck::start_settings_timer() {
705 auto &downstreamconf = get_config()->http2.downstream;
706
707 ev_timer_set(&settings_timer_, downstreamconf.timeout.settings, 0.);
708 ev_timer_start(conn_.loop, &settings_timer_);
709 }
710
stop_settings_timer()711 void LiveCheck::stop_settings_timer() {
712 ev_timer_stop(conn_.loop, &settings_timer_);
713 }
714
settings_ack_received()715 void LiveCheck::settings_ack_received() { settings_ack_received_ = true; }
716
717 namespace {
on_frame_send_callback(nghttp2_session * session,const nghttp2_frame * frame,void * user_data)718 int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame,
719 void *user_data) {
720 auto live_check = static_cast<LiveCheck *>(user_data);
721
722 if (frame->hd.type != NGHTTP2_SETTINGS ||
723 (frame->hd.flags & NGHTTP2_FLAG_ACK)) {
724 return 0;
725 }
726
727 live_check->start_settings_timer();
728
729 return 0;
730 }
731 } // namespace
732
733 namespace {
on_frame_recv_callback(nghttp2_session * session,const nghttp2_frame * frame,void * user_data)734 int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame,
735 void *user_data) {
736 auto live_check = static_cast<LiveCheck *>(user_data);
737
738 if (frame->hd.type != NGHTTP2_SETTINGS ||
739 (frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) {
740 return 0;
741 }
742
743 live_check->stop_settings_timer();
744 live_check->settings_ack_received();
745
746 return 0;
747 }
748 } // namespace
749
connection_made()750 int LiveCheck::connection_made() {
751 int rv;
752
753 nghttp2_session_callbacks *callbacks;
754 rv = nghttp2_session_callbacks_new(&callbacks);
755 if (rv != 0) {
756 return -1;
757 }
758
759 nghttp2_session_callbacks_set_on_frame_send_callback(callbacks,
760 on_frame_send_callback);
761 nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks,
762 on_frame_recv_callback);
763
764 rv = nghttp2_session_client_new(&session_, callbacks, this);
765
766 nghttp2_session_callbacks_del(callbacks);
767
768 if (rv != 0) {
769 return -1;
770 }
771
772 rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, nullptr, 0);
773 if (rv != 0) {
774 return -1;
775 }
776
777 auto must_terminate =
778 addr_->tls && !nghttp2::tls::check_http2_requirement(conn_.tls.ssl);
779
780 if (must_terminate) {
781 if (LOG_ENABLED(INFO)) {
782 LOG(INFO) << "TLSv1.2 was not negotiated. HTTP/2 must not be negotiated.";
783 }
784
785 rv = nghttp2_session_terminate_session(session_,
786 NGHTTP2_INADEQUATE_SECURITY);
787 if (rv != 0) {
788 return -1;
789 }
790 }
791
792 signal_write();
793
794 return 0;
795 }
796
signal_write()797 void LiveCheck::signal_write() { conn_.wlimit.startw(); }
798
799 } // namespace shrpx
800