[libsoup/wip/tingping/cached-resolver] Create SoupCachedResolver



commit 48d734274239d4d411b5c11cb44c974329c2aff4
Author: Patrick Griffis <pgriffis igalia com>
Date:   Tue Feb 26 10:22:55 2019 -0500

    Create SoupCachedResolver
    
    This is a simple implementation of GResolver to add caching
    to an existing GResolver.

 docs/reference/libsoup-2.4-sections.txt |  17 +
 libsoup/meson.build                     |   2 +
 libsoup/soup-cached-resolver.c          | 624 ++++++++++++++++++++++++++++++++
 libsoup/soup-cached-resolver.h          |  39 ++
 libsoup/soup.h                          |   1 +
 tests/cached-resolver-test.c            | 137 +++++++
 tests/meson.build                       |   1 +
 7 files changed, 821 insertions(+)
---
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt
index 354c0783..efda43d7 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -1096,6 +1096,23 @@ SoupCacheResponse
 SoupCacheability
 </SECTION>
 
+<SECTION>
+<FILE>soup-cached-resolver</FILE>
+<TITLE>SoupCachedResolver</TITLE>
+SoupCachedResolver
+SoupCachedResolverType
+soup_cached_resolver_ensure_default
+soup_cached_resolver_new
+<SUBSECTION Standard>
+SOUP_TYPE_CACHED_RESOLVER
+SOUP_IS_CACHED_RESOLVER
+SOUP_IS_CACHED_RESOLVER_CLASS
+SOUP_CACHED_RESOLVER
+SOUP_CACHED_RESOLVER_CLASS
+SOUP_CACHED_RESOLVER_GET_CLASS
+soup_cached_resolver_get_type
+</SECTION>
+
 <SECTION>
 <FILE>soup-content-decoder</FILE>
 <TITLE>SoupContentDecoder</TITLE>
diff --git a/libsoup/meson.build b/libsoup/meson.build
index 5f2a2156..39f0a951 100644
--- a/libsoup/meson.build
+++ b/libsoup/meson.build
@@ -16,6 +16,7 @@ soup_sources = [
   'soup-cache.c',
   'soup-cache-client-input-stream.c',
   'soup-cache-input-stream.c',
+  'soup-cached-resolver.c',
   'soup-client-input-stream.c',
   'soup-connection.c',
   'soup-connection-auth.c',
@@ -114,6 +115,7 @@ soup_introspection_headers = [
   'soup-auth-manager.h',
   'soup-autocleanups.h',
   'soup-cache.h',
+  'soup-cached-resolver.h',
   'soup-content-decoder.h',
   'soup-content-sniffer.h',
   'soup-cookie.h',
diff --git a/libsoup/soup-cached-resolver.c b/libsoup/soup-cached-resolver.c
new file mode 100644
index 00000000..72691719
--- /dev/null
+++ b/libsoup/soup-cached-resolver.c
@@ -0,0 +1,624 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-cached-resolver.c
+ *
+ * Copyright (C) 2019 Igalia S.L.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "soup-cached-resolver.h"
+
+/**
+ * SECTION:soup-cached-resolver
+ * @short_description: Cached DNS Resolver
+ *
+ * #SoupCachedResolver wraps any existing #GResolver to allow
+ * basic caching of DNS responses.
+ *
+ * The cache is designed to be short lived and small and not intended
+ * to fully replace system resolver. It currently only caches resolving
+ * names and does not cache any other record lookups.
+ *
+ * For best performance use in combination with soup_session_prefetch_dns().
+ *
+ * Note that the #GResolver that Gio uses is a global setting so
+ * you will generally opt into it via soup_cached_resolver_ensure_default().
+ * See also g_resolver_set_default() and g_resolver_get_default().
+ */
+
+/* This version introduces querying names for ipv4/ipv6
+ * separately which we cache separately */
+#if GLIB_CHECK_VERSION (2, 59, 0)
+#define N_CACHES 3
+#else
+#define N_CACHES 1
+#endif
+
+struct _SoupCachedResolver
+{
+       GResolver parent_instance;
+       GResolver *wrapped_resolver;
+       GHashTable *dns_caches[N_CACHES];
+       GMutex dns_cache_lock;
+       guint64 max_size;
+};
+
+SOUP_AVAILABLE_IN_2_66
+GType soup_cached_resolver_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_TYPE (SoupCachedResolver, soup_cached_resolver, G_TYPE_RESOLVER)
+
+#define DNS_CACHE_EXPIRE_SECONDS 60
+
+enum {
+       PROP_0,
+       PROP_WRAPPED_RESOLVER,
+       PROP_MAX_SIZE,
+       N_PROPS
+};
+
+/**
+ * soup_cached_resolver_ensure_default:
+ * 
+ * Ensures that the global default #GResolver is a #SoupCachedResolver
+ * or creates a new one wrapping the current default and sets that as
+ * default.
+ *
+ * Since: 2.66
+ */
+void
+soup_cached_resolver_ensure_default (void)
+{
+       GResolver *default_resolver = g_resolver_get_default ();
+       if (!SOUP_IS_CACHED_RESOLVER (default_resolver)) {
+               SoupCachedResolver *resolver = soup_cached_resolver_new (default_resolver);
+               g_resolver_set_default (G_RESOLVER (resolver));
+               g_object_unref (resolver);
+       }
+       g_object_unref (default_resolver);
+}
+
+/**
+ * soup_cached_resolver_new:
+ * @wrapped_resolver: Underlying #GResolver to be cached
+ * 
+ * Note that @wrapped_resolver must not be a #SoupCachedResolver.
+ *
+ * You should generally use soup_cached_resolver_ensure_default()
+ * rather than this API directly.
+ *
+ * Returns: (transfer full): A new #SoupCachedResolver
+ * Since: 2.66
+ */
+SoupCachedResolver *
+soup_cached_resolver_new (GResolver *wrapped_resolver)
+{
+       g_return_val_if_fail (wrapped_resolver != NULL, NULL);
+       g_return_val_if_fail (!SOUP_IS_CACHED_RESOLVER (wrapped_resolver), NULL);
+
+       return g_object_new (SOUP_TYPE_CACHED_RESOVLER,
+                             "wrapped-resolver", wrapped_resolver,
+                             NULL);
+}
+
+typedef struct {
+       GList *addresses; /* owned */
+       gint64 expiration;
+} CachedResponse;
+
+static void
+cached_response_free (CachedResponse *cache)
+{
+       g_resolver_free_addresses (cache->addresses);
+       g_free (cache);
+}
+
+#if GLIB_CHECK_VERSION (2, 59, 0)
+
+static GHashTable *
+get_dns_cache_for_flags (SoupCachedResolver       *self,
+                        GResolverNameLookupFlags  flags)
+{
+       /* A cache is kept for each type of response to avoid
+        * the overcomplication of combining or filtering results.
+        */
+       if (flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV4_ONLY)
+               return self->dns_caches[0];
+       else if (flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV6_ONLY)
+               return self->dns_caches[1];
+       else
+               return self->dns_caches[2];
+}
+
+#else
+
+#define G_RESOLVER_NAME_LOOKUP_FLAGS_DEFAULT 0
+typedef int GResolverNameLookupFlags;
+
+static GHashTable *
+get_dns_cache_for_flags (SoupCachedResolver *self,
+                        int                 ignored)
+{
+       return self->dns_caches[0];
+}
+
+#endif
+
+static gpointer
+copy_object (gconstpointer obj, gpointer user_data)
+{
+       return g_object_ref (G_OBJECT (obj));
+}
+
+static GList *
+copy_addresses (GList *addresses)
+{
+       return g_list_copy_deep (addresses, copy_object, NULL);
+}
+
+static void
+cleanup_dns_cache (SoupCachedResolver *self,
+                   GHashTable         *cache)
+{
+       GHashTableIter iter;
+       CachedResponse *cached;
+       gint64 now = g_get_monotonic_time ();
+       guint64 size = 0;
+
+       g_mutex_lock (&self->dns_cache_lock);
+
+       g_hash_table_iter_init (&iter, cache);
+       while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &cached)) {
+               if (cached->expiration <= now || size > self->max_size)
+                       g_hash_table_iter_remove (&iter);
+               else
+                       ++size;
+       }
+
+       g_mutex_unlock (&self->dns_cache_lock);
+}
+
+static void
+cleanup_all_dns_caches (SoupCachedResolver *self)
+{
+       guint i;
+       for (i = 0; i < G_N_ELEMENTS (self->dns_caches); ++i)
+               cleanup_dns_cache (self, self->dns_caches[i]);
+}
+
+static void
+update_dns_cache (SoupCachedResolver       *self,
+                 const char               *hostname,
+                 GList                    *addresses,
+                 GResolverNameLookupFlags  flags)
+{
+       CachedResponse *cached;
+       GHashTable *cache;
+
+       if (addresses == NULL)
+               return;
+
+       cache = get_dns_cache_for_flags (self, flags);
+       cached = g_new (CachedResponse, 1);
+       cached->addresses = copy_addresses (addresses);
+       cached->expiration = g_get_monotonic_time () + (DNS_CACHE_EXPIRE_SECONDS * 1000);
+
+       /* Cleanup while we are at it. */
+       cleanup_dns_cache (self, cache);
+
+       g_mutex_lock (&self->dns_cache_lock);
+
+       g_hash_table_insert (cache, g_strdup (hostname), cached);
+
+       g_mutex_unlock (&self->dns_cache_lock);
+}
+
+/*
+ * Returns: (transfer full): List of addresses
+ */
+static GList *
+query_dns_cache (SoupCachedResolver       *self,
+                const char               *hostname,
+                GResolverNameLookupFlags  flags)
+{
+       CachedResponse *cached;
+       GHashTable *cache;
+       GList *addresses = NULL;
+       gint64 now = g_get_monotonic_time ();
+
+       cache = get_dns_cache_for_flags (self, flags);
+
+       g_mutex_lock (&self->dns_cache_lock);
+
+       cached = g_hash_table_lookup (cache, hostname);
+       if (cached && cached->expiration > now)
+               addresses = copy_addresses (cached->addresses);
+
+       g_mutex_unlock (&self->dns_cache_lock);
+
+       return addresses;
+}
+
+static void
+reload (GResolver *resolver)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (resolver);
+       guint i;
+
+       g_info ("Flushing DNS Cache");
+
+       /* Empty caches on system DNS changes */
+       for (i = 0; i < G_N_ELEMENTS (self->dns_caches); ++i)
+               g_hash_table_remove_all (self->dns_caches[i]);
+}
+
+#if GLIB_CHECK_VERSION (2, 59, 0)
+
+typedef struct {
+       char *hostname;
+       GResolverNameLookupFlags flags;
+} LookupData;
+
+static LookupData *
+lookup_data_new (const char *hostname, GResolverNameLookupFlags flags)
+{
+       LookupData *lookup_data = g_new (LookupData, 1);
+       lookup_data->hostname = g_strdup (hostname);
+       lookup_data->flags = flags;
+       return lookup_data;
+}
+
+static void
+lookup_data_free (LookupData *lookup_data)
+{
+       g_free (lookup_data->hostname);
+       g_free (lookup_data);
+}
+
+static GList *
+lookup_by_name_with_flags (GResolver                *resolver,
+                          const gchar              *hostname,
+                          GResolverNameLookupFlags  flags,
+                          GCancellable             *cancellable,
+                          GError                  **error)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (resolver);
+       GList *addresses = query_dns_cache (self, hostname, flags);
+
+       if (addresses)
+               return addresses;
+
+       addresses = g_resolver_lookup_by_name_with_flags (self->wrapped_resolver,
+                                                          hostname,
+                                                         flags,
+                                                          cancellable,
+                                                          error);
+       update_dns_cache (self, hostname, addresses, flags);
+       return addresses;
+}
+
+static GList *
+lookup_by_name_with_flags_finish (GResolver    *resolver,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+       g_return_val_if_fail (g_task_is_valid (result, resolver), NULL);
+
+       return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+on_lookup_by_name_with_flags_finish (GObject   *resolver,
+                                    GAsyncResult  *result,
+                                     gpointer      user_data)
+{
+       GTask *task = G_TASK (user_data);
+       SoupCachedResolver *self = g_task_get_source_object (task);
+       LookupData *data = g_task_get_task_data (task);
+       GError *error = NULL;
+       GList *addresses;
+
+       addresses = g_resolver_lookup_by_name_with_flags_finish (G_RESOLVER (resolver), result, &error);
+       if (addresses) {
+               update_dns_cache (self, data->hostname, addresses, data->flags);
+               g_task_return_pointer (task, addresses, (GDestroyNotify) g_resolver_free_addresses);
+       } else
+               g_task_return_error (task, error);
+
+       g_object_unref (task);
+}
+
+static void
+lookup_by_name_with_flags_async (GResolver                *resolver,
+                                const gchar              *hostname,
+                                GResolverNameLookupFlags  flags,
+                                GCancellable             *cancellable,
+                                GAsyncReadyCallback       callback,
+                                gpointer                  user_data)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (resolver);
+       GTask *task = g_task_new (self, cancellable, callback, user_data);
+       GList *cached = query_dns_cache (self, hostname, flags);
+
+       if (cached)
+               g_task_return_pointer (task, cached, (GDestroyNotify) g_resolver_free_addresses);
+       else {
+               g_task_set_task_data (task, lookup_data_new (hostname, flags), (GDestroyNotify) 
lookup_data_free);
+               g_resolver_lookup_by_name_with_flags_async (self->wrapped_resolver,
+                                                            hostname,
+                                                            flags,
+                                                            cancellable,
+                                                            on_lookup_by_name_with_flags_finish,
+                                                            g_steal_pointer (&task));
+       }
+
+       g_clear_object (&task);
+}
+
+#endif
+
+static void
+on_lookup_by_name_finish (GObject      *resolver,
+                          GAsyncResult  *result,
+                          gpointer      user_data)
+{
+       GTask *task = G_TASK (user_data);
+       SoupCachedResolver *self = g_task_get_source_object (task);
+       char *hostname = g_task_get_task_data (task);
+       GError *error = NULL;
+       GList *addresses;
+
+       addresses = g_resolver_lookup_by_name_finish (G_RESOLVER (resolver), result, &error);
+       if (addresses) {
+               update_dns_cache (self, hostname, addresses, G_RESOLVER_NAME_LOOKUP_FLAGS_DEFAULT);
+               g_task_return_pointer (task, addresses, (GDestroyNotify) g_resolver_free_addresses);
+       } else
+               g_task_return_error (task, error);
+
+       g_object_unref (task);
+}
+
+static void
+lookup_by_name_async (GResolver                  *resolver,
+                      const gchar         *hostname,
+                      GCancellable        *cancellable,
+                      GAsyncReadyCallback  callback,
+                      gpointer             user_data)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (resolver);
+       GTask *task = g_task_new (self, cancellable, callback, user_data);
+       GList *cached = query_dns_cache (self, hostname, G_RESOLVER_NAME_LOOKUP_FLAGS_DEFAULT);
+
+       if (cached)
+               g_task_return_pointer (task, cached, (GDestroyNotify) g_resolver_free_addresses);
+       else {
+               g_task_set_task_data (task, g_strdup (hostname), g_free);
+               g_resolver_lookup_by_name_async (self->wrapped_resolver,
+                                                 hostname,
+                                                 cancellable,
+                                                 on_lookup_by_name_finish,
+                                                 g_steal_pointer (&task));
+       }
+
+       g_clear_object (&task);
+}
+
+static GList *
+lookup_by_name_finish (GResolver    *resolver,
+                       GAsyncResult *result,
+                      GError       **error)
+{
+       g_return_val_if_fail (g_task_is_valid (result, resolver), NULL);
+
+       return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static GList *
+lookup_by_name (GResolver     *resolver,
+               const gchar   *hostname,
+               GCancellable  *cancellable,
+               GError       **error)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (resolver);
+       GList *addresses = query_dns_cache (self, hostname, G_RESOLVER_NAME_LOOKUP_FLAGS_DEFAULT);
+
+       if (addresses)
+               return addresses;
+
+       addresses = g_resolver_lookup_by_name (self->wrapped_resolver,
+                                               hostname,
+                                               cancellable,
+                                               error);
+       update_dns_cache (self, hostname, addresses, G_RESOLVER_NAME_LOOKUP_FLAGS_DEFAULT);
+       return addresses;
+}
+
+
+static gchar *
+lookup_by_address (GResolver    *resolver,
+                  GInetAddress  *address,
+                  GCancellable  *cancellable,
+                  GError       **error)
+{
+       return g_resolver_lookup_by_address (SOUP_CACHED_RESOLVER (resolver)->wrapped_resolver, address, 
cancellable, error);
+}
+
+static void
+lookup_by_address_async (GResolver           *resolver,
+                        GInetAddress        *address,
+                        GCancellable        *cancellable,
+                        GAsyncReadyCallback  callback,
+                        gpointer             user_data)
+{
+       g_resolver_lookup_by_address_async (SOUP_CACHED_RESOLVER (resolver)->wrapped_resolver, address, 
cancellable, callback, user_data);
+}
+
+static gchar *
+lookup_by_address_finish (GResolver     *resolver,
+                         GAsyncResult  *result,
+                         GError           **error)
+{
+       return g_resolver_lookup_by_address_finish (SOUP_CACHED_RESOLVER (resolver)->wrapped_resolver, 
result, error);
+}
+
+static GList *
+lookup_records (GResolver            *resolver,
+               const gchar          *rrname,
+               GResolverRecordType   record_type,
+               GCancellable         *cancellable,
+               GError              **error)
+{
+       return g_resolver_lookup_records (SOUP_CACHED_RESOLVER (resolver)->wrapped_resolver, rrname, 
record_type, cancellable, error);
+}
+
+static void
+lookup_records_async (GResolver                  *resolver,
+                      const char          *rrname,
+                      GResolverRecordType  record_type,
+                      GCancellable        *cancellable,
+                      GAsyncReadyCallback  callback,
+                      gpointer             user_data)
+{
+       g_resolver_lookup_records_async (SOUP_CACHED_RESOLVER (resolver)->wrapped_resolver, rrname, 
record_type, cancellable, callback, user_data);
+}
+
+static GList *
+lookup_records_finish (GResolver     *resolver,
+                       GAsyncResult  *result,
+                       GError       **error)
+{
+       return g_resolver_lookup_records_finish (SOUP_CACHED_RESOLVER (resolver)->wrapped_resolver, result, 
error);
+}
+
+static void
+soup_cached_resolver_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (object);
+
+       switch (prop_id)
+       {
+       case PROP_WRAPPED_RESOLVER:
+               g_value_set_object (value, self->wrapped_resolver);
+               break;
+       case PROP_MAX_SIZE:
+               g_value_set_uint64 (value, self->max_size);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+soup_cached_resolver_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (object);
+
+       switch (prop_id)
+       {
+       case PROP_WRAPPED_RESOLVER:
+               g_assert (self->wrapped_resolver == NULL);
+               self->wrapped_resolver = g_value_dup_object (value);
+               g_assert (self->wrapped_resolver != NULL);
+               break;
+       case PROP_MAX_SIZE:
+               self->max_size = g_value_get_uint64 (value);
+               cleanup_all_dns_caches (self);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+       }
+}
+
+static void
+soup_cached_resolver_finalize (GObject *object)
+{
+       SoupCachedResolver *self = SOUP_CACHED_RESOLVER (object);
+       g_clear_object (&self->wrapped_resolver);
+       G_OBJECT_CLASS (soup_cached_resolver_parent_class)->finalize (object);
+}
+
+static void
+soup_cached_resolver_class_init (SoupCachedResolverClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GResolverClass *resolver_class = G_RESOLVER_CLASS (klass);
+
+       object_class->finalize = soup_cached_resolver_finalize;
+       object_class->get_property = soup_cached_resolver_get_property;
+       object_class->set_property = soup_cached_resolver_set_property;
+
+       resolver_class->lookup_by_name = lookup_by_name;
+       resolver_class->lookup_by_name_async = lookup_by_name_async;
+       resolver_class->lookup_by_name_finish = lookup_by_name_finish;
+#if GLIB_CHECK_VERSION (2, 59, 0)
+       resolver_class->lookup_by_name_with_flags = lookup_by_name_with_flags;
+       resolver_class->lookup_by_name_with_flags_async = lookup_by_name_with_flags_async;
+       resolver_class->lookup_by_name_with_flags_finish = lookup_by_name_with_flags_finish;
+#endif
+       resolver_class->lookup_by_address = lookup_by_address;
+       resolver_class->lookup_by_address_async = lookup_by_address_async;
+       resolver_class->lookup_by_address_finish = lookup_by_address_finish;
+       resolver_class->lookup_records = lookup_records;
+       resolver_class->lookup_records_async = lookup_records_async;
+       resolver_class->lookup_records_finish = lookup_records_finish;
+
+       resolver_class->reload = reload;
+
+       /**
+        * SoupCachedResolver:wrapped-resolver:
+        *
+        * The #GResolver that is cached.
+        *
+        * Since: 2.66
+        */
+       g_object_class_install_property (object_class, PROP_WRAPPED_RESOLVER,
+                                        g_param_spec_object ("wrapped-resolver", "wrapped-resolver",
+                                                             "DNS Resolver that is wrapped",
+                                                             G_TYPE_RESOLVER,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_STATIC_STRINGS));
+
+       /**
+        * SoupCachedResolver:max-size:
+        *
+        * The maximum size of the DNS cache.
+        *
+        * Since: 2.66
+        */
+       g_object_class_install_property (object_class, PROP_MAX_SIZE,
+                                        g_param_spec_uint64 ("max-size", "max-size",
+                                                             "Max size of DNS cache",
+                                                             0, G_MAXUINT64, 400,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT | 
G_PARAM_STATIC_STRINGS));
+}
+
+static void
+soup_cached_resolver_init (SoupCachedResolver *self)
+{
+       guint i;  
+       for (i = 0; i < G_N_ELEMENTS (self->dns_caches); ++i)
+               self->dns_caches[i] = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
(GDestroyNotify) cached_response_free);
+}
diff --git a/libsoup/soup-cached-resolver.h b/libsoup/soup-cached-resolver.h
new file mode 100644
index 00000000..4f4c31fe
--- /dev/null
+++ b/libsoup/soup-cached-resolver.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-cached-resolver.h:
+ *
+ * Copyright (C) 2019 Igalia, S.L.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include "soup-version.h"
+
+G_BEGIN_DECLS
+
+#define SOUP_TYPE_CACHED_RESOVLER (soup_cached_resolver_get_type())
+G_DECLARE_FINAL_TYPE (SoupCachedResolver, soup_cached_resolver, SOUP, CACHED_RESOLVER, GResolver)
+
+SOUP_AVAILABLE_IN_2_66
+void soup_cached_resolver_ensure_default (void);
+
+SOUP_AVAILABLE_IN_2_66
+SoupCachedResolver *soup_cached_resolver_new (GResolver *wrapped_resolver);
+
+G_END_DECLS
diff --git a/libsoup/soup.h b/libsoup/soup.h
index 4a3ac2ef..1fdd02e5 100644
--- a/libsoup/soup.h
+++ b/libsoup/soup.h
@@ -19,6 +19,7 @@ extern "C" {
 #include <libsoup/soup-auth-domain-digest.h>
 #include <libsoup/soup-auth-manager.h>
 #include <libsoup/soup-cache.h>
+#include <libsoup/soup-cached-resolver.h>
 #include <libsoup/soup-content-decoder.h>
 #include <libsoup/soup-content-sniffer.h>
 #include <libsoup/soup-cookie.h>
diff --git a/tests/cached-resolver-test.c b/tests/cached-resolver-test.c
new file mode 100644
index 00000000..54540363
--- /dev/null
+++ b/tests/cached-resolver-test.c
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 Igalia S.L.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "test-utils.h"
+
+static void
+test_cached_resolver (void)
+{
+        GResolver *default_resolver = g_resolver_get_default ();
+        GResolver *cached_resolver;
+        GList *results;
+        double default_time, cached_time;
+        guint i;
+
+        /* Just verify caching works at a basic level by being faster */
+
+        g_assert_false (SOUP_IS_CACHED_RESOLVER (default_resolver));
+
+        /* Warm any DNS cache */
+        results = g_resolver_lookup_by_name (default_resolver, "gnome.org", NULL, NULL);
+        g_assert_nonnull (results);
+        g_resolver_free_addresses (results);
+
+        g_test_timer_start ();
+
+        for (i = 0; i < 100; ++i) {
+                results = g_resolver_lookup_by_name (default_resolver, "gnome.org", NULL, NULL);
+                g_assert_nonnull (results);
+                g_resolver_free_addresses (results);
+        }
+        default_time = g_test_timer_elapsed ();
+
+        soup_cached_resolver_ensure_default ();
+        /* Test that its safe to call multiple times */
+        soup_cached_resolver_ensure_default ();
+
+        cached_resolver = g_resolver_get_default ();
+        g_assert_true (SOUP_IS_CACHED_RESOLVER (cached_resolver));
+
+
+        /* Warm the DNS cache */
+        results = g_resolver_lookup_by_name (cached_resolver, "gnome.org", NULL, NULL);
+        g_assert_nonnull (results);
+        g_resolver_free_addresses (results);
+
+        g_test_timer_start ();
+
+        for (i = 0; i < 100; ++i) {
+                results = g_resolver_lookup_by_name (cached_resolver, "gnome.org", NULL, NULL);
+                g_assert_nonnull (results);
+                g_resolver_free_addresses (results);
+        }
+        cached_time = g_test_timer_elapsed ();
+
+        /* Cached will always be faster or else whats the point */
+        g_assert_cmpfloat (default_time, >, cached_time);
+        g_info ("%f > %f", default_time, cached_time);
+
+        g_resolver_set_default (default_resolver);
+        g_object_unref (default_resolver);
+        g_object_unref (cached_resolver);
+}
+
+static void
+on_lookup_by_name_finish (GResolver    *resolver,
+                          GAsyncResult  *result,
+                          gboolean      *done)
+{
+       GError *error = NULL;
+       GList *addresses;
+
+       addresses = g_resolver_lookup_by_name_finish (resolver, result, &error);
+        g_assert_no_error (error);
+        g_assert_nonnull (addresses);
+        g_resolver_free_addresses (addresses);
+        *done = TRUE;
+}
+
+static void
+test_cached_resolver_async (void)
+{
+        GResolver *default_resolver = g_resolver_get_default ();
+        GResolver *cached_resolver;
+        gboolean done = FALSE;
+
+        soup_cached_resolver_ensure_default ();
+        cached_resolver = g_resolver_get_default ();
+
+        /* Just sanity check it works */
+        g_resolver_lookup_by_name_async (cached_resolver, "gnome.org", NULL, (GAsyncReadyCallback) 
on_lookup_by_name_finish, &done);
+
+        while (done != TRUE)
+                g_main_context_iteration (NULL, TRUE);
+
+        /* And again cached */
+        done = FALSE;
+        g_resolver_lookup_by_name_async (cached_resolver, "gnome.org", NULL, (GAsyncReadyCallback)  
on_lookup_by_name_finish, &done);
+
+        while (done != TRUE)
+                g_main_context_iteration (NULL, TRUE);
+
+        g_resolver_set_default (default_resolver);
+        g_object_unref (default_resolver);
+        g_object_unref (cached_resolver);
+}
+
+int
+main (int argc, char **argv)
+{
+       int ret;
+
+       test_init (argc, argv, NULL);
+
+       g_test_add_func ("/cached-resolver", test_cached_resolver);
+        g_test_add_func ("/cached-resolver/async", test_cached_resolver_async);
+
+       ret = g_test_run ();
+
+       test_cleanup ();
+       return ret;
+}
diff --git a/tests/meson.build b/tests/meson.build
index 8176f29b..370a11b3 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -15,6 +15,7 @@ test_resources = gnome.compile_resources('soup-tests',
 # ['name', is_parallel]
 tests = [
   ['cache', true],
+  ['cached-resolver', true],
   ['chunk', true],
   ['chunk-io', true],
   ['coding', true],


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]