[telepathy-gabble/telepathy-gabble-0.8] make caps cache persist to disk using SQLite

Dafydd Harries dafydd.harries at collabora.co.uk
Thu Jan 7 18:14:23 PST 2010


---
 configure.ac                                |    5 +
 src/Makefile.am                             |    4 +-
 src/caps-cache.c                            |  481 ++++++++++++++++++++++++++-
 src/caps-cache.h                            |    2 +-
 tests/twisted/Makefile.am                   |    1 +
 tests/twisted/caps/caps-persistent-cache.py |   81 +++++
 tests/twisted/tools/exec-with-log.sh.in     |    1 +
 7 files changed, 556 insertions(+), 19 deletions(-)
 create mode 100644 tests/twisted/caps/caps-persistent-cache.py

diff --git a/configure.ac b/configure.ac
index b018641..c7e2a94 100644
--- a/configure.ac
+++ b/configure.ac
@@ -165,6 +165,11 @@ AC_CHECK_FUNC(res_query, ,
 
 AC_SUBST(RESOLV_LIBS)
 
+PKG_CHECK_MODULES(SQLITE, [sqlite3])
+
+AC_SUBST(SQLITE_CFLAGS)
+AC_SUBST(SQLITE_LIBS)
+
 dnl Check for code generation tools
 XSLTPROC=
 AC_CHECK_PROGS([XSLTPROC], [xsltproc])
diff --git a/src/Makefile.am b/src/Makefile.am
index ddaa348..5a81d32 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -169,12 +169,12 @@ noinst_LTLIBRARIES = libgabble-convenience.la
 AM_CFLAGS = $(ERROR_CFLAGS) -I$(top_srcdir) -I$(top_builddir) \
 	    @DBUS_CFLAGS@ @GLIB_CFLAGS@ @LOUDMOUTH_CFLAGS@ \
 	    @HANDLE_LEAK_DEBUG_CFLAGS@ @TP_GLIB_CFLAGS@ \
-	    @SOUP_CFLAGS@ @UUID_CFLAGS@ \
+	    @SOUP_CFLAGS@ @UUID_CFLAGS@ @SQLITE_CFLAGS@ \
 	    -I $(top_srcdir)/lib -I $(top_builddir)/lib \
 	    -DG_LOG_DOMAIN=\"gabble\"
 
 ALL_LIBS =  @DBUS_LIBS@ @GLIB_LIBS@ @LOUDMOUTH_LIBS@ @TP_GLIB_LIBS@ \
-	    @SOUP_LIBS@ @UUID_LIBS@
+	    @SOUP_LIBS@ @UUID_LIBS@ @SQLITE_LIBS@
 
 # build gibber first
 all: gibber
diff --git a/src/caps-cache.c b/src/caps-cache.c
index 417396e..d8b51fe 100644
--- a/src/caps-cache.c
+++ b/src/caps-cache.c
@@ -1,6 +1,16 @@
 
+#include "config.h"
 #include "caps-cache.h"
 
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sqlite3.h>
+
+#define DEBUG_FLAG GABBLE_DEBUG_PRESENCE
+#include "debug.h"
+
 G_DEFINE_TYPE (GabbleCapsCache, gabble_caps_cache, G_TYPE_OBJECT)
 
 #define GET_PRIVATE(o) \
@@ -10,15 +20,31 @@ static GabbleCapsCache *shared_cache = NULL;
 
 struct _GabbleCapsCachePrivate
 {
-  GHashTable *cache;
+  gchar *path;
+  sqlite3 *db;
+  guint inserts;
+};
+
+enum
+{
+    PROP_PATH = 1,
 };
 
+static GObject *
+gabble_caps_cache_constructor (
+    GType type, guint n_props, GObjectConstructParam *props);
+
 static void
 gabble_caps_cache_get_property (GObject *object, guint property_id,
                               GValue *value, GParamSpec *pspec)
 {
+  GabbleCapsCache *self = (GabbleCapsCache *) object;
+
   switch (property_id)
     {
+    case PROP_PATH:
+      g_value_set_string (value, self->priv->path);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     }
@@ -28,8 +54,14 @@ static void
 gabble_caps_cache_set_property (GObject *object, guint property_id,
                               const GValue *value, GParamSpec *pspec)
 {
+  GabbleCapsCache *self = (GabbleCapsCache *) object;
+
   switch (property_id)
     {
+    case PROP_PATH:
+      g_free (self->priv->path);
+      self->priv->path = g_value_dup_string (value);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     }
@@ -44,6 +76,17 @@ gabble_caps_cache_dispose (GObject *object)
 static void
 gabble_caps_cache_finalize (GObject *object)
 {
+  GabbleCapsCache *self = GABBLE_CAPS_CACHE (object);
+
+  g_free (self->priv->path);
+  self->priv->path = NULL;
+
+  if (self->priv->db != NULL)
+    {
+      sqlite3_close (self->priv->db);
+      self->priv->db = NULL;
+    }
+
   G_OBJECT_CLASS (gabble_caps_cache_parent_class)->finalize (object);
 }
 
@@ -54,10 +97,125 @@ gabble_caps_cache_class_init (GabbleCapsCacheClass *klass)
 
   g_type_class_add_private (klass, sizeof (GabbleCapsCachePrivate));
 
+  object_class->constructor = gabble_caps_cache_constructor;
   object_class->get_property = gabble_caps_cache_get_property;
   object_class->set_property = gabble_caps_cache_set_property;
   object_class->dispose = gabble_caps_cache_dispose;
   object_class->finalize = gabble_caps_cache_finalize;
+
+  g_object_class_install_property (object_class, PROP_PATH,
+      g_param_spec_string ("path", "Path", "The path to the cache", NULL,
+          G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+          G_PARAM_STATIC_STRINGS));
+}
+
+static gchar *
+get_path (void)
+{
+  gchar *free_dir = NULL, *ret;
+  const gchar *dir, *path;
+
+  /* This should still work if it's the magic value ":memory:". */
+  path = g_getenv ("GABBLE_CAPS_CACHE");
+
+  if (path != NULL)
+    {
+      dir = free_dir = g_path_get_dirname (path);
+      ret = g_strdup (path);
+    }
+  else
+    {
+      dir = g_getenv ("GABBLE_CACHE_DIR");
+
+      if (dir != NULL)
+        {
+          ret = g_build_path (G_DIR_SEPARATOR_S, dir, "caps-cache.db", NULL);
+        }
+      else
+        {
+          ret = g_build_path (G_DIR_SEPARATOR_S,
+              g_get_user_cache_dir (), "telepathy", "gabble", "caps-cache.db",
+              NULL);
+          dir = free_dir = g_path_get_dirname (path);
+        }
+    }
+
+  /* Any errors are ignored here, on the basis that we'll find out the path is
+   * duff when we try to open the database anyway.
+   */
+  g_mkdir_with_parents (dir, 0755);
+  g_free (free_dir);
+  return ret;
+}
+
+static GObject *
+gabble_caps_cache_constructor (
+    GType type, guint n_props, GObjectConstructParam *props)
+{
+  int ret;
+  GabbleCapsCache *self;
+  gchar *error;
+
+  self = (GabbleCapsCache *) G_OBJECT_CLASS (gabble_caps_cache_parent_class)
+      ->constructor (type, n_props, props);
+
+  ret = sqlite3_open (self->priv->path, &self->priv->db);
+
+  if (ret == SQLITE_OK)
+    {
+      DEBUG ("opened database at %s", self->priv->path);
+    }
+  else
+    {
+      DEBUG ("opening database failed: %s", sqlite3_errmsg (self->priv->db));
+
+      /* Can't open it. Nuke it and try again. */
+      sqlite3_close (self->priv->db);
+      ret = unlink (self->priv->path);
+
+      if (!ret)
+        {
+          DEBUG ("removing database failed: %s", strerror (ret));
+
+          /* Can't open it or remove it. Just pretend it isn't there. */
+          self->priv->db = NULL;
+        }
+      else
+        {
+          ret = sqlite3_open (self->priv->path, &self->priv->db);
+
+          if (ret == SQLITE_OK)
+            {
+              DEBUG ("opened database at %s", self->priv->path);
+            }
+          else
+            {
+              DEBUG ("database open after remove failed: %s",
+                  sqlite3_errmsg (self->priv->db));
+              /* Can't open it after removing it. Just pretend it isn't there.
+               */
+
+              sqlite3_close (self->priv->db);
+              self->priv->db = NULL;
+            }
+        }
+    }
+
+  ret = sqlite3_exec (self->priv->db,
+      "CREATE TABLE IF NOT EXISTS capabilities (\n"
+      "  node text PRIMARY KEY,\n"
+      "  namespaces text,\n"
+      "  timestamp int)", NULL, NULL, &error);
+
+  if (ret != SQLITE_OK)
+    {
+      DEBUG ("failed to ensure table exists: %s", error);
+      sqlite3_free (error);
+      sqlite3_close (self->priv->db);
+      self->priv->db = NULL;
+    }
+
+  return (GObject *) self;
 }
 
 static void
@@ -65,15 +223,12 @@ gabble_caps_cache_init (GabbleCapsCache *self)
 {
   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (
       self, GABBLE_TYPE_CAPS_CACHE, GabbleCapsCachePrivate);
-
-  self->priv->cache = g_hash_table_new_full (
-      g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_strfreev);
 }
 
 GabbleCapsCache *
-gabble_caps_cache_new (void)
+gabble_caps_cache_new (const gchar *path)
 {
-  return g_object_new (GABBLE_TYPE_CAPS_CACHE, NULL);
+  return g_object_new (GABBLE_TYPE_CAPS_CACHE, "path", path, NULL);
 }
 
 GabbleCapsCache *
@@ -81,7 +236,11 @@ gabble_caps_cache_dup_shared (void)
 {
   if (shared_cache == NULL)
     {
-      shared_cache = gabble_caps_cache_new ();
+      gchar *path;
+
+      path = get_path ();
+      shared_cache = gabble_caps_cache_new (path);
+      g_free (path);
     }
 
   g_object_ref (shared_cache);
@@ -94,32 +253,322 @@ gabble_caps_cache_free_shared (void)
   if (shared_cache != NULL)
     {
       g_object_unref (shared_cache);
+      shared_cache = NULL;
+    }
+}
+
+static gboolean
+caps_cache_prepare (
+    GabbleCapsCache *self,
+    const gchar *sql,
+    sqlite3_stmt **stmt)
+{
+  gint ret = sqlite3_prepare_v2 (self->priv->db, sql, -1, stmt, NULL);
+
+  if (ret != SQLITE_OK)
+    {
+      DEBUG ("preparing statement failed: %s",
+          sqlite3_errmsg (self->priv->db));
+      return FALSE;
+    }
+
+  g_assert (stmt != NULL);
+  return TRUE;
+}
+
+/* Finalizes @stmt if an error happens. */
+static gboolean
+caps_cache_bind_int (
+    GabbleCapsCache *self,
+    sqlite3_stmt *stmt,
+    gint param,
+    gint value)
+{
+  gint ret = sqlite3_bind_int (stmt, param, value);
+
+  if (ret != SQLITE_OK)
+    {
+      DEBUG ("parameter binding failed: %s", sqlite3_errmsg (self->priv->db));
+      sqlite3_finalize (stmt);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+/* Finalizes @stmt if an error happens.
+ *
+ * Note: the parameter is bound statically, so it mustn't be freed before the
+ * statment is finalized.
+ */
+static gboolean
+caps_cache_bind_text (
+    GabbleCapsCache *self,
+    sqlite3_stmt *stmt,
+    gint param,
+    gint len,
+    const gchar *value)
+{
+  gint ret = sqlite3_bind_text (stmt, param, value, len, SQLITE_STATIC);
+
+  if (ret != SQLITE_OK)
+    {
+      DEBUG ("parameter binding failed: %s", sqlite3_errmsg (self->priv->db));
+      sqlite3_finalize (stmt);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+/* Update cache entry timestmp. */
+static void
+caps_cache_touch (GabbleCapsCache *self, const gchar *node)
+{
+  gint ret;
+  sqlite3_stmt *stmt;
+
+  if (!caps_cache_prepare (self,
+        "UPDATE capabilities SET timestamp=? WHERE node=?", &stmt))
+    return;
+
+  if (!caps_cache_bind_int (self, stmt, 1, time (NULL)))
+    return;
+
+  if (!caps_cache_bind_text (self, stmt, 2, -1, node))
+    return;
+
+  ret = sqlite3_step (stmt);
+
+  if (ret != SQLITE_DONE)
+    {
+      DEBUG ("statement execution failed: %s",
+          sqlite3_errmsg (self->priv->db));
     }
+
+  sqlite3_finalize (stmt);
 }
 
+/* Caller is responsible for freeing the returned list and its contents.
+ */
 gchar **
 gabble_caps_cache_lookup (GabbleCapsCache *self, const gchar *node)
 {
-  return g_strdupv (g_hash_table_lookup (self->priv->cache, node));
+  gint ret;
+  const gchar *value = NULL;
+  sqlite3_stmt *stmt;
+  gchar **uris;
+
+  if (!self->priv->db)
+    /* DB open failed. */
+    return NULL;
+
+  if (!caps_cache_prepare (self,
+        "SELECT namespaces FROM capabilities WHERE node=?", &stmt))
+    return NULL;
+
+  if (!caps_cache_bind_text (self, stmt, 1, -1, node))
+    return NULL;
+
+  ret = sqlite3_step (stmt);
+
+  if (ret == SQLITE_DONE)
+    {
+      /* No result. */
+      DEBUG ("caps cache miss: %s", node);
+      sqlite3_finalize (stmt);
+      return NULL;
+    }
+
+  if (ret != SQLITE_ROW)
+    {
+      DEBUG ("statement execution failed: %s",
+          sqlite3_errmsg (self->priv->db));
+      sqlite3_finalize (stmt);
+      return NULL;
+    }
+
+  DEBUG ("caps cache hit: %s", node);
+  sqlite3_column_bytes (stmt, 0);
+  value = (gchar *) sqlite3_column_text (stmt, 0);
+  uris = g_strsplit (value, "\n", 0);
+  sqlite3_finalize (stmt);
+  caps_cache_touch (self, node);
+  return uris;
 }
 
-void
-gabble_caps_cache_insert (
+static void
+caps_cache_insert (
     GabbleCapsCache *self,
     const gchar *node,
     gchar **caps)
 {
-  GSList *old_caps;
+  gchar *val;
+  gint ret;
+  sqlite3_stmt *stmt;
+
+  if (!caps_cache_prepare (self,
+        "INSERT INTO capabilities (node, namespaces, timestamp) "
+        "VALUES (?, ?, ?)", &stmt))
+    return;
+
+  if (!caps_cache_bind_text (self, stmt, 1, -1, node))
+    return;
 
-  old_caps = g_hash_table_lookup (self->priv->cache, node);
+  val = g_strjoinv ("\n", caps);
 
-  if (old_caps != NULL)
+  if (!caps_cache_bind_text (self, stmt, 2, -1, val))
     {
-      /* XXX: issue warning here? */
+      g_free (val);
       return;
     }
 
-  g_hash_table_insert (
-      self->priv->cache, g_strdup (node), g_strdupv ((gchar **) caps));
+  if (!caps_cache_bind_int (self, stmt, 3, time (NULL)))
+    {
+      g_free (val);
+      return;
+    }
+
+  ret = sqlite3_step (stmt);
+
+  if (ret == SQLITE_CONSTRAINT)
+    {
+      /* Presumably the error is because the key already exists. Ignore it. */
+      sqlite3_finalize (stmt);
+      g_free (val);
+      return;
+    }
+
+  if (ret != SQLITE_DONE)
+    {
+      DEBUG ("statement execution failed: %s",
+          sqlite3_errmsg (self->priv->db));
+      sqlite3_finalize (stmt);
+      g_free (val);
+      return;
+    }
+
+  sqlite3_finalize (stmt);
+  g_free (val);
+}
+
+static gboolean
+caps_cache_count_entries (GabbleCapsCache *self, guint *count)
+{
+  gint ret;
+  sqlite3_stmt *stmt;
+
+  if (!self->priv->db)
+    return FALSE;
+
+  if (!caps_cache_prepare (self, "SELECT COUNT(*) FROM capabilities", &stmt))
+    return FALSE;
+
+  ret = sqlite3_step (stmt);
+
+  if (ret != SQLITE_ROW)
+    {
+      DEBUG ("statement execution failed: %s",
+          sqlite3_errmsg (self->priv->db));
+      sqlite3_finalize (stmt);
+      return FALSE;
+    }
+
+  *count = sqlite3_column_int (stmt, 0);
+  sqlite3_finalize (stmt);
+  return TRUE;
+}
+
+/* If the number of entries is above @high_threshold, remove entries older
+ * than @max_age while the cache is bigger than @low_threshold.
+ */
+static void
+caps_cache_gc (
+    GabbleCapsCache *self,
+    guint high_threshold,
+    guint low_threshold)
+{
+  gint ret;
+  guint count;
+  sqlite3_stmt *stmt;
+
+  if (!caps_cache_count_entries (self, &count))
+    return;
+
+  if (count <= high_threshold)
+    return;
+
+  /* This emulates DELETE ... ORDER ... LIMIT because some Sqlites (e.g.
+   * Debian) ship without SQLITE_ENABLE_UPDATE_DELETE_LIMIT unabled.
+   */
+
+  if (!caps_cache_prepare (self,
+        "DELETE FROM capabilities WHERE oid IN ("
+        "  SELECT oid FROM capabilities"
+        "    ORDER BY timestamp ASC, oid ASC"
+        "    LIMIT ?)", &stmt))
+    return;
+
+  if (!caps_cache_bind_int (self, stmt, 1, count - low_threshold))
+    return;
+
+  ret = sqlite3_step (stmt);
+
+  if (ret != SQLITE_DONE)
+    {
+      DEBUG ("statement execution failed: %s",
+          sqlite3_errmsg (self->priv->db));
+    }
+
+  sqlite3_finalize (stmt);
+  DEBUG ("cache reduced from %d to %d items",
+      count, count - sqlite3_changes (self->priv->db));
+}
+
+static guint
+get_size (void)
+{
+  static gboolean ready = FALSE;
+  static guint size = 1000;
+
+  if (G_UNLIKELY (!ready))
+    {
+      const gchar *str = g_getenv ("GABBLE_CAPS_CACHE_SIZE");
+
+      if (str != NULL)
+        {
+          /* Ignoring return code; size will retain default value on failure.
+           */
+          sscanf (str, "%u", &size);
+        }
+
+      ready = TRUE;
+      /* DEBUG ("caps cache size = %d", size); */
+    }
+
+  return size;
+}
+
+void
+gabble_caps_cache_insert (
+    GabbleCapsCache *self,
+    const gchar *node,
+    gchar **caps)
+{
+  guint size = get_size ();
+
+  if (!self->priv->db)
+    /* DB open failed. */
+    return;
+
+  DEBUG ("caps cache insert: %s", node);
+  caps_cache_insert (self, node, caps);
+
+  /* Remove old entries after every 50th insert. */
+
+  if (self->priv->inserts % 50 == 0)
+    caps_cache_gc (self, size, MAX (1, 0.95 * size));
+
+  self->priv->inserts++;
 }
 
diff --git a/src/caps-cache.h b/src/caps-cache.h
index 2afa6f6..d02e2bf 100644
--- a/src/caps-cache.h
+++ b/src/caps-cache.h
@@ -57,7 +57,7 @@ gabble_caps_cache_insert (
     gchar **caps);
 
 GabbleCapsCache *
-gabble_caps_cache_new (void);
+gabble_caps_cache_new (const gchar *path);
 
 GabbleCapsCache *
 gabble_caps_cache_dup_shared (void);
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
index 4bb8503..8aab13b 100644
--- a/tests/twisted/Makefile.am
+++ b/tests/twisted/Makefile.am
@@ -5,6 +5,7 @@ TWISTED_TESTS = \
 	caps/advertise-draft1.py \
 	caps/advertise-legacy.py \
 	caps/caps-cache.py \
+	caps/caps-persistent-cache.py \
 	caps/compat-bundles.py \
 	caps/double-disco.py \
 	caps/from-bare-jid.py \
diff --git a/tests/twisted/caps/caps-persistent-cache.py b/tests/twisted/caps/caps-persistent-cache.py
new file mode 100644
index 0000000..4750330
--- /dev/null
+++ b/tests/twisted/caps/caps-persistent-cache.py
@@ -0,0 +1,81 @@
+
+from twisted.words.xish import xpath
+
+from servicetest import (
+    assertEquals, assertContains, assertDoesNotContain, EventPattern,
+    )
+from gabbletest import make_presence, exec_test
+from caps_helper import compute_caps_hash, send_disco_reply
+import constants as cs
+import ns
+
+contact_bare_jid = 'macbeth at glamis'
+contact_jid = 'macbeth at glamis/hall'
+client = 'http://telepathy.freedesktop.org/zomg-ponies'
+features = [
+    ns.JINGLE_015,
+    ns.JINGLE_015_AUDIO,
+    ns.JINGLE_015_VIDEO,
+    ns.GOOGLE_P2P,
+    ]
+
+def connect(q, conn):
+    conn.Connect()
+    q.expect('dbus-signal', signal='StatusChanged',
+        args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+def send_presence(q, stream, contact_jid, identity):
+    ver = compute_caps_hash([identity], features, {})
+    stream.send(make_presence(contact_jid, status='Hello',
+        caps={'node': client, 'hash': 'sha-1', 'ver': ver}))
+
+def handle_disco(q, stream, contact_jid, identity):
+    # Gabble tries to resolve a caps hash.
+    ver = compute_caps_hash([identity], features, {})
+    event = q.expect('stream-iq', to=contact_jid, query_ns=ns.DISCO_INFO)
+    assertEquals(client + '#' + ver, event.query.attributes['node'])
+
+    # The bare jid replies.
+    send_disco_reply(stream, event.stanza, [identity], features)
+
+def capabilities_changed(q, contact_handle):
+    streamed_media_caps = (contact_handle, cs.CHANNEL_TYPE_STREAMED_MEDIA,
+        0, 3, 0, cs.MEDIA_CAP_AUDIO | cs.MEDIA_CAP_VIDEO)
+    e = q.expect('dbus-signal', signal='CapabilitiesChanged')
+    assertContains(streamed_media_caps, e.args[0])
+
+def test1(q, bus, conn, stream):
+    connect(q, conn)
+    contact_handle = conn.RequestHandles(cs.HT_CONTACT, [contact_bare_jid])[0]
+    send_presence(q, stream, contact_jid, 'client/pc//thane')
+    handle_disco(q, stream, contact_jid, 'client/pc//thane')
+    capabilities_changed(q, contact_handle)
+
+def test2(q, bus, conn, stream):
+    # The second time around, the capabilities are retrieved from the cache,
+    # so no disco request is sent.
+    connect(q, conn)
+    contact_handle = conn.RequestHandles(cs.HT_CONTACT, [contact_bare_jid])[0]
+    send_presence(q, stream, contact_jid, 'client/pc//thane')
+    capabilities_changed(q, contact_handle)
+
+    # Overflow the cache. 51 is the cache size (during test runs) plus one.
+
+    for i in range(51):
+        overflow_contact_jid = 'witch%d at forest/cauldron' % i
+        overflow_identity = 'client/pc//prophecy%d' % i
+        send_presence(q, stream, overflow_contact_jid, overflow_identity)
+        handle_disco(q, stream, overflow_contact_jid, overflow_identity)
+
+if __name__ == '__main__':
+    # We run test1. The capabilities for macbeth at glamis's client
+    # need to be fetched via disco and are then stored in the cache.
+    exec_test(test1)
+    # We run test2 again. The capabilities are retrieved from the cache, so no
+    # disco request is sent. Then, a bunch of other clients turn up and force
+    # the entry for Macbeth's client out of the cache.
+    exec_test(test2)
+    # We run test1 again. The caps are no longer in the cache, so a disco
+    # request is sent again.
+    exec_test(test1)
+
diff --git a/tests/twisted/tools/exec-with-log.sh.in b/tests/twisted/tools/exec-with-log.sh.in
index ad24118..543a66e 100644
--- a/tests/twisted/tools/exec-with-log.sh.in
+++ b/tests/twisted/tools/exec-with-log.sh.in
@@ -3,6 +3,7 @@
 cd "@abs_top_builddir@/tests/twisted/tools"
 
 export GABBLE_DEBUG=all LM_DEBUG=net GIBBER_DEBUG=all
+export GABBLE_CAPS_CACHE=:memory: GABBLE_CAPS_CACHE_SIZE=50
 ulimit -c unlimited
 exec >> gabble-testing.log 2>&1
 
-- 
1.5.6.5




More information about the telepathy-commits mailing list