aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Pärtel <martin.partel@gmail.com>2014-06-30 22:50:14 +0100
committerMartin Pärtel <martin.partel@gmail.com>2014-06-30 22:50:14 +0100
commit9bf3ec8cd48c835e02ff96cd97d1581b2f9ba5b3 (patch)
tree05e8bfc74fdb13c1724991e3ee57308c7cf4a9a7
parent3f7daee57d1e57ef522447fd601805971bb358c8 (diff)
downloadbindfs-9bf3ec8cd48c835e02ff96cd97d1581b2f9ba5b3.tar.gz
Implemented rate limiter.
Fixes #12.
-rw-r--r--.gitignore1
-rw-r--r--ChangeLog4
-rw-r--r--src/Makefile.am4
-rw-r--r--src/bindfs.124
-rw-r--r--src/bindfs.c75
-rw-r--r--src/misc.c24
-rw-r--r--src/misc.h4
-rw-r--r--src/rate_limiter.c108
-rw-r--r--src/rate_limiter.h51
-rw-r--r--tests/internals/Makefile.am14
-rw-r--r--tests/internals/test_common.c9
-rw-r--r--tests/internals/test_common.h17
-rw-r--r--tests/internals/test_internals.c14
-rw-r--r--tests/internals/test_rate_limiter.c68
-rwxr-xr-xtests/internals/test_rate_limiter_valgrind.sh3
15 files changed, 390 insertions, 30 deletions
diff --git a/.gitignore b/.gitignore
index d579346..cd45975 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,5 +37,6 @@ tests/readdir_inode
tests/utimens_nofollow
tests/*.log
tests/internals/test_internals
+tests/internals/test_rate_limiter
tests/internals/*.log
diff --git a/ChangeLog b/ChangeLog
index f57b6be..ab2c6a9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2014-06-30 Martin Pärtel <martin dot partel at gmail dot com>
+
+ * Implemented rate limiter, an idea by @illuusio (#12).
+
2014-06-11 Martin Pärtel <martin dot partel at gmail dot com>
* Merged patch to fix GID cache by @alajovic. Thanks! This fixes a problem
diff --git a/src/Makefile.am b/src/Makefile.am
index 60cdbf0..c049cc3 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -2,8 +2,8 @@
bin_PROGRAMS = bindfs
-noinst_HEADERS = debug.h permchain.h userinfo.h misc.h usermap.h
-bindfs_SOURCES = bindfs.c debug.c permchain.c userinfo.c misc.c usermap.c
+noinst_HEADERS = debug.h permchain.h userinfo.h misc.h usermap.h rate_limiter.h
+bindfs_SOURCES = bindfs.c debug.c permchain.c userinfo.c misc.c usermap.c rate_limiter.c
AM_CPPFLAGS = ${my_CPPFLAGS} ${fuse_CFLAGS}
AM_CFLAGS = ${my_CFLAGS}
diff --git a/src/bindfs.1 b/src/bindfs.1
index b183a78..8ed5d2f 100644
--- a/src/bindfs.1
+++ b/src/bindfs.1
@@ -204,6 +204,23 @@ The read/write permissions are checked against the (possibly modified)
file permissions inside the mount.
+.SH RATE LIMITS
+Reads and writes through the mount point can be throttled. Throttling works
+by sleeping the required amount of time on each read or write request.
+Throttling imposes one global limit on all readers/writers as opposed to a
+per-process or per-user limit.
+
+Currently, the implementation is not entirely fair. See \fB\%BUGS\fP below.
+
+.TP
+.B \-\-read\-rate=\fIN\fP, \-o read\-rate=\fIN\fP
+Allow at most \fIN\fP bytes per second to be read. \fIN\fP may have one of the
+following (1024-based) suffixes: \fBk\fP, \fBM\fP, \fBG\fP, \fBT\fP.
+
+.TP
+.B \-\-write\-rate=\fIN\fP, \-o write\-rate=\fIN\fP
+Same as above, but for writes.
+
.SH MISCELLANEOUS OPTIONS
.TP
@@ -375,6 +392,13 @@ This may constitute a security risk if you rely on bindfs to reduce
permissions on new files. For this reason, as of version 1.11
bindfs runs in single-threaded mode by default.
+Rate limiting favors the process with the larger block size.
+If two processes compete for read/write access, the one whose read()/write()
+calls specify the larger block size gets to read/write faster.
+The total rate limit is maintained though, and clients with equal block sizes
+and a similar rate of requests are treated fairly as long as the kernel orders
+their requests fairly.
+
Please report bugs and/or send pull requests to
\fBhttps://github.com/mpartel/bindfs/issues\fP.
diff --git a/src/bindfs.c b/src/bindfs.c
index e94402d..1ca24ed 100644
--- a/src/bindfs.c
+++ b/src/bindfs.c
@@ -68,10 +68,11 @@
#include <fuse_opt.h>
#include "debug.h"
+#include "misc.h"
#include "permchain.h"
+#include "rate_limiter.h"
#include "userinfo.h"
#include "usermap.h"
-#include "misc.h"
/* SETTINGS */
static struct Settings {
@@ -91,6 +92,9 @@ static struct Settings {
UserMap* usermap; /* From the --map option. */
UserMap* usermap_reverse;
+ RateLimiter* read_limiter;
+ RateLimiter* write_limiter;
+
enum CreatePolicy {
CREATE_AS_USER,
CREATE_AS_MOUNTER
@@ -766,6 +770,10 @@ static int bindfs_read(const char *path, char *buf, size_t size, off_t offset,
int res;
(void) path;
+ if (settings.read_limiter) {
+ rate_limiter_wait(settings.read_limiter, size);
+ }
+
res = pread(fi->fh, buf, size, offset);
if (res == -1)
res = -errno;
@@ -779,6 +787,10 @@ static int bindfs_write(const char *path, const char *buf, size_t size,
int res;
(void) path;
+ if (settings.write_limiter) {
+ rate_limiter_wait(settings.write_limiter, size);
+ }
+
res = pwrite(fi->fh, buf, size, offset);
if (res == -1)
res = -errno;
@@ -956,24 +968,24 @@ static void print_usage(const char *progname)
" -V --version Print version number and exit.\n"
"\n"
"File ownership:\n"
- " -u --force-user Set file owner.\n"
- " -g --force-group Set file group.\n"
- " -m --mirror Comma-separated list of users who will see\n"
+ " -u --force-user=... Set file owner.\n"
+ " -g --force-group=... Set file group.\n"
+ " -m --mirror=... Comma-separated list of users who will see\n"
" themselves as the owners of all files.\n"
- " -M --mirror-only Like --mirror but disallow access for\n"
+ " -M --mirror-only=... Like --mirror but disallow access for\n"
" all other users.\n"
" --map=user1/user2:... Let user2 see files of user1 as his own.\n"
"\n"
"Permission bits:\n"
- " -p --perms Specify permissions, similar to chmod\n"
+ " -p --perms=... Specify permissions, similar to chmod\n"
" e.g. og-x,og+rD,u=rwX,g+rw or 0644,a+X\n"
"\n"
"File creation policy:\n"
" --create-as-user New files owned by creator (default for root). *\n"
" --create-as-mounter New files owned by fs mounter (default for users).\n"
- " --create-for-user New files owned by specified user. *\n"
- " --create-for-group New files owned by specified group. *\n"
- " --create-with-perms Alter permissions of new files.\n"
+ " --create-for-user=... New files owned by specified user. *\n"
+ " --create-for-group=... New files owned by specified group. *\n"
+ " --create-with-perms=... Alter permissions of new files.\n"
"\n"
"Chown policy:\n"
" --chown-normal Try to chown the original files (the default).\n"
@@ -989,7 +1001,7 @@ static void print_usage(const char *progname)
" --chmod-normal Try to chmod the original files (the default).\n"
" --chmod-ignore Have all chmods fail silently.\n"
" --chmod-deny Have all chmods fail with 'permission denied'.\n"
- " --chmod-filter Change permissions of chmod requests.\n"
+ " --chmod-filter=... Change permissions of chmod requests.\n"
" --chmod-allow-x Allow changing file execute bits in any case.\n"
"\n"
"Extended attribute policy:\n"
@@ -997,6 +1009,10 @@ static void print_usage(const char *progname)
" --xattr-ro Read-only xattr operations.\n"
" --xattr-rw Read-write xattr operations (the default).\n"
"\n"
+ "Rate limits:\n"
+ " --read-rate=... Limit to bytes/sec that can be read.\n"
+ " --write-rate=... Limit to bytes/sec that can be written.\n"
+ "\n"
"Miscellaneous:\n"
" -n --no-allow-other Do not add -o allow_other to fuse options.\n"
" --realistic-permissions Hide permission bits for actions mounter can't do.\n"
@@ -1331,6 +1347,16 @@ static void atexit_func()
{
free(settings.original_working_dir);
settings.original_working_dir = NULL;
+ if (settings.read_limiter) {
+ rate_limiter_destroy(settings.read_limiter);
+ free(settings.read_limiter);
+ settings.read_limiter = NULL;
+ }
+ if (settings.write_limiter) {
+ rate_limiter_destroy(settings.write_limiter);
+ free(settings.write_limiter);
+ settings.write_limiter = NULL;
+ }
usermap_destroy(settings.usermap);
settings.usermap = NULL;
usermap_destroy(settings.usermap_reverse);
@@ -1361,6 +1387,8 @@ int main(int argc, char *argv[])
char *mirror;
char *mirror_only;
char *map;
+ char *read_rate;
+ char *write_rate;
char *create_for_user;
char *create_for_group;
char *create_with_perms;
@@ -1395,6 +1423,9 @@ int main(int argc, char *argv[])
OPT_OFFSET2("--map=%s", "map=%s", map, -1),
OPT_OFFSET3("-n", "--no-allow-other", "no-allow-other", no_allow_other, -1),
+ OPT_OFFSET2("--read-rate=%s", "read-rate=%s", read_rate, -1),
+ OPT_OFFSET2("--write-rate=%s", "write-rate=%s", write_rate, -1),
+
OPT2("--create-as-user", "create-as-user", OPTKEY_CREATE_AS_USER),
OPT2("--create-as-mounter", "create-as-mounter", OPTKEY_CREATE_AS_MOUNTER),
OPT_OFFSET2("--create-for-user=%s", "create-for-user=%s", create_for_user, -1),
@@ -1435,6 +1466,8 @@ int main(int argc, char *argv[])
settings.permchain = permchain_create();
settings.usermap = usermap_create();
settings.usermap_reverse = usermap_create();
+ settings.read_limiter = NULL;
+ settings.write_limiter = NULL;
settings.new_uid = -1;
settings.new_gid = -1;
settings.create_for_uid = -1;
@@ -1500,6 +1533,28 @@ int main(int argc, char *argv[])
}
}
+ /* Parse rate limits */
+ if (od.read_rate) {
+ double rate;
+ if (parse_byte_count(od.read_rate, &rate) && rate > 0) {
+ settings.read_limiter = malloc(sizeof(RateLimiter));
+ rate_limiter_init(settings.read_limiter, rate, &gettimeofday_clock);
+ } else {
+ fprintf(stderr, "Error: Invalid --read-rate.\n");
+ return 1;
+ }
+ }
+ if (od.write_rate) {
+ double rate;
+ if (parse_byte_count(od.write_rate, &rate) && rate > 0) {
+ settings.write_limiter = malloc(sizeof(RateLimiter));
+ rate_limiter_init(settings.write_limiter, rate, &gettimeofday_clock);
+ } else {
+ fprintf(stderr, "Error: Invalid --write-rate.\n");
+ return 1;
+ }
+ }
+
/* Parse usermap */
if (od.map) {
if (getuid() != 0) {
diff --git a/src/misc.c b/src/misc.c
index 799f555..4442127 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -108,7 +108,7 @@ const char *my_dirname(char *path)
}
}
-void grow_array_impl(void **array, int* capacity, int member_size)
+void grow_array_impl(void **array, int *capacity, int member_size)
{
int new_cap = *capacity;
if (new_cap == 0) {
@@ -121,6 +121,28 @@ void grow_array_impl(void **array, int* capacity, int member_size)
*capacity = new_cap;
}
+int parse_byte_count(const char *str, double *result)
+{
+ char* end;
+ double base = strtod(str, &end);
+ long mul = 1;
+ if (*end == '\0') {
+ mul = 1L;
+ } else if (strcmp(end, "k") == 0) {
+ mul = 1024L;
+ } else if (strcmp(end, "M") == 0) {
+ mul = 1024L * 1024L;
+ } else if (strcmp(end, "G") == 0) {
+ mul = 1024L * 1024L * 1024L;
+ } else if (strcmp(end, "T") == 0) {
+ mul = 1024L * 1024L * 1024L * 1024L;
+ } else {
+ return 0;
+ }
+ *result = base * mul;
+ return 1;
+}
+
void init_arena(struct arena *a, int initial_capacity)
{
a->size = 0;
diff --git a/src/misc.h b/src/misc.h
index 8b00f8d..dc654d6 100644
--- a/src/misc.h
+++ b/src/misc.h
@@ -50,8 +50,10 @@ const char *my_dirname(char *path);
than `*capacity` (may be 0) and stores the new capacity
in `*capacity`. */
#define grow_array(array, capacity, member_size) grow_array_impl((void**)(array), (capacity), (member_size))
-void grow_array_impl(void **array, int* capacity, int member_size);
+void grow_array_impl(void **array, int *capacity, int member_size);
+/* Returns 1 on success, 0 on syntax error. */
+int parse_byte_count(const char *str, double *result);
/* Simple arena allocation for when it's convenient to
grow multiple times and deallocate all at once. */
diff --git a/src/rate_limiter.c b/src/rate_limiter.c
new file mode 100644
index 0000000..d1c701a
--- /dev/null
+++ b/src/rate_limiter.c
@@ -0,0 +1,108 @@
+/*
+ Copyright 2014 Martin Pärtel <martin.partel@gmail.com>
+
+ This file is part of bindfs.
+
+ bindfs is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ bindfs 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with bindfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#define _POSIX_C_SOURCE 199309L /* for nanosleep() */
+
+#include "rate_limiter.h"
+#include <assert.h>
+#include <errno.h>
+#include <pthread.h>
+#include <time.h>
+#include <sys/time.h>
+
+const double rate_limiter_idle_credit = -0.2;
+
+double gettimeofday_clock()
+{
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+ return tv.tv_sec + tv.tv_usec * 0.000001;
+}
+
+static void sleep_seconds(double s)
+{
+ struct timespec ts;
+ ts.tv_sec = (time_t)s;
+ ts.tv_nsec = (long)((s - ts.tv_sec) * 1000000000L);
+ /* Guard against float imprecision */
+ if (ts.tv_nsec > 999999999L) {
+ ts.tv_nsec = 999999999L;
+ } else if (ts.tv_nsec < 0L) {
+ ts.tv_nsec = 0L;
+ }
+
+ nanosleep(&ts, NULL);
+}
+
+void rate_limiter_init(RateLimiter *limiter, double rate, double (*clock)())
+{
+ limiter->rate = rate;
+ limiter->clock = clock;
+ limiter->last_modified = limiter->clock();
+ limiter->accumulated_sleep_time = rate_limiter_idle_credit;
+
+ pthread_mutexattr_t attr;
+ int status = pthread_mutexattr_init(&attr);
+ assert(status == 0);
+ status = pthread_mutex_init(&limiter->mutex, &attr);
+ assert(status == 0);
+ status = pthread_mutexattr_destroy(&attr);
+ assert(status == 0);
+}
+
+void rate_limiter_wait(RateLimiter* limiter, size_t size)
+{
+ double time_to_sleep = rate_limiter_wait_nosleep(limiter, size);
+ sleep_seconds(time_to_sleep);
+}
+
+double rate_limiter_wait_nosleep(RateLimiter* limiter, size_t size)
+{
+ int status = pthread_mutex_lock(&limiter->mutex);
+ assert(status == 0);
+
+ double time_to_add = size / limiter->rate;
+
+ double now = limiter->clock();
+ double elapsed = now - limiter->last_modified;
+ if (elapsed < 0) {
+ elapsed = 0;
+ }
+
+ double time_to_sleep = limiter->accumulated_sleep_time;
+ time_to_sleep -= elapsed;
+ if (time_to_sleep < rate_limiter_idle_credit) {
+ time_to_sleep = rate_limiter_idle_credit;
+ }
+ time_to_sleep += time_to_add;
+ limiter->accumulated_sleep_time = time_to_sleep;
+
+ limiter->last_modified = now;
+
+ status = pthread_mutex_unlock(&limiter->mutex);
+ assert(status == 0);
+
+ return time_to_sleep;
+}
+
+void rate_limiter_destroy(RateLimiter *limiter)
+{
+ int status = pthread_mutex_destroy(&limiter->mutex);
+ assert(status == 0);
+}
diff --git a/src/rate_limiter.h b/src/rate_limiter.h
new file mode 100644
index 0000000..a332294
--- /dev/null
+++ b/src/rate_limiter.h
@@ -0,0 +1,51 @@
+/*
+ Copyright 2014 Martin Pärtel <martin.partel@gmail.com>
+
+ This file is part of bindfs.
+
+ bindfs is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ bindfs 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with bindfs. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef INC_BINDFS_RATE_LIMITER_H
+#define INC_BINDFS_RATE_LIMITER_H
+
+#include <string.h>
+#include <pthread.h>
+
+/* When we are idle, we allow some time to be "credited" to the next writer.
+ * Otherwise, the short pause between requests would "go to waste", lowering
+ * the throughput when there is only one requester. */
+const double rate_limiter_idle_credit;
+
+typedef struct RateLimiter {
+ double rate; /* bytes / second */
+ double (*clock)();
+ double last_modified;
+ double accumulated_sleep_time;
+ pthread_mutex_t mutex;
+} RateLimiter;
+
+double gettimeofday_clock();
+
+/* 0 on success, error number on error. */
+void rate_limiter_init(RateLimiter* limiter, double rate, double (*clock)());
+/* Blocks until the rate limiter clears `size` units. */
+void rate_limiter_wait(RateLimiter* limiter, size_t size);
+/* Updates the rate limiter like `rate_limiter_wait` but does not actually
+ * sleep. Returns the time that the caller is expected to sleep. */
+double rate_limiter_wait_nosleep(RateLimiter* limiter, size_t size);
+/* Destroys the rate limiter. No wait_for_permit calls may be active. */
+void rate_limiter_destroy(RateLimiter* limiter);
+
+#endif
diff --git a/tests/internals/Makefile.am b/tests/internals/Makefile.am
index c3116e6..3006399 100644
--- a/tests/internals/Makefile.am
+++ b/tests/internals/Makefile.am
@@ -1,9 +1,15 @@
-noinst_PROGRAMS = test_internals
-test_internals_SOURCES = test_internals.c $(top_builddir)/src/misc.c
+noinst_HEADERS = test_common.h
+noinst_PROGRAMS = test_internals test_rate_limiter
+test_internals_SOURCES = test_internals.c test_common.c $(top_builddir)/src/misc.c
+test_rate_limiter_SOURCES = test_rate_limiter.c test_common.c $(top_builddir)/src/rate_limiter.c
-test_internals_CPPFLAGS = ${my_CPPFLAGS} ${fuse_CFLAGS} -I$(top_builddir)/src
+test_internals_CPPFLAGS = ${my_CPPFLAGS} ${fuse_CFLAGS} -I. -I$(top_builddir)/src
test_internals_CFLAGS = ${my_CFLAGS}
test_internals_LDADD = ${my_LDFLAGS}
-TESTS = test_internals_valgrind.sh
+test_rate_limiter_CPPFLAGS = ${my_CPPFLAGS} ${fuse_CFLAGS} -I. -I$(top_builddir)/src
+test_rate_limiter_CFLAGS = ${my_CFLAGS}
+test_rate_limiter_LDADD = ${my_LDFLAGS}
+
+TESTS = test_internals_valgrind.sh test_rate_limiter_valgrind.sh
diff --git a/tests/internals/test_common.c b/tests/internals/test_common.c
new file mode 100644
index 0000000..6c10292
--- /dev/null
+++ b/tests/internals/test_common.c
@@ -0,0 +1,9 @@
+
+#include "test_common.h"
+
+int failures = 0;
+
+int run_suite(void (*suite)()) {
+ suite();
+ return (failures > 0) ? 1 : 0;
+}
diff --git a/tests/internals/test_common.h b/tests/internals/test_common.h
new file mode 100644
index 0000000..160d9b9
--- /dev/null
+++ b/tests/internals/test_common.h
@@ -0,0 +1,17 @@
+
+#ifndef INC_BINDFS_TEST_COMMON_H
+#define INC_BINDFS_TEST_COMMON_H
+
+#include <stdio.h>
+#include <math.h>
+
+extern int failures;
+
+#define TEST_ASSERT(expr) do { if (!(expr)) { printf("Assertion failed: %s:%d: `%s'\n", __FILE__, __LINE__, #expr); failures++; } } while (0);
+#define NEAR(a, b, eps) (fabs((a) - (b)) < (eps))
+
+int run_suite(void (*suite)());
+
+#define TEST_MAIN(suite) int main() { return run_suite(suite); }
+
+#endif
diff --git a/tests/internals/test_internals.c b/tests/internals/test_internals.c
index cf7a18b..aba4045 100644
--- a/tests/internals/test_internals.c
+++ b/tests/internals/test_internals.c
@@ -1,13 +1,9 @@
+#include "test_common.h"
#include "misc.h"
#include <string.h>
-#include <stdio.h>
#include <stdlib.h>
-int failures = 0;
-
-#define TEST_ASSERT(expr) do { if (!(expr)) { printf("Assertion failed: `%s'\n", #expr); failures++; } } while (0);
-
void test_my_dirname(char *arg, const char *expected)
{
char *orig = strdup(arg);
@@ -56,10 +52,4 @@ void my_dirname_suite()
test_my_dirname(buf, "..");
}
-int main()
-{
- my_dirname_suite();
-
- return (failures > 0) ? 1 : 0;
-}
-
+TEST_MAIN(my_dirname_suite);
diff --git a/tests/internals/test_rate_limiter.c b/tests/internals/test_rate_limiter.c
new file mode 100644
index 0000000..93cdda5
--- /dev/null
+++ b/tests/internals/test_rate_limiter.c
@@ -0,0 +1,68 @@
+
+#include "test_common.h"
+#include "rate_limiter.h"
+
+static const double epsilon = 0.000000000001;
+
+static volatile double time_now = 123123.0;
+
+double test_clock()
+{
+ return time_now;
+}
+
+void computes_correct_sleep_times()
+{
+ time_now = 123123.0;
+ RateLimiter limiter;
+ rate_limiter_init(&limiter, 10, &test_clock);
+
+ double sleep_time = rate_limiter_wait_nosleep(&limiter, 30);
+ TEST_ASSERT(NEAR(3.0 + rate_limiter_idle_credit, sleep_time, epsilon));
+ sleep_time = rate_limiter_wait_nosleep(&limiter, 20);
+ TEST_ASSERT(NEAR(5.0 + rate_limiter_idle_credit, sleep_time, epsilon));
+
+ time_now += 0.5;
+ sleep_time = rate_limiter_wait_nosleep(&limiter, 30);
+ TEST_ASSERT(NEAR(7.5 + rate_limiter_idle_credit, sleep_time, epsilon));
+
+ rate_limiter_destroy(&limiter);
+}
+
+void works_after_being_idle()
+{
+ time_now = 123123.0;
+ RateLimiter limiter;
+ rate_limiter_init(&limiter, 10, &test_clock);
+
+ double sleep_time = rate_limiter_wait_nosleep(&limiter, 30);
+ TEST_ASSERT(NEAR(3.0 + rate_limiter_idle_credit, sleep_time, epsilon));
+ time_now += 100;
+
+ sleep_time = rate_limiter_wait_nosleep(&limiter, 20);
+ TEST_ASSERT(NEAR(2 + rate_limiter_idle_credit, sleep_time, epsilon));
+
+ rate_limiter_destroy(&limiter);
+}
+
+void sleeps_correct_amount()
+{
+ RateLimiter limiter;
+ rate_limiter_init(&limiter, 10, &gettimeofday_clock);
+
+ double start = gettimeofday_clock();
+ rate_limiter_wait(&limiter, 5);
+ double elapsed = gettimeofday_clock() - start;
+ TEST_ASSERT(NEAR(0.5, elapsed, 0.2));
+
+ rate_limiter_destroy(&limiter);
+}
+
+void rate_limiter_suite()
+{
+ computes_correct_sleep_times();
+ works_after_being_idle();
+ sleeps_correct_amount();
+}
+
+TEST_MAIN(rate_limiter_suite);
diff --git a/tests/internals/test_rate_limiter_valgrind.sh b/tests/internals/test_rate_limiter_valgrind.sh
new file mode 100755
index 0000000..e291360
--- /dev/null
+++ b/tests/internals/test_rate_limiter_valgrind.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd `dirname "$0"`
+valgrind --error-exitcode=100 ./test_rate_limiter