#include "config.h" #include "caps-cache.h" #include #include #include #include #define DEBUG_FLAG GABBLE_DEBUG_PRESENCE #include "debug.h" G_DEFINE_TYPE (GabbleCapsCache, gabble_caps_cache, G_TYPE_OBJECT) #define GET_PRIVATE(o) \ (G_TYPE_INSTANCE_GET_PRIVATE ((o), GABBLE_TYPE_CAPS_CACHE, GabbleCapsCachePrivate)) static GabbleCapsCache *shared_cache = NULL; struct _GabbleCapsCachePrivate { 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); } } 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); } } static void gabble_caps_cache_dispose (GObject *object) { G_OBJECT_CLASS (gabble_caps_cache_parent_class)->dispose (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); } static void gabble_caps_cache_class_init (GabbleCapsCacheClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (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 (ret); } } /* 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 gabble_caps_cache_init (GabbleCapsCache *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE ( self, GABBLE_TYPE_CAPS_CACHE, GabbleCapsCachePrivate); } GabbleCapsCache * gabble_caps_cache_new (const gchar *path) { return g_object_new (GABBLE_TYPE_CAPS_CACHE, "path", path, NULL); } GabbleCapsCache * gabble_caps_cache_dup_shared (void) { if (shared_cache == NULL) { gchar *path; path = get_path (); shared_cache = gabble_caps_cache_new (path); g_free (path); } g_object_ref (shared_cache); return shared_cache; } void 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) { 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; } static void caps_cache_insert ( GabbleCapsCache *self, const gchar *node, gchar **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; val = g_strjoinv ("\n", caps); if (!caps_cache_bind_text (self, stmt, 2, -1, val)) { g_free (val); return; } 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++; }