[folks] core: Rate-limit AvatarCache.store_avatar() to prevent FD exhaustion
- From: Philip Withnall <pwithnall src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [folks] core: Rate-limit AvatarCache.store_avatar() to prevent FD exhaustion
- Date: Mon, 7 Apr 2014 20:02:33 +0000 (UTC)
commit 2001d7adaff969a7c900dce613a00d4668cae01c
Author: Philip Withnall <philip tecnocode co uk>
Date: Sun Apr 6 11:10:16 2014 +0100
core: Rate-limit AvatarCache.store_avatar() to prevent FD exhaustion
If connecting to a new EDS address book, or doing something else which
may cause a torrent of AvatarCache.store_avatar() calls, it is possible
to hit the operating system’s file descriptor limit, which causes
further store operations to fail.
Implement a simple FIFO rate limit on concurrent
AvatarCache.store_avatar() calls to try and ensure this won’t happen. It
is still possible if the process calls store_avatar() several times
while already near the FD limit, but then failure is pretty inevitable
anyway, and is already handled gracefully.
The changes are implemented in a self-contained wrapper function around
store_avatar(), and only result in memory allocations in the queueing
case.
A test case is included. This does not affect the AvatarCache API or
ABI.
https://bugzilla.gnome.org/show_bug.cgi?id=705742
NEWS | 1 +
folks/avatar-cache.vala | 58 +++++++++++++++++++++++++++++++++++++++++
tests/folks/avatar-cache.vala | 51 ++++++++++++++++++++++++++++++++++++
3 files changed, 110 insertions(+), 0 deletions(-)
---
diff --git a/NEWS b/NEWS
index d1cf948..951f666 100644
--- a/NEWS
+++ b/NEWS
@@ -21,6 +21,7 @@ Bugs fixed:
• Bug 727405 — setting FOLKS_DISABLE_LINKING to on does not work
• Bug 722579 — Contacts are displayed for Google accounts where contact
management is disabled in GOA
+ • Bug 705742 — Implement rate limiting in AvatarCache.store_avatar()
API changes:
• Add Individual.display_name
diff --git a/folks/avatar-cache.vala b/folks/avatar-cache.vala
index 78ff67b..eee4e81 100644
--- a/folks/avatar-cache.vala
+++ b/folks/avatar-cache.vala
@@ -35,12 +35,22 @@ using GLib;
* same namespace, so callers must ensure that the IDs they use for avatars are
* globally unique (e.g. by using the corresponding { link Folks.Persona.uid}).
*
+ * Ongoing store operations ({ link Folks.AvatarCache.store_avatar}) are rate
+ * limited to try and prevent file descriptor exhaustion. Load operations
+ * ({ link Folks.AvatarCache.load_avatar}) must be rate limited by the client,
+ * as the file I/O occurs when calling { link LoadableIcon.load} rather than
+ * when retrieving the { link LoadableIcon} from the cache.
+ *
* @since 0.6.0
*/
public class Folks.AvatarCache : Object
{
private static weak AvatarCache? _instance = null; /* needs to be locked */
private File _cache_directory;
+ private uint _n_ongoing_stores = 0;
+ private Queue<DelegateWrapper> _pending_stores =
+ new Queue<DelegateWrapper> ();
+ private const uint _max_n_ongoing_stores = 10;
/**
* Private constructor for an instance of the avatar cache. The singleton
@@ -104,6 +114,9 @@ public class Folks.AvatarCache : Object
/**
* Fetch an avatar from the cache by its globally unique ID.
*
+ * It is up to the caller to ensure that file I/O is rate-limited when loading
+ * many avatars in parallel, by limiting calls to { link LoadableIcon.load}.
+ *
* @param id the globally unique ID for the avatar
* @return Avatar from the cache, or ``null`` if it doesn't exist in the cache
* @throws GLib.Error if checking for existence of the cache file failed
@@ -134,6 +147,9 @@ public class Folks.AvatarCache : Object
* ID (e.g. an asynchronous call may be made, and a subsequent asynchronous
* call may begin before the first has finished).
*
+ * Concurrent file I/O may be rate limited within each { link AvatarCache}
+ * instance to avoid file descriptor exhaustion.
+ *
* @param id the globally unique ID for the avatar
* @param avatar the avatar data to cache
* @return a URI for the file storing the cached avatar
@@ -144,6 +160,41 @@ public class Folks.AvatarCache : Object
public async string store_avatar (string id, LoadableIcon avatar)
throws GLib.Error
{
+ string avatar_uri = "";
+
+ if (this._n_ongoing_stores > AvatarCache._max_n_ongoing_stores)
+ {
+ /* Add to the pending queue. */
+ var wrapper = new DelegateWrapper ();
+ wrapper.cb = store_avatar.callback;
+ this._pending_stores.push_tail ((owned) wrapper);
+ yield;
+ }
+
+ /* Do the actual store operation. */
+ try
+ {
+ this._n_ongoing_stores++;
+ avatar_uri = yield this._store_avatar_unlimited (id, avatar);
+ }
+ finally
+ {
+ this._n_ongoing_stores--;
+
+ /* If there is a store operation pending, resume it, FIFO-style. */
+ var wrapper = this._pending_stores.pop_head ();
+ if (wrapper != null)
+ {
+ wrapper.cb ();
+ }
+ }
+
+ return avatar_uri;
+ }
+
+ private async string _store_avatar_unlimited (string id, LoadableIcon avatar)
+ throws GLib.Error
+ {
var dest_avatar_file = this._get_avatar_file (id);
debug ("Storing avatar '%s' in file '%s'.", id,
@@ -264,3 +315,10 @@ public class Folks.AvatarCache : Object
}
}
}
+
+/* See: https://mail.gnome.org/archives/vala-list/2011-June/msg00005.html */
+[Compact]
+private class DelegateWrapper
+{
+ public SourceFunc cb;
+}
diff --git a/tests/folks/avatar-cache.vala b/tests/folks/avatar-cache.vala
index c0f4808..d25245c 100644
--- a/tests/folks/avatar-cache.vala
+++ b/tests/folks/avatar-cache.vala
@@ -43,6 +43,7 @@ public class AvatarCacheTests : Folks.TestCase
this.add_test ("store-and-load-avatar", this.test_store_and_load_avatar);
this.add_test ("store-avatar-overwrite",
this.test_store_avatar_overwrite);
+ this.add_test ("store-many-avatars", this.test_store_many_avatars);
this.add_test ("load-avatar-non-existent",
this.test_load_avatar_non_existent);
this.add_test ("remove-avatar", this.test_remove_avatar);
@@ -216,6 +217,56 @@ public class AvatarCacheTests : Folks.TestCase
this._assert_avatars_equal (this._avatar, avatar);
}
+ public void test_store_many_avatars ()
+ {
+ /* Test storing hundreds of avatars in parallel. This should *not* cause
+ * the process to run out of FDs. */
+ const uint n_avatars = 1000;
+ uint n_remaining = n_avatars;
+
+ for (uint i = 0; i < n_avatars; i++)
+ {
+ var id = "test-store-many-avatars-%u".printf (i);
+
+ this._cache.store_avatar.begin (id, this._avatar, (obj, res) =>
+ {
+ try
+ {
+ this._cache.store_avatar.end (res);
+ n_remaining--;
+ }
+ catch (GLib.Error e)
+ {
+ error ("Error storing avatar %u: %s", i, e.message);
+ }
+ });
+ }
+
+ /* Wait for all the operations to finish. The idle callback should always
+ * be queued behind all the I/O operations. */
+ while (n_remaining > 0)
+ {
+ Idle.add (() =>
+ {
+ this._main_loop.quit ();
+ return false;
+ });
+
+ this._main_loop.run ();
+ }
+
+ /* Load the avatars again and check they're all OK. */
+ for (uint i = 0; i < n_avatars; i++)
+ {
+ var id = "test-store-many-avatars-%u".printf (i);
+ var avatar = this._assert_load_avatar (id);
+
+ assert (avatar != null);
+ assert (avatar is LoadableIcon);
+ this._assert_avatars_equal (this._avatar, avatar);
+ }
+ }
+
public void test_load_avatar_non_existent ()
{
// Load a non-existent avatar.
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]