summaryrefslogtreecommitdiffstats
path: root/src/lib/cpu-limit.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/cpu-limit.c')
-rw-r--r--src/lib/cpu-limit.c200
1 files changed, 200 insertions, 0 deletions
diff --git a/src/lib/cpu-limit.c b/src/lib/cpu-limit.c
new file mode 100644
index 0000000..754717a
--- /dev/null
+++ b/src/lib/cpu-limit.c
@@ -0,0 +1,200 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "lib-signals.h"
+#include "time-util.h"
+#include "cpu-limit.h"
+
+#include <sys/time.h>
+#include <sys/resource.h>
+
+struct cpu_limit {
+ struct cpu_limit *parent;
+
+ enum cpu_limit_type type;
+ unsigned int cpu_limit_secs;
+ struct rusage initial_usage;
+
+ 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)
+{
+ xcpu_signal_counter++;
+}
+
+static unsigned int
+cpu_limit_get_usage_msecs_with(struct cpu_limit *climit,
+ enum cpu_limit_type type,
+ const struct rusage *rusage)
+{
+ struct timeval cpu_usage = { 0, 0 };
+ int usage_diff;
+
+ 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);
+
+ struct timeval initial_total = { 0, 0 };
+ if ((type & CPU_LIMIT_TYPE_USER) != 0)
+ timeval_add(&initial_total, &climit->initial_usage.ru_utime);
+ if ((type & CPU_LIMIT_TYPE_SYSTEM) != 0)
+ timeval_add(&initial_total, &climit->initial_usage.ru_stime);
+ usage_diff = timeval_diff_msecs(&cpu_usage, &initial_total);
+ i_assert(usage_diff >= 0);
+
+ return (unsigned int)usage_diff;
+}
+
+unsigned int
+cpu_limit_get_usage_msecs(struct cpu_limit *climit, enum cpu_limit_type type)
+{
+ struct rusage rusage;
+
+ /* Query cpu usage so far */
+ if (getrusage(RUSAGE_SELF, &rusage) < 0)
+ i_fatal("getrusage() failed: %m");
+
+ return cpu_limit_get_usage_msecs_with(climit, type, &rusage);
+}
+
+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_secs, enum cpu_limit_type type)
+{
+ struct cpu_limit *climit;
+ struct rusage rusage;
+
+ i_assert(cpu_limit_secs > 0);
+ i_assert(type != 0);
+
+ climit = i_new(struct cpu_limit, 1);
+ climit->parent = cpu_limit;
+ climit->type = type;
+ climit->cpu_limit_secs = cpu_limit_secs;
+
+ /* Query current limit */
+ if (climit->parent == NULL) {
+ if (getrlimit(RLIMIT_CPU, &orig_limit) < 0)
+ i_fatal("getrlimit() failed: %m");
+ }
+
+ /* Query cpu usage so far */
+ if (getrusage(RUSAGE_SELF, &rusage) < 0)
+ i_fatal("getrusage() failed: %m");
+ climit->initial_usage = rusage;
+
+ if (climit->parent == NULL) {
+ lib_signals_set_handler(SIGXCPU, LIBSIG_FLAG_RESTART,
+ cpu_limit_handler, NULL);
+ }
+
+ cpu_limit = climit;
+ cpu_limit_update_rlimit();
+ return climit;
+}
+
+void cpu_limit_deinit(struct cpu_limit **_climit)
+{
+ struct cpu_limit *climit = *_climit;
+
+ *_climit = NULL;
+ if (climit == NULL)
+ return;
+
+ 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, 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;
+}