// This helper allocates memory and prevents the compiler from // optimizing that allocation away by locking the allocated pages. static Try<void*> allocateRSS(const Bytes& size) { #ifndef __APPLE__ // Make sure that all pages that are going to be mapped into the // address space of this process become unevictable. This is needed // for testing cgroups OOM killer. if (mlockall(MCL_FUTURE) != 0) { return ErrnoError("Failed to make pages to be mapped unevictable"); } #endif void* rss = nullptr; if (posix_memalign(&rss, os::pagesize(), size.bytes()) != 0) { return ErrnoError("Failed to increase RSS memory, posix_memalign"); } // Use memset to map pages into the memory space of this process. memset(rss, 1, size.bytes()); #ifdef __APPLE__ // Locking a page makes it unevictable in the kernel. This is needed // for testing cgroups OOM killer. // NOTE: We use 'mlock' here because 'mlockall' is left // unimplemented on OS X. if (mlock(rss, size.bytes()) != 0) { return ErrnoError("Failed to make mapped pages unevictable"); } #endif return rss; }
static Try<Nothing> increasePageCache(const vector<string>& tokens) { const Bytes UNIT = Megabytes(1); if (tokens.size() < 2) { return Error("Expect at least one argument"); } Try<Bytes> size = Bytes::parse(tokens[1]); if (size.isError()) { return Error("The first argument '" + tokens[1] + "' is not a byte size"); } // TODO(chzhcn): Currently, we assume the current working directory // is a temporary directory and will be cleaned up when the test // finishes. Since the child process will inherit the current // working directory from the parent process, that means the test // that uses this helper probably needs to inherit from // TemporaryDirectoryTest. Consider relaxing this constraint. Try<string> path = os::mktemp(path::join(os::getcwd(), "XXXXXX")); if (path.isError()) { return Error("Failed to create a temporary file: " + path.error()); } Try<int> fd = os::open(path.get(), O_WRONLY); if (fd.isError()) { return Error("Failed to open file: " + fd.error()); } // NOTE: We are doing round-down here to calculate the number of // writes to do. for (uint64_t i = 0; i < size.get().bytes() / UNIT.bytes(); i++) { // Write UNIT size to disk at a time. The content isn't important. Try<Nothing> write = os::write(fd.get(), string(UNIT.bytes(), 'a')); if (write.isError()) { os::close(fd.get()); return Error("Failed to write file: " + write.error()); } // Use fsync to make sure data is written to disk. if (fsync(fd.get()) == -1) { // Save the error message because os::close below might // overwrite the errno. const string message = os::strerror(errno); os::close(fd.get()); return Error("Failed to fsync: " + message); } } os::close(fd.get()); return Nothing(); }
// This function should be async-signal-safe but it isn't: at least // posix_memalign, mlock, memset and perror are not safe. int consumeMemory(const Bytes& _size, const Duration& duration, int pipes[2]) { // In child process ::close(pipes[1]); int buf; // Wait until the parent signals us to continue. while (::read(pipes[0], &buf, sizeof(buf)) == -1 && errno == EINTR); ::close(pipes[0]); size_t size = static_cast<size_t>(_size.bytes()); void* buffer = NULL; if (posix_memalign(&buffer, getpagesize(), size) != 0) { perror("Failed to allocate page-aligned memory, posix_memalign"); abort(); } // We use mlock and memset here to make sure that the memory // actually gets paged in and thus accounted for. if (mlock(buffer, size) != 0) { perror("Failed to lock memory, mlock"); abort(); } if (memset(buffer, 1, size) != buffer) { perror("Failed to fill memory, memset"); abort(); } os::sleep(duration); return 0; }
// Tests whether a file's size is reported by os::stat::size as expected. // Tests all four combinations of following a link or not and of a file // or a link as argument. Also tests that an error is returned for a // non-existing file. TEST_F(OsTest, Size) { const string& file = path::join(os::getcwd(), UUID::random().toString()); const Bytes size = 1053; ASSERT_SOME(os::write(file, string(size.bytes(), 'X'))); // The reported file size should be the same whether following links // or not, given that the input parameter is not a link. EXPECT_SOME_EQ(size, os::stat::size(file, os::stat::FOLLOW_SYMLINK)); EXPECT_SOME_EQ(size, os::stat::size(file, os::stat::DO_NOT_FOLLOW_SYMLINK)); EXPECT_ERROR(os::stat::size("aFileThatDoesNotExist")); const string& link = path::join(os::getcwd(), UUID::random().toString()); ASSERT_SOME(fs::symlink(file, link)); // Following links we expect the file's size, not the link's. EXPECT_SOME_EQ(size, os::stat::size(link, os::stat::FOLLOW_SYMLINK)); // Not following links, we expect the string length of the linked path. EXPECT_SOME_EQ(Bytes(file.size()), os::stat::size(link, os::stat::DO_NOT_FOLLOW_SYMLINK)); }
TYPED_TEST(MemIsolatorTest, MemUsage) { slave::Flags flags; Try<Isolator*> isolator = TypeParam::create(flags); CHECK_SOME(isolator); ExecutorInfo executorInfo; executorInfo.mutable_resources()->CopyFrom( Resources::parse("mem:1024").get()); ContainerID containerId; containerId.set_value(UUID::random().toString()); // Use a relative temporary directory so it gets cleaned up // automatically with the test. Try<string> dir = os::mkdtemp(path::join(os::getcwd(), "XXXXXX")); ASSERT_SOME(dir); ContainerConfig containerConfig; containerConfig.mutable_executor_info()->CopyFrom(executorInfo); containerConfig.set_directory(dir.get()); AWAIT_READY(isolator.get()->prepare( containerId, containerConfig)); MemoryTestHelper helper; ASSERT_SOME(helper.spawn()); ASSERT_SOME(helper.pid()); // Set up the reaper to wait on the subprocess. Future<Option<int>> status = process::reap(helper.pid().get()); // Isolate the subprocess. AWAIT_READY(isolator.get()->isolate(containerId, helper.pid().get())); const Bytes allocation = Megabytes(128); EXPECT_SOME(helper.increaseRSS(allocation)); Future<ResourceStatistics> usage = isolator.get()->usage(containerId); AWAIT_READY(usage); EXPECT_GE(usage.get().mem_rss_bytes(), allocation.bytes()); // Ensure the process is killed. helper.cleanup(); // Make sure the subprocess was reaped. AWAIT_READY(status); // Let the isolator clean up. AWAIT_READY(isolator.get()->cleanup(containerId)); delete isolator.get(); }
static Option<Error> validateSize(const Bytes& value) { if (value.bytes() < static_cast<uint64_t>(os::pagesize())) { return Error( "Expected --max_stdout_size and --max_stderr_size of " "at least " + stringify(os::pagesize()) + " bytes"); } return None(); }
TYPED_TEST(MemIsolatorTest, MemUsage) { Flags flags; Try<Isolator*> isolator = TypeParam::create(flags); CHECK_SOME(isolator); // A PosixLauncher is sufficient even when testing a cgroups isolator. Try<Launcher*> launcher = PosixLauncher::create(flags); ExecutorInfo executorInfo; executorInfo.mutable_resources()->CopyFrom( Resources::parse("mem:1024").get()); ContainerID containerId; containerId.set_value("memory_usage"); AWAIT_READY(isolator.get()->prepare(containerId, executorInfo)); int pipes[2]; ASSERT_NE(-1, ::pipe(pipes)); lambda::function<int()> inChild = lambda::bind( &consumeMemory, Megabytes(256), Seconds(10), pipes); Try<pid_t> pid = launcher.get()->fork(containerId, inChild); ASSERT_SOME(pid); // Set up the reaper to wait on the forked child. Future<Option<int> > status = process::reap(pid.get()); // Continue in the parent. ::close(pipes[0]); // Isolate the forked child. AWAIT_READY(isolator.get()->isolate(containerId, pid.get())); // Now signal the child to continue. int buf; ASSERT_LT(0, ::write(pipes[1], &buf, sizeof(buf))); ::close(pipes[1]); // Wait up to 5 seconds for the child process to consume 256 MB of memory; ResourceStatistics statistics; Bytes threshold = Megabytes(256); Duration waited = Duration::zero(); do { Future<ResourceStatistics> usage = isolator.get()->usage(containerId); AWAIT_READY(usage); statistics = usage.get(); // If we meet our usage expectations, we're done! if (statistics.mem_rss_bytes() >= threshold.bytes()) { break; } os::sleep(Seconds(1)); waited += Seconds(1); } while (waited < Seconds(5)); EXPECT_LE(threshold.bytes(), statistics.mem_rss_bytes()); // Ensure all processes are killed. AWAIT_READY(launcher.get()->destroy(containerId)); // Make sure the child was reaped. AWAIT_READY(status); // Let the isolator clean up. AWAIT_READY(isolator.get()->cleanup(containerId)); delete isolator.get(); delete launcher.get(); }
TYPED_TEST(MemIsolatorTest, MemUsage) { slave::Flags flags; Try<Isolator*> isolator = TypeParam::create(flags); CHECK_SOME(isolator); // A PosixLauncher is sufficient even when testing a cgroups isolator. Try<Launcher*> launcher = PosixLauncher::create(flags); ExecutorInfo executorInfo; executorInfo.mutable_resources()->CopyFrom( Resources::parse("mem:1024").get()); ContainerID containerId; containerId.set_value("memory_usage"); // Use a relative temporary directory so it gets cleaned up // automatically with the test. Try<string> dir = os::mkdtemp(path::join(os::getcwd(), "XXXXXX")); ASSERT_SOME(dir); AWAIT_READY( isolator.get()->prepare(containerId, executorInfo, dir.get(), None())); int pipes[2]; ASSERT_NE(-1, ::pipe(pipes)); Try<pid_t> pid = launcher.get()->fork( containerId, "/bin/sh", vector<string>(), Subprocess::FD(STDIN_FILENO), Subprocess::FD(STDOUT_FILENO), Subprocess::FD(STDERR_FILENO), None(), None(), lambda::bind(&consumeMemory, Megabytes(256), Seconds(10), pipes)); ASSERT_SOME(pid); // Set up the reaper to wait on the forked child. Future<Option<int> > status = process::reap(pid.get()); // Continue in the parent. ASSERT_SOME(os::close(pipes[0])); // Isolate the forked child. AWAIT_READY(isolator.get()->isolate(containerId, pid.get())); // Now signal the child to continue. char dummy; ASSERT_LT(0, ::write(pipes[1], &dummy, sizeof(dummy))); ASSERT_SOME(os::close(pipes[1])); // Wait up to 5 seconds for the child process to consume 256 MB of memory; ResourceStatistics statistics; Bytes threshold = Megabytes(256); Duration waited = Duration::zero(); do { Future<ResourceStatistics> usage = isolator.get()->usage(containerId); AWAIT_READY(usage); statistics = usage.get(); // If we meet our usage expectations, we're done! if (statistics.mem_rss_bytes() >= threshold.bytes()) { break; } os::sleep(Seconds(1)); waited += Seconds(1); } while (waited < Seconds(5)); EXPECT_LE(threshold.bytes(), statistics.mem_rss_bytes()); // Ensure all processes are killed. AWAIT_READY(launcher.get()->destroy(containerId)); // Make sure the child was reaped. AWAIT_READY(status); // Let the isolator clean up. AWAIT_READY(isolator.get()->cleanup(containerId)); delete isolator.get(); delete launcher.get(); }