]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib: cpu-limit - Redesign the API
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Mon, 15 Mar 2021 16:18:07 +0000 (18:18 +0200)
committertimo.sirainen <timo.sirainen@open-xchange.com>
Tue, 16 Mar 2021 10:04:53 +0000 (10:04 +0000)
The new API no longer has a signal callback, just a cpu_limit_exceeded()
function that needs to be periodically called to check if the limit has
exceeded. The callback could be added back if really necessary, but it's
just too easy to use signal handlers unsafely.

The new API also supports separating limits for user and system CPU usage.
It also attempts to guard against any unexpected kernel behavior resulting
from unclear behavior on how exactly the getrlimit(), setrlimit() and XCPU
signals interact.

src/lib/cpu-limit.c
src/lib/cpu-limit.h
src/lib/test-cpu-limit.c

index 2ad35a23572c432168ff42a5d017bd8646004cda..754717aed58ffc265946b7e3ab99894056698130 100644 (file)
@@ -8,34 +8,26 @@
 #include <sys/time.h>
 #include <sys/resource.h>
 
-static struct cpu_limit *volatile cpu_limit = NULL;
-
 struct cpu_limit {
        struct cpu_limit *parent;
 
+       enum cpu_limit_type type;
+       unsigned int cpu_limit_secs;
        struct rusage initial_usage;
-       struct rlimit old_limit, limit;
 
-       void (*callback)(void *context);
-       void *context;
+       bool limit_reached;
 };
 
+static struct cpu_limit *cpu_limit;
+static struct rlimit orig_limit, last_set_rlimit;
+static volatile sig_atomic_t xcpu_signal_counter;
+static sig_atomic_t checked_signal_counter;
+static unsigned int rlim_cur_adjust_secs;
+
 static void
 cpu_limit_handler(const siginfo_t *si ATTR_UNUSED, void *context ATTR_UNUSED)
 {
-       struct cpu_limit *climit = cpu_limit;
-
-       while (climit != NULL) {
-               if (climit->callback != NULL)
-                       climit->callback(climit->context);
-               climit->callback = NULL;
-               climit->context = NULL;
-
-               if (climit->parent == NULL ||
-                   climit->limit.rlim_cur < climit->parent->limit.rlim_cur)
-                       break;
-               climit = climit->parent;
-       }
+       xcpu_signal_counter++;
 }
 
 static unsigned int
@@ -74,27 +66,79 @@ cpu_limit_get_usage_msecs(struct cpu_limit *climit, enum cpu_limit_type type)
        return cpu_limit_get_usage_msecs_with(climit, type, &rusage);
 }
 
-#undef cpu_limit_init
+static bool
+cpu_limit_update_recursive(struct cpu_limit *climit,
+                          const struct rusage *rusage,
+                          unsigned int *max_wait_secs)
+{
+       if (climit == NULL)
+               return FALSE;
+       if (cpu_limit_update_recursive(climit->parent, rusage, max_wait_secs)) {
+               /* parent's limit reached */
+               climit->limit_reached = TRUE;
+               return TRUE;
+       }
+       unsigned int secs_used =
+               cpu_limit_get_usage_msecs_with(climit, climit->type, rusage)/1000;
+       if (secs_used >= climit->cpu_limit_secs) {
+               climit->limit_reached = TRUE;
+               return TRUE;
+       }
+       unsigned int secs_left = climit->cpu_limit_secs - secs_used;
+       if (*max_wait_secs > secs_left)
+               *max_wait_secs = secs_left;
+       return FALSE;
+}
+
+static void cpu_limit_update_rlimit(void)
+{
+       struct rusage rusage;
+       struct rlimit rlimit;
+       unsigned int max_wait_secs = UINT_MAX;
+
+       if (getrusage(RUSAGE_SELF, &rusage) < 0)
+               i_fatal("getrusage() failed: %m");
+
+       (void)cpu_limit_update_recursive(cpu_limit, &rusage, &max_wait_secs);
+       if (max_wait_secs == UINT_MAX) {
+               /* All the limits have reached now. Restore the original
+                  limits. */
+               rlimit = orig_limit;
+       } else {
+               struct timeval tv_limit = rusage.ru_utime;
+               timeval_add(&tv_limit, &rusage.ru_stime);
+
+               i_zero(&rlimit);
+               /* Add +1 second to round up. */
+               rlimit.rlim_cur = tv_limit.tv_sec +
+                       max_wait_secs + 1 + rlim_cur_adjust_secs;
+               rlimit.rlim_max = orig_limit.rlim_max;
+       }
+       if (last_set_rlimit.rlim_cur != rlimit.rlim_cur) {
+               last_set_rlimit = rlimit;
+               if (setrlimit(RLIMIT_CPU, &rlimit) < 0)
+                       i_fatal("setrlimit() failed: %m");
+       }
+}
+
 struct cpu_limit *
-cpu_limit_init(unsigned int cpu_limit_sec,
-              void (*limit_callback)(void *context), void *context)
+cpu_limit_init(unsigned int cpu_limit_secs, enum cpu_limit_type type)
 {
        struct cpu_limit *climit;
        struct rusage rusage;
 
-       i_assert(cpu_limit_sec > 0);
+       i_assert(cpu_limit_secs > 0);
+       i_assert(type != 0);
 
        climit = i_new(struct cpu_limit, 1);
        climit->parent = cpu_limit;
-       climit->callback = limit_callback;
-       climit->context = context;
+       climit->type = type;
+       climit->cpu_limit_secs = cpu_limit_secs;
 
        /* Query current limit */
        if (climit->parent == NULL) {
-               if (getrlimit(RLIMIT_CPU, &climit->old_limit) < 0)
+               if (getrlimit(RLIMIT_CPU, &orig_limit) < 0)
                        i_fatal("getrlimit() failed: %m");
-       } else {
-               climit->old_limit = climit->parent->limit;
        }
 
        /* Query cpu usage so far */
@@ -102,48 +146,13 @@ cpu_limit_init(unsigned int cpu_limit_sec,
                i_fatal("getrusage() failed: %m");
        climit->initial_usage = rusage;
 
-       climit->limit = climit->old_limit;
-       /* rlimit is in seconds. Truncate initial_usage to seconds for the
-          initial sanity check. */
-       struct timeval initial_total = climit->initial_usage.ru_utime;
-       timeval_add(&initial_total, &climit->initial_usage.ru_stime);
-       climit->limit.rlim_cur = initial_total.tv_sec;
-
-       if (climit->limit.rlim_max != RLIM_INFINITY &&
-           climit->limit.rlim_cur > climit->limit.rlim_max) {
-               i_fatal("CPU resource limit already exceeded (%ld > %ld)",
-                       (long)climit->limit.rlim_cur,
-                       (long)climit->limit.rlim_max);
-       }
-       climit->limit.rlim_cur += cpu_limit_sec;
-       /* Add extra 1 second to the limit to try to avoid the limit from
-          triggering too early (although it still can trigger a few ms too
-          early). */
-       climit->limit.rlim_cur++;
-
        if (climit->parent == NULL) {
                lib_signals_set_handler(SIGXCPU, LIBSIG_FLAG_RESTART,
-                                       cpu_limit_handler, climit);
-       } else {
-               if (climit->limit.rlim_cur > climit->parent->limit.rlim_cur)
-                       climit->limit.rlim_cur = climit->parent->limit.rlim_cur;
+                                       cpu_limit_handler, NULL);
        }
 
-       if (climit->limit.rlim_max != RLIM_INFINITY &&
-           climit->limit.rlim_cur > climit->limit.rlim_max)
-               climit->limit.rlim_cur = climit->limit.rlim_max;
-
-       if (setrlimit(RLIMIT_CPU, &climit->limit) < 0)
-               i_fatal("setrlimit() failed: %m");
-
        cpu_limit = climit;
-       if (climit->parent != NULL && climit->callback != NULL &&
-           climit->parent->callback == NULL) {
-               /* Resolve race condition: parent hit limit before we fully
-                  initialized. */
-               raise(SIGXCPU);
-       }
-
+       cpu_limit_update_rlimit();
        return climit;
 }
 
@@ -155,12 +164,37 @@ void cpu_limit_deinit(struct cpu_limit **_climit)
        if (climit == NULL)
                return;
 
-       cpu_limit = climit->parent;
-       if (setrlimit(RLIMIT_CPU, &climit->old_limit) < 0)
-               i_fatal("setrlimit() failed: %m");
+       i_assert(climit == cpu_limit);
 
+       cpu_limit = climit->parent;
+       cpu_limit_update_rlimit();
        if (climit->parent == NULL)
-               lib_signals_unset_handler(SIGXCPU, cpu_limit_handler, climit);
-
+               lib_signals_unset_handler(SIGXCPU, cpu_limit_handler, NULL);
        i_free(climit);
 }
+
+bool cpu_limit_exceeded(struct cpu_limit *climit)
+{
+       static struct timeval tv_last = { 0, 0 };
+       struct timeval tv_now;
+
+       if (checked_signal_counter != xcpu_signal_counter) {
+               i_gettimeofday(&tv_now);
+               if (tv_last.tv_sec != 0 &&
+                   timeval_diff_msecs(&tv_now, &tv_last) < 1000) {
+                       /* Additional sanity check: We're getting here more
+                          rapidly than once per second. This isn't expected
+                          to happen, but at least in theory it could happen
+                          because rlim_cur isn't clearly calculated from just
+                          the user+system CPU usage. So in case rlim_cur is
+                          too low and keeps firing XCPU signal, try to
+                          increase rlim_cur by 1 second. Eventually it should
+                          become large enough. */
+                       rlim_cur_adjust_secs++;
+               }
+
+               checked_signal_counter = xcpu_signal_counter;
+               cpu_limit_update_rlimit();
+       }
+       return climit->limit_reached;
+}
index 07c1fb547c0450fc92b486dde1b2dc3a062171fe..b6e45aedc610a66bdece6b01f0e756627754c8d5 100644 (file)
@@ -9,14 +9,10 @@ enum cpu_limit_type {
 };
 #define CPU_LIMIT_TYPE_ALL (CPU_LIMIT_TYPE_USER | CPU_LIMIT_TYPE_SYSTEM)
 
-typedef void cpu_limit_callback_t(void *context);
-
-/* Call the callback when the CPU time limit is exceeded. The callback is called
-   in signal handler context, so be careful. The limit is enforced until
-   cpu_limit_deinit() is called. This uses setrlimit() with RLIMIT_CPU
-   internally, which counts both user and system CPU time. Once all limits
-   created by this API are released, the original CPU resource limits are
-   restored (if any).
+/* Start tracking CPU usage. This internally uses setrlimit(RLIMIT_CPU) to
+   trigger SIGXCPU to avoid constantly calling getrlimit() to check if the CPU
+   usage has reached a limit. Once all limits created by this API are released,
+   the original CPU resource limits are restored (if any).
 
    CPU time limits can be nested, i.e. they are never independent. The outer
    limits contain the bounded maximum limit for the inner limits. For example
@@ -27,9 +23,9 @@ typedef void cpu_limit_callback_t(void *context);
     - Infinite loop
    The inner loop's limit won't even be reached here. After the inner loops
    runs for 25 seconds, the outer loop's 30s limit is reached. This causes
-   both the inner and the other limit's callback to be called. It's expected
-   that the inner execution stops and returns back to the outer execution,
-   which notices that the outer execution has also reached the limit.
+   both the inner and the other limit's cpu_limit_exceeded() to return TRUE.
+   It's expected that the inner execution stops and returns back to the outer
+   execution, which notices that the outer execution has also reached the limit.
 
    Another example where the inner limit is reached:
     - Set 30s CPU limit (outer limit)
@@ -38,16 +34,27 @@ typedef void cpu_limit_callback_t(void *context);
     - Infinite loop
    Here the inner 10s limit is reached, and the inner execution stops. The
    outer execution could still run for another 15 seconds.
- */
+
+   Example usage:
+
+   bool limit_reached = FALSE;
+   limit = cpu_limit_init(5, CPU_LIMIT_TYPE_ALL);
+   while (long_operation_iterate_once()) {
+     if (cpu_limit_exceeded(limit)) {
+       limit_reached = TRUE; // operation took >=5 secs
+       break;
+     }
+   }
+   cpu_limit_deinit(&limit);
+*/
 struct cpu_limit *
-cpu_limit_init(unsigned int cpu_limit_sec,
-              cpu_limit_callback_t *callback, void *context);
-#define cpu_limit_init(cpu_limit_sec, callback, context) \
-       cpu_limit_init(cpu_limit_sec - \
-               CALLBACK_TYPECHECK(callback, void (*)(typeof(context))), \
-               (cpu_limit_callback_t *)callback, context)
+cpu_limit_init(unsigned int cpu_limit_secs, enum cpu_limit_type type);
 void cpu_limit_deinit(struct cpu_limit **_climit);
 
+/* Returns TRUE if the CPU limit has been exceeded for this limit or any of its
+   parents. */
+bool cpu_limit_exceeded(struct cpu_limit *climit);
+
 unsigned int cpu_limit_get_usage_msecs(struct cpu_limit *climit,
                                       enum cpu_limit_type type);
 
index 62d182563134356d92a0d4a7ea325f57b232927d..029004b61f19c5b2fb494fb9f6dea72f68e064d9 100644 (file)
 #define ALLOW_MSECS_BELOW 500
 #define ALLOW_MSECS_ABOVE 1500
 
-static bool limit_exceeded1, limit_exceeded2;
 static const char *const test_path = ".test.cpulimit";
 
-static void cpu_limit_callback1(void *context ATTR_UNUSED)
-{
-       limit_exceeded1 = TRUE;
-}
-
-static void cpu_limit_callback2(void *context ATTR_UNUSED)
-{
-       limit_exceeded2 = TRUE;
-}
-
-static struct timeval get_cpu_time(void)
+static struct timeval get_cpu_time(enum cpu_limit_type type)
 {
        struct rusage rusage;
-       struct timeval cpu_usage;
+       struct timeval cpu_usage = { 0, 0 };
 
        /* Query cpu usage so far */
        if (getrusage(RUSAGE_SELF, &rusage) < 0)
                i_fatal("getrusage() failed: %m");
-       cpu_usage = rusage.ru_utime;
-       timeval_add(&cpu_usage, &rusage.ru_stime);
+       if ((type & CPU_LIMIT_TYPE_USER) != 0)
+               timeval_add(&cpu_usage, &rusage.ru_utime);
+       if ((type & CPU_LIMIT_TYPE_SYSTEM) != 0)
+               timeval_add(&cpu_usage, &rusage.ru_stime);
        return cpu_usage;
 }
 
@@ -56,24 +47,24 @@ static void test_cpu_loop_once(void)
        i_close_fd(&fd);
 }
 
-static void test_cpu_limit_simple(void)
+static void
+test_cpu_limit_simple(enum cpu_limit_type type, const char *type_str)
 {
        struct cpu_limit *climit;
        struct timeval usage, cpu;
        int diff_msecs;
 
-       test_begin("cpu limit - simple");
+       test_begin(t_strdup_printf("cpu limit - simple (%s)", type_str));
 
        lib_signals_init();
-       climit = cpu_limit_init(2, cpu_limit_callback1, NULL);
-       usage = get_cpu_time();
+       climit = cpu_limit_init(2, type);
+       usage = get_cpu_time(type);
 
-       limit_exceeded1 = FALSE;
-       while (!limit_exceeded1)
+       while (!cpu_limit_exceeded(climit))
                test_cpu_loop_once();
 
        cpu_limit_deinit(&climit);
-       cpu = get_cpu_time();
+       cpu = get_cpu_time(type);
        diff_msecs = timeval_diff_msecs(&cpu, &usage);
        test_assert_cmp(diff_msecs, >=, 2000 - ALLOW_MSECS_BELOW);
        test_assert_cmp(diff_msecs, <=, 2000 + ALLOW_MSECS_ABOVE);
@@ -82,30 +73,28 @@ static void test_cpu_limit_simple(void)
        test_end();
 }
 
-static void test_cpu_limit_nested(void)
+static void test_cpu_limit_nested(enum cpu_limit_type type, const char *type_str)
 {
        struct cpu_limit *climit1, *climit2;
        struct timeval usage1, usage2, cpu;
        unsigned int n;
        int diff_msecs;
 
-       test_begin("cpu limit - nested");
+       test_begin(t_strdup_printf("cpu limit - nested (%s)", type_str));
 
        lib_signals_init();
-       climit1 = cpu_limit_init(3, cpu_limit_callback1, NULL);
-       usage1 = get_cpu_time();
+       climit1 = cpu_limit_init(3, type);
+       usage1 = get_cpu_time(type);
 
-       limit_exceeded1 = FALSE;
-       while (!limit_exceeded1 && !test_has_failed()) {
-               climit2 = cpu_limit_init(1, cpu_limit_callback2, NULL);
-               usage2 = get_cpu_time();
+       while (!cpu_limit_exceeded(climit1) && !test_has_failed()) {
+               climit2 = cpu_limit_init(1, type);
+               usage2 = get_cpu_time(type);
 
-               limit_exceeded2 = FALSE;
-               while (!limit_exceeded2 && !test_has_failed())
+               while (!cpu_limit_exceeded(climit2) && !test_has_failed())
                        test_cpu_loop_once();
 
                cpu_limit_deinit(&climit2);
-               cpu = get_cpu_time();
+               cpu = get_cpu_time(type);
                /* we may have looped only for a short time in case climit1
                   was triggered during this loop. */
                diff_msecs = timeval_diff_msecs(&cpu, &usage2);
@@ -113,7 +102,7 @@ static void test_cpu_limit_nested(void)
        }
 
        cpu_limit_deinit(&climit1);
-       cpu = get_cpu_time();
+       cpu = get_cpu_time(type);
        diff_msecs = timeval_diff_msecs(&cpu, &usage1);
        test_assert_cmp(diff_msecs, >=, 3000 - ALLOW_MSECS_BELOW);
        test_assert_cmp(diff_msecs, <=, 3000 + ALLOW_MSECS_ABOVE);
@@ -121,29 +110,27 @@ static void test_cpu_limit_nested(void)
        lib_signals_deinit();
        test_end();
 
-       test_begin("cpu limit - nested2");
+       test_begin(t_strdup_printf("cpu limit - nested2 (%s)", type_str));
 
        lib_signals_init();
-       climit1 = cpu_limit_init(3, cpu_limit_callback1, NULL);
-       usage1 = get_cpu_time();
+       climit1 = cpu_limit_init(3, type);
+       usage1 = get_cpu_time(type);
 
-       limit_exceeded1 = FALSE;
        n = 0;
-       while (!limit_exceeded1 && !test_has_failed()) {
+       while (!cpu_limit_exceeded(climit1) && !test_has_failed()) {
                if (++n >= 3) {
                        /* Consume last second in top cpu limit */
                        test_cpu_loop_once();
                        continue;
                }
-               climit2 = cpu_limit_init(1, cpu_limit_callback2, NULL);
-               usage2 = get_cpu_time();
+               climit2 = cpu_limit_init(1, type);
+               usage2 = get_cpu_time(type);
 
-               limit_exceeded2 = FALSE;
-               while (!limit_exceeded2 && !test_has_failed())
+               while (!cpu_limit_exceeded(climit2) && !test_has_failed())
                        test_cpu_loop_once();
 
                cpu_limit_deinit(&climit2);
-               cpu = get_cpu_time();
+               cpu = get_cpu_time(type);
                /* we may have looped only for a short time in case climit1
                   was triggered during this loop. */
                diff_msecs = timeval_diff_msecs(&cpu, &usage2);
@@ -151,7 +138,7 @@ static void test_cpu_limit_nested(void)
        }
 
        cpu_limit_deinit(&climit1);
-       cpu = get_cpu_time();
+       cpu = get_cpu_time(type);
        diff_msecs = timeval_diff_msecs(&cpu, &usage1);
        test_assert_cmp(diff_msecs, >=, 3000 - ALLOW_MSECS_BELOW);
        test_assert_cmp(diff_msecs, <=, 3000 + ALLOW_MSECS_ABOVE);
@@ -163,6 +150,10 @@ static void test_cpu_limit_nested(void)
 
 void test_cpu_limit(void)
 {
-       test_cpu_limit_simple();
-       test_cpu_limit_nested();
+       test_cpu_limit_simple(CPU_LIMIT_TYPE_USER, "user");
+       test_cpu_limit_simple(CPU_LIMIT_TYPE_SYSTEM, "system");
+       test_cpu_limit_simple(CPU_LIMIT_TYPE_ALL, "all");
+       test_cpu_limit_nested(CPU_LIMIT_TYPE_USER, "user");
+       test_cpu_limit_nested(CPU_LIMIT_TYPE_SYSTEM, "system");
+       test_cpu_limit_nested(CPU_LIMIT_TYPE_ALL, "all");
 }