#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
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 */
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;
}
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;
+}
};
#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
- 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)
- 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);
#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;
}
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);
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);
}
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);
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);
}
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);
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");
}