mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-21 00:01:24 +02:00
212 lines
6.2 KiB
C++
212 lines
6.2 KiB
C++
#include "runtime/memory_limit.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/types.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <charconv>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
namespace {
|
|
|
|
constexpr std::uint64_t kBytesPerKibibyte = 1024U;
|
|
constexpr std::uint64_t kBytesPerMebibyte = 1024U * 1024U;
|
|
constexpr int kWatchdogPollMilliseconds = 100;
|
|
|
|
void WriteAllBestEffort(int descriptor, std::string_view bytes) {
|
|
std::size_t offset = 0;
|
|
while (offset < bytes.size()) {
|
|
const ssize_t written =
|
|
write(descriptor, bytes.data() + offset, bytes.size() - offset);
|
|
if (written > 0) {
|
|
offset += static_cast<std::size_t>(written);
|
|
continue;
|
|
}
|
|
if (written < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool ParseKibibyteField(std::string_view status, std::string_view field,
|
|
std::uint64_t* bytes) {
|
|
const std::size_t field_position = status.find(field);
|
|
if (field_position == std::string_view::npos ||
|
|
(field_position != 0 && status[field_position - 1] != '\n')) {
|
|
return false;
|
|
}
|
|
std::size_t value_begin = field_position + field.size();
|
|
while (value_begin < status.size() &&
|
|
(status[value_begin] == ' ' || status[value_begin] == '\t')) {
|
|
++value_begin;
|
|
}
|
|
std::uint64_t kibibytes = 0;
|
|
const char* begin = status.data() + value_begin;
|
|
const char* end = status.data() + status.size();
|
|
const std::from_chars_result parsed =
|
|
std::from_chars(begin, end, kibibytes);
|
|
if (parsed.ec != std::errc() || parsed.ptr == begin ||
|
|
kibibytes > UINT64_MAX / kBytesPerKibibyte) {
|
|
return false;
|
|
}
|
|
const char* suffix = parsed.ptr;
|
|
while (suffix < end && (*suffix == ' ' || *suffix == '\t')) {
|
|
++suffix;
|
|
}
|
|
if (end - suffix < 2 || suffix[0] != 'k' || suffix[1] != 'B') {
|
|
return false;
|
|
}
|
|
*bytes = kibibytes * kBytesPerKibibyte;
|
|
return true;
|
|
}
|
|
|
|
ProcessMemoryUsage ReadCurrentProcessMemory() {
|
|
std::array<char, 32768> buffer{};
|
|
const int status_file = open("/proc/self/status", O_RDONLY | O_CLOEXEC);
|
|
if (status_file < 0) {
|
|
return {};
|
|
}
|
|
ssize_t total = 0;
|
|
while (static_cast<std::size_t>(total) < buffer.size()) {
|
|
const ssize_t read_result =
|
|
read(status_file, buffer.data() + total, buffer.size() - total);
|
|
if (read_result > 0) {
|
|
total += read_result;
|
|
continue;
|
|
}
|
|
if (read_result < 0 && errno == EINTR) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
(void)close(status_file);
|
|
if (total <= 0) {
|
|
return {};
|
|
}
|
|
return ParseProcessStatusMemory(
|
|
std::string_view(buffer.data(), static_cast<std::size_t>(total)));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ProcessMemoryUsage ParseProcessStatusMemory(std::string_view status) {
|
|
ProcessMemoryUsage usage;
|
|
if (!ParseKibibyteField(status, "VmRSS:", &usage.resident_bytes)) {
|
|
return usage;
|
|
}
|
|
std::uint64_t swap_bytes = 0;
|
|
if (status.find("VmSwap:") != std::string_view::npos &&
|
|
!ParseKibibyteField(status, "VmSwap:", &swap_bytes)) {
|
|
return {};
|
|
}
|
|
usage.swap_bytes = swap_bytes;
|
|
usage.valid = usage.resident_bytes <=
|
|
UINT64_MAX - usage.swap_bytes;
|
|
return usage;
|
|
}
|
|
|
|
CgroupMemoryLimitResult MaybeReexecWithCgroupMemoryLimit(
|
|
int argc, char* const argv[], std::uint64_t limit_bytes,
|
|
const std::vector<std::string>* reexec_arguments) {
|
|
CgroupMemoryLimitResult result;
|
|
(void)argc;
|
|
(void)argv;
|
|
(void)reexec_arguments;
|
|
if (limit_bytes == 0) {
|
|
result.detail = "memory limit is disabled";
|
|
return result;
|
|
}
|
|
result.detail = "automatic cgroup scopes are disabled";
|
|
return result;
|
|
}
|
|
|
|
MemoryLimitWatchdog::~MemoryLimitWatchdog() { Stop(); }
|
|
|
|
bool MemoryLimitWatchdog::Start(std::uint64_t limit_bytes,
|
|
std::string* error) {
|
|
if (running_ || limit_bytes == 0) {
|
|
if (error != nullptr) {
|
|
*error = running_ ? "memory watchdog is already running"
|
|
: "memory watchdog requires a non-zero limit";
|
|
}
|
|
return false;
|
|
}
|
|
limit_bytes_ = limit_bytes;
|
|
stop_requested_.store(false, std::memory_order_release);
|
|
const int start_error =
|
|
worker_.Start(&MemoryLimitWatchdog::ThreadEntry, this, 0);
|
|
if (start_error != 0) {
|
|
limit_bytes_ = 0;
|
|
if (error != nullptr) {
|
|
*error = std::string("cannot start memory watchdog: ") +
|
|
std::strerror(start_error);
|
|
}
|
|
return false;
|
|
}
|
|
running_ = true;
|
|
return true;
|
|
}
|
|
|
|
void MemoryLimitWatchdog::Stop() {
|
|
if (!running_) {
|
|
return;
|
|
}
|
|
stop_requested_.store(true, std::memory_order_release);
|
|
const OwnedPthreadWaitResult wait = worker_.WaitFor(-1, 10);
|
|
if (!wait.joined()) {
|
|
std::_Exit(EXIT_FAILURE);
|
|
}
|
|
running_ = false;
|
|
limit_bytes_ = 0;
|
|
}
|
|
|
|
void* MemoryLimitWatchdog::ThreadEntry(void* context) {
|
|
static_cast<MemoryLimitWatchdog*>(context)->Run();
|
|
return nullptr;
|
|
}
|
|
|
|
void MemoryLimitWatchdog::Run() {
|
|
const timespec interval = {
|
|
0, static_cast<long>(kWatchdogPollMilliseconds) * 1000L * 1000L};
|
|
while (!stop_requested_.load(std::memory_order_acquire)) {
|
|
const ProcessMemoryUsage usage = ReadCurrentProcessMemory();
|
|
if (usage.valid && usage.committed_bytes() >= limit_bytes_) {
|
|
const std::uint64_t used_mb =
|
|
usage.committed_bytes() / kBytesPerMebibyte;
|
|
const std::uint64_t limit_mb = limit_bytes_ / kBytesPerMebibyte;
|
|
std::array<char, 256> message{};
|
|
const int length = std::snprintf(
|
|
message.data(), message.size(),
|
|
"[memory] limit exceeded: RSS+swap=%llu MiB, limit=%llu MiB; "
|
|
"terminating Mocktail with exit 137\n",
|
|
static_cast<unsigned long long>(used_mb),
|
|
static_cast<unsigned long long>(limit_mb));
|
|
if (length > 0) {
|
|
const std::size_t write_size =
|
|
std::min(static_cast<std::size_t>(length), message.size() - 1U);
|
|
WriteAllBestEffort(
|
|
STDERR_FILENO,
|
|
std::string_view(message.data(), write_size));
|
|
}
|
|
std::_Exit(137);
|
|
}
|
|
timespec remaining = interval;
|
|
while (nanosleep(&remaining, &remaining) != 0 && errno == EINTR &&
|
|
!stop_requested_.load(std::memory_order_acquire)) {
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace runtime
|
|
} // namespace mocktail
|