summaryrefslogtreecommitdiff
path: root/ext/socket
diff options
context:
space:
mode:
authorYusuke Endoh <mame@ruby-lang.org>2023-10-18 18:48:29 +0900
committerYusuke Endoh <mame@ruby-lang.org>2023-10-24 12:22:53 +0900
commit3dc311bdc8badb680267f5a10e0c467ddd9dfe4c (patch)
treed1a853baf41ec43fdc28bf4653881850e7f5fe3a /ext/socket
parentefd58f19ea473ac34d27658997eee6af3521e1d9 (diff)
Make rb_getaddrinfo interruptible
When pthread_create is available, rb_getaddrinfo creates a pthread and executes getaddrinfo(3) in it. The caller thread waits for the pthread to complete, but detaches it if interrupted. This allows name resolution to be interuppted by Timeout.timeout, etc. even if it takes a long time (for example, when the DNS server does not respond). [Feature #19965]
Diffstat (limited to 'ext/socket')
-rw-r--r--ext/socket/extconf.rb7
-rw-r--r--ext/socket/raddrinfo.c186
2 files changed, 191 insertions, 2 deletions
diff --git a/ext/socket/extconf.rb b/ext/socket/extconf.rb
index 93bd850c34..6eec869d69 100644
--- a/ext/socket/extconf.rb
+++ b/ext/socket/extconf.rb
@@ -327,6 +327,8 @@ end
net/if_dl.h
arpa/nameser.h
resolv.h
+ pthread.h
+ sched.h
].each {|h|
if have_header(h, headers)
headers << h
@@ -700,6 +702,11 @@ SRC
end
end
+ have_func("pthread_create")
+ have_func("pthread_detach")
+ have_func("pthread_setaffinity_np")
+ have_func("sched_getcpu")
+
$VPATH << '$(topdir)' << '$(top_srcdir)'
create_makefile("socket")
end
diff --git a/ext/socket/raddrinfo.c b/ext/socket/raddrinfo.c
index 705fa3bc9c..fa7510371b 100644
--- a/ext/socket/raddrinfo.c
+++ b/ext/socket/raddrinfo.c
@@ -12,12 +12,17 @@
// GETADDRINFO_IMPL == 0 : call getaddrinfo/getnameinfo directly
// GETADDRINFO_IMPL == 1 : call getaddrinfo/getnameinfo without gvl (but uncancellable)
+// GETADDRINFO_IMPL == 2 : call getaddrinfo/getnameinfo in a dedicated pthread
+// (and if the call is interrupted, the pthread is detached)
#ifndef GETADDRINFO_IMPL
# ifdef GETADDRINFO_EMU
# define GETADDRINFO_IMPL 0
-# else
+# elif !defined(HAVE_PTHREAD_CREATE) || !defined(HAVE_PTHREAD_DETACH)
# define GETADDRINFO_IMPL 1
+# else
+# define GETADDRINFO_IMPL 2
+# include "ruby/thread_native.h"
# endif
#endif
@@ -333,6 +338,183 @@ rb_getaddrinfo(const char *hostp, const char *portp, const struct addrinfo *hint
return (int)(VALUE)rb_thread_call_without_gvl(nogvl_getaddrinfo, &arg, RUBY_UBF_IO, 0);
}
+#elif GETADDRINFO_IMPL == 2
+
+struct getaddrinfo_arg
+{
+ char *node, *service;
+ struct addrinfo hints;
+ struct addrinfo *ai;
+ int err, refcount, done, cancelled;
+ rb_nativethread_lock_t lock;
+ rb_nativethread_cond_t cond;
+};
+
+static struct getaddrinfo_arg *
+allocate_getaddrinfo_arg(const char *hostp, const char *portp, const struct addrinfo *hints)
+{
+ size_t hostp_offset = sizeof(struct getaddrinfo_arg);
+ size_t portp_offset = hostp_offset + (hostp ? strlen(hostp) + 1 : 0);
+ size_t bufsize = portp_offset + (portp ? strlen(portp) + 1 : 0);
+
+ char *buf = malloc(bufsize);
+ if (!buf) {
+ rb_gc();
+ buf = malloc(bufsize);
+ if (!buf) return NULL;
+ }
+ struct getaddrinfo_arg *arg = (struct getaddrinfo_arg *)buf;
+
+ if (hostp) {
+ arg->node = buf + hostp_offset;
+ strcpy(arg->node, hostp);
+ }
+ else {
+ arg->node = NULL;
+ }
+
+ if (portp) {
+ arg->service = buf + portp_offset;
+ strcpy(arg->service, portp);
+ }
+ else {
+ arg->service = NULL;
+ }
+
+ arg->hints = *hints;
+ arg->ai = NULL;
+
+ arg->refcount = 2;
+ arg->done = arg->cancelled = 0;
+
+ rb_nativethread_lock_initialize(&arg->lock);
+ rb_native_cond_initialize(&arg->cond);
+
+ return arg;
+}
+
+static void
+free_getaddrinfo_arg(struct getaddrinfo_arg *arg)
+{
+ rb_native_cond_destroy(&arg->cond);
+ rb_nativethread_lock_destroy(&arg->lock);
+ free(arg);
+}
+
+static void *
+do_getaddrinfo(void *ptr)
+{
+ struct getaddrinfo_arg *arg = (struct getaddrinfo_arg *)ptr;
+
+ int err;
+ err = getaddrinfo(arg->node, arg->service, &arg->hints, &arg->ai);
+#ifdef __linux__
+ /* On Linux (mainly Ubuntu 13.04) /etc/nsswitch.conf has mdns4 and
+ * it cause getaddrinfo to return EAI_SYSTEM/ENOENT. [ruby-list:49420]
+ */
+ if (err == EAI_SYSTEM && errno == ENOENT)
+ err = EAI_NONAME;
+#endif
+
+ int need_free = 0;
+ rb_nativethread_lock_lock(&arg->lock);
+ arg->err = err;
+ if (arg->cancelled) {
+ freeaddrinfo(arg->ai);
+ }
+ else {
+ arg->done = 1;
+ rb_native_cond_signal(&arg->cond);
+ }
+ if (--arg->refcount == 0) need_free = 1;
+ rb_nativethread_lock_unlock(&arg->lock);
+
+ if (need_free) free_getaddrinfo_arg(arg);
+
+ return 0;
+}
+
+static void *
+wait_getaddrinfo(void *ptr)
+{
+ struct getaddrinfo_arg *arg = (struct getaddrinfo_arg *)ptr;
+ rb_nativethread_lock_lock(&arg->lock);
+ while (!arg->done && !arg->cancelled) {
+ rb_native_cond_wait(&arg->cond, &arg->lock);
+ }
+ rb_nativethread_lock_unlock(&arg->lock);
+ return 0;
+}
+
+static void
+cancel_getaddrinfo(void *ptr)
+{
+ struct getaddrinfo_arg *arg = (struct getaddrinfo_arg *)ptr;
+ rb_nativethread_lock_lock(&arg->lock);
+ arg->cancelled = 1;
+ rb_native_cond_signal(&arg->cond);
+ rb_nativethread_lock_unlock(&arg->lock);
+}
+
+static int
+rb_getaddrinfo(const char *hostp, const char *portp, const struct addrinfo *hints, struct addrinfo **ai)
+{
+ int retry;
+ struct getaddrinfo_arg *arg;
+ int err;
+
+start:
+ retry = 0;
+
+ arg = allocate_getaddrinfo_arg(hostp, portp, hints);
+ if (!arg) {
+ return ENOMEM;
+ }
+
+ pthread_t th;
+ if (pthread_create(&th, 0, do_getaddrinfo, arg) != 0) {
+ free_getaddrinfo_arg(arg);
+ return EAGAIN;
+ }
+
+ pthread_detach(th);
+#if defined(HAVE_PTHREAD_SETAFFINITY_NP) && defined(HAVE_SCHED_GETCPU)
+ cpu_set_t tmp_cpu_set;
+ CPU_ZERO(&tmp_cpu_set);
+ CPU_SET(sched_getcpu(), &tmp_cpu_set);
+ pthread_setaffinity_np(th, sizeof(cpu_set_t), &tmp_cpu_set);
+#endif
+
+ rb_thread_call_without_gvl2(wait_getaddrinfo, arg, cancel_getaddrinfo, arg);
+
+ int need_free = 0;
+ rb_nativethread_lock_lock(&arg->lock);
+ if (arg->done) {
+ err = arg->err;
+ if (err == 0) *ai = arg->ai;
+ }
+ else if (arg->cancelled) {
+ err = EAGAIN;
+ }
+ else {
+ // If already interrupted, rb_thread_call_without_gvl2 may return without calling wait_getaddrinfo.
+ // In this case, it could be !arg->done && !arg->cancelled.
+ arg->cancelled = 1; // to make do_getaddrinfo call freeaddrinfo
+ retry = 1;
+ }
+ if (--arg->refcount == 0) need_free = 1;
+ rb_nativethread_lock_unlock(&arg->lock);
+
+ if (need_free) free_getaddrinfo_arg(arg);
+
+ // If the current thread is interrupted by asynchronous exception, the following raises the exception.
+ // But if the current thread is interrupted by timer thread, the following returns; we need to manually retry.
+ rb_thread_check_ints();
+ if (retry) goto start;
+
+ return err;
+}
+
#endif
#if GETADDRINFO_IMPL == 0
@@ -345,7 +527,7 @@ rb_getnameinfo(const struct sockaddr *sa, socklen_t salen,
return getnameinfo(sa, salen, host, hostlen, serv, servlen, flags);
}
-#elif GETADDRINFO_IMPL == 1
+#elif GETADDRINFO_IMPL == 1 || GETADDRINFO_IMPL == 2 // tmp
struct getnameinfo_arg
{