pax_global_header00006660000000000000000000000064140647053370014523gustar00rootroot0000000000000052 comment=e6b78d77c7e4fe45e3c40dbb5675cc372bf052a2 oomd-0.5.0/000077500000000000000000000000001406470533700124635ustar00rootroot00000000000000oomd-0.5.0/.clang-format000066400000000000000000000047701406470533700150460ustar00rootroot00000000000000--- AccessModifierOffset: -1 AlignAfterOpenBracket: AlwaysBreak AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlinesLeft: true AlignOperands: false AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: true AlwaysBreakTemplateDeclarations: true BinPackArguments: false BinPackParameters: false BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: false AfterNamespace: false AfterObjCDeclaration: false AfterStruct: false AfterUnion: false BeforeCatch: false BeforeElse: false IndentBraces: false BreakBeforeBinaryOperators: None BreakBeforeBraces: Attach BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false BreakAfterJavaFieldAnnotations: false BreakStringLiterals: false ColumnLimit: 80 CommentPragmas: '^ IWYU pragma:' ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false ForEachMacros: [ FOR_EACH, FOR_EACH_R, FOR_EACH_RANGE, ] IncludeCategories: - Regex: '^<.*\.h(pp)?>' Priority: 1 - Regex: '^<.*' Priority: 2 - Regex: '.*' Priority: 3 IndentCaseLabels: true IndentWidth: 2 IndentWrappedFunctionNames: false KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: '' MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None ObjCBlockIndentWidth: 2 ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: false PenaltyBreakBeforeFirstCallParameter: 1 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 1000 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 200 PointerAlignment: Left ReflowComments: true SortIncludes: true SpaceAfterCStyleCast: false SpaceBeforeAssignmentOperators: true SpaceBeforeParens: ControlStatements SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInContainerLiterals: true SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp11 TabWidth: 8 UseTab: Never ... oomd-0.5.0/.github/000077500000000000000000000000001406470533700140235ustar00rootroot00000000000000oomd-0.5.0/.github/workflows/000077500000000000000000000000001406470533700160605ustar00rootroot00000000000000oomd-0.5.0/.github/workflows/ci.yml000066400000000000000000000013771406470533700172060ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: branches: - master jobs: build: name: Build and test repo runs-on: ubuntu-latest strategy: matrix: include: - cc: gcc-8 cxx: g++-8 - cc: clang cxx: clang++ env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} steps: - run: sudo apt-get update - name: Install dependencies run: > sudo apt-get install pkg-config libsystemd-dev libjsoncpp-dev googletest meson g++-8 - name: Checkout code uses: actions/checkout@v2 - run: meson build/ - run: ninja -C build/ - run: ninja -C build/ test oomd-0.5.0/.gitignore000066400000000000000000000002641406470533700144550ustar00rootroot00000000000000.DS_Store /build/ /tags node_modules lib/core/metadata.js lib/core/MetadataBlog.js website/translated_docs website/build/ website/yarn.lock website/node_modules website/i18n/* oomd-0.5.0/CODE_OF_CONDUCT.md000066400000000000000000000064341406470533700152710ustar00rootroot00000000000000# Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq oomd-0.5.0/CONTRIBUTING.md000066400000000000000000000027551406470533700147250ustar00rootroot00000000000000# Contributing to oomd We want to make contributing to this project as easy and transparent as possible. ## Our Development Process We review pull requests internally (but also publically as applicable). Following a successful review, we commit internally and sync back to open source. ## Pull Requests We actively welcome your pull requests. 1. Fork the repo and create your branch from `master`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes. 5. Make sure your code lints. 6. If you haven't already, complete the Contributor License Agreement ("CLA"). ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## Coding Style * 2 spaces for indentation rather than tabs * 80 character line length ## License By contributing to oomd, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. oomd-0.5.0/LICENSE000066400000000000000000000432301406470533700134720ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. oomd-0.5.0/README.md000066400000000000000000000067321406470533700137520ustar00rootroot00000000000000# oomd [![Build Status](https://github.com/facebookincubator/oomd/workflows/CI/badge.svg?branch=master)](https://github.com/facebookincubator/oomd/actions?query=workflow%3ACI+branch%3Amaster) oomd is *userspace* Out-Of-Memory (OOM) killer for linux systems. ## Background Out of memory killing has historically happened inside kernel space. On a [memory overcommitted][0] linux system, malloc(2) and friends usually never fail. However, if an application dereferences the returned pointer and the system has run out of physical memory, the linux kernel is forced to take extreme measures, up to and including killing processes. This is sometimes a slow and painful process because the kernel can spend an unbounded amount of time swapping in and out pages and evicting the page cache. Furthermore, [configuring policy][1] is not very flexible while being somewhat complicated. oomd aims to solve this problem in userspace. oomd leverages PSI and cgroupv2 to monitor a system holistically. oomd then takes corrective action in userspace before an OOM occurs in kernel space. Corrective action is configured via a flexible plugin system, in which custom code can be written. By default, this involves killing offending processes. This enables an unparalleled level of flexibility where each workload can have custom protection rules. Furthermore, time spent livelocked in kernelspace is minimized. In practice at Facebook, we've regularly seen 30 minute host lockups go away entirely. ## Installing on Debian 11+ or Ubuntu 20.04+ `# apt install oomd` ## Installing from RPMs on Fedora oomd is packaged in Fedora as of Fedora 32 and can be installed with: $ sudo dnf install oomd Finally, enable and start it with: $ sudo systemctl enable --now oomd.service ## Building from source Note that oomd requires PSI to function. This kernel feature has been merged into the 4.20 release. oomd currently depends on [meson][2] and [jsoncpp][4]. [libsystemd][6] is an optional dependency. oomd also requires GCC 8+ or clang 6+. Other compilers have not been tested. $ git clone https://github.com/facebookincubator/oomd $ cd oomd/oomd $ meson build && ninja -C build $ cd build && sudo ninja install ## Configuration See [docs/configuration.md](docs/configuration.md) for a high level overview and some examples. See [docs/core_plugins.md](docs/core_plugins.md) for a quick reference on core plugin capabilities. See [docs/production_setup.md](docs/production_setup.md) for guidelines on how oomd should be set up in a production environment. ## Running tests oomd depends on [gtest/gmock][5] to run tests. Installing gtest/gmock from master is preferred. If meson detects gtest/gmock is installed, meson will generate build rules for tests. $ cd oomd $ rm -rf build $ meson build && ninja test -C build ## Writing custom plugins It is both possible and encouraged to write custom plugins. The codebase is designed to make writing plugins as easy as possible. See [docs/writing_a_plugin.md](docs/writing_a_plugin.md) for a tutorial. ## Help / Discussion / Support Join our **#oomd** channel on irc.freenode.net! ## License oomd is GPL 2 licensed, as found in the [LICENSE](LICENSE) file. [0]: https://www.kernel.org/doc/Documentation/vm/overcommit-accounting [1]: https://lwn.net/Articles/317814/ [2]: http://mesonbuild.com/ [4]: https://github.com/open-source-parsers/jsoncpp [5]: https://github.com/google/googletest [6]: https://github.com/systemd/systemd/tree/master/src/libsystemd/ oomd-0.5.0/clang-format.sh000077500000000000000000000003271406470533700153760ustar00rootroot00000000000000#!/bin/bash set -euo pipefail if ! command -v clang-format &> /dev/null; then echo "Please install clang-format first" exit 1 fi find . \( -iname '*.[ch]' -o -iname '*.cpp' \) -exec clang-format -i {} \; oomd-0.5.0/docs/000077500000000000000000000000001406470533700134135ustar00rootroot00000000000000oomd-0.5.0/docs/auxiliary_plugins.md000066400000000000000000000005461406470533700175120ustar00rootroot00000000000000# Auxiliary plugins Auxiliary plugins are plugins that are either not generic enough to be be considered core or require optional dependencies. # Actions ## systemd_restart ### Arguments service post_action_delay=15 (optional) dry=false (optional) ### Description Restarts systemd service: `service`. STOP on success, CONTINUE otherwise. oomd-0.5.0/docs/configuration.md000066400000000000000000000150221406470533700166040ustar00rootroot00000000000000# Configuration ## Design principles oomd is designed to be as flexible and as extensible as possible. To that end, oomd is configured via a declarative configuration file. The idea is you can have a set of memory protection rules that are orthogonal and intuitive to reason about. In a sense it's a lot like iptables chains work (but much better, I promise). ## Schema oomd configs have a loosely defined BNF: ARG: : NAME: PLUGIN: { "name": NAME, "args": { ARG[,ARG[,...]] } } DETECTOR: PLUGIN DETECTOR_GROUP: [ NAME, DETECTOR[,DETECTOR[,...]] ] ACTION: PLUGIN DROPIN: "disable-on-drop-in": , "detectors": , "actions": SILENCE_LOGS: "silence-logs": "NAME[,NAME[,...]]" POST_ACTION_DELAY: "post_action_delay": "" PREKILL_HOOK_TIMEOUT: "prekill_hook_timeout": "" RULESET: [ NAME, DROPIN, SILENCE_LOGS, POST_ACTION_DELAY, PREKILL_HOOK_TIMEOUT, "detectors": [ [DETECTOR_GROUP[,DETECTOR_GROUP[,...]]] ], "actions": [ [ACTION[,ACTION[,...]]] ], ] ROOT: { "rulesets": [ RULESET[,RULESET[,...]] ], "prekill_hooks": [ PLUGIN ] } In plain english, the general idea is that each oomd config one or more RULESETs. Each RULESET has a set of DETECTOR_GROUPs and a set of ACTIONs. Each DETECTOR_GROUP has a set of DETECTORs. Both DETECTORs and ACTIONs are PLUGIN types. That means _everything_ is a plugin in oomd. The rules on how a conforming config is evaluated at runtime are described in the next section. See [prekill_hooks.md](prekill_hooks.md) for details of the experimental "prekill_hooks" feature. ### Notes * For `SILENCE_LOGS`, the currently supported log entities are * `engine`: oomd engine logs * `plugins`: logs written by plugins * `post_action_delay` may be overridden by an action plugin's arg of the same name. After an ACTION returns STOP, the ruleset is paused for post_action_delay seconds. ## Runtime evaluation rules * Every plugin must return CONTINUE, STOP or ASYNC_PAUSE. * CONTINUE * For DETECTORs, noop chain * For ACTIONs, continue executing the current ACTION chain * STOP * For DETECTORs, evaluate the current DETECTOR_GROUP chain to false * For ACTIONs, abort execution of the current ACTION chain * ASYNC_PAUSE * For DETECTORs, not supported. If used, noop (in other words, CONTINUE) * For ACTIONs, pause the action chain until the next event loop tick. * DETECTOR_GROUPs evaluate true if and only if all DETECTORs in the chain return CONTINUE * For each RULESET, if _any_ DETECTOR_GROUP fires, the associated ACTION chain will begin execution * ACTIONs may take multiple event loop ticks to complete. Returning ASYNC_PAUSE allows other RULESETs and all DETECTORs to run concurrently. An ACTION returning ASNYC_PAUSE will be run() again on the next tick, allowing it to do more work and either re-ASYNC_PAUSE, or STOP or CONTINUE. If it CONTINUEs, the ACTION chain will resume executing the subsequent ACTION plugins. ### Notes * For each event loop tick, all DETECTORs and DETECTOR_GROUPs will be run. This is to allow any detectors implementing sliding windows, if any, to update their windows ## Example This example uses the JSON front end. At time of writing (11/20/18), JSON is the only supported config front end. The config compiler has been designed with extensibility in mind as well. It would not be difficult to add another config front end. { "rulesets": [ { "name": "memory pressure protection", "detectors": [ [ "workload is under pressure and system is under a lot of pressure", { "name": "pressure_rising_beyond", "args": { "cgroup": "workload.slice", "resource": "memory", "threshold": "5", "duration": "15" } }, { "name": "pressure_rising_beyond", "args": { "cgroup": "system.slice", "resource": "memory", "threshold": "40", "duration": "15" } } ], [ "system is under a lot of pressure", { "name": "pressure_rising_beyond", "args": { "cgroup": "system.slice", "resource": "memory", "threshold": "80", "duration": "30" } } ] ], "actions": [ { "name": "kill_by_memory_size_or_growth", "args": { "cgroup": "system.slice/*" } } ] }, { "name": "low swap protection", "detectors": [ [ "swap is running low", { "name": "swap_free", "args": { "threshold_pct": "15" } } ] ], "actions": [ { "name": "kill_by_swap_usage", "args": { "cgroup": "system.slice/*,workload.slice/workload-wdb.slice/*,workload.slice/workload-tw.slice/*" } } ] } ] } This config, in english, says the following: * If the workload is under a memory pressure AND the system is under a moderate amount of pressure, kill a memory hog in the system * If the systems is under a lot of memory pressure, kill a memory hog in the system * If the system is running low on swap (this can cause pathological conditions), kill the cgroup using the most swap across the system and workloads. oomd-0.5.0/docs/core_plugins.md000066400000000000000000000223441406470533700164330ustar00rootroot00000000000000# Core plugins Note that this document is organized in two sections: detectors and actions. In theory, there's nothing stopping you from using a detector as an action and an action as a detector. The oomd runtime is completely agnostic. However, it would probably not be very useful in many cases. # Detectors ## pressure_rising_beyond ### Arguments cgroup resource threshold duration fast_fall_ratio=0.85 (optional) ### Description `cgroup` specifies the "parent" cgroup(s) to monitor. Eg. if cgroup=system.slice, we would be monitoring everything inside system.slice. `cgroup` supports multi-cgroup and wildcard paths. Eg: cgroup=workload.slice/workload-*.slice,system.slice Note that extra spaces are not permitted betwen ','s. The root host can be encoded as "/". `resource` is io|memory CONTINUE if 1m pressure > `threshold` for longer than `duration` && trending above threshold (10s > `threshold`) && 10s not falling rapidly. STOP otherwise. ## memory_above ### Arguments cgroup threshold (optional) threshold_anon (optional) duration ### Description `cgroup` has the same semantics and features as `pressure_rising_beyond`. `threshold` and `threshold_anon` take either an absolute memory amount or a percentage of total memory used. Either one of these parameters must be specified. When both are specified, only `threshold_anon` is effective. An absolute memory amount threshold accepts combinations of K|M|G|T suffixed components. For example, `1.5M 32K 512` is interpreted as `1.5 * 2^20 + 32 * 2^10 + 512` bytes. NOTE: FOR BACKWARD COMPATIBILITY, A BARE NUMBER IS INTERPRETED AS MEGABYTES. A percentage threshold must be in the format `N%`, where `0 <= N <= 100`. If `threshold` is specified, CONTINUE if total memory usage > `threshold` longer than `duration`, STOP otherwise. If `threshold_anon` is specified, CONTINUE if anonymous memory usage > `threshold_anon` longer than `duration`, STOP otherwise. ## pressure_above ### Arguments cgroup resource threshold duration ### Description `cgroup` has the same semantics and features as `pressure_rising_beyond`. `resource` is io|memory CONTINUE if 10s pressure > `threshold` longer than `duration` STOP otherwise. ## memory_reclaim ### Arguments cgroup duration ### Description `cgroup` has the same semantics and features as `pressure_rising_beyond`. CONTINUE if `cgroup`'s memory has been reclaimed in the past `duration` period. STOP otherwise. ## swap_free ### Arguments threshold_pct ### Description CONTINUE if percentage of free:total swap drops below `threshold_pct` %. STOP otherwise. ## exists ### Arguments cgroup negate=false (optional) ### Description `cgroup` supports comma separated arguments and wildcards. When `negate` is `false`, if `cgroup` exists, CONTINUE. STOP otherwise. When `negate` is `true`, if `cgroup` doesn't exist, CONTINUE. STOP otherwise. ## nr_dying_descendants ### Arguments cgroup count lte=true (optional) negate=false (optional) ### Description `cgroup` supports comma separated arguments and wildcards. The plugin triggers if any of the globbed cgroups matches the condition. When `lte` is `true`, if `nr_dying_descendants(cgroup) <= count`, CONTINUE. STOP otherwise. When `lte` is `false`, if `nr_dying_descendants(cgroup) > count`, CONTINUE. STOP otherwise. ## dump_cgroup_overview ### Arguments cgroup always=false ### Description `cgroup` has the same semantics and features as `pressure_rising_beyond`. However, this detector cannot monitor the root host. Dumps the system overview for `cgroup` to stderr if memory pressure is non-negligible. If `always` is set to `true`, then cgroup overviews will always be printed. Always returns CONTINUE. # Actions ## kill_by_memory_size_or_growth ### Arguments cgroup recursive=false (optional) size_threshold=50 (optional) min_growth_ratio=1.25 (optional) growing_size_percentile=80 (optional) post_action_delay=15 (optional) dry=false (optional) always_continue=false (optional) ### Description `cgroup` specifies the cgroup(s) that should be considered for killing. Eg. if cgroup=system.slice/*, everything inside system.slice would be considered for killing. `cgroup` also support multi-cgroup and wildcard paths. Eg. cgroup=workload.slice/workload-*.slice/*,system.slice/* Note that extra spaces are not permitted betwen ','s. If `recursive` is set, walk down the cgroup tree looking for the best leaf to kill. Comparisons happen locally, between siblings, using the kill plugin's specific heuristics. The cgroups listed in `cgroup` are treated as the initial set of siblings. If you want a cgroup subtree to be killed all together or not at all, set its memory.oom.group=1. One might express the example above using `recursive` as cgroup=workload.slice/workload-*.slice/,system.slice/ recursive=true Note the lack of trailing "*". Kill the biggest (memory.current - memory.low) child cgroup if larger than `size_threshold` percent or kill the fastest growing over `min_growth_ratio` of the biggest `growing_size_percentile` by size. True if killed something, which terminates actions. Disables its action chain for `post_action_delay` if a kill was performed. Other rulesets will run normally. This plugins' ruleset's detectors will run, but will not trigger any actions. If `dry` is set to true, does not actually perform kill but prints via logs what kill it would have done. Avoids killing cgroups which aren't experiencing memory pressure at all as they aren't contributing the pressure anyway. cgroups that are killed have the "trusted.oomd_kill" xattr set to the number of SIGKILLs sent to resident processes. cgroups with the "trusted.oomd_prefer" xattr set will be killed before any other cgroups, even if others are better choices by the above logic. A cgroup with "trusted.oomd_avoid" will not be killed unless there are no other cgroups to kill. If multiple cgroups are "trusted.oomd_prefer"ed, the above logic will be used to pick between them. If a cgroup has both of these xattrs it is considered "prefer". The xattrs must be set on cgroups targeted in the `cgroup` arg; they will have no effect if set on ancestors of the targeted cgroups. STOP if killed something (even if dry=true), unless `always_continue`. CONTINUE otherwise. ## kill_by_swap_usage ### Arguments cgroup recursive=false (optional) threshold=1 (optional) post_action_delay=15 (optional) dry=false (optional) always_continue=false (optional) ### Description `cgroup` and `recursive` follow the same semantics and options as `kill_by_memory_size_or_growth`. oomd_prefer/oomd_avoid xattrs are respected the same way as well. `threshold` follows the same semantics and options as `memory_above`. `post_action_delay` and `dry` follow the same semantics and options as `kill_by_memory_size_or_growth` Kills the child with the largest swap usage. cgroups that are killed have the "trusted.oomd_kill" xattr set to the number of SIGKILLs sent to resident processes. STOP if killed something (even if dry=true), unless `always_continue`. CONTINUE otherwise. ## kill_by_pressure ### Arguments cgroup recursive=false (optional) resource post_action_delay=15 (optional) dry=false (optional) always_continue=false (optional) ### Description `cgroup` and `recursive` follow the same semantics and options as `kill_by_memory_size_or_growth`. oomd_prefer/oomd_avoid xattrs are respected the same way as well. `resource` is io|memory `post_action_delay` and `dry` follow the same semantics and options as `kill_by_memory_size_or_growth` Kills the child generating the most pressure. cgroups that are killed have the "trusted.oomd_kill" xattr set to the number of SIGKILLs sent to resident processes. STOP if killed something (even if dry=true), unless `always_continue`. CONTINUE otherwise. ## kill_by_io_cost ### Arguments cgroup recursive=false (optional) post_action_delay=15 (optional) dry=false (optional) always_continue=false (optional) ### Description `cgroup` and `recursive` follow the same semantics and options as `kill_by_memory_size_or_growth`. oomd_prefer/oomd_avoid xattrs are respected the same way as well. `post_action_delay` and `dry` follow the same semantics and options as `kill_by_memory_size_or_growth` Kills the child generating the most io cost. cgroups that are killed have the "trusted.oomd_kill" xattr set to the number of SIGKILLs sent to resident processes. STOP if killed something (even if dry=true), unless `always_continue`. CONTINUE otherwise. ## kill_by_pg_scan ### Arguments cgroup recursive=false (optional) post_action_delay=15 (optional) dry=false (optional) always_continue=false (optional) ### Description `cgroup` and `recursive` follow the same semantics and options as `kill_by_memory_size_or_growth`. oomd_prefer/oomd_avoid xattrs are respected the same way as well. `post_action_delay` and `dry` follow the same semantics and options as `kill_by_memory_size_or_growth` Kills the child with the highest pg scan rate. cgroups that are killed have the "trusted.oomd_kill" xattr set to the number of SIGKILLs sent to resident processes. STOP if killed something (even if dry=true), unless `always_continue`. CONTINUE otherwise. oomd-0.5.0/docs/drop_in_configs.md000066400000000000000000000112741406470533700171040ustar00rootroot00000000000000# Drop-in configs Drop-in configs are a way to modify oomd's rule engine at runtime. For details on regular (non-drop-in) configuration, please see [configuration.md](configuration.md). ## Background Drop-in configs are useful in environments hosting container or container-like environments. For example, consider the following cgroup hierarchy: ``` / ├── system.slice │   └── chef.service └── workload.slice └── workload-container ├── sidecar └── task ├── nested-containers │   ├── container1 │   ├── container2 │   └── container3 └── nested-containers-sidecar ``` oomd configs will usually set `task` as the kill target. This is correct and optimal most of the time. However, in cases where containers are nested, eg. when a container hosts its own container-like environment, this becomes suboptimal. In our example, `task` holds its own set of nested containers, `container1`, `container2`, and `container3`. With the standard oomd config, all three nested containers will be killed if any of the three OOM. What would be nice is if `task` could tell oomd to kill one of the three nested containers. Then also if the container is migrated off the host, the modification is removed. This is what drop-in configs accomplish. ## Configuration ### Base config You must make base config modifications to enable drop-in configs. `disable-on-drop-in` disables execution of the targeted base ruleset if a drop-in config is added. `detectors` and `actions` enable drop-in configs for detector groups and the action chain, respectively. By default all options are off. ### Drop-in config Drop-in configs use the exact same format as base configs with the following caveat: * Both detector groups and actions may be omitted ## Usage Run oomd with `--drop-in-dir DIR` to have oomd monitor `DIR` for drop-in configs. Whenever a file is modified-in or moved-into `DIR`, oomd will try to parse the file as an oomd config. If it parses and compiles, oomd will try to inject the drop-in config into the engine. Filenames beginning with '.' are ignored. ### Mechanism * Every drop-in config must target a ruleset in the base config * Targeting is done with the ruleset `name` * If there is more than one match, the first base ruleset will be chosen * If more than one drop-in config is present, they are added last-in-first-out (LIFO) order * A single drop-in config may contain more than one ruleset * Drop-in configs override the targeted ruleset on detector group or action group granularity * Behind the scenes, oomd will clone a copy of the targeted ruleset and replace the dropped-in detector groups and action groups. That new ruleset will be run before the targeted ruleset. ## Example ### Base config ``` { "rulesets": [ { "name": "user session protection", "drop-in": { "detectors": true, "actions": true, "disable-on-drop-in": true }, "detectors": [ [ "user pressure above 60 for 30s", { "name": "dump_cgroup_overview", "args": { "cgroup": "user.slice,workload.slice,www.slice" } }, { "name": "pressure_above", "args": { "cgroup": "user.slice,workload.slice,www.slice", "resource": "memory", "threshold": "60", "duration": "30" } }, { "name": "memory_reclaim", "args": { "cgroup": "user.slice,workload.slice,www.slice", "duration": "10" } } ] ], "actions": [ { "name": "continue" } ] } ] } ``` ### Drop-in config ``` { "rulesets": [ { "name": "user session protection", "detectors": [ [ "system pressure above 80 for 60s", { "name": "dump_cgroup_overview", "args": { "cgroup": "system.slice", "always": true } } ] ] } ] } ``` oomd-0.5.0/docs/io_cost.md000066400000000000000000000057711406470533700154060ustar00rootroot00000000000000# IO Cost IO cost is one of the many metrics (swap, memory, pressure, etc) logged by oomd periodically, based on which the plugins can decide if an alarm should be triggered, or which cgroup should be cleaned up. This documentation provides some background about IO cost in the context of oomd. ## What is IO Cost IO cost is a unit-less metric measuring how much load is put on some IO devices by a cgroup. It is based on a simple model using bandwidth and iops of read, write, and trim to approximate some definition of load. In order to calculate IO cost, oomd reads the [`io.stat`](https://facebookmicrosites.github.io/cgroup2/docs/io-controller.html#interface-files) file in each cgroup, from where the cumulative IOs and bytes of read/write/trim are obtained. Then the difference between two consecutive intervals divided by the duration is bandwidth and iops. The dot-product between some coefficients and the measurements is the IO cost. By default, these are the coefficients for HDD and SSD devices: ``` static const struct Oomd::IOCostCoeffs default_hdd_coeffs = { .read_iops = 1.31e-3, .readbw = 1.13e-7, .write_iops = 2.58e-1, .writebw = 5.04e-7, .trim_iops = 0, .trimbw = 0, }; static const struct Oomd::IOCostCoeffs default_ssd_coeffs = { .read_iops = 1.21e-2, .readbw = 6.25e-7, .write_iops = 1.07e-3, .writebw = 2.61e-7, .trim_iops = 2.37e-2, .trimbw = 9.10e-10, }; ``` These number are obtained through experiments with disks running on Facebook servers. They may not match other IO devices and therefore running experiments with your own devices is recommended. ## How to Configure IO Cost IO cost in oomd is configured by command line arguments: ``` --device DEVS --hdd-coeffs COEFFS --ssd-coeffs COEFFS ``` ### `--device DEVS` This option tells oomd what are the root devices, or ones that will contribute to calculating the IO cost. The `io.stat` file can have multiple lines, one for each IO device that the cgroup has interacted with. Only lines belong to the root devices will be used to calculated the IO cost. This option expects `DEVS` in the format of comma separated `:` pairs of devices, e.g. `252:1,253:1` for two devices, one with major=252 and minor=1 and the other with major=253 and minor=1. This will tell oomd to calculate IO cost by summing the bandwitdh and iops data of both `252:1` and `253:1`. ### `--hdd-coeffs COEFFS` This option specifies an alternative to the `default_hdd_coeffs` shown above. It expects a comma separated list of numeric values, which will be coefficients in the order of read iops, read bandwidth, write iops, write bandwidth, trim iops, and trim bandwidth. See `std::stod` for support numeric formats. If less than 6 values are passed, remaining ones are set zero. Coefficients for trim are likely ignored and should not be passed. ### `--ssd-coeffs COEFFS` This option specifies an alternative to the `default_ssd_coeffs` shown above. It has the same format as `--hdd-coeffs` but coefficients for trim should be included. oomd-0.5.0/docs/prekill_hooks.md000066400000000000000000000125461406470533700166120ustar00rootroot00000000000000# Prekill Hooks Prekill hooks are an experimental generic, pluggable way to do work just before oomd kills a cgroup. ## Background Owners of an oomed process may want a heap dump or other memory statistics of the killed program at the time it died to get insight into potential misbehavior. Prekill hooks direct oomd to collect these metrics, or do other arbitrary work, just before it kills a cgroup. It is a generic interface not tied to any particular metric collection approach or, specifically metric collection at all. Hooks may timeout, and should not be assumed to run to completion. Process owners should know the kernel may oom kill their code separately from oomd, in which case prekill hooks will obviously not run at all. ## Configuration Prekill hooks are configured the oomd.json config json in a top-level "prekill_hooks" key, adjacent to "rulesets". Prekill hooks are at the top level because they run on every kill oomd makes, across all rulesets. Prekill hooks are not interchangeable with plugins but are configured in the same way, via "name" and "args". Hooks can't be used where plugins are expected, and vice versa. { "rulesets": [ ... ], "prekill_hooks": [ { "name": "hypothetical_prekill_hook", "args": { "cgroup": "/foo,/bar/*/baz" } } ] } On a kill, the oomd runs the first configured prekill hook whose "cgroup" arg matches the path of the cgroup to be killed. At most one prekill hook runs per kill. Dropins may contain prekill_hooks. Dropped-in prekill hooks get priority over those in the base configuration. Like ruleset dropins, prekill hook dropins added later get higher priority. The "cgroup" arg is a list of comma-separated patterns. Patterns are cgroup paths, except path components may be "*". No other glob matching works except star for a single whole path component. A cgroup path matches a pattern if it 1) exactly matches the pattern, 2) is an ancestor of a path that would match the pattern, or 3) is a descendant of a path that matches the pattern. To run on all kills, set `"cgroup": "/"`. Rulesets may set a "prekill_hook_timeout" in seconds. If unset, the default is 5 seconds. { "rulesets": [ { "name": "memory pressure protection", "prekill_hook_timeout": "30", "detectors": [...], "actions": [...] ], "prekill_hooks": [...] } The prekill hook timeout sets a window for all prekill hooks in an action chain to finish running. For example, consider: - a ruleset with two kill plugin actions and a 5s prekill hook timeout - the action chain fires - the first action targets /foo.slice and fires a prekill hook on it - the prekill hook finishes in 3s - /foo.slice fails to die, so the first action returns CONTINUE - the second kill plugin runs, targets /bar.slice, and fires a prekill hook The second prekill hook only has 2s to run before it times out, since it's been 3s (or more) since the action chain started, and the action chain set a 5s max window for prekill hooks to run. ## API Prekill hook implementers should subclass PrekillHook and PrekillHookInvocation and implement these core methods: /* same as BasePlugin::init(args, context) */ int PrekillHook::init( const Engine::PluginArgs& args, const PluginConstructionContext& context); /* main method for a hook, called just before the cgroup is killed */ std::unique_ptr PrekillHook::fire( const CgroupContext&); /* Invocation object returned from fire() is polled to see when the hook has finished running, and killing may begin */ bool PrekillHookInvocation::didFinish() /* Invocation object is destructed either when it finishes, or early if it times out */ PrekillHookInvocation::~PrekillHookInvocation() Hooks are kicked off with PrekillHook::fire(cgroup) with the cgroup oomd intends to kill. Oomd is designed as a single threaded event loop, so fire() shouldn't do long work that blocks the main thread. Instead, it vends an Invocation object which will be polled every main loop tick (typically 1s) for didFinish(). The cgroup will not be killed until didFinish() returns true, or we reach a timeout. If oomd determines a PrekillHookInvocation timed out, it is destructed and PrekillHookInvocation::~PrekillHookInvocation() called. The destructor will be called before the cgroup is killed, regardless of whether the hook timed out or didFinish() returned true. All methods (fire, didFinish, ~PrekillHookInvocation) will be always be called on the main thread and should not block for nontrivial time. If blocking work is needed, it should be done in other threads, possibly spawned in PrekillHook::init(). ## Guarantees - At most one prekill hook will be running per ruleset at any moment. There may be multiple instances of a prekill hook running at the same time, as part of different rulesets. - If a prekill hook is run on a cgroup, the cgroup is not guaranteed to die. Oomd may fail to kill it. (Oomd will then pick a different cgroup to try to kill, and again call the prekill hook on its new target before trying to kill it.) - PrekillHooks are not guaranteed to outlive the Invocations they fire(). Invocations should encapsulate any data they need to run to completion. oomd-0.5.0/docs/production_setup.md000066400000000000000000000057521406470533700173540ustar00rootroot00000000000000# Production setup The document covers how oomd is set up in production at Facebook. ## Host setup ### kernel command line The follow command line arguments must be set: * `swapaccount=1` * or equivalent, if the kernel has this turned on via compile time flags * `systemd.unified_cgroup_hierarchy=1` * or equivalent, if systemd has this turned on via compile time flags ### cgroup2 The host must be running unified cgroup (cgroup2) alone. oomd is not designed to support mixed (legacy and unified) hierarchies. oomd expects the cgroup2 filesystem to be mounted at `/sys/fs/cgroup` but the default can be changed with `--cgroup-fs`. ### systemd The host must be managed by systemd. Furthermore, resource accounting must be turned on for all units monitored by oomd. The easiest way to turn on resource accounting is by changing the system defaults: ``` DefaultCPUAccounting=true DefaultIOAccounting=true DefaultMemoryAccounting=true DefaultTasksAccounting=true ``` Refer to https://www.freedesktop.org/software/systemd/man/systemd-system.conf.html#DefaultCPUAccounting= for more details. ### kernel w/ PSI (pressure stall information) oomd requires PSI to function. Kernels 4.20 and above should have PSI. Verify your kernel has been compiled with PSI by running: ``` $ zcat /proc/config.gz | grep CONFIG_PSI CONFIG_PSI=y ``` ### swap The system must have swap enabled for oomd to function correctly. With swap enabled, the system spends enough time swapping pages to let oomd react. Without swap, the system enters a livelocked state much more quickly and may prevent oomd from doing in a reasonable amount of time. While this kind of sounds like a crutch, swap is generally very good on modern systems. See https://chrisdown.name/2018/01/02/in-defence-of-swap.html for more details on swap. The current recommendation for swap size is at least 1x size of physical memory. ## Service setup oomd must be run in a protected cgroup. In other words, we use a specialized systemd service setup to guarantee oomd resources so that oomd can act in resource starved scenarios. You typically group host critical services in their own special cgroup. Perhaps named `hostcritical.slice`. oomd should be grouped in here with other host critical services like `sshd.service`. `hostcritical.slice` should be guaranteed a minimum amount of reserved memory via `memory.min`. This essentially `mlockall`s oomd except the kernel sets aside the memory ahead of time. A portion of that memory - 64M should be enough - should be given to oomd. The resulting config should look something like: ``` $ systemctl cat oomd.service | grep Memory MemoryMin=64M MemoryLow=64M $ systemctl show fb-oomd.service | grep ControlGroup ControlGroup=/hostcritical.slice/oomd.service ``` TODO: document io.latency config ## Monitoring Stats are currently collected by grep'ing through oomd logs files. This is obviously very brittle but work is underway to provide a structured stats interface. TODO: document the WIP structured stats collection interface oomd-0.5.0/docs/release_process.md000066400000000000000000000015721406470533700171200ustar00rootroot00000000000000# Release process This document describes how to release a new oomd version. ## Branching model Every change goes directly onto master. Releases will simply tag a commit from master. ## Semantic versioning We choose to follow semantic versioning. Note that this doesn't matter much for major version < 1 but will matter a lot for >= 1.0.0 releases. ## Tagging a release 1. Make sure master builds and passes all tests. 1. Update the `version` field in `meson.build`. The format is `v..`. 1. Tag a release. We do this in the github UI by clicking "releases" (on same line as "commits"), then "Draft a new release". The tag should be the same as in `meson.build`. The title should be in `X.Y.Z` format. The tag description should include some high level notes and a link to the appropriate commit log. Please see previous releases for an example. oomd-0.5.0/docs/stats.md000066400000000000000000000024651406470533700151020ustar00rootroot00000000000000# Structured stats collection The structured stats collection is a threadsafe key-value store, with `std::string` for keys and `int` for values. It can be also be queried from a different process using the StatsClient class. ## Internal API ### Get stats `std::unordered_map getStats()` Returns an unordered map copy of the current stats. ### Increment a key-value pair in stats `int setStats(const std::string& key, int val)` This increments the value of the corresponding key by val. Returns 0 upon success, and 1 on error. ### Set a key-value pair in stats `int setStats(const std::string& key, int val)` Sets the corresponding key in stats to val. Returns 0 upon success, and 1 on error. ### Reset stats `int Oomd::resetStats()` Sets the value of all existing key-value pairs in the stats collection to 0. Returns 0 upon success, and 1 on error. ## External interface Command line flags: ##### `-d` Dumps all accumulated stats to stdout in a JSON string. $ /path/to/binary -d { "oomd.kills_structured" : 1, "oomd.restarts_structured" : 2 } $ /path/to/binary -d | jq '.["oomd.kills_structured"]' 1 #####`-r` Reset stats by setting all values to 0. Notes: If both `-d` and `-r` are included, `-d` is completed first. oomd-0.5.0/docs/writing_a_kill_plugin.md000066400000000000000000000165371406470533700203250ustar00rootroot00000000000000# Writing a kill plugin Kill plugins are regular plugins: they inherit from `BaseKillPlugin` which inherits from `BasePlugin`. Read [writing_a_plugin.md](writing_a_plugin.md) first; everything in that doc applies to kill plugins as well. # BaseKillPlugin default behavior Kill plugins are responsible for the policy picking which cgroup to kill out of a set of options. The mechanism of killing, with support for all the standard kill plugin behavior, is implemented by `BaseKillPlugin`. `BaseKillPlugin` subclasses by default have support for the `cgroup`, `recursive`, `dry`, `debug`, and `post_action_delay` params as described in [core_plugins.md](core_plugins.md). Additionally, plugins that follow the `BaseKillPlugin` template respect oomd.prefer and oomd.avoid, though technically that’s not part of `BaseKillPlugin`. # Interface The `BaseKillPlugin` interface is found in `plugins/BaseKillPlugin.h`. `BaseKillPlugin` is a pure virtual class that defines what is expected of each plugin. This document assumes you have already read through `engine/BasePlugin.h`. If you have not, please do. There are two methods you must override: virtual std::vector rankForKilling( OomdContext& ctx, std::vector& cgroups, std::function& olog_kill_fn_out) = 0; virtual void ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) = 0; and two you may want to override: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context); virtual void prerun(OomdContext& context) {}; Note that these are different from the 3 `BasePlugin` methods `run`, `init`, and `prerun`. `run` has been implemented for you in `BaseKillPlugin`, and will call out to the subclass’ `rankForKilling` and `ologKillTarget` implementations. `init` has a default implementation which you may wish to override, but unlike `BasePlugin`s, are not required to. `prerun` is the same. # Anatomy of KillIOCost When creating a new kill plugin, it’s easiest to copy the files of an existing kill plugin and follow their format. KillIOCost is a simple, useful plugin that uses most of the APIs we’re interested in. It’s spread across 3 files, plus entries in the TARGETS and meson.build files. ### KillIOCost.h #include "oomd/plugins/BaseKillPlugin.h" namespace Oomd { template class KillIOCost : public Base { KillIOCost inherits from a templated base class to facilitate unit testing. The base class is always `BaseKillPlugin`, except in CorePluginsTest.cpp where we pass in `BaseKillPluginMock` to mocks out the killing. It's safe to assume `Base = BaseKillPlugin`. public: void prerun(OomdContext& ctx) override; static KillIOCost* create() { return new KillIOCost(); } ~KillIOCost() override = default; protected: std::vector rankForKilling( OomdContext& ctx, std::vector& cgroups, std::function& olog_kill_fn_out) override; KillIOCost implements `prerun` and `rankForKilling`. Other plugins may want to override `init` as well. }; } // namespace Oomd #include "oomd/plugins/KillIOCost-inl.h" Because KillIOCost has a templated base class, its method implementations can't be in a `.cpp` file. ### KillIOCost.cpp #include "oomd/plugins/KillIOCost.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(kill_by_io_cost, KillIOCost<>::create); } // namespace Oomd The `.cpp` file just registers the `kill_by_io_cost` plugin. List the `.cpp` file in TARGETS and meson.build or the plugin will not be registered. ### KillIOCost-inl.h namespace Oomd { template int KillIOCost::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { // additional arg parsing and initialization here return Base::init(args, context); } `BaseKillPlugin` implements `init(...)`, parsing the `cgroup`, `recursive`, `post_action_delay`, `dry`, and `debug` plugin args by default. To eg. support additional plugin-specific arguments, override init(...) and include a call to `Base::init` as above. In the real code `KillIOCost` does not override `init(...)` because `BaseKillPlugin::init(...)` is sufficient. template void KillIOCost::prerun(OomdContext& ctx) { // Make sure temporal counters be available when run() is invoked Base::prerunOnCgroups( ctx, [](const auto& cgroup_ctx) { cgroup_ctx.io_cost_rate(); }); } `Base::prerunOnCgroups(...)` supports `"recursive"` by default. template std::vector KillIOCost::rankForKilling( OomdContext& ctx, const std::vector& cgroups) { `BaseKillPlugin::run` calls `rankForKilling` repeatedly as it walks down the cgroup tree. `BaseKillPlugin::run` handles getting the CgroupContexts from the plugin's `"cgroup"` arg, recursing (or not) if the plugin's `"recursive"` arg is set, respecting memory.oom.group, and actually killing the appropriate pids. `KillIOCost::rankForKilling(...)` is responsible for picking which cgroup to kill from among the plugin's `"cgroup"` argument, or among a set of siblings if we're recursing. See comment in `BaseKillPlugin.h` for in-depth details. We return a sorted vector, instead of the single best cgroup, because if killing the best-choice fails, we'll try to kill the next-best, and so on. return OomdContext::sortDescWithKillPrefs( cgroups, [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.io_cost_rate().value_or(0); }); `sortDescWithKillPrefs` adds default support for `oomd_prefer` and `oomd_avoid`. } template void KillIOCost::ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& /* unused */) { OLOG << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on io cost generation at " << target.io_cost_rate().value_or(0); } `ologKillTarget` is called every time a cgroup is chosen from the cgroups returned from `rankForKilling`. KillIOCost uses it to log io_cost_rate() to help readers of the logs understand why this cgroup was chosen. `ologKillTarget` will be called at least once per `rankForKilling`, on the first element returned by `rankForKilling`. If killing that cgroup fails, `ologKillTarget` will be called on subsequent elems returned by `rankForKilling`, in order, as we try to kill them. If `"recursive"` is set, `ologKillTarget` will be called on every cgroup in the path down to the victim leaf cgroup. The 3rd argument of `ologKillTarget` is the set of cgroups `target` was selected from. See KillMemoryGrowth for an example where this is useful to log. } } // namespace Oomd oomd-0.5.0/docs/writing_a_plugin.md000066400000000000000000000174461406470533700173120ustar00rootroot00000000000000# Writing a plugin Plugins are at the core of oomd. Everything that implements business logic must be done in a plugin. If you haven't already, you should read [configuration.md](configuration.md). That doc explains the high level goals of plugins. If you're writing a kill plugin, you can get most common kill plugin functionality by inheriting from BaseKillPlugin. Read this doc, then read [writing_a_kill_plugin.md](writing_a_kill_plugin.md). ## Interface The `BasePlugin` interface is found in `engine/BasePlugin.h`. `BasePlugin` is a pure virtual class that defines what is expected of each plugin. This document assumes you have already read through `engine/BasePlugin.h`. If you have not, please do. Ignoring the comments and less relevant bits, every plugin must implement the following two methods: virtual int init( const PluginArgs& args, const PluginConstructionContext& context) = 0; /* where PluginArgs is an alias of std::unordered_map */ virtual PluginRet run(OomdContext& context) = 0; and optionally implement: virtual void prerun(OomdContext& context){} ### init(..) The `init(..)` method is called by the config compiler. The config compiler transforms (typically) JSON configuration into actual data structures oomd will work with. As part of the compilation process, oomd will run `init(..)` on every instantiated plugin. `MonitoredResources& resources` is a typedef'd std::unordered_set of strings. Plugins should insert any cgroups they want accounting on into `resources`. Accounting information will be passed back to the plugin at each event loop tick in `run(..)` via `OomdContext& context`. Kill plugins will typically place any cgroups they are instructed to possibly kill into `resources`. `const PluginArgs& args` is a map of arguments that are provided to the plugin. Each plugin, as defined by the config schema, is allowed to have a JSON object containing string->string key/value pairs representing the configuration. `const PluginConstructionContext& context` holds other init()-time context. `context->cgroupFs()` is the cgroup fs that oomd will monitor, as set from the --cgroup-fs command line flag, defaulting to /sys/fs/cgroup. If plugin initialization is success, the plugin must return zero. A non-zero return value will fail the config compilation process (and usually exit the process). Plugins are encouraged to print a useful error message before returning non-zero. ### run(..) `run(..)` is called by the core oomd runtime each event loop tick. The duration between each tick can be configured via `--interval,-i` on the command line. `run(..)` is the work horse function of every plugin. This is where most, if not all, of the work is expected to be done. You can do pretty much whatever you want in the plugin. Make syscalls, inspect the file system, mess with other plugins by modifying `OomdContext`, name it. (Not to imply doing all of those things are a productive idea). `OomdContext& context` is an object that contains state about the system. `OomdContext&` typically contains accounting information on every cgroup that the core oomd runtime has been instructed to monitor. This means that cgroups other plugins placed into `resources` in `init(..)` will also be available. ### prerun(..) `prerun(..)` is called by the core oomd runtime each event loop tick, before `run(..)` has been called on any plugin. It is guaranteed to be invoked as long as the plugin is enabled, even if it is an action plugin and not triggered. Therefore, it is designed to execute stateful logic, such as calculating sliding window metrics, storing time when a threshold is exceeded, etc. If the plugin may rely on temporal cgroup counters such as average usage and io cost rate (see `CgroupContext.h`) in `run(..)`, it must implement `prerun(..)` to retrieve temporal counters for all of its cgroups to keep them from getting stale. See `KillIOCost` for example. ## Plugin registration You might have wondered, how does the config compiler know which plugin name maps to which C++ class? This section goes into the details of plugin registration. oomd employs a static plugin registration system. In other words, oomd plugins will insert themselves into a map of plugin name -> plugin factory method. The details of static registration are out of the scope of this document, but plenty of sources exist online that explain the details. In short, static registration ensures that the map of X -> Y will be fully populated before the program reaches `int main()`. Plugins are required to register themselves to the plugin registry via the `REGISTER_PLUGIN` macro defined in `oomd/PluginRegistry.h`. If you do not register your plugin and try to use it in a config, the compilation process will fail and oomd will not start up. Note that `REGISTER_PLUGIN` must be a static factory method that returns a pointer to an instance of your plugin allocated on the heap. There is not currently support to register custom deleter functions. This means you may not use custom allocators (by overloading `new`/`delete` or otherwise) to create instances of your plugin. ## Logging Plugins are encouraged to use the oomd logging facilities. `OLOG` is an ostream style macro that prints logs asynchronously. This is useful as systems under intense memory pressure are not usually able to write to filesystems or output things to stdout/sterr. It's usually not a good idea to log inline on a production host, as writes can block indefinitely, thus limiting the utility of oomd. `OLOG` is also smart enough to log inline (ie not async) when run in unit tests. Logging async in unit tests can mess with gtest output parsing. ## Anatomy of ContinuePlugin There is a functioning (but useless) example plugin included in oomd's set of core plugins: ContinuePlugin. You can find the code at `plugins/ContinuePlugin.h` and `plugins/ContinuePlugin.cpp.` ### ContinuePlugin.h #pragma once #include "oomd/engine/BasePlugin.h" namespace Oomd { Plugins do not need to exist in the `Oomd` namespace but it'll save you some typing. class ContinuePlugin : public Engine::BasePlugin { All plugins must derive from `BasePlugin`, as discussed in the previous section. public: int init( Engine::MonitoredResources& /* unused */, const Engine::PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } The `init(..)` method is implemented inline here. Note that we do not examine our arguments or register any resource requirements. We return 0 to signify success. Engine::PluginRet run(OomdContext& /* unused */) override { return Engine::PluginRet::CONTINUE; } Our plugin does nothing besides return CONTINUE. static ContinuePlugin* create() { return new ContinuePlugin(); } } // namespace Oomd This is our static factory method. Note it has to be static, as the config compiler uses the static plugin registry, and all factory functions in the plugin registry must be static. ~ContinuePlugin() = default; We can use the default destructor. ### ContinuePlugin.cpp #include "oomd/plugins/ContinuePlugin.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(continue, ContinuePlugin::create); } // namespace Oomd The only thing our cpp file does is register our plugin into the plugin registry. Carefully note that we did not register our plugin in the header file. Doing that might work for now, but will cause (somewhat cryptic) errors if someone decides to include your plugin header and subclass some things. What happens in this bad case is that there will be multiple places where your plugin will be registered. The `REGISTER_PLUGIN` macro is designed such that collision will occur if you try to register > 1 plugin with the same name. oomd-0.5.0/man/000077500000000000000000000000001406470533700132365ustar00rootroot00000000000000oomd-0.5.0/man/oomd.1000066400000000000000000000040161406470533700142570ustar00rootroot00000000000000.TH "oomd" "1" .SH NAME oomd \- Userspace Out-Of-Memory (OOM) killer for Linux systems .SH SYNOPSIS .B oomd .RI [ OPTION "] ..." .SH DESCRIPTION .B oomd leverages PSI and cgroupv2 to monitor a system holistically. .B oomd then takes corrective action in userspace before an OOM occurs in kernel space. Corrective action is configured via a flexible plugin system, in which custom code can be written. By default, this involves killing offending processes. This enables an unparalleled level of flexibility where each workload can have custom protection rules. Furthermore, time spent livedlocked in kernelspace is minimized. .SH OPTIONS .TP .B \-\-help, \-h Show this help message and exit .TP .B \-\-version, \-v Print version and exit .TP .BI "\-\-config, \-C " CONFIG Config file (default: \fI/etc/oomd/oomd.json\fR) .TP .BI "\-\-interval, \-i " INTERVAL Event loop polling interval (default: 5) .TP .BI "\-\-cgroup\-fs, \-f " FS Cgroup2 filesystem mount point (default: \fI/sys/fs/cgroup\fR) .TP .BI "\-\-check\-config, \-c " CONFIG Check config file (default: \fI/etc/oomd/oomd.json\fR) .TP .B \-\-list\-plugins, \-l List all available plugins .TP .BI "\-\-drop\-in\-dir, \-w " DIR Directory to watch for drop in configs .TP .BI "\-\-socket\-path, \-s " PATH Specify stats socket path (default: \fI/run/oomd/oomd\-stats.socket\fR) .TP .B \-\-dump\-stats, \-d Dump accumulated stats .TP .B \-\-reset\-stats, \-r Reset stats collection .TP .BI "\-\-device " DEVS Comma separated <\fImajor\fR>:<\fIminor\fR> pairs for IO cost calculation (default: none) .TP .BI "\-\-ssd\-coeffs " COEFFS Comma separated values for SSD IO cost calculation (default: see doc) .TP .BI "\-\-hdd\-coeffs " COEFFS Comma separated values for HDD IO cost calculation (default: see doc) .SH SEE ALSO https://github.com/facebookincubator/oomd#configuration .SH AUTHOR .B oomd is written by .UR https://opensource.fb.com/ Facebook, Inc .UE . This manual page was written by .MT mmyangfl@\:gmail.com Yangfl .ME for the Debian Project (and may be used by others). oomd-0.5.0/meson.build000066400000000000000000000140201406470533700146220ustar00rootroot00000000000000project('oomd', 'cpp', version : 'v0.5.0', meson_version : '>= 0.45', license : 'GPL2', default_options : ['stdsplit=false', 'cpp_std=c++17']) cpp_args = ['-DMESON_BUILD'] inc = include_directories('src') # Plumbing to #define a GIT_VERSION so Main.cpp can print it out vcs_tagger = [meson.source_root() + '/vcs_tagger.sh', meson.source_root(), meson.project_version()] version_h = vcs_tag( input : 'src/oomd/include/Version.h.in', output : 'Version.h', command : vcs_tagger) versiondep = declare_dependency(sources : version_h) srcs = files(''' src/oomd/CgroupContext.cpp src/oomd/PluginConstructionContext.cpp src/oomd/Log.cpp src/oomd/Oomd.cpp src/oomd/OomdContext.cpp src/oomd/Stats.cpp src/oomd/StatsClient.cpp src/oomd/PluginRegistry.cpp src/oomd/config/ConfigCompiler.cpp src/oomd/config/ConfigTypes.cpp src/oomd/config/JsonConfigParser.cpp src/oomd/dropin/DropInServiceAdaptor.cpp src/oomd/dropin/FsDropInService.cpp src/oomd/engine/DetectorGroup.cpp src/oomd/engine/Engine.cpp src/oomd/engine/Ruleset.cpp src/oomd/include/Assert.cpp src/oomd/include/CgroupPath.cpp src/oomd/plugins/BaseKillPlugin.cpp src/oomd/plugins/ContinuePlugin.cpp src/oomd/plugins/StopPlugin.cpp src/oomd/plugins/DummyPrekillHook.cpp src/oomd/plugins/DumpCgroupOverview.cpp src/oomd/plugins/DumpKillInfoNoOp.cpp src/oomd/plugins/MemoryAbove.cpp src/oomd/plugins/MemoryReclaim.cpp src/oomd/plugins/NrDyingDescendants.cpp src/oomd/plugins/PressureAbove.cpp src/oomd/plugins/PressureRisingBeyond.cpp src/oomd/plugins/Senpai.cpp src/oomd/plugins/SwapFree.cpp src/oomd/plugins/Exists.cpp src/oomd/plugins/KillIOCost.cpp src/oomd/plugins/KillMemoryGrowth.cpp src/oomd/plugins/KillSwapUsage.cpp src/oomd/plugins/KillPgScan.cpp src/oomd/plugins/KillPressure.cpp src/oomd/util/Fs.cpp src/oomd/util/Util.cpp src/oomd/util/PluginArgParser.cpp '''.split()) fixture_srcs = files(''' src/oomd/fixtures/FsFixture.cpp src/oomd/util/Fixture.cpp '''.split()) deps = [versiondep, dependency('jsoncpp'), dependency('threads')] # Optional dependencies systemd_dep = dependency('libsystemd', required: false) if systemd_dep.found() srcs += files(''' src/oomd/plugins/systemd/BaseSystemdPlugin.cpp src/oomd/plugins/systemd/SystemdRestart.cpp '''.split()) deps += [systemd_dep] endif oomd_lib = static_library('oomd', srcs, include_directories : inc, cpp_args : cpp_args, install : false, dependencies : deps) oomd_fixture_lib = static_library('oomd_fixture', fixture_srcs, include_directories : inc, cpp_args : cpp_args, dependencies : deps, link_with : oomd_lib) executable('oomd', files('src/oomd/Main.cpp'), include_directories : inc, cpp_args : cpp_args, link_args : ['-lstdc++fs'], dependencies : deps, install: true, link_whole : oomd_lib) prefixdir = get_option('prefix') bindir = join_paths(prefixdir, get_option('bindir')) sysconfdir = join_paths(prefixdir, get_option('sysconfdir')) oomdconfdir = join_paths(sysconfdir, 'oomd') systemunitdir = join_paths(prefixdir, 'lib/systemd/system') substs = configuration_data() substs.set('bindir', bindir) substs.set('sysconfdir', sysconfdir) substs.set('oomdconfdir', oomdconfdir) substs.set('systemunitdir', systemunitdir) configure_file( input : 'src/oomd/etc/oomd.service.in', output : 'oomd.service', configuration : substs, install_dir : systemunitdir) configure_file( input : 'src/oomd/etc/desktop.json', output : 'oomd.json', configuration : substs, install_dir : oomdconfdir) install_man('man/oomd.1') # Core tests # TODO: Normally we'd use a dictionary here, but they are only supported as of # Meson version 0.47.0. Ubuntu 18.04 provides only Meson version 0.45.1. core_tests = [ ['config', files('src/oomd/config/JsonConfigParserTest.cpp')], ['util', files('src/oomd/util/FixtureTest.cpp', 'src/oomd/util/FsTest.cpp', 'src/oomd/util/ScopeGuardTest.cpp', 'src/oomd/util/SystemMaybeTest.cpp', 'src/oomd/util/UtilTest.cpp', 'src/oomd/util/PluginArgParserTest.cpp')], ['cgctx', files('src/oomd/CgroupContextTest.cpp')], ['context', files('src/oomd/OomdContextTest.cpp')], ['log', files('src/oomd/LogTest.cpp')], ['assert', files('src/oomd/include/AssertTest.cpp')], ['cpath', files('src/oomd/include/CgroupPathTest.cpp')], ['compiler', files('src/oomd/config/ConfigCompilerTest.cpp')], ['plugin', files('src/oomd/plugins/CorePluginsTest.cpp')], ['stats', files('src/oomd/StatsTest.cpp')], ['dropin', files('src/oomd/dropin/DropInServiceAdaptorTest.cpp', 'src/oomd/dropin/FsDropInServiceTest.cpp')], ] # Optional tests if systemd_dep.found() core_tests += [['systemd_plugin', files('src/oomd/plugins/systemd/SystemdPluginsTest.cpp')]] endif gtest_dep = dependency('gtest', main : true, required : false) gmock_dep = dependency('gmock', required : false) if gtest_dep.found() and gmock_dep.found() deps += [gtest_dep, gmock_dep] foreach test_source_tuple : core_tests test_name = test_source_tuple[0] sources = test_source_tuple[1] executable_name_suffix = test_name + '_tests' test_executable = executable('oomd_' + executable_name_suffix, sources, include_directories : inc, cpp_args : cpp_args, dependencies : deps, link_whole : [oomd_lib, oomd_fixture_lib]) test(executable_name_suffix, test_executable, workdir : meson.source_root() + '/src') endforeach endif oomd-0.5.0/src/000077500000000000000000000000001406470533700132525ustar00rootroot00000000000000oomd-0.5.0/src/oomd/000077500000000000000000000000001406470533700142105ustar00rootroot00000000000000oomd-0.5.0/src/oomd/CgroupContext.cpp000066400000000000000000000347661406470533700175400ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/CgroupContext.h" #include #include "oomd/OomdContext.h" namespace Oomd { std::optional CgroupContext::make( OomdContext& ctx, const CgroupPath& cgroup) { auto fd = Fs::DirFd::open(cgroup.absolutePath()); if (!fd) { return std::nullopt; } return CgroupContext(ctx, cgroup, std::move(*fd)); } CgroupContext::CgroupContext( OomdContext& ctx, const CgroupPath& path, Fs::DirFd&& dirFd) : ctx_(ctx), cgroup_(path), cgroup_dir_(std::move(dirFd)), data_(std::make_unique()) {} std::optional CgroupContext::createChildCgroupCtx( const std::string& child_name) const { CgroupPath child_path(cgroup().getChild(child_name)); auto fd = cgroup_dir_.openChildDir(child_name); if (fd) { return CgroupContext(ctx_, child_path, std::move(*fd)); } return std::nullopt; } bool CgroupContext::refresh() { archive_ = { .average_usage = data_->average_usage, .io_cost_cumulative = data_->io_cost_cumulative, .pg_scan_cumulative = data_->pg_scan_cumulative}; *data_ = {}; return Fs::isCgroupValid(cgroup_dir_); } /* * Use macro to define proxy functions to access the underlying data * object. If a field is not set, set it with the result of a given * expression. If the expression returns an error, we set the err to * INVALID_CGROUP if it's not nullptr. * * Because data_ is a pointer, we can do this lazy set and get with const * function signature. */ namespace { template void proxy( SystemMaybe maybe, std::optional& field, CgroupContext::Error* err) { if (!maybe) { if (err) { *err = CgroupContext::Error::INVALID_CGROUP; } field = std::nullopt; } else { field = std::move(*maybe); } } template void proxy(T val, S& field, CgroupContext::Error* err) { field = std::move(val); if (!field && err) { *err = CgroupContext::Error::INVALID_CGROUP; } } } // namespace #define __PROXY(field, expr, rettype) \ rettype CgroupContext::field(Error* err) const { \ if (!data_->field) { \ proxy((expr), data_->field, err); \ } \ return data_->field; \ } #define FIELD_TYPE(field) decltype(CgroupContext::CgroupData::field) #define PROXY(field, expr) __PROXY(field, expr, FIELD_TYPE(field)) #define PROXY_CONST_REF(field, expr) \ __PROXY(field, expr, const FIELD_TYPE(field)&) PROXY_CONST_REF(children, getChildren()) PROXY_CONST_REF(mem_pressure, getMemPressure(Fs::PressureType::FULL)) PROXY_CONST_REF(mem_pressure_some, getMemPressure(Fs::PressureType::SOME)) PROXY_CONST_REF(io_pressure, getIoPressure(Fs::PressureType::FULL)) PROXY_CONST_REF(io_pressure_some, getIoPressure(Fs::PressureType::SOME)) PROXY_CONST_REF(memory_stat, Fs::getMemstatAt(cgroup_dir_)) PROXY_CONST_REF(io_stat, Fs::readIostatAt(cgroup_dir_)) PROXY(id, cgroup_dir_.inode()) PROXY(current_usage, getMemcurrent()) PROXY(swap_usage, Fs::readSwapCurrentAt(cgroup_dir_)) PROXY(swap_max, Fs::readSwapMaxAt(cgroup_dir_)) PROXY(memory_low, Fs::readMemlowAt(cgroup_dir_)) PROXY(memory_min, Fs::readMemminAt(cgroup_dir_)) PROXY(memory_high, Fs::readMemhighAt(cgroup_dir_)) PROXY(memory_high_tmp, Fs::readMemhightmpAt(cgroup_dir_)) PROXY(memory_max, Fs::readMemmaxAt(cgroup_dir_)) PROXY(nr_dying_descendants, Fs::getNrDyingDescendantsAt(cgroup_dir_)) PROXY(is_populated, Fs::readIsPopulatedAt(cgroup_dir_)) PROXY(kill_preference, Fs::readKillPreferenceAt(cgroup_dir_)) PROXY(oom_group, Fs::readMemoryOomGroupAt(cgroup_dir_)) PROXY(effective_swap_max, getEffectiveSwapMax(err)) PROXY(effective_swap_util_pct, getEffectiveSwapUtilPct(err)) PROXY(effective_swap_free, getEffectiveSwapFree(err)) PROXY(io_cost_cumulative, getIoCostCumulative(err)) PROXY(pg_scan_cumulative, getPgScanCumulative(err)) PROXY(memory_protection, getMemoryProtection(err)) PROXY(io_cost_rate, getIoCostRate(err)) PROXY(average_usage, getAverageUsage(err)) PROXY(pg_scan_rate, getPgScanRate(err)) std::optional CgroupContext::anon_usage(Error* err) const { if (const auto& stat = memory_stat(err)) { if (auto anon = stat->find("anon"); anon != stat->end()) { return anon->second; } else if (err) { *err = Error::INVALID_CGROUP; } } return 0; } std::optional CgroupContext::effective_usage( Error* err, int64_t memory_scale, int64_t memory_adj) const { if (!current_usage(err) || !memory_protection(err)) { return std::nullopt; } return *current_usage() * memory_scale - *memory_protection() + memory_adj; } std::optional CgroupContext::memory_growth(Error* err) const { if (!current_usage(err) || !average_usage(err)) { return std::nullopt; } if (average_usage(err) == 0) { // avoid div by 0. average_usage() = 0 implies current_usage() = 0 because // of how average_usage is calculated. return 0; } return static_cast(*current_usage()) / *average_usage(); } std::vector CgroupContext::getChildren() const { auto dirents = Fs::readDirAt(fd(), Fs::DE_DIR); if (!dirents) { return {}; } return dirents->dirs; } namespace { template std::optional to_opt(SystemMaybe maybe) { if (maybe) { return std::move(*maybe); } return std::nullopt; } } // namespace std::optional CgroupContext::getMemPressure( Fs::PressureType type) const { return to_opt( cgroup_.isRoot() ? Fs::readRootMempressure(type) : Fs::readMempressureAt(cgroup_dir_, type)); } std::optional CgroupContext::getIoPressure( Fs::PressureType type) const { return to_opt( cgroup_.isRoot() ? Fs::readRootIopressure(type) : Fs::readIopressureAt(cgroup_dir_, type)); } std::optional CgroupContext::getMemcurrent() const { return to_opt( cgroup_.isRoot() ? Fs::readRootMemcurrent() : Fs::readMemcurrentAt(cgroup_dir_)); } namespace { std::optional rawProtection( const CgroupContext& ctx, CgroupContext::Error* err = nullptr) { if (!ctx.current_usage(err) || !ctx.memory_min(err) || !ctx.memory_low(err)) { return std::nullopt; } return std::min( ctx.current_usage(), std::max(ctx.memory_min(), ctx.memory_low())); } std::optional normalizedProtection( const CgroupContext& ctx, const CgroupContext& parent_ctx, int64_t protection_sum, CgroupContext::Error* err = nullptr) { if (protection_sum == 0) { // If the cgroup isn't using any memory then it's trivially true it's // not receiving any protection return 0; } else { auto raw_prot = rawProtection(ctx, err); if (!raw_prot) { return std::nullopt; } auto parent_prot = parent_ctx.memory_protection(err); if (!parent_prot) { return std::nullopt; } return *raw_prot * std::min(1.0, 1.0 * *parent_prot / protection_sum); } } } // namespace std::optional CgroupContext::getEffectiveSwapMax(Error* err) const { if (cgroup_.isRoot()) { return ctx_.getSystemContext().swaptotal; } auto parent_cgroup = cgroup_.getParent(); auto parent_ctx = ctx_.addToCacheAndGet(parent_cgroup); if (!parent_ctx) { if (err) { *err = Error::INVALID_CGROUP; } return std::nullopt; } if (auto parent_effective_swap_max = parent_ctx->get().effective_swap_max(err); !parent_effective_swap_max) { return std::nullopt; } else if (auto self_swap_max = swap_max(err); !self_swap_max) { return std::nullopt; } else { return std::min(*parent_effective_swap_max, *self_swap_max); } } // Looks up the hierarchy to determine which level has the lowest // swap free (max - usage) and returns that value. This is // useful for detecting or avoiding swap depletion. Value may be // negative. std::optional CgroupContext::getEffectiveSwapFree(Error* err) const { if (cgroup_.isRoot()) { const auto& sys = ctx_.getSystemContext(); return sys.swaptotal - sys.swapused; } auto swap_max_opt = swap_max(err); if (!swap_max_opt) { return std::nullopt; } auto swap_usage_opt = swap_usage(err); if (!swap_usage_opt) { return std::nullopt; } auto local_free = *swap_max_opt - *swap_usage_opt; auto parent_cgroup = cgroup_.getParent(); auto parent_ctx = ctx_.addToCacheAndGet(parent_cgroup); if (!parent_ctx) { if (err) { *err = Error::INVALID_CGROUP; } return std::nullopt; } if (auto parent_effective_swap_free = parent_ctx->get().effective_swap_free(err); !parent_effective_swap_free) { return std::nullopt; } else { return std::min(*parent_effective_swap_free, local_free); } } // Looks up the hierarchy to determine which level has the highest // swap utilization (usage / max) and returns that value. This is // useful for detecting or avoiding swap depletion. std::optional CgroupContext::getEffectiveSwapUtilPct(Error* err) const { if (cgroup_.isRoot()) { const auto& sys = ctx_.getSystemContext(); if (sys.swaptotal == 0) { return 0; } return static_cast(sys.swapused) / static_cast(sys.swaptotal); } auto swap_max_opt = swap_max(err); if (!swap_max_opt) { return std::nullopt; } if (*swap_max_opt == 0) { return 0; } auto swap_usage_opt = swap_usage(err); if (!swap_usage_opt) { return std::nullopt; } auto local_util_pct = static_cast(*swap_usage_opt) / static_cast(*swap_max_opt); auto parent_cgroup = cgroup_.getParent(); auto parent_ctx = ctx_.addToCacheAndGet(parent_cgroup); if (!parent_ctx) { if (err) { *err = Error::INVALID_CGROUP; } return std::nullopt; } if (auto parent_effective_swap_util_pct = parent_ctx->get().effective_swap_util_pct(err); !parent_effective_swap_util_pct) { return std::nullopt; } else { return std::max(*parent_effective_swap_util_pct, local_util_pct); } } /* * Calculate memory protection, taking into account actual distribution of * memory protection. * * Let's say R(cgrp) is the raw protection amount a cgroup has according to * its own config, P(cgrp) is the amount of actual protection it gets. This * function returns P(cgrp). * * Let R(cgrp) = min(cgrp.memory.current, max(cgrp.memory.min, * cgrp.memory.low)) * * Then, P(cgrp) = R(cgrp) * min(1.0, P(parent) / (Sum of L(child) for each * children of parent)) */ std::optional CgroupContext::getMemoryProtection(Error* err) const { if (cgroup_.isRoot()) { return current_usage(err); } auto parent_cgroup = cgroup_.getParent(); if (parent_cgroup.isRoot()) { // We're at a top level cgroup where P(cgrp) == R(cgrp) return rawProtection(*this, err); } auto parent_ctx = ctx_.addToCacheAndGet(parent_cgroup); if (!parent_ctx) { if (err) { *err = Error::INVALID_CGROUP; } return std::nullopt; } std::unordered_set sibling_cgroups; std::vector siblings; if (auto children = parent_ctx->get().children(err)) { for (const auto& name : *children) { sibling_cgroups.insert(parent_cgroup.getChild(name)); } siblings = ctx_.addToCacheAndGet(sibling_cgroups); } else { return std::nullopt; } int64_t protection_sum = 0; for (auto sibling_ctx : siblings) { protection_sum += rawProtection(sibling_ctx).value_or(0); } return normalizedProtection(*this, *parent_ctx, protection_sum, err); } std::optional CgroupContext::getIoCostCumulative(Error* err) const { if (!io_stat(err)) { return std::nullopt; } const auto& params = ctx_.getParams(); double cost = 0.0; // calculate the sum of cumulative io cost on all devices. for (const auto& stat : *io_stat()) { // only keep stats from devices we care auto dev = params.io_devs.find(stat.dev_id); if (dev == params.io_devs.end()) { continue; } IOCostCoeffs coeffs; switch (dev->second) { case DeviceType::SSD: coeffs = params.ssd_coeffs; break; case DeviceType::HDD: coeffs = params.hdd_coeffs; break; } // Dot product between dev io stat and io cost coeffs. A more sensible way // is to do dot product between rate of change (bandwidth, iops) with // coeffs but since the coeffs are constant, we can calculate rate of // change later. cost += stat.rios * coeffs.read_iops + stat.rbytes * coeffs.readbw + stat.wios * coeffs.write_iops + stat.wbytes * coeffs.writebw + stat.dios * coeffs.trim_iops + stat.dbytes * coeffs.trimbw; } return cost; } std::optional CgroupContext::getPgScanCumulative( Error* err = nullptr) const { static constexpr auto kPgScan = "pgscan"; if (const auto& memstat = memory_stat(err)) { if (auto pos = memstat->find(kPgScan); pos != memstat->end()) { return std::make_optional(pos->second); } else { throw std::runtime_error("Bad memory.stat format: missing pgscan entry"); } } return std::nullopt; } std::optional CgroupContext::getAverageUsage(Error* err) const { if (!current_usage(err)) { return std::nullopt; } auto prev_avg = archive_.average_usage.value_or(0); auto decay = ctx_.getParams().average_size_decay; return prev_avg * ((decay - 1) / decay) + (*current_usage() / decay); } std::optional CgroupContext::getIoCostRate(Error* err) const { if (!io_cost_cumulative(err)) { return std::nullopt; } return !archive_.io_cost_cumulative ? 0.0 : *io_cost_cumulative() - *archive_.io_cost_cumulative; } std::optional CgroupContext::getPgScanRate(Error* err) const { if (!pg_scan_cumulative(err) || !archive_.pg_scan_cumulative) { return std::nullopt; } return *pg_scan_cumulative() - *archive_.pg_scan_cumulative; } } // namespace Oomd oomd-0.5.0/src/oomd/CgroupContext.h000066400000000000000000000222601406470533700171670ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include "oomd/include/CgroupPath.h" #include "oomd/include/Types.h" #include "oomd/util/Fs.h" namespace Oomd { class OomdContext; /* * Storage class for cgroup states. Data are retrieved from cgroupfs on access * and cached until refresh() is called. */ class CgroupContext { public: // Error type when retrieving CgroupContext fields enum class Error { NO_ERROR = 0, // Cgroup no longer exists INVALID_CGROUP, }; /* * Create a cgroup context from its containing OomdContext and its path, which * must not be a glob pattern. * * OomdContext is required because some data are sibling-aware, i.e. values of * sibling cgroups affects its own value. It also stores ContextParams so * CgroupContext won't have to duplicate it. * * As OomdContext is guaranteed to last longer than CgroupContext, use * raw reference instead of smart pointer. */ static std::optional make( OomdContext& ctx, const CgroupPath& cgroup); /* * To get children of a cgroup, use OomdContext::addChildrenToCacheAndGet. * This method is dangerous to use directly because the CgroupContext it * returns is not yet in any OomdContext cache. */ std::optional createChildCgroupCtx( const std::string& child_name) const; /* * Check if cgroup still exists and archive current data for temporal * counters. Only called by the containing OomdContext, which has access to * the mutable instance. */ bool refresh(); const Fs::DirFd& fd() const { return cgroup_dir_; } const CgroupPath& cgroup() const { return cgroup_; } OomdContext& oomd_ctx() const { return ctx_; } // Use by plugins to identify a CgroupContext across // intervals. CgroupPath, cgroup dir_fd, and memory address can all // be recycled if cgroup has been recreated. This id is guaranteed // to be non-zero and unique to each cgroup, but semantics is // implementation details. using Id = uint64_t; // Accessors to cgroup fields. If error is encountered, std::nullopt will be // returned and err set to corresponding error enum if it's not nullptr. // Otherwise, err will stay the same and an optional with value returned. // Names of child cgroups (not full path) const std::optional>& children( Error* err = nullptr) const; const std::optional& mem_pressure( Error* err = nullptr) const; const std::optional& mem_pressure_some( Error* err = nullptr) const; const std::optional& io_pressure( Error* err = nullptr) const; const std::optional& io_pressure_some( Error* err = nullptr) const; const std::optional>& memory_stat( Error* err = nullptr) const; const std::optional& io_stat(Error* err = nullptr) const; std::optional id(Error* err = nullptr) const; std::optional current_usage(Error* err = nullptr) const; std::optional swap_usage(Error* err = nullptr) const; std::optional swap_max(Error* err = nullptr) const; std::optional memory_low(Error* err = nullptr) const; std::optional memory_min(Error* err = nullptr) const; std::optional memory_high(Error* err = nullptr) const; std::optional memory_high_tmp(Error* err = nullptr) const; std::optional memory_max(Error* err = nullptr) const; std::optional nr_dying_descendants(Error* err = nullptr) const; std::optional is_populated(Error* err = nullptr) const; std::optional kill_preference(Error* err = nullptr) const; std::optional oom_group(Error* err = nullptr) const; // swap_max taking into account ancestor configs std::optional effective_swap_max(Error* err = nullptr) const; // Available swap for this cgroup taking into account usage and limits of // ancestors. Value may be negative. std::optional effective_swap_free(Error* err = nullptr) const; // Largest percentage of swap consumed by this cgroup taking into // account usage and limits of ancestors std::optional effective_swap_util_pct(Error* err = nullptr) const; // memory_{min,low} taking into account the distribution of it std::optional memory_protection(Error* err = nullptr) const; // Dot product between io stat and coeffs std::optional io_cost_cumulative(Error* err = nullptr) const; std::optional pg_scan_cumulative(Error* err = nullptr) const; // Below are temporal data counters, which need to be retrieved every interval // to become accurate. Must invoke them in plugin prerun() if they may be used // in run(). // Moving average memory usage std::optional average_usage(Error* err = nullptr) const; // Change of cumulative io cost between intervals (not normalized by time) std::optional io_cost_rate(Error* err = nullptr) const; // Change of cumulative pg_scan between intervals (not normalized by time) std::optional pg_scan_rate(Error* err = nullptr) const; // Non-cached derived counters std::optional anon_usage(Error* err = nullptr) const; std::optional effective_usage( Error* err = nullptr, int64_t memory_scale = 1, int64_t memory_adj = 0) const; // if you use memory_growth() you must in prerun load average_usage() std::optional memory_growth(Error* err = nullptr) const; private: explicit CgroupContext( OomdContext& ctx, const CgroupPath& path, Fs::DirFd&& dirFd); // Test only friend class TestHelper; std::vector getChildren() const; std::optional getMemPressure(Fs::PressureType type) const; std::optional getIoPressure(Fs::PressureType type) const; std::optional getMemcurrent() const; std::optional getEffectiveSwapMax(Error* err) const; std::optional getEffectiveSwapFree(Error* err) const; std::optional getEffectiveSwapUtilPct(Error* err) const; std::optional getMemoryProtection(Error* err) const; std::optional getIoCostCumulative(Error* err) const; std::optional getPgScanCumulative(Error* err) const; std::optional getAverageUsage(Error* err) const; std::optional getIoCostRate(Error* err) const; std::optional getPgScanRate(Error* err) const; struct CgroupData { std::optional> children; std::optional mem_pressure; std::optional mem_pressure_some; std::optional io_pressure; std::optional io_pressure_some; std::optional> memory_stat; std::optional io_stat; std::optional id; std::optional current_usage; std::optional swap_usage; std::optional memory_low; std::optional memory_min; std::optional memory_high; std::optional memory_high_tmp; std::optional memory_max; std::optional nr_dying_descendants; std::optional is_populated; std::optional kill_preference; std::optional oom_group; std::optional swap_max; // Cached derived data std::optional effective_swap_max; std::optional effective_swap_free; std::optional effective_swap_util_pct; std::optional memory_protection; std::optional io_cost_cumulative; std::optional pg_scan_cumulative; // Temporal counters std::optional average_usage; std::optional io_cost_rate; std::optional pg_scan_rate; }; // Data required to calculate temporal counters struct CgroupArchivedData { std::optional average_usage; std::optional io_cost_cumulative; std::optional pg_scan_cumulative; }; OomdContext& ctx_; CgroupPath cgroup_; // This dir fd will be invalid whenever the cgroup is gone. Store it to // prevent race when a cgroup with exact same name is recreated after removal. // We check validity in refresh(). If invalid, the dir fd will be closed and // OomdContext will remove this CgroupContext. Fs::DirFd cgroup_dir_; std::unique_ptr data_; CgroupArchivedData archive_{}; }; } // namespace Oomd oomd-0.5.0/src/oomd/CgroupContextTest.cpp000066400000000000000000000603701406470533700203660ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "oomd/CgroupContext.h" #include "oomd/Log.h" #include "oomd/OomdContext.h" #include "oomd/util/Fixture.h" #include "oomd/util/TestHelper.h" using namespace Oomd; using namespace testing; using F = Fixture; class CgroupContextTest : public Test { public: CgroupContextTest() { params_.io_devs = {{"1:10", DeviceType::HDD}, {"1:11", DeviceType::SSD}}; params_.hdd_coeffs = { .read_iops = 6, .readbw = 5, .write_iops = 4, .writebw = 3, .trim_iops = 2, .trimbw = 1}; params_.ssd_coeffs = { .read_iops = 1, .readbw = 2, .write_iops = 3, .writebw = 4, .trim_iops = 5, .trimbw = 6}; ctx_ = OomdContext(params_); } protected: void SetUp() override { tempDir_ = Fixture::mkdtempChecked(); ctx_ = OomdContext(params_); } void TearDown() override { F::rmrChecked(tempDir_); } OomdContext ctx_; ContextParams params_; std::string tempDir_; }; TEST_F(CgroupContextTest, MonitorRootHost) { std::string cgroup2fs_mntpt = ASSERT_SYS_OK(Fs::getCgroup2MountPoint()); if (cgroup2fs_mntpt.empty()) { #ifdef GTEST_SKIP GTEST_SKIP() << "Host not running cgroup2"; #else // GTEST_SKIP() is in gtest 1.9.x (note: starting at 1.9.x, // gtest is living at master) and it's unlikely that it will // ever get packaged. OLOG << "Host not running cgroup2"; return; #endif } if (cgroup2fs_mntpt == "/sys/fs/cgroup/unified/") { // If we have the cgroup tree in "hybrid" mode, this will fail when we test // to get the cgroup context, since we will not find any memory stat files // in that tree. Let's also skip this test in that case. // TODO: A better approach would be to find which controllers are mounted in // the cgroup2 mountpoint, but for now this should be enough to get us to // pass our tests on Travis-CI. #ifdef GTEST_SKIP GTEST_SKIP() << "This test does not support hybrid hierarchy"; #else OLOG << "This test does not support hybrid hierarchy"; return; #endif } OomdContext ctx; auto cgroup_ctx = ctx.addToCacheAndGet(CgroupPath(cgroup2fs_mntpt, "/")); ASSERT_TRUE(cgroup_ctx); // If we're running the test, I should hope the root host is using memory // otherwise we've really stumbled onto a competitive advantage. EXPECT_GT(cgroup_ctx->get().current_usage(), 0); } TEST_F(CgroupContextTest, UniqueId) { F::materialize(F::makeDir( tempDir_, {F::makeDir("A"), F::makeDir("B"), F::makeDir("C"), F::makeDir("D")})); std::unordered_set ids; for (const CgroupContext& cgroup_ctx : ctx_.addToCacheAndGet( std::unordered_set{CgroupPath(tempDir_, "*")})) { if (auto id = cgroup_ctx.id()) { ids.emplace(*id); } } EXPECT_EQ(ids.size(), 4); } TEST_F(CgroupContextTest, MemoryProtection) { F::materialize(F::makeDir( tempDir_, {F::makeDir( "A", {F::makeFile("memory.current", "10000\n"), F::makeFile("memory.low", "2000\n"), F::makeFile("memory.min", "2500\n"), F::makeDir( "B", {F::makeFile("memory.current", "1000\n"), F::makeFile("memory.low", "250\n"), F::makeFile("memory.min", "200\n"), F::makeDir( "C", {F::makeFile("memory.current", "0\n"), F::makeFile("memory.low", "250\n"), F::makeFile("memory.min", "200\n")}), F::makeDir( "D", {F::makeFile("memory.current", "1000\n"), F::makeFile("memory.low", "0\n"), F::makeFile("memory.min", "0\n")})}), F::makeDir( "E", {F::makeFile("memory.current", "250\n"), F::makeFile("memory.low", "1000\n"), F::makeFile("memory.min", "200\n"), F::makeDir( "F", {F::makeFile("memory.current", "2000\n"), F::makeFile("memory.low", "2000\n"), F::makeFile("memory.min", "2000\n")}), F::makeDir( "G", {F::makeFile("memory.current", "3000\n"), F::makeFile("memory.low", "3000\n"), F::makeFile("memory.min", "3000\n")})})})})); // Top level slice gets whatever protection it claims auto cgroup_ctx = ctx_.addToCacheAndGet(CgroupPath(tempDir_, "A")); ASSERT_TRUE(cgroup_ctx); EXPECT_EQ(cgroup_ctx->get().memory_protection(), 2500); // If sum of sibling protection is less than parent protection, all get // whatever they claim cgroup_ctx = ctx_.addToCacheAndGet(CgroupPath(tempDir_, "A/B")); ASSERT_TRUE(cgroup_ctx); EXPECT_EQ(cgroup_ctx->get().memory_protection(), 250); cgroup_ctx = ctx_.addToCacheAndGet(CgroupPath(tempDir_, "A/E")); ASSERT_TRUE(cgroup_ctx); EXPECT_EQ(cgroup_ctx->get().memory_protection(), 250); // If sum of sibling protection is zero, all get zero cgroup_ctx = ctx_.addToCacheAndGet(CgroupPath(tempDir_, "A/B/C")); ASSERT_TRUE(cgroup_ctx); EXPECT_EQ(cgroup_ctx->get().memory_protection(), 0); cgroup_ctx = ctx_.addToCacheAndGet(CgroupPath(tempDir_, "A/B/D")); ASSERT_TRUE(cgroup_ctx); EXPECT_EQ(cgroup_ctx->get().memory_protection(), 0); // If sum of sibling protection exceeds parent protection, split in proportion cgroup_ctx = ctx_.addToCacheAndGet(CgroupPath(tempDir_, "A/E/F")); ASSERT_TRUE(cgroup_ctx); EXPECT_EQ(cgroup_ctx->get().memory_protection(), 100); cgroup_ctx = ctx_.addToCacheAndGet(CgroupPath(tempDir_, "A/E/G")); ASSERT_TRUE(cgroup_ctx); EXPECT_EQ(cgroup_ctx->get().memory_protection(), 150); } /* * Verify that CgroupContext won't read from a recreated cgroup. */ TEST_F(CgroupContextTest, DistinguishRecreate) { F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", // Dummy file to make cgroup valid {F::makeFile("cgroup.controllers"), F::makeFile("memory.current", "123\n")})})); auto cgroup_ctx = ASSERT_EXISTS( CgroupContext::make(ctx_, CgroupPath(tempDir_, "system.slice"))); ASSERT_EQ(cgroup_ctx.current_usage(), 123); // Remove cgroup and recreate one with the exact same name F::rmrChecked(tempDir_ + "/system.slice"); F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", {F::makeFile("cgroup.controllers"), F::makeFile("memory.current", "234\n")})})); // This CgroupContext should no longer be valid EXPECT_FALSE(cgroup_ctx.refresh()); // Files under removed-and-recreated cgroup should no longer be accessible auto err = CgroupContext::Error::NO_ERROR; EXPECT_EQ(cgroup_ctx.current_usage(&err), std::nullopt); EXPECT_EQ(err, CgroupContext::Error::INVALID_CGROUP); } /* * Verify expected values are read from fs. * Verify data are cached and not affected by fs changes. * Verify fs changes are reflected once refresh() is called. */ TEST_F(CgroupContextTest, DataLifeCycle) { // Set up cgroup control files F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", // Dummy file to make cgroup valid {F::makeFile("cgroup.controllers"), F::makeFile( "cgroup.stat", {"nr_descendants 2\n" "nr_dying_descendants 1\n"}), F::makeFile( "io.pressure", {"some avg10=0.04 avg60=0.03 avg300=0.02 total=12345\n" "full avg10=0.03 avg60=0.02 avg300=0.01 total=23456\n"}), F::makeFile( "io.stat", {"1:10" " rbytes=1111111 wbytes=2222222 rios=33 wios=44" " dbytes=5555555555 dios=6\n" "1:11" " rbytes=2222222 wbytes=3333333 rios=44 wios=55" " dbytes=6666666666 dios=7\n"}), F::makeFile("memory.current", {"1122334455\n"}), F::makeFile("memory.high", {"2233445566\n"}), F::makeFile("memory.high.tmp", {"max 0\n"}), F::makeFile("memory.low", {"11223344\n"}), F::makeFile("memory.max", {"3344556677\n"}), F::makeFile("memory.min", {"112233\n"}), F::makeFile( "memory.pressure", {"some avg10=0.31 avg60=0.21 avg300=0.11 total=1234567\n" "full avg10=0.30 avg60=0.20 avg300=0.10 total=2345678\n"}), F::makeFile( "memory.stat", {"anon 123456789\n" "file 12345678\n" "pgscan 4567890123\n"}), F::makeFile("memory.swap.current", {"1234\n"}), F::makeFile("memory.swap.max", {"1024\n"}), F::makeDir("service1.service", {}), F::makeDir("service2.service", {}), F::makeDir("service3.service", {})})})); auto cgroup_ctx = ASSERT_EXISTS( CgroupContext::make(ctx_, CgroupPath(tempDir_, "system.slice"))); std::decay_t children; std::decay_t mem_pressure; std::decay_t io_pressure; std::decay_t memory_stat; std::decay_t io_stat; decltype(cgroup_ctx.current_usage()) current_usage; decltype(cgroup_ctx.swap_usage()) swap_usage; decltype(cgroup_ctx.swap_max()) swap_max; decltype(cgroup_ctx.effective_swap_max()) effective_swap_max; decltype(cgroup_ctx.memory_low()) memory_low; decltype(cgroup_ctx.memory_min()) memory_min; decltype(cgroup_ctx.memory_high()) memory_high; decltype(cgroup_ctx.memory_high_tmp()) memory_high_tmp; decltype(cgroup_ctx.memory_max()) memory_max; decltype(cgroup_ctx.nr_dying_descendants()) nr_dying_descendants; decltype(cgroup_ctx.memory_protection()) memory_protection; decltype(cgroup_ctx.io_cost_cumulative()) io_cost_cumulative; decltype(cgroup_ctx.pg_scan_cumulative()) pg_scan_cumulative; decltype(cgroup_ctx.average_usage()) average_usage; decltype(cgroup_ctx.io_cost_rate()) io_cost_rate; decltype(cgroup_ctx.pg_scan_rate()) pg_scan_rate; auto set_and_check_fields = [&]() { children = cgroup_ctx.children(); mem_pressure = cgroup_ctx.mem_pressure(); io_pressure = cgroup_ctx.io_pressure(); memory_stat = cgroup_ctx.memory_stat(); io_stat = cgroup_ctx.io_stat(); current_usage = cgroup_ctx.current_usage(); swap_usage = cgroup_ctx.swap_usage(); swap_max = cgroup_ctx.swap_max(); effective_swap_max = cgroup_ctx.effective_swap_max(); memory_low = cgroup_ctx.memory_low(); memory_min = cgroup_ctx.memory_min(); memory_high = cgroup_ctx.memory_high(); memory_high_tmp = cgroup_ctx.memory_high_tmp(); memory_max = cgroup_ctx.memory_max(); nr_dying_descendants = cgroup_ctx.nr_dying_descendants(); memory_protection = cgroup_ctx.memory_protection(); io_cost_cumulative = cgroup_ctx.io_cost_cumulative(); pg_scan_cumulative = cgroup_ctx.pg_scan_cumulative(); average_usage = cgroup_ctx.average_usage(); io_cost_rate = cgroup_ctx.io_cost_rate(); pg_scan_rate = cgroup_ctx.pg_scan_rate(); ASSERT_TRUE(children); ASSERT_TRUE(mem_pressure); ASSERT_TRUE(io_pressure); ASSERT_TRUE(memory_stat); ASSERT_TRUE(io_stat); ASSERT_TRUE(current_usage); ASSERT_TRUE(swap_usage); ASSERT_TRUE(swap_max); ASSERT_TRUE(effective_swap_max); ASSERT_TRUE(memory_low); ASSERT_TRUE(memory_min); ASSERT_TRUE(memory_high); ASSERT_TRUE(memory_high_tmp); ASSERT_TRUE(memory_max); ASSERT_TRUE(nr_dying_descendants); ASSERT_TRUE(memory_protection); ASSERT_TRUE(io_cost_cumulative); ASSERT_TRUE(pg_scan_cumulative); ASSERT_TRUE(average_usage); ASSERT_TRUE(io_cost_rate); }; set_and_check_fields(); using memory_stat_t = decltype(memory_stat); // Check basic readings EXPECT_THAT( *children, UnorderedElementsAre( "service1.service", "service2.service", "service3.service")); EXPECT_FLOAT_EQ(mem_pressure->sec_10, 0.3); EXPECT_FLOAT_EQ(mem_pressure->sec_60, 0.2); EXPECT_FLOAT_EQ(mem_pressure->sec_300, 0.1); EXPECT_EQ( mem_pressure->total, std::optional(2345678)); EXPECT_FLOAT_EQ(io_pressure->sec_10, 0.03); EXPECT_FLOAT_EQ(io_pressure->sec_60, 0.02); EXPECT_FLOAT_EQ(io_pressure->sec_300, 0.01); EXPECT_EQ( io_pressure->total, std::optional(23456)); EXPECT_EQ( memory_stat, memory_stat_t( {{"anon", 123456789}, {"file", 12345678}, {"pgscan", 4567890123}})); EXPECT_EQ( io_stat, IOStat( {{"1:10", 1111111, 2222222, 33, 44, 5555555555, 6}, {"1:11", 2222222, 3333333, 44, 55, 6666666666, 7}})); EXPECT_EQ(current_usage, 1122334455); EXPECT_EQ(swap_usage, 1234); EXPECT_EQ(swap_max, 1024); EXPECT_EQ(effective_swap_max, 0); EXPECT_EQ(memory_low, 11223344); EXPECT_EQ(memory_min, 112233); EXPECT_EQ(memory_high, 2233445566); EXPECT_EQ(memory_high_tmp, std::numeric_limits::max()); EXPECT_EQ(memory_max, 3344556677); EXPECT_EQ(nr_dying_descendants, 1); EXPECT_EQ(memory_protection, 11223344); // int64_t(1122334455 / 4.0) EXPECT_EQ(average_usage, 280583613); // 1111111*5 + 2222222*3 + 33*6 + 44*4 + 5555555555*1 + 6*2 + // 2222222*2 + 3333333*4 + 44*1 + 55*3 + 6666666666*6 + 7*5 EXPECT_EQ(io_cost_cumulative, 45585556178); EXPECT_EQ(io_cost_rate, 0); EXPECT_EQ(pg_scan_cumulative, 4567890123); EXPECT_EQ(pg_scan_rate, std::nullopt); // Update most of control files (by adding 1, 0.1 or 0.01) F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", {F::makeFile( "cgroup.stat", {"nr_descendants 3\n" "nr_dying_descendants 2\n"}), F::makeFile( "io.pressure", {"some avg10=0.04 avg60=0.03 avg300=0.02 total=12346\n" "full avg10=0.04 avg60=0.03 avg300=0.02 total=23457\n"}), F::makeFile( "io.stat", {"1:10" " rbytes=1111112 wbytes=2222223 rios=34 wios=45" " dbytes=5555555556 dios=7\n" "1:11" " rbytes=2222223 wbytes=3333334 rios=45 wios=56" " dbytes=6666666667 dios=8\n"}), F::makeFile("memory.current", {"1122334456\n"}), F::makeFile("memory.high", {"2233445567\n"}), F::makeFile("memory.high.tmp", {"max 0\n"}), F::makeFile("memory.low", {"11223345\n"}), F::makeFile("memory.min", {"112234\n"}), F::makeFile("memory.max", {"3344556678\n"}), F::makeFile( "memory.pressure", {"some avg10=0.31 avg60=0.21 avg300=0.11 total=1234568\n" "full avg10=0.31 avg60=0.21 avg300=0.11 total=2345679\n"}), F::makeFile( "memory.stat", {"anon 123456790\n" "file 12345679\n" "pgscan 5678901234\n"}), F::makeFile("memory.swap.current", {"1235\n"}), F::makeFile("memory.swap.max", {"2048\n"}), F::makeDir("service1.service", {}), F::makeDir("service2.service", {}), F::makeDir("service3.service", {}), F::makeDir("service4.service", {})})})); F::rmrChecked(tempDir_ + "/system.slice/service2.service"); // All values should stay the same EXPECT_EQ(cgroup_ctx.children(), children); EXPECT_EQ(cgroup_ctx.mem_pressure(), mem_pressure); EXPECT_EQ(cgroup_ctx.io_pressure(), io_pressure); EXPECT_EQ(cgroup_ctx.memory_stat(), memory_stat); EXPECT_EQ(cgroup_ctx.io_stat(), io_stat); EXPECT_EQ(cgroup_ctx.current_usage(), current_usage); EXPECT_EQ(cgroup_ctx.swap_usage(), swap_usage); EXPECT_EQ(cgroup_ctx.swap_max(), swap_max); EXPECT_EQ(cgroup_ctx.effective_swap_max(), effective_swap_max); EXPECT_EQ(cgroup_ctx.memory_low(), memory_low); EXPECT_EQ(cgroup_ctx.memory_min(), memory_min); EXPECT_EQ(cgroup_ctx.memory_high(), memory_high); EXPECT_EQ(cgroup_ctx.memory_high_tmp(), memory_high_tmp); EXPECT_EQ(cgroup_ctx.memory_max(), memory_max); EXPECT_EQ(cgroup_ctx.nr_dying_descendants(), nr_dying_descendants); EXPECT_EQ(cgroup_ctx.memory_protection(), memory_protection); EXPECT_EQ(cgroup_ctx.average_usage(), average_usage); EXPECT_EQ(cgroup_ctx.io_cost_cumulative(), io_cost_cumulative); EXPECT_EQ(cgroup_ctx.io_cost_rate(), io_cost_rate); EXPECT_EQ(cgroup_ctx.pg_scan_cumulative(), pg_scan_cumulative); EXPECT_EQ(cgroup_ctx.pg_scan_rate(), pg_scan_rate); // Call refresh() to clear cache and retrieve values again ASSERT_TRUE(cgroup_ctx.refresh()); set_and_check_fields(); // Data are now updated EXPECT_THAT( *children, UnorderedElementsAre( "service1.service", "service3.service", "service4.service")); EXPECT_FLOAT_EQ(mem_pressure->sec_10, 0.31); EXPECT_FLOAT_EQ(mem_pressure->sec_60, 0.21); EXPECT_FLOAT_EQ(mem_pressure->sec_300, 0.11); EXPECT_EQ( mem_pressure->total, std::optional(2345679)); EXPECT_FLOAT_EQ(io_pressure->sec_10, 0.04); EXPECT_FLOAT_EQ(io_pressure->sec_60, 0.03); EXPECT_FLOAT_EQ(io_pressure->sec_300, 0.02); EXPECT_EQ( io_pressure->total, std::optional(23457)); EXPECT_EQ( memory_stat, memory_stat_t( {{"anon", 123456790}, {"file", 12345679}, {"pgscan", 5678901234}})); EXPECT_EQ( io_stat, IOStat( {{"1:10", 1111112, 2222223, 34, 45, 5555555556, 7}, {"1:11", 2222223, 3333334, 45, 56, 6666666667, 8}})); EXPECT_EQ(current_usage, 1122334456); EXPECT_EQ(swap_usage, 1235); EXPECT_EQ(swap_max, 2048); EXPECT_EQ(effective_swap_max, 0); EXPECT_EQ(memory_low, 11223345); EXPECT_EQ(memory_min, 112234); EXPECT_EQ(memory_high, 2233445567); EXPECT_EQ(memory_high_tmp, std::numeric_limits::max()); EXPECT_EQ(memory_max, 3344556678); EXPECT_EQ(nr_dying_descendants, 2); EXPECT_EQ(memory_protection, 11223345); // itn64_t(280583613 * (3.0 / 4.0) + 1122334456 / 4.0) EXPECT_EQ(average_usage, 491021323); // 1111112*5 + 2222223*3 + 34*6 + 45*4 + 5555555556*1 + 7*2 + // 2222223*2 + 3333334*4 + 45*1 + 56*3 + 6666666667*6 + 8*5 EXPECT_EQ(io_cost_cumulative, 45585556220); EXPECT_EQ(io_cost_rate, 45585556220 - 45585556178); EXPECT_EQ(pg_scan_cumulative, 5678901234); EXPECT_EQ(pg_scan_rate, 5678901234 - 4567890123); } TEST_F(CgroupContextTest, EffectiveSwapMax) { ctx_.setSystemContext(SystemContext{.swaptotal = 3000}); F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", {F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "1000\n"), F::makeDir( "oomd.service", {F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "2000\n")})})})); auto cgroup_ctx = ASSERT_EXISTS(CgroupContext::make( ctx_, CgroupPath(tempDir_, "system.slice/oomd.service"))); ASSERT_TRUE(cgroup_ctx.effective_swap_max()); ASSERT_EQ(*cgroup_ctx.effective_swap_max(), 1000); } TEST_F(CgroupContextTest, EffectiveSwapFree) { ctx_.setSystemContext(SystemContext{.swaptotal = 400, .swapused = 100}); F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", {F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "max\n"), F::makeFile("memory.swap.current", "100\n"), F::makeDir( "oomd.service", { F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "200\n"), F::makeFile("memory.swap.current", "100\n"), })}), F::makeDir( "foo.slice", {F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "0\n"), F::makeFile("memory.swap.current", "0\n")})})); { // system.slice is limited by the total amount of the swap on the // machine: 400 - 100 = 300 auto cgroup_ctx = ASSERT_EXISTS( CgroupContext::make(ctx_, CgroupPath(tempDir_, "system.slice"))); ASSERT_TRUE(cgroup_ctx.effective_swap_free()); ASSERT_EQ(*cgroup_ctx.effective_swap_free(), 300); } { // oomd.service is limited by memory.swap.max auto cgroup_ctx = ASSERT_EXISTS(CgroupContext::make( ctx_, CgroupPath(tempDir_, "system.slice/oomd.service"))); ASSERT_TRUE(cgroup_ctx.effective_swap_free()); ASSERT_EQ(*cgroup_ctx.effective_swap_free(), 100); } { // swap.max of 0 on foo.slice results in 0% util auto cgroup_ctx = ASSERT_EXISTS( CgroupContext::make(ctx_, CgroupPath(tempDir_, "foo.slice"))); ASSERT_TRUE(cgroup_ctx.effective_swap_free()); ASSERT_EQ(*cgroup_ctx.effective_swap_free(), 0); } } TEST_F(CgroupContextTest, EffectiveSwapUtilPct) { ctx_.setSystemContext(SystemContext{.swaptotal = 400, .swapused = 100}); F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", {F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "max\n"), F::makeFile("memory.swap.current", "100\n"), F::makeDir( "oomd.service", { F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "200\n"), F::makeFile("memory.swap.current", "100\n"), })}), F::makeDir( "foo.slice", {F::makeFile("cgroup.controllers"), F::makeFile("memory.swap.max", "0\n"), F::makeFile("memory.swap.current", "0\n")})})); { // system.slice is limited by the total amount of the swap on the // machine - 100 / 400 = 0.25 auto cgroup_ctx = ASSERT_EXISTS( CgroupContext::make(ctx_, CgroupPath(tempDir_, "system.slice"))); ASSERT_TRUE(cgroup_ctx.effective_swap_util_pct()); ASSERT_EQ(*cgroup_ctx.effective_swap_util_pct(), 0.25); } { // oomd.service is limited by memory.swap.max auto cgroup_ctx = ASSERT_EXISTS(CgroupContext::make( ctx_, CgroupPath(tempDir_, "system.slice/oomd.service"))); ASSERT_TRUE(cgroup_ctx.effective_swap_util_pct()); ASSERT_EQ(*cgroup_ctx.effective_swap_util_pct(), 0.5); } { // swap.max of 0 on foo.slice results in 0% util auto cgroup_ctx = ASSERT_EXISTS( CgroupContext::make(ctx_, CgroupPath(tempDir_, "foo.slice"))); ASSERT_TRUE(cgroup_ctx.effective_swap_util_pct()); ASSERT_EQ(*cgroup_ctx.effective_swap_util_pct(), 0); } } TEST_F(CgroupContextTest, EffectiveSwapUtilPctNoSwap) { // Check that with no swap available on the root slice, this still works ctx_.setSystemContext(SystemContext{.swaptotal = 0, .swapused = 0}); { auto cgroup_ctx = ASSERT_EXISTS(CgroupContext::make(ctx_, CgroupPath(tempDir_, ""))); ASSERT_TRUE(cgroup_ctx.effective_swap_util_pct()); ASSERT_EQ(*cgroup_ctx.effective_swap_util_pct(), 0); } } /* * Verify that CgroupContext won't read from a recreated cgroup. */ TEST_F(CgroupContextTest, MemoryGrowthDoesntDivideByZero) { F::materialize(F::makeDir( tempDir_, {F::makeDir( "system.slice", // Dummy file to make cgroup valid {F::makeFile("cgroup.controllers"), F::makeFile("memory.current", "0\n")})})); auto cgroup_ctx = ASSERT_EXISTS( CgroupContext::make(ctx_, CgroupPath(tempDir_, "system.slice"))); ASSERT_TRUE(cgroup_ctx.average_usage()); ASSERT_EQ(*cgroup_ctx.average_usage(), 0); ASSERT_NO_THROW(cgroup_ctx.memory_growth()); ASSERT_TRUE(cgroup_ctx.memory_growth()); ASSERT_EQ(*cgroup_ctx.memory_growth(), 0); } oomd-0.5.0/src/oomd/Log.cpp000066400000000000000000000121541406470533700154400ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/Log.h" #include #include #include #include #include #include #include #include #include "oomd/util/Util.h" namespace Oomd { LogStream::LogStream() : sink_(Log::get()) {} LogStream::LogStream(LogBase& sink) : sink_(sink) {} bool& LogStream::enabled() { static thread_local bool enabled = true; return enabled; } LogStream::~LogStream() { if (!enabled() || skip_) { return; } stream_ << std::endl; sink_.debugLog(stream_.str()); } Log::Log(int kmsg_fd, std::ostream& debug_sink, bool inl) : kmsg_fd_(kmsg_fd), inline_(inl) { // Start async debug log flushing thread if we are not inline logging if (!inline_) { io_thread_ = std::thread([this, &debug_sink] { this->ioThread(debug_sink); }); } } Log::~Log() { if (kmsg_fd_ >= 0) { ::close(kmsg_fd_); } { std::lock_guard lock(state_.lock); state_.ioThreadRunning = false; } state_.cv.notify_all(); if (io_thread_.joinable()) { io_thread_.join(); } } bool Log::init(const std::string& kmsg_path) { int kmsg_fd = ::open(kmsg_path.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0644); if (kmsg_fd < 0) { perror("open"); std::cerr << "Unable to open outfile " << kmsg_path << ", not logging\n"; return false; } bool inline_logging = std::getenv("INLINE_LOGGING") ? true : false; Log::get(kmsg_fd, std::cerr, inline_logging); return true; } Log& Log::get(int kmsg_fd, std::ostream& debug_sink, bool inl) { static Log singleton(kmsg_fd, debug_sink, inl); return singleton; } std::unique_ptr Log::get_for_unittest(int kmsg_fd, std::ostream& debug_sink, bool inl) { return std::unique_ptr(new Log(kmsg_fd, debug_sink, inl)); } void Log::kmsgLog(const std::string& buf, const std::string& prefix) const { if (kmsg_fd_ >= 0) { std::string message(buf); if (prefix.size() > 0) { message.insert(0, prefix + ": "); } // Make sure message ends with newline or kernel won't show it immediately if (message.size() != 0 && message.back() != '\n') { message.push_back('\n'); } auto ret = Util::writeFull(kmsg_fd_, message.data(), message.size()); if (ret == -1) { perror("error writing"); OLOG << "Unable to write log to output file"; } } else { OLOG << "kmsg logging disabled b/c kmsg_fd_=" << kmsg_fd_; } OLOG << buf; } void Log::debugLog(std::string&& buf) { // If we are doing inline logging, we don't want to pass the buffer to the // async queue if (inline_) { std::cerr << "(inl) " << buf; return; } std::unique_lock lock(state_.lock); if (buf.size() + state_.curSize > state_.maxSize) { state_.numDiscarded++; return; } auto* q = state_.getCurrentQueue(); q->emplace_back(std::move(buf)); state_.curSize += buf.size(); state_.cv.notify_one(); } void Log::ioThread(std::ostream& debug_sink) { bool io_thread_running = true; while (io_thread_running) { std::vector* q = nullptr; size_t numDiscarded; // Swap the rx/tx queues { std::unique_lock lock(state_.lock); q = state_.getCurrentQueue(); // Wait until we have stuff to write state_.cv.wait( lock, [this, q] { return !state_.ioThreadRunning || q->size(); }); io_thread_running = state_.ioThreadRunning; numDiscarded = state_.numDiscarded; state_.curSize = 0; state_.numDiscarded = 0; state_.ioTick++; // flips the last bit that getCurrentQueue uses } for (auto& buf : *q) { debug_sink << buf; } if (numDiscarded) { debug_sink << "...\n" << numDiscarded << " messages dropped\n...\n"; } debug_sink << std::flush; // clear() doesn't shrink capacity, only invalidates contents q->clear(); } } template <> LogStream& LogStream::operator<<(const Control& ctrl) { switch (ctrl) { case Control::DISABLE: enabled() = false; break; case Control::ENABLE: skip_ = true; enabled() = true; break; // Missing default to protect against future enum vals } return *this; } template <> LogStream& LogStream::operator<<(const Offset& offset) { int indent = offset.n - stream_.tellp(); if (indent > 0) { stream_ << std::string(offset.n - stream_.tellp(), ' '); } return *this; } } // namespace Oomd oomd-0.5.0/src/oomd/Log.h000066400000000000000000000106061406470533700151050ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include #include #include namespace Oomd { class LogBase { public: virtual ~LogBase() = default; virtual void kmsgLog(const std::string& buf, const std::string& prefix) const = 0; virtual void debugLog(std::string&& buf) = 0; }; class Log : public LogBase { public: Log(const Log& other) = delete; Log& operator=(const Log& other) = delete; ~Log() override; static bool init(const std::string& kmsg_path); static Log& get(int kmsg_fd = -1, std::ostream& debug_sink = std::cerr, bool inl = true); static std::unique_ptr get_for_unittest(int kmsg_fd, std::ostream& debug_sink, bool inl); void kmsgLog(const std::string& buf, const std::string& prefix) const override; void debugLog(std::string&& buf) override; private: struct AsyncLogState { // The IO thread processes one queue, calling thread writes to other std::array, 2> queues; // Last bit is used to choose queue to process uint64_t ioTick{0}; // Notifies the I/O thread to stop bool ioThreadRunning{true}; // We only store maxSize bytes before we start dropping messages size_t curSize{0}; const size_t maxSize{1024 * 1024}; size_t numDiscarded{0}; std::condition_variable cv; std::mutex lock; std::vector* getCurrentQueue() { return &queues[ioTick & 0x01]; } }; // only get() is allowed to construct explicit Log(int kmsg_fd, std::ostream& debug_sink, bool inl); void ioThread(std::ostream& debug_sink); int kmsg_fd_{-1}; bool inline_{true}; std::thread io_thread_; AsyncLogState state_; }; class LogStream { public: enum class Control { DISABLE, ENABLE, }; class Offset { public: uint64_t n; }; LogStream(); explicit LogStream(LogBase& sink); ~LogStream(); template LogStream& operator<<(const T& v) { // If we've previously received a control token to enable the logs, // skip_ should be set b/c we didn't want to print an empty log line. // However, in the event that a user follows the control token with // actual log messages, we should make sure to process the entire log. // // Eg // OLOG << LogStream::Control::ENABLE << "we should print this"; if (skip_) { skip_ = false; } // Don't store logs unless we're enabled. This helps with interleaved // disabled and enabled states. Eg // OLOG << LogStream::Control::DISABLE << "blah" // << LogStream::Control::ENABLE << "asdf"; // // Only "asdf" should be printed. // // Without this check, we'd get "blahasdf". if (enabled()) { stream_ << v; } return *this; } private: static bool& enabled(); // Set this flag to skip processing this log message bool skip_{false}; std::ostringstream stream_; LogBase& sink_; }; // Must declare explicit specialization in *namespace* scope (class scope // doesn't count) for some weird reason according to the C++ spec. template <> LogStream& LogStream::operator<<(const Control& ctrl); template <> LogStream& LogStream::operator<<(const Offset& offset); template static void OOMD_KMSG_LOG(Args&&... args) { Log::get().kmsgLog(std::forward(args)...); } #ifdef __FILE_NAME__ #define FILENAME __FILE_NAME__ #elif defined __BASE_NAME__ #define FILENAME __BASE_NAME__ #else #define FILENAME __FILE__ #endif // This has to be a macro so __FILE__ and __LINE__ are captured #define OLOG ::Oomd::LogStream() << "[" << FILENAME << ":" << __LINE__ << "] " } // namespace Oomd oomd-0.5.0/src/oomd/LogTest.cpp000066400000000000000000000132101406470533700162720ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/OomdContext.h" using namespace Oomd; class MockLog : public LogBase { public: ~MockLog() override = default; void kmsgLog(const std::string& /* unused */, const std::string& /* unused */) const override { return; } void debugLog(std::string&& buf) override { lines.emplace_back(std::move(buf)); } std::vector lines; }; class LogTestKmsg : public ::testing::Test { public: std::string test_string{"Testing basic logger output!"}; std::string test_prefix{"oomd"}; std::string test_complex_string{ "oomd kill: 10.00 60.00 600.00 Evil 1999 detector:swap,12345MB killer:Evaporate "}; protected: std::pair, std::ifstream> get_logger_and_file() { std::string outfile = "/tmp/logtest.XXXXXX"; int fd = ::mkstemp(&outfile[0]); auto logger = Log::get_for_unittest(fd, std::cerr, /* inl= */ false); std::ifstream result_file(outfile); if (::remove(outfile.c_str()) != 0) { perror("remove"); } return std::make_pair(std::move(logger), std::move(result_file)); } }; /* Test the plain kmsgLog(buf) interface. */ TEST_F(LogTestKmsg, VerifyOutputSimple) { auto logger_and_file = get_logger_and_file(); auto& logger = logger_and_file.first; auto& result_file = logger_and_file.second; logger->kmsgLog(test_string, test_prefix); /* check output */ std::string compare_string; getline(result_file, compare_string); /* verify log contents */ EXPECT_EQ(compare_string, test_prefix.append(": " + test_string)); } TEST(LogTestAsyncDebug, CoupleLines) { std::stringstream sstr; { // Do not use inline logging for our log tests because // 1) we want to test async logging behavior // 2) we correctly destroy the Log object during the test which // correctly joins the async thread auto logger = Log::get_for_unittest(STDERR_FILENO, sstr, /* inl= */ false); logger->debugLog(std::string("line one\n")); logger->debugLog(std::string("line2\n")); } EXPECT_EQ(sstr.str(), "line one\nline2\n"); } TEST(LogTestAsyncDebug, LotsOfLines) { for (int i = 0; i < 10; ++i) { std::stringstream expected; std::stringstream sstr; { // See comment in CoupleLines auto logger = Log::get_for_unittest(STDERR_FILENO, sstr, /* inl= */ false); for (int j = 0; j < 150; ++j) { logger->debugLog(std::to_string(j) + "_line\n"); expected << j << "_line\n"; } } EXPECT_EQ(sstr.str(), expected.str()); } } TEST(LogStream, Basic) { MockLog log; LogStream(log) << "hello world!"; LogStream(log) << "hello world 2!"; ASSERT_EQ(log.lines.size(), 2); EXPECT_EQ(log.lines[0], "hello world!\n"); EXPECT_EQ(log.lines[1], "hello world 2!\n"); } TEST(LogStream, ControlDisable) { MockLog log; LogStream(log) << "one"; LogStream(log) << LogStream::Control::DISABLE; LogStream(log) << "two"; LogStream(log) << "three"; LogStream(log) << LogStream::Control::ENABLE; LogStream(log) << "four"; LogStream(log) << "five"; ASSERT_EQ(log.lines.size(), 3); EXPECT_EQ(log.lines[0], "one\n"); EXPECT_EQ(log.lines[1], "four\n"); EXPECT_EQ(log.lines[2], "five\n"); } TEST(LogStream, ControlDisableEnableAdvanced) { MockLog log; LogStream(log) << "one"; LogStream(log) << LogStream::Control::DISABLE << "after disable"; LogStream(log) << "two"; LogStream(log) << "three"; LogStream(log) << LogStream::Control::ENABLE << "after enable"; ASSERT_EQ(log.lines.size(), 2); EXPECT_EQ(log.lines[0], "one\n"); EXPECT_EQ(log.lines[1], "after enable\n"); log.lines.clear(); LogStream(log) << LogStream::Control::DISABLE << "after disable" << LogStream::Control::ENABLE << "after enable"; ASSERT_EQ(log.lines.size(), 1); EXPECT_EQ(log.lines[0], "after enable\n"); } TEST(LogStream, ControlMany) { MockLog log; LogStream(log) << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << LogStream::Control::DISABLE << LogStream::Control::ENABLE << "hello world"; ASSERT_EQ(log.lines.size(), 1); EXPECT_EQ(log.lines[0], "hello world\n"); } oomd-0.5.0/src/oomd/Main.cpp000066400000000000000000000347051406470533700156110ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #ifdef MESON_BUILD #include #else #include #endif #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/Oomd.h" #include "oomd/PluginRegistry.h" #include "oomd/Stats.h" #include "oomd/StatsClient.h" #include "oomd/config/ConfigCompiler.h" #include "oomd/config/JsonConfigParser.h" #include "oomd/include/CgroupPath.h" #include "oomd/include/CoreStats.h" #include "oomd/include/Defines.h" #include "oomd/util/Fs.h" #include "oomd/util/Util.h" #ifdef MESON_BUILD #include "Version.h" // @manual #else #define GIT_VERSION "unknown" #endif #ifdef MESON_BUILD namespace fs = std::filesystem; #else namespace fs = std::experimental::filesystem; #endif static constexpr auto kConfigFilePath = "/etc/oomd.json"; static constexpr auto kCgroupFsRoot = "/sys/fs/cgroup"; static constexpr auto kRuntimeDir = "/run/oomd"; static constexpr auto kRuntimeLock = "oomd.lock"; static constexpr auto kStatsSocket = "oomd-stats.socket"; static constexpr auto kKmsgPath = "/dev/kmsg"; static const struct Oomd::IOCostCoeffs default_hdd_coeffs = { .read_iops = 1.31e-3, .readbw = 1.13e-7, .write_iops = 2.58e-1, .writebw = 5.04e-7, .trim_iops = 0, .trimbw = 0, }; static const struct Oomd::IOCostCoeffs default_ssd_coeffs = { .read_iops = 1.21e-2, .readbw = 6.25e-7, .write_iops = 1.07e-3, .writebw = 2.61e-7, .trim_iops = 2.37e-2, .trimbw = 9.10e-10, }; static void printUsage() { std::cerr << "usage: oomd [OPTION]...\n\n" "optional arguments:\n" " --help, -h Show this help message and exit\n" " --version, -v Print version and exit\n" " --config, -C CONFIG Config file (default: /etc/oomd.json)\n" " --interval, -i INTERVAL Event loop polling interval (default: 5)\n" " --cgroup-fs, -f FS Cgroup2 filesystem mount point (default: /sys/fs/cgroup)\n" " --check-config, -c CONFIG Check config file (default: /etc/oomd.json)\n" " --list-plugins, -l List all available plugins\n" " --drop-in-dir, -w DIR Directory to watch for drop in configs\n" " --runtime-dir, -D PATH Directory for runtime files (default: /run/oomd)\n" " --dump-stats, -d Dump accumulated stats\n" " --reset-stats, -r Reset stats collection\n" " --device DEVS Comma separated : pairs for IO cost calculation (default: none)\n" " --ssd-coeffs COEFFS Comma separated values for SSD IO cost calculation (default: see doc)\n" " --hdd-coeffs COEFFS Comma separated values for HDD IO cost calculation (default: see doc)\n" " --kmsg-override PATH File to log kills to (default: /dev/kmsg)" << std::endl; } static bool system_reqs_met() { // 4.20 mempressure file auto psi = Oomd::Fs::readFileByLine("/proc/pressure/memory"); if (psi && psi->size()) { return true; } // Experimental mempressure file psi = Oomd::Fs::readFileByLine("/proc/mempressure"); if (psi && psi->size()) { return true; } std::cerr << "PSI not detected. Is your system running a new enough kernel?\n"; return false; } static bool cgroup_fs_valid(const std::string& path) { auto cgroup2ParentPath = Oomd::Fs::getCgroup2MountPoint(); if (!cgroup2ParentPath) { return false; } return Oomd::Fs::isUnderParentPath(*cgroup2ParentPath, path); } static std::unique_ptr parseConfig( const std::string& flag_conf_file) { std::ifstream conf_file(flag_conf_file, std::ios::in); if (!conf_file.is_open()) { std::cerr << "Could not open confg_file=" << flag_conf_file << std::endl; return nullptr; } std::stringstream buf; buf << conf_file.rdbuf(); Oomd::Config2::JsonConfigParser json_parser; auto ir = json_parser.parse(buf.str()); if (!ir) { std::cerr << "Could not parse conf_file=" << flag_conf_file << std::endl; return nullptr; } return ir; } static Oomd::IOCostCoeffs parseCoeffs(const std::string& str_coeffs) { Oomd::IOCostCoeffs coeffs = {}; auto parts = Oomd::Util::split(str_coeffs, ','); auto coeff_fields = { &coeffs.read_iops, &coeffs.readbw, &coeffs.write_iops, &coeffs.writebw, &coeffs.trim_iops, &coeffs.trimbw}; size_t idx = 0; for (auto& coeff_field : coeff_fields) { *coeff_field = idx >= parts.size() ? 0 : std::stod(parts[idx]); idx++; } return coeffs; } static Oomd::SystemMaybe> parseDevices(const std::string& str_devices) { std::unordered_map io_devs; auto parts = Oomd::Util::split(str_devices, ','); for (const auto& dev_id : parts) { auto dev_type = Oomd::Fs::getDeviceType(dev_id); if (!dev_type) { return SYSTEM_ERROR(dev_type.error()); } io_devs[dev_id] = *dev_type; } return io_devs; } static void initializeCoreStats() { // Zero out core keys so that there aren't any "missing" keys // until the associated event occurs for (const char* key : Oomd::CoreStats::kAllKeys) { Oomd::setStat(key, 0); } } static bool initRuntimeDir(const fs::path& runtime_dir) { std::array err_buf = {}; std::error_code ec; // Ignore return value of fs::create_directories because it indicates // whether directories were created or not. We don't care about this // information, only whether or not the directories exist without error // after the call. fs::create_directories(runtime_dir); if (ec) { OLOG << "Failed to create runtime directory=" << runtime_dir << ": " << ec; return false; } fs::path lockfile = runtime_dir / kRuntimeLock; // Don't bother storing file lock FD. Just let it close when process exits. int lockfd = ::open(lockfile.c_str(), O_CREAT, S_IRUSR | S_IWUSR); if (lockfd < 0) { OLOG << "Failed to open lock file=" << lockfile << ": " << ::strerror_r(errno, err_buf.data(), err_buf.size()); return false; } if (::flock(lockfd, LOCK_EX | LOCK_NB)) { OLOG << "Failed to acquire exclusive runtime lock=" << lockfile << ": " << ::strerror_r(errno, err_buf.data(), err_buf.size()); return false; } return true; } enum MainOptions { OPT_DEVICE = 256, // avoid collision with char OPT_SSD_COEFFS, OPT_HDD_COEFFS, }; int main(int argc, char** argv) { std::string flag_conf_file = kConfigFilePath; std::string cgroup_fs = kCgroupFsRoot; std::string drop_in_dir; std::string runtime_dir = std::string(kRuntimeDir); std::string stats_socket_path = runtime_dir + "/" + kStatsSocket; std::string dev_id; std::string kmsg_path = kKmsgPath; int interval = 5; bool should_check_config = false; int option_index = 0; int c = 0; bool should_dump_stats = false; bool should_reset_stats = false; Oomd::SystemMaybe> io_devs; struct Oomd::IOCostCoeffs hdd_coeffs = default_hdd_coeffs; struct Oomd::IOCostCoeffs ssd_coeffs = default_ssd_coeffs; const char* const short_options = "hvC:w:i:f:c:lD:dr"; option long_options[] = { option{"help", no_argument, nullptr, 'h'}, option{"version", no_argument, nullptr, 'v'}, option{"config", required_argument, nullptr, 'C'}, option{"interval", required_argument, nullptr, 'i'}, option{"cgroup-fs", required_argument, nullptr, 'f'}, option{"check-config", required_argument, nullptr, 'c'}, option{"list-plugins", no_argument, nullptr, 'l'}, option{"drop-in-dir", required_argument, nullptr, 'w'}, option{"runtime-dir", required_argument, nullptr, 'D'}, option{"dump-stats", no_argument, nullptr, 'd'}, option{"reset-stats", no_argument, nullptr, 'r'}, option{"device", required_argument, nullptr, OPT_DEVICE}, option{"ssd-coeffs", required_argument, nullptr, OPT_SSD_COEFFS}, option{"hdd-coeffs", required_argument, nullptr, OPT_HDD_COEFFS}, option{"kmsg-override", required_argument, nullptr, 'k'}, option{nullptr, 0, nullptr, 0}}; while ((c = getopt_long( argc, argv, short_options, long_options, &option_index)) != -1) { size_t parsed_len; bool parse_error = false; switch (c) { case 'h': printUsage(); return 0; case 'v': std::cout << GIT_VERSION << std::endl; return 0; case 'C': flag_conf_file = std::string(optarg); break; case 'c': should_check_config = true; flag_conf_file = std::string(optarg); break; case 'w': drop_in_dir = std::string(optarg); break; case 'l': std::cerr << "List of plugins oomd was compiled with:\n"; for (const auto& plugin_name : Oomd::getPluginRegistry().getRegistered()) { std::cerr << " " << plugin_name << "\n"; } return 0; case 'i': try { interval = std::stoi(optarg, &parsed_len); } catch (const std::invalid_argument& e) { parse_error = true; } if (parse_error || interval < 1 || parsed_len != strlen(optarg)) { std::cerr << "Interval not a >0 integer: " << optarg << std::endl; return 1; } break; case 'f': cgroup_fs = std::string(optarg); break; case 'D': runtime_dir = std::string(optarg); stats_socket_path = runtime_dir + "/" + kStatsSocket; break; case 'd': should_dump_stats = true; break; case 'r': should_reset_stats = true; break; case OPT_DEVICE: io_devs = parseDevices(optarg); if (!io_devs) { std::cerr << "Invalid devices: " << io_devs.error().what() << '\n'; return 1; } break; case OPT_SSD_COEFFS: try { ssd_coeffs = parseCoeffs(optarg); } catch (const std::invalid_argument& e) { std::cerr << "Invalid SSD coefficients: " << e.what() << '\n'; return 1; } break; case OPT_HDD_COEFFS: try { hdd_coeffs = parseCoeffs(optarg); } catch (const std::invalid_argument& e) { std::cerr << "Invalid HDD coefficients: " << e.what() << '\n'; return 1; } break; case 'k': kmsg_path = std::string(optarg); break; case 0: break; case '?': std::cerr << "Unknown option or missing argument\n"; printUsage(); return 1; default: return 1; } } if (optind < argc) { std::cerr << "Non-option argument is not supported: "; while (optind < argc) { std::cerr << "\"" << argv[optind++] << "\" "; } std::cerr << std::endl; printUsage(); return 1; } if (should_dump_stats) { try { Oomd::StatsClient client(stats_socket_path); auto map = client.getStats(); if (!map) { std::cerr << "Failed to retrieve stats" << std::endl; return 1; } Json::Value root(Json::objectValue); for (const auto& pair : *map) { root[pair.first] = pair.second; } std::cout << root.toStyledString() << std::endl; } catch (const std::runtime_error& e) { std::cerr << e.what() << std::endl; return 1; } if (!should_reset_stats) { return 0; } } if (should_reset_stats) { try { Oomd::StatsClient client(stats_socket_path); int res = client.resetStats(); if (res != 0) { std::cerr << "Reset stats error: received error code= " << res << std::endl; return 1; } } catch (const std::runtime_error& e) { std::cerr << e.what() << std::endl; return 1; } return 0; } // Init oomd logging code // // NB: do not use OLOG before initializing logging. Doing so will disable // kmsg logging (due to some weird setup code required to get unit testing // correct). Be careful not to make any oomd library calls before initing // logging. if (!Oomd::Log::init(kmsg_path)) { std::cerr << "Logging failed to initialize. Try running with sudo\n"; return 1; } if (should_check_config) { auto ir = parseConfig(flag_conf_file); if (!ir) { return 1; } Oomd::PluginConstructionContext compile_context(cgroup_fs); auto engine = Oomd::Config2::compile(*ir, compile_context); if (!engine) { OLOG << "Config is not valid"; return 1; } return 0; } // // Daemon code below here // if (!initRuntimeDir(runtime_dir)) { return 1; } // NB: do not start stats module unless we are going to daemonize if (!Oomd::Stats::init(stats_socket_path)) { OLOG << "Stats module failed to initialize"; return 1; } initializeCoreStats(); if (!system_reqs_met()) { std::cerr << "System requirements not met\n"; return EXIT_CANT_RECOVER; } if (!cgroup_fs_valid(cgroup_fs)) { std::cerr << cgroup_fs << " is not a valid cgroup2 filesystem" << std::endl; return EXIT_CANT_RECOVER; } std::cerr << "oomd running with conf_file=" << flag_conf_file << " interval=" << interval << std::endl; auto ir = parseConfig(flag_conf_file); if (!ir) { return EXIT_CANT_RECOVER; } Oomd::PluginConstructionContext compile_context(cgroup_fs); auto engine = Oomd::Config2::compile(*ir, compile_context); if (!engine) { OLOG << "Config failed to compile"; return EXIT_CANT_RECOVER; } Oomd::Oomd oomd( std::move(ir), std::move(engine), interval, cgroup_fs, drop_in_dir, *io_devs, hdd_coeffs, ssd_coeffs); return oomd.run(); } oomd-0.5.0/src/oomd/Oomd.cpp000066400000000000000000000102221406470533700156070ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/Oomd.h" #include #include #include #include "oomd/CgroupContext.h" #include "oomd/Log.h" #include "oomd/dropin/FsDropInService.h" #include "oomd/include/Assert.h" #include "oomd/include/Defines.h" #include "oomd/util/Fs.h" #include "oomd/util/Util.h" namespace Oomd { Oomd::Oomd( std::unique_ptr ir_root, std::unique_ptr engine, int interval, const std::string& cgroup_fs, const std::string& drop_in_dir, const std::unordered_map& io_devs, const IOCostCoeffs& hdd_coeffs, const IOCostCoeffs& ssd_coeffs) : interval_(interval), ir_root_(std::move(ir_root)), engine_(std::move(engine)) { ContextParams params{ .io_devs = io_devs, .hdd_coeffs = hdd_coeffs, .ssd_coeffs = ssd_coeffs, }; ctx_ = OomdContext(params); if (drop_in_dir.size()) { fs_drop_in_service_ = FsDropInService::create(cgroup_fs, *ir_root_, *engine_, drop_in_dir); } } Oomd::~Oomd() = default; void Oomd::updateContext() { // Update information about swapfree SystemContext system_ctx; auto swaps = Fs::readFileByLine("/proc/swaps"); // TODO(dschatzberg): Handle error here if (swaps) { // For each swap, tally up used and total for (size_t i = 1; i < swaps->size(); ++i) { auto parts = Util::split((*swaps)[i], '\t'); // The /proc/swaps format is pretty bad. The first field is padded by // spaces but the rest of the fields are padded by '\t'. Since we don't // really care about the first field, we'll just split by '\t'. OCHECK_EXCEPT( parts.size() == 4, std::runtime_error("/proc/swaps malformed")); system_ctx.swaptotal += std::stoll(parts[1]) * 1024; // Values are in KB system_ctx.swapused += std::stoll(parts[2]) * 1024; // Values are in KB } } auto swappiness = Fs::getSwappiness(); if (swappiness) { system_ctx.swappiness = *swappiness; } if (auto vmstat_opt = Fs::getVmstat()) { system_ctx.vmstat = *vmstat_opt; // Factor for calculating moving average const static double factor60 = std::exp(-interval_.count() / 60.0); const static double factor300 = std::exp(-interval_.count() / 300.0); auto& prev_system_ctx = ctx_.getSystemContext(); if (prev_system_ctx.vmstat.size() > 0) { auto swapout_bps = (system_ctx.vmstat.at("pswpout") - prev_system_ctx.vmstat.at("pswpout")) * 4096.0 / interval_.count(); system_ctx.swapout_bps_60 = swapout_bps + factor60 * (prev_system_ctx.swapout_bps_60 - swapout_bps); system_ctx.swapout_bps_300 = swapout_bps + factor300 * (prev_system_ctx.swapout_bps_300 - swapout_bps); } } ctx_.setSystemContext(system_ctx); ctx_.setPrekillHooksHandler([&](const CgroupContext& cgroup_ctx) { return engine_->firePrekillHook(cgroup_ctx, ctx_); }); ctx_.refresh(); ctx_.bumpCurrentTick(); } int Oomd::run() { if (!engine_) { OLOG << "Could not run engine. Your config file is probably invalid\n"; return EXIT_CANT_RECOVER; } OLOG << "Running oomd"; while (true) { /* sleep override */ std::this_thread::sleep_for(interval_); if (fs_drop_in_service_) { fs_drop_in_service_->updateDropIns(); } updateContext(); // Prerun all the plugins engine_->prerun(ctx_); // Run all the plugins engine_->runOnce(ctx_); } return 0; } } // namespace Oomd oomd-0.5.0/src/oomd/Oomd.h000066400000000000000000000032001406470533700152520ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include "oomd/OomdContext.h" namespace Oomd { namespace Config2::IR { struct Root; } namespace Engine { class Engine; } class DropInServiceAdaptor; class Oomd { public: Oomd( std::unique_ptr ir_root, std::unique_ptr engine, int interval, const std::string& cgroup_fs, const std::string& drop_in_dir, const std::unordered_map& io_devs = {}, const IOCostCoeffs& hdd_coeffs = {}, const IOCostCoeffs& ssd_coeffs = {}); ~Oomd(); void updateContext(); int run(); private: // runtime settings std::chrono::seconds interval_{0}; std::unique_ptr ir_root_; std::unique_ptr engine_; std::unique_ptr fs_drop_in_service_; OomdContext ctx_; }; } // namespace Oomd oomd-0.5.0/src/oomd/OomdContext.cpp000066400000000000000000000200001406470533700171470ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/OomdContext.h" #include #include "oomd/Log.h" #include "oomd/engine/Engine.h" #include "oomd/util/Fs.h" namespace Oomd { std::vector OomdContext::cgroups() const { std::vector keys; for (const auto& pair : cgroups_) { keys.emplace_back(pair.first); } return keys; } std::optional OomdContext::addToCacheAndGet( const CgroupPath& cgroup) { // Return cached cgroup if already exists if (auto pos = cgroups_.find(cgroup); pos != cgroups_.end()) { return pos->second; } if (auto ctx = CgroupContext::make(*this, cgroup)) { return cgroups_.emplace(cgroup, std::move(*ctx)).first->second; } return std::nullopt; } std::vector OomdContext::addToCacheAndGet( const std::unordered_set& cgroups) { std::unordered_set all_resolved; std::vector ret; for (const auto& cgroup : cgroups) { auto resolved = cgroup.resolveWildcard(); all_resolved.insert(resolved.begin(), resolved.end()); } for (const auto& resolved : all_resolved) { if (auto cgroup_ctx = addToCacheAndGet(resolved)) { ret.push_back(*cgroup_ctx); } } return ret; } std::optional OomdContext::addChildToCacheAndGet( const CgroupContext& cgroup_ctx, const std::string& child) { if (auto child_ctx = cgroup_ctx.createChildCgroupCtx(child)) { const CgroupPath& child_path = child_ctx->cgroup(); const auto& pair = cgroups_.emplace(child_path, std::move(*child_ctx)); return std::make_optional( pair.first->second); } else { OLOG << "failed to get child of " << cgroup_ctx.cgroup().relativePath() << " named " << child; return std::nullopt; } } std::vector OomdContext::addChildrenToCacheAndGet(const CgroupContext& cgroup_ctx) { std::vector ret; CgroupContext::Error err; std::unordered_set child_paths; if (auto children = cgroup_ctx.children(&err)) { for (const auto& name : *children) { if (const auto& child_ctx = addChildToCacheAndGet(cgroup_ctx, name)) { ret.push_back(*child_ctx); } else { OLOG << "failed to get child of " << cgroup_ctx.cgroup().relativePath() << " named " << name; } } } else { OLOG << "failed to get children of cgroup " << cgroup_ctx.cgroup().relativePath(); } return ret; } const ActionContext& OomdContext::getActionContext() const { return action_context_; } void OomdContext::setActionContext(const ActionContext& context) { action_context_ = context; } const SystemContext& OomdContext::getSystemContext() const { return system_ctx_; } void OomdContext::setSystemContext(const SystemContext& context) { system_ctx_ = context; } uint64_t OomdContext::getCurrentTick() { return current_tick_; } void OomdContext::bumpCurrentTick() { current_tick_++; } const std::optional OomdContext::getInvokingRuleset() { return invoking_ruleset_; } void OomdContext::setInvokingRuleset(std::optional ruleset) { invoking_ruleset_ = ruleset; } void OomdContext::dump() { std::vector cgroups; for (auto& pair : cgroups_) { cgroups.push_back(pair.second); } dump(cgroups); } void OomdContext::dump( const std::vector& cgroup_ctxs, const bool skip_negligible) { auto cgmax = std::numeric_limits::max(); OLOG << "Dumping OomdContext: "; for (const CgroupContext& cgroup_ctx : cgroup_ctxs) { auto mem_pressure = cgroup_ctx.mem_pressure().value_or(ResourcePressure{}); auto io_pressure = cgroup_ctx.io_pressure().value_or(ResourcePressure{}); auto current_usage = cgroup_ctx.current_usage().value_or(0); auto average_usage = cgroup_ctx.average_usage().value_or(0); auto memory_low = cgroup_ctx.memory_low().value_or(0); auto memory_min = cgroup_ctx.memory_min().value_or(0); auto memory_high = cgroup_ctx.memory_high().value_or(cgmax); auto memory_high_tmp = cgroup_ctx.memory_high_tmp().value_or(cgmax); auto memory_max = cgroup_ctx.memory_max().value_or(cgmax); auto memory_protection = cgroup_ctx.memory_protection().value_or(0); auto anon_usage = cgroup_ctx.anon_usage().value_or(0); auto swap_usage = cgroup_ctx.swap_usage().value_or(0); auto io_cost_cumulative = cgroup_ctx.io_cost_cumulative().value_or(0); auto io_cost_rate = cgroup_ctx.io_cost_rate().value_or(0); auto pg_scan_cumulative = cgroup_ctx.pg_scan_cumulative().value_or(0); auto pg_scan_rate = cgroup_ctx.pg_scan_rate().value_or(0); auto kill_preference = cgroup_ctx.kill_preference().value_or(KillPreference::NORMAL); if (skip_negligible) { // don't show if <1% pressure && <.1% usage auto meminfo = Fs::getMeminfo(); // TODO(dschatzberg) report error if (meminfo) { const float press_min = 1; const int64_t mem_min = (*meminfo)["MemTotal"] / 1000; const int64_t swap_min = (*meminfo)["SwapTotal"] / 1000; if (!(mem_pressure.sec_10 >= press_min || mem_pressure.sec_60 >= press_min || mem_pressure.sec_300 >= press_min || io_pressure.sec_10 >= press_min || io_pressure.sec_60 >= press_min || io_pressure.sec_300 >= press_min || current_usage > mem_min || average_usage > mem_min || swap_usage > swap_min)) { continue; } } } OLOG << "name=" << cgroup_ctx.cgroup().relativePath(); OLOG << " pressure=" << mem_pressure.sec_10 << ":" << mem_pressure.sec_60 << ":" << mem_pressure.sec_300 << "-" << io_pressure.sec_10 << ":" << io_pressure.sec_60 << ":" << io_pressure.sec_300; OLOG << " mem=" << (current_usage >> 20) << "MB" << " mem_avg=" << (average_usage >> 20) << "MB" << " mem_low=" << (memory_low >> 20) << "MB" << " mem_min=" << (memory_min >> 20) << "MB" << " mem_high=" << (memory_high >> 20) << "MB" << " mem_high_tmp=" << (memory_high_tmp >> 20) << "MB" << " mem_max=" << (memory_max >> 20) << "MB" << " mem_prot=" << (memory_protection >> 20) << "MB" << " anon=" << (anon_usage >> 20) << "MB" << " swap_usage=" << (swap_usage >> 20) << "MB"; OLOG << " io_cost_cumulative=" << io_cost_cumulative << " io_cost_rate=" << io_cost_rate; OLOG << " pg_scan_cumulative=" << pg_scan_cumulative << " pg_scan_rate=" << pg_scan_rate; OLOG << " kill_preference=" << kill_preference; } } void OomdContext::refresh() { auto it = cgroups_.begin(); while (it != cgroups_.end()) { it = it->second.refresh() ? std::next(it) : cgroups_.erase(it); } } void OomdContext::setPrekillHooksHandler( std::function>( const CgroupContext& cgroup_ctx)> prekill_hook_handler) { prekill_hook_handler_ = prekill_hook_handler; } std::optional> OomdContext::firePrekillHook(const CgroupContext& cgroup_ctx) { if (!prekill_hook_handler_) { return std::nullopt; } return prekill_hook_handler_(cgroup_ctx); } } // namespace Oomd oomd-0.5.0/src/oomd/OomdContext.h000066400000000000000000000147261406470533700166360ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include #include #include #include #include #include "oomd/CgroupContext.h" #include "oomd/include/CgroupPath.h" #include "oomd/include/Types.h" namespace Oomd { namespace Engine { class Ruleset; class PrekillHook; class PrekillHookInvocation; } // namespace Engine struct ActionContext { std::string ruleset_name; std::string detectorgroup; std::string action_group_run_uuid; std::optional prekill_hook_timeout_ts{ std::nullopt}; }; struct ContextParams { // TODO(dlxu): migrate to ring buffer for raw datapoints so plugins // can calculate weighted average themselves double average_size_decay{4.0}; // root io device IDs (:) and their device types (SSD/HDD) std::unordered_map io_devs; IOCostCoeffs hdd_coeffs; IOCostCoeffs ssd_coeffs; }; class OomdContext { public: // Immutable CgroupContext type returned from public APIs, which has life time // valid for the interval being accessed. Plugins should never store it. using ConstCgroupContextRef = std::reference_wrapper; explicit OomdContext(const ContextParams& params = {}) : params_(params) {} ~OomdContext() = default; OomdContext(OomdContext&& other) noexcept = default; OomdContext& operator=(OomdContext&& other) = default; std::vector cgroups() const; const ContextParams& getParams() const { return params_; } /* * Add a cgroup to cache if not already exist, and return the result. If it's * invalid, return std::nullopt. */ std::optional addToCacheAndGet( const CgroupPath& cgroup); /* * Add a set of cgroups to cache if not already exist, and return the result. * Cgroup paths may contain glob pattern, which will be expanded if valid. * Returned CgroupContexts are all valid and won't contain duplicate. */ std::vector addToCacheAndGet( const std::unordered_set& cgroups); /* * Get child of cgroup, adding it to the cache if it doesn't exist yet. */ std::optional addChildToCacheAndGet( const CgroupContext& cgroup_ctx, const std::string& child); /* * Get children of cgroup, adding them to the cache if they don't exist yet. */ std::vector addChildrenToCacheAndGet( const CgroupContext& cgroup_ctx); /* * Add a set of cgroups to cache, and return the resulting CgroupContext * sorting in descending order by the get_key functor, which accepts a const * reference of CgroupContext and returns something comparable. */ template std::vector reverseSort( const std::unordered_set& cgroups, Functor&& get_key) { auto sorted = addToCacheAndGet(cgroups); std::sort(sorted.begin(), sorted.end(), [&](const auto& a, const auto& b) { return get_key(a.get()) > get_key(b.get()); }); return sorted; } /* * Sorts cgroups by kill_preference, then get_key. Highest first to lowest * last. Returns new vec; does not mutate cgroups arg. */ template static std::vector sortDescWithKillPrefs( const std::vector& cgroups, Functor&& get_key) { auto sorted = cgroups; std::sort(sorted.begin(), sorted.end(), [&](const auto& a, const auto& b) { return std::make_tuple( a.get().kill_preference().value_or(KillPreference::NORMAL), get_key(a.get())) > std::make_tuple( b.get().kill_preference().value_or(KillPreference::NORMAL), get_key(b.get())); }); return sorted; } /* * Dumps OomdContext state to stderr */ void dump(); static void dump( const std::vector& vec, const bool skip_negligible = false); /* * Used to let action plugins know which ruleset and detector group * triggered it */ const ActionContext& getActionContext() const; void setActionContext(const ActionContext& context); /* * Used to let action plugins retrieve and set info stored in struct */ const SystemContext& getSystemContext() const; void setSystemContext(const SystemContext& context); /* * Used for plugins to know how long it's been since their last run() */ uint64_t getCurrentTick(); void bumpCurrentTick(); /* * Lets action plugins call pause_actions(post_action_delay) on their owning * ruleset. Only available while the ruleset is running its action chain. */ const std::optional getInvokingRuleset(); void setInvokingRuleset(std::optional ruleset); /* * Used to let kill plugins invoke prekill hooks */ std::optional> firePrekillHook( const CgroupContext& cgroup_ctx); void setPrekillHooksHandler( std::function< std::optional>( const CgroupContext& cgroup_ctx)> prekill_hook_handler); /* * Refresh all cgroups and remove ones no longer exist. */ void refresh(); private: // Test only friend class TestHelper; struct ContextParams params_; std::unordered_map cgroups_; ActionContext action_context_; SystemContext system_ctx_; uint64_t current_tick_{0}; std::optional invoking_ruleset_{std::nullopt}; std::function>( const CgroupContext& cgroup_ctx)> prekill_hook_handler_{nullptr}; }; } // namespace Oomd oomd-0.5.0/src/oomd/OomdContextTest.cpp000066400000000000000000000124751406470533700200300ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "oomd/OomdContext.h" #include "oomd/util/Fixture.h" #include "oomd/util/TestHelper.h" using namespace Oomd; using namespace testing; class OomdContextTest : public ::testing::Test { public: OomdContextTest() = default; OomdContext ctx; protected: using F = Fixture; void SetUp() override { tempdir_ = F::mkdtempChecked(); } void TearDown() override { F::rmrChecked(tempdir_); } std::string tempdir_; }; namespace std { // Check if two reference_wrappers are exactly the same bool operator==( const OomdContext::ConstCgroupContextRef& lhs, const OomdContext::ConstCgroupContextRef& rhs) { return std::addressof(lhs.get()) == std::addressof(rhs.get()); }; } // namespace std TEST_F(OomdContextTest, MoveConstructor) { CgroupPath path(tempdir_, "asdf"); EXPECT_FALSE(ctx.addToCacheAndGet(path)); F::materialize(F::makeDir(tempdir_, {F::makeDir("asdf")})); OomdContext other; TestHelper::setCgroupData( other, path, TestHelper::CgroupData{ .current_usage = 1, .memory_low = 2, .average_usage = 3}); ctx = std::move(other); ASSERT_THAT(ctx.cgroups(), ElementsAre(path)); auto moved_cgroup_ctx = ctx.addToCacheAndGet(path); ASSERT_TRUE(moved_cgroup_ctx); EXPECT_EQ(moved_cgroup_ctx->get().current_usage(), 1); EXPECT_EQ(moved_cgroup_ctx->get().memory_low(), 2); EXPECT_EQ(moved_cgroup_ctx->get().average_usage(), 3); } TEST_F(OomdContextTest, CgroupKeys) { F::materialize(F::makeDir(tempdir_, {F::makeDir("asdf"), F::makeDir("wow")})); CgroupPath p1(tempdir_, "asdf"); CgroupPath p2(tempdir_, "wow"); EXPECT_EQ(ctx.cgroups().size(), 0); EXPECT_TRUE(ctx.addToCacheAndGet(p1)); EXPECT_TRUE(ctx.addToCacheAndGet(p2)); EXPECT_THAT(ctx.cgroups(), UnorderedElementsAre(p1, p2)); } TEST_F(OomdContextTest, CgroupKeyRoot) { CgroupPath root(tempdir_, "/"); EXPECT_TRUE(ctx.addToCacheAndGet(root)); EXPECT_THAT(ctx.cgroups(), ElementsAre(root)); } TEST_F(OomdContextTest, GetMultiple) { F::materialize(F::makeDir( tempdir_, {F::makeDir("dir1"), F::makeDir("dir2"), F::makeDir("dir3"), F::makeFile("file1")})); CgroupPath p1(tempdir_, "dir1"); CgroupPath p2(tempdir_, "dir2"); CgroupPath p3(tempdir_, "dir3"); CgroupPath p4(tempdir_, "file1"); CgroupPath p5(tempdir_, "NOT_EXIST"); auto cgroups = ctx.addToCacheAndGet({p1, p2, p3, p4, p5}); auto cg1 = ctx.addToCacheAndGet(p1); auto cg2 = ctx.addToCacheAndGet(p2); auto cg3 = ctx.addToCacheAndGet(p3); ASSERT_TRUE(cg1); ASSERT_TRUE(cg2); ASSERT_TRUE(cg3); EXPECT_THAT(cgroups, UnorderedElementsAre(*cg1, *cg2, *cg3)); CgroupPath p6(tempdir_, "*"); EXPECT_THAT( ctx.addToCacheAndGet(std::unordered_set{p6}), UnorderedElementsAreArray(cgroups)); // Check no duplicates EXPECT_THAT( ctx.addToCacheAndGet({p1, p2, p3, p4, p5, p6}), UnorderedElementsAreArray(cgroups)); // Check empty result EXPECT_EQ(ctx.addToCacheAndGet({p4, p5}).size(), 0); EXPECT_EQ(ctx.addToCacheAndGet({}).size(), 0); } TEST_F(OomdContextTest, SortContext) { F::materialize(F::makeDir( tempdir_, {F::makeDir( "biggest", {F::makeFile("memory.current", "99999999\n"), F::makeFile("memory.low", "1\n")}), F::makeDir( "smallest", {F::makeFile("memory.current", "1\n"), F::makeFile("memory.low", "4\n")}), F::makeDir( "asdf", {F::makeFile("memory.current", "88888888\n"), F::makeFile("memory.low", "2\n")}), F::makeDir( "fdsa", {F::makeFile("memory.current", "77777777\n"), F::makeFile("memory.low", "3\n")})})); CgroupPath p1(tempdir_, "biggest"); CgroupPath p2(tempdir_, "smallest"); CgroupPath p3(tempdir_, "asdf"); CgroupPath p4(tempdir_, "fdsa"); auto sorted = ctx.reverseSort({p1, p2, p3, p4}, [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.current_usage().value_or(0); }); auto cg1 = ctx.addToCacheAndGet(p1); auto cg2 = ctx.addToCacheAndGet(p2); auto cg3 = ctx.addToCacheAndGet(p3); auto cg4 = ctx.addToCacheAndGet(p4); ASSERT_TRUE(cg1); ASSERT_TRUE(cg2); ASSERT_TRUE(cg3); ASSERT_TRUE(cg4); EXPECT_THAT(sorted, ElementsAre(*cg1, *cg3, *cg4, *cg2)); CgroupPath p5(tempdir_, "*est"); CgroupPath p6(tempdir_, "{biggest,smallest,asdf,fdsa}"); CgroupPath p7(tempdir_, "NOT_EXIST"); sorted = ctx.reverseSort({p5, p6, p7}, [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.memory_low().value_or(0); }); EXPECT_THAT(sorted, ElementsAre(*cg2, *cg4, *cg3, *cg1)); } oomd-0.5.0/src/oomd/PluginConstructionContext.cpp000066400000000000000000000004441406470533700221340ustar00rootroot00000000000000#include "oomd/PluginConstructionContext.h" namespace Oomd { PluginConstructionContext::PluginConstructionContext( const std::string& cgroup_fs) : cgroup_fs_(cgroup_fs) {} const std::string& PluginConstructionContext::cgroupFs() const { return cgroup_fs_; } } // namespace Oomd oomd-0.5.0/src/oomd/PluginConstructionContext.h000066400000000000000000000025521406470533700216030ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include namespace Oomd { class PluginConstructionContext { public: PluginConstructionContext(const std::string& cgroup_fs); ~PluginConstructionContext() = default; PluginConstructionContext(const PluginConstructionContext& other) = default; PluginConstructionContext(PluginConstructionContext&& other) = default; PluginConstructionContext& operator=(const PluginConstructionContext& other) = default; PluginConstructionContext& operator=(PluginConstructionContext&& other) = default; const std::string& cgroupFs() const; private: std::string cgroup_fs_; }; } // namespace Oomd oomd-0.5.0/src/oomd/PluginRegistry.cpp000066400000000000000000000020171406470533700177030ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "PluginRegistry.h" namespace Oomd { PluginRegistry& getPluginRegistry() { static PluginRegistry r; return r; } PluginRegistry& getPrekillHookRegistry() { static PluginRegistry r; return r; } } // namespace Oomd oomd-0.5.0/src/oomd/PluginRegistry.h000066400000000000000000000041111406470533700173450ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include "oomd/engine/BasePlugin.h" #include "oomd/engine/PrekillHook.h" namespace Oomd { template class PluginRegistry { public: using FactoryFunction = std::function; using FactoryMap = std::unordered_map; bool add(const std::string& name, FactoryFunction fac) { if (map_.find(name) != map_.end()) { return false; } map_[name] = fac; return true; } T* create(const std::string& name) { if (map_.find(name) == map_.end()) { return nullptr; } return map_[name](); } std::vector getRegistered() const { std::vector list_plugins; for (auto const& data : map_) { list_plugins.push_back(data.first); } return list_plugins; } private: FactoryMap map_; }; PluginRegistry& getPluginRegistry(); #define REGISTER_PLUGIN(plugin_name, create_func) \ bool plugin_name##_plugin_entry = \ getPluginRegistry().add(#plugin_name, (create_func)) PluginRegistry& getPrekillHookRegistry(); #define REGISTER_PREKILL_HOOK(hook_name, create_func) \ bool hook_name##_plugin_entry = \ getPrekillHookRegistry().add(#hook_name, (create_func)) } // namespace Oomd oomd-0.5.0/src/oomd/Stats.cpp000066400000000000000000000201211406470533700160060ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "oomd/Stats.h" #include "oomd/StatsClient.h" #include "oomd/include/Assert.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace Oomd { Stats::Stats(const std::string& stats_socket_path) : stats_socket_path_(stats_socket_path) { if (!this->startSocket()) { throw std::runtime_error("Socket thread failed to start"); } } Stats::~Stats() { std::array err_buf = {}; statsThreadRunning_ = false; auto client = StatsClient(stats_socket_path_); client.closeSocket(); std::unique_lock lock(thread_mutex_); if (!thread_exited_.wait_for(lock, std::chrono::seconds(5), [this] { return this->thread_count_ == 0; })) { OLOG << "Closing stats error: timed out waiting for a thread"; // Crash here because client threads _must_ timeout in < 5s. // If we don't crash then we risk the client threads accessing destructed // members. Note this should never happen. OCHECK(false); } lock.unlock(); if (stats_thread_.joinable()) { stats_thread_.join(); } if (::unlink(serv_addr_.sun_path) < 0) { OLOG << "Closing stats error: unlinking socket path: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); } if (::close(sockfd_) < 0) { OLOG << "Closing stats error: closing stats socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); } } Stats& Stats::get(const std::string& stats_socket_path) { static Stats singleton(stats_socket_path); return singleton; } std::unique_ptr Stats::get_for_unittest( const std::string& stats_socket_path) { return std::unique_ptr(new Stats(stats_socket_path)); } bool Stats::init(const std::string& stats_socket_path) { try { Stats::get(stats_socket_path); } catch (const std::runtime_error& e) { OLOG << "Initializing singleton failed: " << e.what(); return false; } isInitInternal() = true; return true; } bool Stats::isInit() { return isInitInternal(); } bool& Stats::isInitInternal() { static bool init = false; return init; } bool Stats::startSocket() { std::array err_buf = {}; sockfd_ = ::socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd_ < 0) { OLOG << "Error creating socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size() - 1); return false; } ::memset(&serv_addr_, '\0', sizeof(serv_addr_)); serv_addr_.sun_family = AF_UNIX; ::strcpy(serv_addr_.sun_path, stats_socket_path_.c_str()); if (::unlink(serv_addr_.sun_path) < 0 && errno != ENOENT) { OLOG << "Pre-unlinking of socket path failed. " << serv_addr_.sun_path << ". Errno: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); return false; } if (::bind(sockfd_, (struct sockaddr*)&serv_addr_, sizeof(serv_addr_)) < 0) { OLOG << "Error binding stats collection socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); return false; } if (::chmod(stats_socket_path_.c_str(), 0666) < 0) { OLOG << "Unable to set permissions on " << stats_socket_path_; return false; } if (::listen(sockfd_, 5) < 0) { OLOG << "Error listening at socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); return false; } stats_thread_ = std::thread([this] { this->runSocket(); }); return true; } void Stats::runSocket() { sockaddr_un cli_addr; socklen_t clilen = sizeof(cli_addr); std::array err_buf = {}; while (statsThreadRunning_) { int sockfd = ::accept(sockfd_, (struct sockaddr*)&cli_addr, &clilen); if (sockfd < 0) { OLOG << "Stats server error: accepting connection: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); continue; } const timeval io_timeout{.tv_sec = 2, .tv_usec = 0}; const void* time_ptr = static_cast(&io_timeout); ::setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, time_ptr, sizeof io_timeout); ::setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, time_ptr, sizeof io_timeout); std::unique_lock lock(thread_mutex_); ++thread_count_; std::thread msg_thread_ = std::thread([this, sockfd] { this->processMsg(sockfd); }); msg_thread_.detach(); lock.unlock(); thread_exited_.notify_one(); } } void Stats::processMsg(int sockfd) { std::array err_buf = {}; OOMD_SCOPE_EXIT { if (::close(sockfd) < 0) { OLOG << "Stats server error: closing file descriptor: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); } }; char mode = 'a'; char byte_buf; int num_read = 0; for (; num_read < 32; num_read++) { int res = ::read(sockfd, &byte_buf, 1); if (res < 0) { // Error reading OLOG << "Stats server error: reading from socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); return; } else if (res == 0) { // EOF reached break; } // We read a char if (byte_buf == '\n' || byte_buf == '\0') { break; } if (num_read == 0) { // We only care about the first char mode = byte_buf; } } if (num_read == 0) { OLOG << "Stats server error: no msg received"; } Json::Value root; root["error"] = 0; Json::Value body(Json::objectValue); switch (mode) { case 'g': for (auto const& pair : getAll()) { body[pair.first] = pair.second; } break; case 'r': Stats::reset(); break; case '0': break; default: root["error"] = 1; OLOG << "Stats server error: received unknown request: " << mode; } root["body"] = body; std::string ret = root.toStyledString(); if (Util::writeFull(sockfd, ret.c_str(), strlen(ret.c_str())) < 0) { OLOG << "Stats server error: writing to socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()); } std::unique_lock lock(thread_mutex_); thread_count_--; lock.unlock(); thread_exited_.notify_one(); } std::unordered_map Stats::getAll() { std::lock_guard lock(stats_mutex_); return stats_; } int Stats::increment(const std::string& key, int val) { std::lock_guard lock(stats_mutex_); stats_[key] = stats_[key] + val; return 0; } int Stats::set(const std::string& key, int val) { std::lock_guard lock(stats_mutex_); stats_[key] = val; return 0; } int Stats::reset() { std::lock_guard lock(stats_mutex_); for (const auto& pair : stats_) { stats_[pair.first] = 0; } return 0; } std::unordered_map getStats() { if (!Stats::isInit()) { OLOG << "Warning: stats module not initialized"; return {}; } return Stats::get().getAll(); } int incrementStat(const std::string& key, int val) { if (!Stats::isInit()) { OLOG << "Warning: stats module not initialized"; return 1; } return Stats::get().increment(key, val); } int setStat(const std::string& key, int val) { if (!Stats::isInit()) { OLOG << "Warning: stats module not initialized"; return 1; } return Stats::get().set(key, val); } int resetStats() { if (!Stats::isInit()) { OLOG << "Warning: stats module not initialized"; return 1; } return Stats::get().reset(); } } // namespace Oomd oomd-0.5.0/src/oomd/Stats.h000066400000000000000000000053671406470533700154720ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include "oomd/Log.h" namespace Oomd { class Stats { public: Stats(const Stats& other) = delete; Stats& operator=(const Stats& other) = delete; ~Stats(); /* * Initializes stats singleton */ static bool init(const std::string& stats_socket_path); static bool isInit(); /* * Returns stats singleton */ static Stats& get(const std::string& stats_socket_path = ""); /* * Returns ptr to Stats object for unit tests */ static std::unique_ptr get_for_unittest( const std::string& stats_socket_path); /* * Returns a copy of current stats */ std::unordered_map getAll(); /* * Increments designated key in stats by passed val. * If key is not present, initializes key to val. */ int increment(const std::string& key, int val); /* * Sets designated key to equal val in stats collection */ int set(const std::string& key, int val); /* * Sets all existing values in stats to 0 */ int reset(); private: static bool& isInitInternal(); /* * Ensure only get() can construct */ explicit Stats(const std::string& stats_socket_path); // Opens socket for incoming stats queries bool startSocket(); /* * Accepts new connections to socket and delegates * created fds to processMsg() */ void runSocket(); /* * Processes a msg on a given fd */ void processMsg(int sockfd); // Notifies the stats socket thread to stop std::atomic statsThreadRunning_{true}; std::mutex stats_mutex_; std::string stats_socket_path_; sockaddr_un serv_addr_; int sockfd_; std::unordered_map stats_; std::thread stats_thread_; std::mutex thread_mutex_; std::atomic thread_count_{0}; std::condition_variable thread_exited_; }; /* * Public API */ std::unordered_map getStats(); int incrementStat(const std::string& key, int val); int setStat(const std::string& key, int val); int resetStats(); } // namespace Oomd oomd-0.5.0/src/oomd/StatsClient.cpp000066400000000000000000000117501406470533700171550ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/StatsClient.h" #include #include #include #include #include #include #include #include #include #include "oomd/Stats.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace { std::optional parseJson(const std::string& input) { Json::Value ret; std::string errs; Json::CharReaderBuilder rbuilder; std::istringstream sinput(input); bool ok = Json::parseFromStream(rbuilder, sinput, &ret, &errs); if (!ok) { std::cerr << "Unable to parse JSON: " << errs; return std::nullopt; } return ret; } } // namespace namespace Oomd { StatsClient::StatsClient(const std::string& stats_socket_path) : stats_socket_path_(stats_socket_path) { serv_addr_.sun_family = AF_UNIX; ::strcpy(serv_addr_.sun_path, stats_socket_path_.c_str()); } std::optional> StatsClient::getStats() { const auto msg = msgSocket("g"); if (!msg) { return std::nullopt; } auto root = parseJson(*msg); if (!root) { return std::nullopt; } try { if ((*root)["error"].asInt()) { std::cerr << "StatsClient error: received error code=" << (*root)["error"].toStyledString() << std::endl; return std::nullopt; } } catch (const std::exception& e) { std::cerr << "StatsClient error: parsed error value not an int" << std::endl; return std::nullopt; } const auto& body = (*root)["body"]; std::unordered_map ret_map; for (const auto& key : body.getMemberNames()) { ret_map[key] = body[key].asInt(); } return ret_map; } int StatsClient::resetStats() { const auto msg = msgSocket("r"); if (!msg) { return 1; } auto root = parseJson(*msg); if (!root) { return 1; } try { return (*root)["error"].asInt(); } catch (const std::exception& e) { std::cerr << "StatsClient error: parsed error value not an int" << std::endl; return 1; } } int StatsClient::closeSocket() { const auto msg = msgSocket("0"); if (!msg) { return 1; } auto root = parseJson(*msg); if (!root) { return 1; } try { return (*root)["error"].asInt(); } catch (const std::exception& e) { std::cerr << "StatsClient error: parsed error value not an int" << std::endl; return 1; } } std::optional StatsClient::msgSocket(std::string msg) { std::array err_buf = {}; int sockfd = ::socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd < 0) { std::cerr << "Error: creating client socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()) << std::endl; return std::nullopt; } OOMD_SCOPE_EXIT { if (::close(sockfd) < 0) { std::cerr << "Error: shutting down client socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()) << std::endl; } }; const timeval io_timeout{.tv_sec = 2, .tv_usec = 0}; const void* time_ptr = static_cast(&io_timeout); ::setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, time_ptr, sizeof io_timeout); ::setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, time_ptr, sizeof io_timeout); if (::connect(sockfd, (struct sockaddr*)&serv_addr_, sizeof(serv_addr_)) < 0) { std::cerr << "Error: connecting to stats socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()) << "\nSocket path: " << serv_addr_.sun_path << std::endl; return std::nullopt; } msg += '\n'; if (Util::writeFull(sockfd, msg.c_str(), strlen(msg.c_str())) < 0) { std::cerr << "Error: writing to stats socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()) << std::endl; return std::nullopt; } std::string ret = ""; std::array msg_buf = {}; while (true) { int n = Util::readFull(sockfd, msg_buf.data(), msg_buf.size() - 1); if (n < 0) { std::cerr << "Error: reading from stats socket: " << ::strerror_r(errno, err_buf.data(), err_buf.size()) << std::endl; return std::nullopt; } else if (n == 0) { break; } msg_buf[n] = '\0'; ret += std::string(msg_buf.data()); } return ret; } } // namespace Oomd oomd-0.5.0/src/oomd/StatsClient.h000066400000000000000000000027601406470533700166230ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include namespace Oomd { class StatsClient { public: explicit StatsClient(const std::string& stats_socket_path); /* * Connect to socket and return the current stats */ std::optional> getStats(); /* * Connect to socket and reset the current stats */ int resetStats(); /* * Connect to socket and send a closing message */ int closeSocket(); /* * Send a message to the socket. Appends a \n delimiter to the message * before sending. Returns the the socket's response. */ std::optional msgSocket(std::string msg); private: std::string stats_socket_path_; sockaddr_un serv_addr_; }; } // namespace Oomd oomd-0.5.0/src/oomd/StatsTest.cpp000066400000000000000000000152751406470533700166640ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/Stats.h" #include #include #include #include #include #include #include #include "oomd/StatsClient.h" #include "oomd/util/Util.h" using namespace Oomd; namespace { std::optional parseJson(const std::string& input) { Json::Value ret; std::string errs; Json::CharReaderBuilder rbuilder; std::istringstream sinput(input); bool ok = Json::parseFromStream(rbuilder, sinput, &ret, &errs); if (!ok) { std::cerr << "Unable to parse JSON: " << errs; return std::nullopt; } return ret; } } // namespace class StatsTest : public ::testing::Test { public: std::string socket_path; protected: std::unique_ptr get_instance() { socket_path = "/tmp/oomd-XXXXXX.socket"; ::mkstemps(socket_path.data(), 7); return Stats::get_for_unittest(socket_path); } }; TEST_F(StatsTest, BasicStats) { auto stats = get_instance(); ASSERT_TRUE(stats); EXPECT_EQ(stats->getAll().size(), 0); // Increment and check EXPECT_EQ(stats->increment("one", 1), 0); EXPECT_EQ(stats->increment("two", 2), 0); EXPECT_EQ(stats->getAll().size(), 2); // Clear and check vals EXPECT_EQ(stats->reset(), 0); auto map = stats->getAll(); EXPECT_EQ(map.size(), 2); for (auto& pair : map) { EXPECT_EQ(pair.second, 0); } // Increment with existing keys EXPECT_EQ(stats->increment("one", 1), 0); EXPECT_EQ(stats->set("two", 2), 0); EXPECT_EQ(stats->increment("two", 2), 0); map = stats->getAll(); EXPECT_EQ(map.size(), 2); EXPECT_EQ(map["one"], 1); EXPECT_EQ(map["two"], 2 * 2); } TEST_F(StatsTest, InvalidSocketPath) { std::string invalid_path = "/var/"; EXPECT_THROW(Oomd::Stats::get_for_unittest(invalid_path), std::runtime_error); } TEST_F(StatsTest, InvalidRequests) { auto stats_ptr = get_instance(); ASSERT_NE(stats_ptr, nullptr); auto client = StatsClient(socket_path); { const std::string msg = "g"; auto msg_opt = client.msgSocket(msg); ASSERT_TRUE(msg_opt); auto root = parseJson(*msg_opt); ASSERT_TRUE(root); EXPECT_EQ((*root)["error"], 0); } { const std::string msg = "xlskdjfksdj"; auto msg_opt = client.msgSocket(msg); ASSERT_TRUE(msg_opt); auto root = parseJson(*msg_opt); ASSERT_TRUE(root); EXPECT_EQ((*root)["error"], 1); } { const std::string msg = "ysdf\nasdf"; auto msg_opt = client.msgSocket(msg); ASSERT_FALSE(msg_opt); } { const std::string msg = "zsdfasdfasdfasdfasdfasdfasdfadsfasdfasdf"; auto msg_opt = client.msgSocket(msg); ASSERT_FALSE(msg_opt); } { const std::string msg = ""; auto msg_opt = client.msgSocket(msg); ASSERT_TRUE(msg_opt); auto root = parseJson(*msg_opt); ASSERT_TRUE(root); EXPECT_EQ((*root)["error"], 1); } } TEST_F(StatsTest, BasicGetAndClear) { auto stats = get_instance(); auto client = StatsClient(socket_path); EXPECT_EQ(client.getStats()->size(), 0); int limit = 3; { // Stats increments, Client Gets and checks for (int i = 0; i < limit; i++) { EXPECT_EQ(stats->increment(std::to_string(i), i), 0); } EXPECT_EQ(stats->getAll().size(), limit); auto client_map_ptr = client.getStats(); ASSERT_TRUE(client_map_ptr); auto& client_map = *client_map_ptr; EXPECT_EQ(client_map.size(), limit); for (int i = 0; i < limit; i++) { EXPECT_EQ(client_map[std::to_string(i)], i); } } { // Client Clears, Stats/Client Gets and checks EXPECT_EQ(client.resetStats(), 0); auto stats_map = stats->getAll(); EXPECT_EQ(stats_map.size(), limit); for (int i = 0; i < limit; i++) { EXPECT_EQ(stats_map.at(std::to_string(i)), 0); } auto client_map_ptr = client.getStats(); ASSERT_TRUE(client_map_ptr); auto& client_map = *client_map_ptr; EXPECT_EQ(client_map.size(), limit); for (int i = 0; i < limit; i++) { EXPECT_EQ(stats_map.at(std::to_string(i)), 0); } } } TEST_F(StatsTest, LongJson) { auto stats = get_instance(); auto client = StatsClient(socket_path); EXPECT_EQ(client.getStats()->size(), 0); int limit = 10000; { // Stats increments, Client Gets and checks for (int i = 0; i < limit; i++) { EXPECT_EQ(stats->increment(std::to_string(i), i), 0); } EXPECT_EQ(stats->getAll().size(), limit); auto client_map_ptr = client.getStats(); ASSERT_TRUE(client_map_ptr); auto& client_map = *client_map_ptr; EXPECT_EQ(client_map.size(), limit); for (int i = 0; i < limit; i++) { EXPECT_EQ(client_map[std::to_string(i)], i); } } { // Client Clears, Stats/Client Gets and checks EXPECT_EQ(client.resetStats(), 0); auto stats_map = stats->getAll(); EXPECT_EQ(stats_map.size(), limit); for (int i = 0; i < limit; i++) { EXPECT_EQ(stats_map.at(std::to_string(i)), 0); } auto client_map_ptr = client.getStats(); ASSERT_TRUE(client_map_ptr); auto& client_map = *client_map_ptr; EXPECT_EQ(client_map.size(), limit); for (int i = 0; i < limit; i++) { EXPECT_EQ(stats_map.at(std::to_string(i)), 0); } } } TEST_F(StatsTest, MultiThreadedMany) { sockaddr_un serv_addr_; auto stats = get_instance(); serv_addr_.sun_family = AF_UNIX; ::strcpy(serv_addr_.sun_path, socket_path.c_str()); const int num_threads = 50; std::array sockfds; for (int i = 0; i < num_threads; i++) { int sockfd = ::socket(AF_UNIX, SOCK_STREAM, 0); sockfds[i] = sockfd; EXPECT_GE(sockfd, 0); int connected = ::connect(sockfd, (struct sockaddr*)&serv_addr_, sizeof(serv_addr_)); EXPECT_GE(connected, 0); std::string msg = "g\n"; int res = Util::writeFull(sockfd, msg.c_str(), strlen(msg.c_str())); EXPECT_GE(res, 0); } // server threads are now blocked on write() for (int i = 0; i < num_threads; i++) { std::array msg_buf = {}; int res = Util::readFull(sockfds[i], msg_buf.data(), msg_buf.size() - 1); EXPECT_GE(res, 0); EXPECT_EQ(::close(sockfds[i]), 0); } } oomd-0.5.0/src/oomd/config/000077500000000000000000000000001406470533700154555ustar00rootroot00000000000000oomd-0.5.0/src/oomd/config/ConfigCompiler.cpp000066400000000000000000000200341406470533700210600ustar00rootroot00000000000000/* Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/config/ConfigCompiler.h" #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/engine/BasePlugin.h" #include "oomd/engine/DetectorGroup.h" #include "oomd/engine/EngineTypes.h" #include "oomd/engine/Ruleset.h" #include "oomd/util/Util.h" namespace { template std::unique_ptr compilePluginGeneric( Oomd::PluginRegistry& registry, const T& plugin, const Oomd::PluginConstructionContext& context) { if (plugin.name.empty()) { OLOG << "Plugin is missing name"; return nullptr; } std::unique_ptr instance(registry.create(plugin.name)); if (!instance) { OLOG << "Could not locate plugin=" << plugin.name << " in plugin registry"; return nullptr; } instance->setName(plugin.name); int ret = instance->init(plugin.args, context); if (ret != 0) { OLOG << "Plugin=" << plugin.name << " failed to init() with code=" << ret; return nullptr; } return instance; } template std::unique_ptr compilePlugin( const T& plugin, const Oomd::PluginConstructionContext& context) { return compilePluginGeneric( Oomd::getPluginRegistry(), plugin, context); } std::unique_ptr compilePrekillHook( const Oomd::Config2::IR::PrekillHook& hook, const Oomd::PluginConstructionContext& context) { return compilePluginGeneric< Oomd::Config2::IR::PrekillHook, Oomd::Engine::PrekillHook>(Oomd::getPrekillHookRegistry(), hook, context); } std::unique_ptr compileDetectorGroup( const Oomd::Config2::IR::DetectorGroup& group, const Oomd::PluginConstructionContext& context) { std::vector> detectors; if (group.name.empty()) { OLOG << "DetectorGroup is missing name"; return nullptr; } if (group.detectors.empty()) { OLOG << "DetectorGroup is missing Detectors"; return nullptr; } for (const auto& detector : group.detectors) { auto compiled_plugin = compilePlugin(detector, context); if (!compiled_plugin) { return nullptr; } detectors.emplace_back(std::move(compiled_plugin)); } return std::make_unique( group.name, std::move(detectors)); } std::unique_ptr compileRuleset( const Oomd::Config2::IR::Ruleset& ruleset, bool dropin, const Oomd::PluginConstructionContext& context) { uint32_t silenced_logs = 0; int post_action_delay = DEFAULT_POST_ACTION_DELAY; int prekill_hook_timeout = DEFAULT_PREKILL_HOOK_TIMEOUT; std::vector> detector_groups; std::vector> actions; if (ruleset.name.empty()) { OLOG << "Ruleset is missing name"; return nullptr; } // Log silencing field is optional if (ruleset.silence_logs.size()) { auto copy = ruleset.silence_logs; Oomd::Util::trim(copy); auto parts = Oomd::Util::split(copy, ','); for (auto& part : parts) { Oomd::Util::trim(part); if (part == "engine") { silenced_logs |= Oomd::Engine::LogSources::ENGINE; } else if (part == "plugins") { silenced_logs |= Oomd::Engine::LogSources::PLUGINS; } else { OLOG << "Unrecognized log source=" << part; return nullptr; } } } if (!dropin && (ruleset.dgs.empty() || ruleset.acts.empty())) { OLOG << "Ruleset is missing DetectorGroups or missing Actions"; return nullptr; } // post_action_delay field is optional if (ruleset.post_action_delay.size()) { post_action_delay = std::stoi(ruleset.post_action_delay); if (post_action_delay < 0) { OLOG << "Ruleset post_action_delay must be non-negative"; return nullptr; } } // prekill_hook_timeout field is optional if (ruleset.prekill_hook_timeout.size()) { prekill_hook_timeout = std::stoi(ruleset.prekill_hook_timeout); if (prekill_hook_timeout < 0) { OLOG << "Ruleset prekill_hook_timeout must be non-negative"; return nullptr; } } for (const auto& dg : ruleset.dgs) { auto compiled_detectorgroup = compileDetectorGroup(dg, context); if (!compiled_detectorgroup) { return nullptr; } detector_groups.emplace_back(std::move(compiled_detectorgroup)); } for (const auto& action : ruleset.acts) { auto compiled_action = compilePlugin(action, context); if (!compiled_action) { return nullptr; } actions.emplace_back(std::move(compiled_action)); } return std::make_unique( ruleset.name, std::move(detector_groups), std::move(actions), ruleset.dropin.disable_on_drop_in, ruleset.dropin.detectorgroups_enabled, ruleset.dropin.actiongroup_enabled, silenced_logs, post_action_delay, prekill_hook_timeout); } } // namespace namespace Oomd { namespace Config2 { std::unique_ptr compile( const IR::Root& root, const PluginConstructionContext& context) { std::vector> rulesets; for (const auto& ruleset : root.rulesets) { auto compiled_ruleset = compileRuleset(ruleset, false, context); if (!compiled_ruleset) { return nullptr; } rulesets.emplace_back(std::move(compiled_ruleset)); } std::vector> prekill_hooks; for (const auto& prekill_hook : root.prekill_hooks) { auto compiled_prekill_hook_plugin = compilePrekillHook(prekill_hook, context); if (!compiled_prekill_hook_plugin) { return nullptr; } prekill_hooks.emplace_back(std::move(compiled_prekill_hook_plugin)); } return std::make_unique( std::move(rulesets), std::move(prekill_hooks)); } std::optional compileDropIn( const IR::Root& root, const IR::Root& dropin, const PluginConstructionContext& context) { Engine::DropInUnit ret; for (const auto& dropin_rs : dropin.rulesets) { bool found_target = false; for (const auto& rs : root.rulesets) { if (rs.name == dropin_rs.name) { found_target = true; auto target = compileRuleset(rs, false, context); if (!target) { return std::nullopt; } auto compiled_drop = compileRuleset(dropin_rs, true, context); if (!compiled_drop) { return std::nullopt; } if (!target->mergeWithDropIn(std::move(compiled_drop))) { OLOG << "Could not merge drop in ruleset=" << dropin_rs.name; return std::nullopt; } ret.rulesets.emplace_back(std::move(target)); break; } } if (!found_target) { OLOG << "Could not locate targeted ruleset=" << dropin_rs.name; return std::nullopt; } } for (const auto& prekill_hook : dropin.prekill_hooks) { auto compiled_prekill_hook_plugin = compilePrekillHook(prekill_hook, context); if (!compiled_prekill_hook_plugin) { OLOG << "Unknown PrekillHook \"" << prekill_hook.name << "\""; return std::nullopt; } ret.prekill_hooks.emplace_back(std::move(compiled_prekill_hook_plugin)); } return ret; } } // namespace Config2 } // namespace Oomd oomd-0.5.0/src/oomd/config/ConfigCompiler.h000066400000000000000000000031111406470533700205220ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include "oomd/PluginConstructionContext.h" #include "oomd/config/ConfigTypes.h" #include "oomd/engine/Engine.h" namespace Oomd { namespace Config2 { /* * Compiles IR into Oomd::Engine data structures. Also performs full * validation on the IR. If the IR is invalid, @method compile will * print error messages and return a nullptr. */ std::unique_ptr compile( const IR::Root& root, const PluginConstructionContext& context); /* * Compiles a drop in ruleset against a @class IR::Root config. Has * the same semantics as @method compile. The compiled ruleset * must be injected into an existing @class Engine::Engine. */ std::optional compileDropIn( const IR::Root& root, const IR::Root& dropin, const PluginConstructionContext& context); } // namespace Config2 } // namespace Oomd oomd-0.5.0/src/oomd/config/ConfigCompilerTest.cpp000066400000000000000000000774151406470533700217370ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include "oomd/Log.h" #include "oomd/OomdContext.h" #include "oomd/PluginRegistry.h" #include "oomd/config/ConfigCompiler.h" #include "oomd/config/ConfigTypes.h" #include "oomd/engine/BasePlugin.h" #include "oomd/engine/Engine.h" #include "oomd/engine/PrekillHook.h" #include "oomd/util/TestHelper.h" using namespace Oomd; using namespace Oomd::Config2; using namespace Oomd::Engine; namespace { int prerun_count; int prerun_stored_count; int count; int stored_count; bool controlled_detector_on; std::unordered_map prekill_hook_count; void reset_counters() { prerun_count = 0; prerun_stored_count = 0; count = 0; stored_count = 0; controlled_detector_on = false; prekill_hook_count.clear(); } } // namespace static constexpr auto kRandomCgroupFs = "oomd/fixtures/cgroup"; namespace Oomd { class ContinuePlugin : public BasePlugin { public: int init( const PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& /* unused */) override { return PluginRet::CONTINUE; } static ContinuePlugin* create() { return new ContinuePlugin(); } ~ContinuePlugin() override = default; }; class StopPlugin : public BasePlugin { public: int init( const PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& /* unused */) override { return PluginRet::STOP; } static StopPlugin* create() { return new StopPlugin(); } ~StopPlugin() override = default; }; class IncrementCountPlugin : public BasePlugin { public: int init( const PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& /* unused */) override { ++count; return PluginRet::CONTINUE; } static IncrementCountPlugin* create() { return new IncrementCountPlugin(); } ~IncrementCountPlugin() override = default; }; class StoreCountPlugin : public BasePlugin { public: int init( const PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& /* unused */) override { stored_count = count; prerun_stored_count = prerun_count; return PluginRet::CONTINUE; } static StoreCountPlugin* create() { return new StoreCountPlugin(); } ~StoreCountPlugin() override = default; }; class ControlledDetectorPlugin : public BasePlugin { public: int init( const PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& /* unused */) override { return controlled_detector_on ? PluginRet::CONTINUE : PluginRet::STOP; } static ControlledDetectorPlugin* create() { return new ControlledDetectorPlugin(); } ~ControlledDetectorPlugin() override = default; }; class AsyncPausePlugin : public BasePlugin { int pause_count_{3}; int pauses_left_; public: int init( const PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { pauses_left_ = pause_count_; return 0; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& /* unused */) override { bool will_pause = pauses_left_ > 0; if (will_pause) { pauses_left_--; return PluginRet::ASYNC_PAUSED; } else { pauses_left_ = pause_count_; return PluginRet::CONTINUE; } } static AsyncPausePlugin* create() { return new AsyncPausePlugin(); } ~AsyncPausePlugin() override = default; }; class NoInitPlugin : public BasePlugin { public: int init( const PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 1; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& /* unused */) override { return PluginRet::CONTINUE; } static NoInitPlugin* create() { return new NoInitPlugin(); } ~NoInitPlugin() override = default; }; class NoOpPrekillHook : public PrekillHook { public: int init(const PluginArgs& args, const PluginConstructionContext& context) override { this->argParser_.addArgument("id", id_); return PrekillHook::init(args, context); } static NoOpPrekillHook* create() { return new NoOpPrekillHook(); } class NoOpPrekillHookInvocation : public PrekillHookInvocation { public: bool didFinish() override { return true; } NoOpPrekillHookInvocation() = default; ~NoOpPrekillHookInvocation() override = default; }; std::unique_ptr fire( const CgroupContext& /* unused */, const ActionContext& /* unused */) override { ++prekill_hook_count[id_]; return std::unique_ptr( new NoOpPrekillHookInvocation()); } ~NoOpPrekillHook() override = default; std::string id_; }; class KillPlugin : public BasePlugin { public: int init(const PluginArgs& args, const PluginConstructionContext& context) override { cgroupFs_ = context.cgroupFs(); cgroupPath_ = args.at("cgroup"); return 0; } void prerun(OomdContext& /* unused */) override { ++prerun_count; } PluginRet run(OomdContext& ctx) override { CgroupPath cgroup_path(cgroupFs_, cgroupPath_); TestHelper::setCgroupData(ctx, cgroup_path, TestHelper::CgroupData{}); auto cgroup_ctx = EXPECT_EXISTS(ctx.addToCacheAndGet(cgroup_path)); ctx.firePrekillHook(cgroup_ctx); return PluginRet::STOP; } static KillPlugin* create() { return new KillPlugin(); } ~KillPlugin() override = default; std::string cgroupFs_; std::string cgroupPath_; }; REGISTER_PLUGIN(Continue, ContinuePlugin::create); REGISTER_PLUGIN(Stop, StopPlugin::create); REGISTER_PLUGIN(IncrementCount, IncrementCountPlugin::create); REGISTER_PLUGIN(StoreCount, StoreCountPlugin::create); REGISTER_PLUGIN(ControlledDetector, ControlledDetectorPlugin::create); REGISTER_PLUGIN(AsyncPause, AsyncPausePlugin::create); REGISTER_PLUGIN(NoInit, NoInitPlugin::create); REGISTER_PREKILL_HOOK(NoOpPrekillHook, NoOpPrekillHook::create); REGISTER_PLUGIN(Kill, KillPlugin::create); } // namespace Oomd class CompilerTest : public ::testing::Test { public: CompilerTest() { reset_counters(); } std::unique_ptr<::Oomd::Engine::Engine> compile() { const PluginConstructionContext compile_context(kRandomCgroupFs); return Config2::compile(root, compile_context); } OomdContext context; IR::Root root; }; class DropInCompilerTest : public ::testing::Test { public: DropInCompilerTest() { reset_counters(); } std::unique_ptr<::Oomd::Engine::Engine> compileBase() { const PluginConstructionContext compile_context(kRandomCgroupFs); return compile(root, compile_context); } std::optional compileDropIn() { const PluginConstructionContext compile_context(kRandomCgroupFs); return ::Oomd::Config2::compileDropIn(root, dropin_ir, compile_context); } OomdContext context; IR::Root root; IR::Root dropin_ir; }; TEST_F(CompilerTest, IncrementCount) { IR::Detector cont; cont.name = "Continue"; IR::Action increment; increment.name = "IncrementCount"; IR::DetectorGroup dgroup{"group1", {std::move(cont)}}; IR::Ruleset ruleset{"ruleset1", {std::move(dgroup)}, {std::move(increment)}}; root.rulesets.emplace_back(std::move(ruleset)); auto engine = compile(); ASSERT_TRUE(engine); for (int i = 0; i < 3; ++i) { engine->runOnce(context); } EXPECT_EQ(count, 3); } TEST_F(CompilerTest, MultiGroupIncrementCount) { IR::Detector cont; cont.name = "Continue"; IR::Detector stop; stop.name = "Stop"; IR::Action increment; increment.name = "IncrementCount"; IR::DetectorGroup dgroup1{"group1", {cont}}; IR::DetectorGroup dgroup2{"group2", {stop}}; IR::DetectorGroup dgroup3{"group3", {cont}}; IR::Ruleset ruleset1{"ruleset1", {dgroup1}, {increment}}; IR::Ruleset ruleset2{"ruleset2", {dgroup2}, {increment}}; IR::Ruleset ruleset3{"ruleset3", {dgroup3}, {increment}}; root.rulesets.emplace_back(std::move(ruleset1)); root.rulesets.emplace_back(std::move(ruleset2)); root.rulesets.emplace_back(std::move(ruleset3)); auto engine = compile(); ASSERT_TRUE(engine); for (int i = 0; i < 3; ++i) { engine->runOnce(context); } EXPECT_EQ(count, 6); } TEST_F(CompilerTest, AsyncAction) { IR::Action cont{IR::Plugin{.name = "Continue"}}; IR::Action inc{IR::Plugin{.name = "IncrementCount"}}; IR::Action stop{IR::Plugin{.name = "Stop"}}; IR::Action pause{IR::Plugin{.name = "AsyncPause"}}; IR::DetectorGroup dg{ .name = "dg", .detectors = {IR::Detector{IR::Plugin{.name = "Continue"}}}}; // Each enabled plugin will prerun() once, including action that's not taken. root.rulesets = { IR::Ruleset{ .name = "async_pausing", .dgs = {dg}, .acts = {inc, inc, pause, // (1) inc, inc, inc, pause, // (2) inc, stop}, .post_action_delay = "0"}, // (3) IR::Ruleset{ .name = "concurrently_always_running", .dgs = {dg}, .acts = {inc}, .post_action_delay = "0"}, }; auto engine = compile(); ASSERT_TRUE(engine); auto run_once = [&] { const int count_before = count; engine->prerun(context); engine->runOnce(context); int delta_count = count - count_before; return delta_count; }; // Run 3 times to check for ruleset's state is reset when finished executing // action chain. behavior. AsyncPausePlugin resets itself. for (int i = 0; i < 3; i++) { // reset count on each run count = 0; EXPECT_EQ(run_once(), 3); // (1) + 1 from concurrently_always_running // 2 more pauses in 3-run pause, each w/ 1 from concurrently_always_running EXPECT_EQ(run_once(), 1); EXPECT_EQ(run_once(), 1); EXPECT_EQ(run_once(), 4); // (2) + 1 from concurrently_always_running // 2 more pauses in 3-run pause EXPECT_EQ(run_once(), 1); EXPECT_EQ(run_once(), 1); EXPECT_EQ(run_once(), 2); // (3) + 1 from concurrently_always_running } } TEST_F(CompilerTest, TwoChainsIndependentlyPaused) { IR::Action cont{IR::Plugin{.name = "Continue"}}; IR::Action inc{IR::Plugin{.name = "IncrementCount"}}; IR::Action stop{IR::Plugin{.name = "Stop"}}; IR::Action pause{IR::Plugin{.name = "AsyncPause"}}; IR::DetectorGroup always_yes{ .name = "dg", .detectors = {IR::Detector{IR::Plugin{.name = "Continue"}}}}; // Each enabled plugin will prerun() once, including action that's not taken. root.rulesets = { IR::Ruleset{ .name = "A", .dgs = {always_yes}, .acts = {inc, inc, pause, // (1) inc, inc, inc, pause, // (2) inc, stop}, .post_action_delay = "0"}, // (3) IR::Ruleset{ .name = "B", .dgs = {IR::DetectorGroup{ .name = "ctld", .detectors = {IR::Detector{ IR::Plugin{.name = "ControlledDetector"}}}}}, .acts = {inc, inc, inc, pause, // (4) inc, inc}, .post_action_delay = "0"}}; // (5) auto engine = compile(); ASSERT_TRUE(engine); auto run_once = [&] { const int count_before = count; engine->prerun(context); engine->runOnce(context); int delta_count = count - count_before; return delta_count; }; // Run 3 times to check for ruleset's state is reset when finished executing // action chain. behavior. AsyncPausePlugin resets itself. for (int i = 0; i < 1; i++) { // reset count on each run count = 0; EXPECT_EQ(run_once(), 2); // A.1, B idle EXPECT_EQ(run_once(), 0); // A.1 pause 2, B idle controlled_detector_on = true; // while A is paused, start B EXPECT_EQ(run_once(), 3); // A.1 pause 3, B.4 controlled_detector_on = false; EXPECT_EQ(run_once(), 3); // A.2, B.4 pause 2 EXPECT_EQ(run_once(), 0); // A.2 pause 2, B.4 pause 3 EXPECT_EQ(run_once(), 2); // A.2 pause 3, B.5 EXPECT_EQ(run_once(), 1); // A.3, B idle } } TEST_F(CompilerTest, IncrementCountNoop) { IR::Detector stop; stop.name = "Stop"; IR::Detector cont; cont.name = "Continue"; IR::Action increment; increment.name = "IncrementCount"; IR::DetectorGroup dgroup{"group1", {std::move(cont), std::move(stop)}}; IR::Ruleset ruleset{"ruleset1", {std::move(dgroup)}, {std::move(increment)}}; root.rulesets.emplace_back(std::move(ruleset)); auto engine = compile(); ASSERT_TRUE(engine); for (int i = 0; i < 3; ++i) { engine->runOnce(context); } EXPECT_EQ(count, 0); } TEST_F(CompilerTest, PrekillHook) { IR::PrekillHook hook{IR::Plugin{.name = "NoOpPrekillHook"}}; hook.args["cgroup"] = "/"; hook.args["id"] = "only-hook"; root.prekill_hooks.push_back(std::move(hook)); IR::Detector cont; cont.name = "Continue"; IR::Action kill; kill.name = "Kill"; kill.args["cgroup"] = "/workload.slice"; IR::DetectorGroup dgroup{"group1", {std::move(cont)}}; IR::Ruleset ruleset{ .name = "ruleset1", .dgs = {std::move(dgroup)}, .acts = {std::move(kill)}}; ruleset.post_action_delay = "0"; root.rulesets.emplace_back(std::move(ruleset)); OomdContext ctx_; auto engine = compile(); ASSERT_TRUE(engine); context.setPrekillHooksHandler([&](const CgroupContext& cgroup_ctx) { return engine->firePrekillHook(cgroup_ctx, ctx_); }); for (int i = 0; i < 3; i++) { engine->runOnce(context); decltype(prekill_hook_count) expectation = {{"only-hook", i + 1}}; ASSERT_EQ(prekill_hook_count, expectation); } } TEST_F(DropInCompilerTest, PrerunCount) { IR::Plugin cont{.name = "Continue"}; IR::Plugin stop{.name = "Stop"}; // Each enabled plugin will prerun() once, including action that's not taken. root.rulesets = { // 2 / 0 plugins with/without dropin IR::Ruleset{ .name = "disabled_dropin_target", .dgs = {IR::DetectorGroup{ .name = "group1", .detectors = {IR::Detector{cont}}}}, .acts = {IR::Action{cont}}, .dropin = IR::DropIn{ .disable_on_drop_in = true, .actiongroup_enabled = true}, .post_action_delay = "0"}, // 3 plugins (action won't be taken as we stop early) IR::Ruleset{ .name = "enabled_dropin_target", .dgs = {IR::DetectorGroup{ .name = "group1", .detectors = {IR::Detector{stop}, IR::Detector{cont}}}}, .acts = {IR::Action{cont}}, .dropin = IR::DropIn{ .disable_on_drop_in = false, .actiongroup_enabled = true}, .post_action_delay = "0"}, // 2 plugins (StoreCount stores prerun_count) IR::Ruleset{ .name = "rs", .dgs = {IR::DetectorGroup{ .name = "group1", .detectors = {IR::Detector{cont}}}}, .acts = {IR::Action{IR::Plugin{.name = "StoreCount"}}}, .post_action_delay = "0"}, }; // Plugins from dropin should also be prerun() dropin_ir.rulesets = { // 1 + 2 = 3 plugins IR::Ruleset{ .name = "disabled_dropin_target", .acts = {IR::Action{IR::Plugin{.name = "IncrementCount"}}, IR::Action{IR::Plugin{.name = "IncrementCount"}}}, .post_action_delay = "0"}, // 2 + 3 = 5 plugins IR::Ruleset{ .name = "enabled_dropin_target", .acts = {IR::Action{IR::Plugin{.name = "IncrementCount"}}, IR::Action{IR::Plugin{.name = "IncrementCount"}}, IR::Action{IR::Plugin{.name = "IncrementCount"}}}, .post_action_delay = "0"}, }; auto engine = compileBase(); ASSERT_TRUE(engine); engine->prerun(context); engine->runOnce(context); EXPECT_EQ(prerun_count, 2 + 3 + 2); auto dropin = compileDropIn(); ASSERT_TRUE(dropin.has_value()); EXPECT_EQ(dropin->rulesets.size(), 2); EXPECT_TRUE(engine->addDropInRuleset("0", std::move(dropin->rulesets.at(0)))); EXPECT_TRUE(engine->addDropInRuleset("1", std::move(dropin->rulesets.at(1)))); prerun_count = 0; engine->prerun(context); engine->runOnce(context); EXPECT_EQ(prerun_count, 0 + 3 + 2 + 3 + 5); } TEST_F(CompilerTest, NoInitPlugin) { IR::Detector noinit; noinit.name = "NoInit"; IR::Action reg; reg.name = "Register"; IR::DetectorGroup dgroup{"group1", {std::move(noinit)}}; IR::Ruleset ruleset{"ruleset1", {std::move(dgroup)}, {std::move(reg)}}; root.rulesets.emplace_back(std::move(ruleset)); auto engine = compile(); EXPECT_FALSE(engine); } TEST_F(CompilerTest, SilenceLogsParse) { IR::Detector cont; cont.name = "Continue"; IR::Action cont_act; cont_act.name = "Continue"; IR::DetectorGroup dgroup{.name = "group1", .detectors = {cont}}; IR::Ruleset ruleset{ .name = "ruleset1", .dgs = {std::move(dgroup)}, .acts = {cont_act}, .silence_logs = "engine,plugins", .post_action_delay = "0", }; root.rulesets.emplace_back(std::move(ruleset)); auto engine = compile(); EXPECT_TRUE(engine); root.rulesets[0].silence_logs = " engine, plugins \n"; engine = compile(); EXPECT_TRUE(engine); root.rulesets[0].silence_logs = "engine,asdf"; engine = compile(); EXPECT_FALSE(engine); } TEST_F(DropInCompilerTest, DropInConfig) { IR::Detector cont; cont.name = "Continue"; IR::Action noop; noop.name = "Continue"; IR::DetectorGroup dg{"dg", {cont}}; IR::Ruleset rs{"rs", {dg}, {noop}, IR::DropIn{.actiongroup_enabled = true}}; root.rulesets.emplace_back(std::move(rs)); IR::Ruleset dropin_rs; dropin_rs.name = "rs"; IR::Action increment; increment.name = "IncrementCount"; dropin_rs.acts.emplace_back(std::move(increment)); dropin_ir.rulesets.emplace_back(std::move(dropin_rs)); auto engine = compileBase(); ASSERT_TRUE(engine); engine->runOnce(context); EXPECT_EQ(count, 0); auto dropin = compileDropIn(); ASSERT_TRUE(dropin.has_value()); EXPECT_EQ(dropin->rulesets.size(), 1); EXPECT_TRUE(engine->addDropInRuleset("0", std::move(dropin->rulesets.at(0)))); engine->runOnce(context); EXPECT_EQ(count, 1); } TEST_F(DropInCompilerTest, MultipleDropInConfigOrdering) { IR::Detector cont; cont.name = "Continue"; IR::Action increment; increment.name = "IncrementCount"; IR::DetectorGroup dg{"dg", {cont}}; IR::Ruleset rs{ "rs", {dg}, {increment}, IR::DropIn{.actiongroup_enabled = true}}; root.rulesets.emplace_back(std::move(rs)); // First drop in config IR::Ruleset dropin_rs; dropin_rs.name = "rs"; dropin_rs.acts.emplace_back(increment); dropin_ir.rulesets.emplace_back(std::move(dropin_rs)); // Compile the base config auto engine = compileBase(); ASSERT_TRUE(engine); // Add the first drop in config in auto dropin = compileDropIn(); ASSERT_TRUE(dropin.has_value()); EXPECT_EQ(dropin->rulesets.size(), 1); EXPECT_TRUE(engine->addDropInRuleset("0", std::move(dropin->rulesets.at(0)))); // Second drop in config dropin_rs = {}; dropin_rs.name = "rs"; IR::Action store; store.name = "StoreCount"; dropin_rs.acts.emplace_back(std::move(store)); dropin_ir.rulesets[0] = std::move(dropin_rs); // Now add the second drop in config in. // // We expect this to be run before the previous drop in config auto dropin2 = compileDropIn(); ASSERT_TRUE(dropin2.has_value()); EXPECT_EQ(dropin2->rulesets.size(), 1); EXPECT_TRUE( engine->addDropInRuleset("1", std::move(dropin2->rulesets.at(0)))); engine->runOnce(context); EXPECT_EQ(count, 2); EXPECT_EQ(stored_count, 0); } TEST_F(DropInCompilerTest, DisablesBase) { IR::Detector cont; cont.name = "Continue"; IR::Action increment; increment.name = "IncrementCount"; IR::DetectorGroup dg{"dg", {cont}}; IR::Ruleset rs{ "rs", {dg}, {increment}, IR::DropIn{ .disable_on_drop_in = true, .actiongroup_enabled = true, }}; root.rulesets.emplace_back(std::move(rs)); IR::Ruleset dropin_rs; dropin_rs.name = "rs"; IR::Action noop; noop.name = "Continue"; dropin_rs.acts.emplace_back(noop); dropin_ir.rulesets.emplace_back(std::move(dropin_rs)); auto engine = compileBase(); ASSERT_TRUE(engine); engine->runOnce(context); EXPECT_EQ(count, 1); auto dropin = compileDropIn(); ASSERT_TRUE(dropin.has_value()); EXPECT_EQ(dropin->rulesets.size(), 1); EXPECT_TRUE(engine->addDropInRuleset("0", std::move(dropin->rulesets.at(0)))); engine->runOnce(context); EXPECT_EQ(count, 1); } TEST_F(DropInCompilerTest, PermissionDenied) { IR::Detector cont; cont.name = "Continue"; IR::Action increment; increment.name = "IncrementCount"; IR::DetectorGroup dg{"dg", {cont}}; IR::Ruleset rs{ "rs", {dg}, {increment}, IR::DropIn{ .disable_on_drop_in = false, .detectorgroups_enabled = false, .actiongroup_enabled = false, }}; root.rulesets.emplace_back(std::move(rs)); IR::Ruleset dropin_rs; dropin_rs.name = "rs"; IR::Action noop; noop.name = "Continue"; dropin_rs.acts.emplace_back(noop); dropin_ir.rulesets.emplace_back(std::move(dropin_rs)); auto engine = compileBase(); ASSERT_TRUE(engine); auto dropin = compileDropIn(); EXPECT_FALSE(dropin.has_value()); } TEST_F(DropInCompilerTest, RemoveDropIn) { // Base ruleset increments twice IR::Detector cont; cont.name = "Continue"; IR::Action increment; increment.name = "IncrementCount"; IR::DetectorGroup dg{"dg", {cont}}; IR::Ruleset rs{ "rs", {dg}, {increment, increment}, IR::DropIn{.actiongroup_enabled = true}}; root.rulesets.emplace_back(std::move(rs)); // Drop in ruleset increments once IR::Ruleset dropin_rs; dropin_rs.name = "rs"; dropin_rs.acts.emplace_back(increment); dropin_ir.rulesets.emplace_back(std::move(dropin_rs)); auto engine = compileBase(); ASSERT_TRUE(engine); engine->runOnce(context); // Only the base EXPECT_EQ(count, 2); auto dropin = compileDropIn(); ASSERT_TRUE(dropin.has_value()); EXPECT_EQ(dropin->rulesets.size(), 1); EXPECT_TRUE(engine->addDropInRuleset("0", std::move(dropin->rulesets.at(0)))); engine->runOnce(context); // Base and drop in EXPECT_EQ(count, 5); engine->removeDropInConfig("0"); engine->runOnce(context); // Only the base EXPECT_EQ(count, 7); } TEST_F(DropInCompilerTest, MultipleRulesetDropin) { IR::Detector cont; cont.name = "Continue"; IR::Action noop; noop.name = "Continue"; IR::DetectorGroup dg{"dg", {cont}}; IR::Ruleset rs{ "rs", {dg}, {noop}, IR::DropIn{ .disable_on_drop_in = true, .actiongroup_enabled = true, }}; IR::Ruleset rs2{ "rs2", {dg}, {noop}, IR::DropIn{ .disable_on_drop_in = true, .actiongroup_enabled = true, }}; root.rulesets.emplace_back(std::move(rs)); root.rulesets.emplace_back(std::move(rs2)); // Drop-in ruleset 1 IR::Ruleset dropin_rs; dropin_rs.name = "rs"; IR::Action increment; increment.name = "IncrementCount"; dropin_rs.acts.emplace_back(increment); // Drop-in ruleset 2 IR::Ruleset dropin_rs2; dropin_rs2.name = "rs2"; dropin_rs2.acts.emplace_back(increment); dropin_ir.rulesets.emplace_back(std::move(dropin_rs)); dropin_ir.rulesets.emplace_back(std::move(dropin_rs2)); auto engine = compileBase(); ASSERT_TRUE(engine); engine->runOnce(context); EXPECT_EQ(count, 0); auto dropin = compileDropIn(); ASSERT_TRUE(dropin.has_value()); EXPECT_EQ(dropin->rulesets.size(), 2); EXPECT_TRUE(engine->addDropInRuleset("0", std::move(dropin->rulesets.at(0)))); EXPECT_TRUE(engine->addDropInRuleset("0", std::move(dropin->rulesets.at(1)))); engine->runOnce(context); EXPECT_EQ(count, 2); // Now see if both are removed engine->removeDropInConfig("0"); engine->runOnce(context); EXPECT_EQ(count, 2); } class PrekillHookDropinTest : public CompilerTest { public: void compileEngineWithBaseConfigPrekillHooks( std::vector hooks_ir) { root.prekill_hooks = hooks_ir; // a single ruleset that will always kill cgroup_to_kill_ IR::Detector cont; cont.name = "Continue"; IR::Action kill; kill.name = "Kill"; kill.args["cgroup"] = cgroup_to_kill_; IR::DetectorGroup dgroup{"group1", {std::move(cont)}}; IR::Ruleset ruleset{ .name = "ruleset1", .dgs = {std::move(dgroup)}, .acts = {std::move(kill)}}; ruleset.post_action_delay = "0"; root.rulesets.emplace_back(std::move(ruleset)); OomdContext ctx_; engine_ = compile(); EXPECT_TRUE(engine_); context.setPrekillHooksHandler([&](const CgroupContext& cgroup_ctx) { return engine_->firePrekillHook(cgroup_ctx, ctx_); }); } void addDropin(const std::string& tag, IR::Root ir) { const PluginConstructionContext compile_context(kRandomCgroupFs); auto dropin_unit = ASSERT_EXISTS( ::Oomd::Config2::compileDropIn(root, ir, compile_context)); engine_->addDropInConfig(tag, std::move(dropin_unit)); } void expectHook(const std::string& hook_id) { for (int i = 0; i < 3; i++) { engine_->runOnce(context); decltype(prekill_hook_count) expectation = {{hook_id, i + 1}}; ASSERT_EQ(prekill_hook_count, expectation); } reset_counters(); } std::unique_ptr engine_; std::string cgroup_to_kill_ = "/workload.slice"; }; TEST_F(PrekillHookDropinTest, DropinsTakePrecedence) { compileEngineWithBaseConfigPrekillHooks( {IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/irrelivant"}, {"id", "irrelivant-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/workload.slice"}, {"id", "first-matching-base-config-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/workload.slice"}, {"id", "second-matching-base-config-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "catchall-base-config-hook"}}}}}); // without any dropins expectHook("first-matching-base-config-hook"); // with one dropin addDropin( "dropin-1", IR::Root{ .prekill_hooks = {IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "dropin-1-hook"}}}}}}); expectHook("dropin-1-hook"); } TEST_F(PrekillHookDropinTest, MostRecentWins) { compileEngineWithBaseConfigPrekillHooks( {IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "first-base-config-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "second-base-config-hook"}}}}}); expectHook("first-base-config-hook"); addDropin( "dropin-1", IR::Root{ .prekill_hooks = {IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "dropin-1-hook"}}}}}}); expectHook("dropin-1-hook"); addDropin( "dropin-2", IR::Root{ .prekill_hooks = { IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/irrelivant"}, {"id", "irrelivant-2nd-dropin-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/workload.slice"}, {"id", "first-matching-2nd-dropin-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/workload.slice"}, {"id", "second-matching-2nd-dropin-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = { {"cgroup", "/"}, {"id", "catchall-2nd-dropin-hook"}}}}}}); expectHook("first-matching-2nd-dropin-hook"); // removing dropin-1, dropin-2 still takes precidence engine_->removeDropInConfig("dropin-1"); expectHook("first-matching-2nd-dropin-hook"); // removing dropin-2, now only base config is left engine_->removeDropInConfig("dropin-2"); expectHook("first-base-config-hook"); } TEST_F(PrekillHookDropinTest, RemoveAndReAddBumpsPriority) { compileEngineWithBaseConfigPrekillHooks( {IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "first-base-config-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "second-base-config-hook"}}}}}); expectHook("first-base-config-hook"); addDropin( "dropin-1", IR::Root{ .prekill_hooks = {IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "dropin-1-hook"}}}}}}); expectHook("dropin-1-hook"); addDropin( "dropin-2", IR::Root{ .prekill_hooks = { IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/irrelivant"}, {"id", "irrelivant-2nd-dropin-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/workload.slice"}, {"id", "first-matching-2nd-dropin-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/workload.slice"}, {"id", "second-matching-2nd-dropin-hook"}}}}, IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = { {"cgroup", "/"}, {"id", "catchall-2nd-dropin-hook"}}}}}}); expectHook("first-matching-2nd-dropin-hook"); // removing dropin-1, dropin-2 still takes precidence engine_->removeDropInConfig("dropin-1"); expectHook("first-matching-2nd-dropin-hook"); // removing dropin-2, now only base config is left addDropin( "dropin-1", IR::Root{ .prekill_hooks = {IR::PrekillHook{IR::Plugin{ .name = "NoOpPrekillHook", .args = {{"cgroup", "/"}, {"id", "new-dropin-1-hook"}}}}}}); expectHook("new-dropin-1-hook"); } oomd-0.5.0/src/oomd/config/ConfigTypes.cpp000066400000000000000000000067021406470533700204200ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "oomd/Log.h" #include "oomd/config/ConfigTypes.h" namespace { /* * Make sure all log lines get consistent indentation, even if OLOG prefixes * vary because some lines numbers are 3 digits and some are 2. */ Oomd::LogStream::Offset getIndentSpaces(uint64_t depth) { return Oomd::LogStream::Offset{.n = ::strlen(FILENAME) + 7 + depth * 2}; } } // namespace namespace Oomd { namespace Config2 { namespace IR { void dumpIR(const Root& root) { int indent = 0; // Prekill hooks OLOG << getIndentSpaces(indent) << root.prekill_hooks.size() << " PrekillHooks="; ++indent; for (const auto& hook : root.prekill_hooks) { OLOG << getIndentSpaces(indent) << "Hook=" << hook.name; ++indent; // Print arguments OLOG << getIndentSpaces(indent) << "Args="; ++indent; for (const auto& pair : hook.args) { OLOG << getIndentSpaces(indent) << pair.first << "=" << pair.second; } indent -= 2; } --indent; // Rulesets OLOG << getIndentSpaces(indent) << root.rulesets.size() << " Rulesets="; ++indent; for (const auto& ruleset : root.rulesets) { OLOG << getIndentSpaces(indent) << "Ruleset=" << ruleset.name; ++indent; // Print DropIn config OLOG << getIndentSpaces(indent) << "DropIn="; ++indent; OLOG << getIndentSpaces(indent) << "Detectors=" << ruleset.dropin.detectorgroups_enabled; OLOG << getIndentSpaces(indent) << "Actions=" << ruleset.dropin.actiongroup_enabled; OLOG << getIndentSpaces(indent) << "DisableOnDrop=" << ruleset.dropin.disable_on_drop_in; --indent; OLOG << getIndentSpaces(indent) << "SilenceLogs=" << ruleset.silence_logs; // Print DetectorGroup's for (const auto& dg : ruleset.dgs) { OLOG << getIndentSpaces(indent) << "DetectorGroup=" << dg.name; ++indent; // Print Detectors for (const auto& d : dg.detectors) { OLOG << getIndentSpaces(indent) << "Detector=" << d.name; ++indent; OLOG << getIndentSpaces(indent) << "Args="; ++indent; // Print arguments for (const auto& pair : d.args) { OLOG << getIndentSpaces(indent) << pair.first << "=" << pair.second; } indent -= 2; } --indent; } // Print Action's for (const auto& act : ruleset.acts) { OLOG << getIndentSpaces(indent) << "Action=" << act.name; ++indent; OLOG << getIndentSpaces(indent) << "Args="; ++indent; // Print arguments for (const auto& pair : act.args) { OLOG << getIndentSpaces(indent) << pair.first << "=" << pair.second; } indent -= 2; } --indent; } --indent; } } // namespace IR } // namespace Config2 } // namespace Oomd oomd-0.5.0/src/oomd/config/ConfigTypes.h000066400000000000000000000051331406470533700200620ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include namespace Oomd { namespace Config2 { namespace IR { // The IR (intermediate representation) namespace holds the fundamental // datastructures responsible for oomd runtime behavior. The IR was created to // decouple the config language (eg JSON) from the generic behavior of oomd. To // add another config frontend, for example an iptables-like syntax, one only // needs to create a parsing function that can construct an struct IR::Root. // The IR::Root can then be passed to the ConfigCompiler. // // In the most general sense, oomd is configured to wait for DetectorGroups to // fire. If a DetectorGroup fires, then corresponding Actions are taken. A // DetectorGroup is made up of one or more Detectors. // // Both Detector and Action derive from the base Plugin class. A Plugin can // return CONTINUE or STOP. If all Detectors in a DetectorGroup return // CONTINUE, that is to say it does not terminate the Detector chain with a // STOP, then the corresponding Action chain is run. An Action chain runs until // a plugin in the chain returns STOP. struct Plugin { std::string name; std::unordered_map args; }; struct Detector : Plugin {}; struct Action : Plugin {}; struct PrekillHook : Plugin {}; struct DetectorGroup { std::string name; std::vector detectors; }; struct DropIn { bool disable_on_drop_in{false}; bool detectorgroups_enabled{false}; bool actiongroup_enabled{false}; }; struct Ruleset { std::string name; std::vector dgs; std::vector acts; DropIn dropin; std::string silence_logs; std::string post_action_delay; std::string prekill_hook_timeout; }; struct Root { std::vector rulesets; std::vector prekill_hooks; }; void dumpIR(const Root& root); } // namespace IR } // namespace Config2 } // namespace Oomd oomd-0.5.0/src/oomd/config/JsonConfigParser.cpp000066400000000000000000000104121406470533700213730ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/config/JsonConfigParser.h" #include #include #include #include "oomd/Log.h" namespace { void getJson(Json::Value& root, const std::string& input) { std::string errs; Json::CharReaderBuilder rbuilder; rbuilder["allowComments"] = true; rbuilder["collectComments"] = false; std::istringstream sinput(input); bool ok = Json::parseFromStream(rbuilder, sinput, &root, &errs); if (!ok) { OLOG << "Unable to parse JSON: " << errs; throw std::runtime_error("Unable to parse JSON"); } } template T parsePlugin(const Json::Value& plugin) { if (!plugin.isObject()) { return {}; } const auto& name = plugin["name"]; if (!name.isString()) { return {}; } T ret; ret.name = name.asString(); const auto& json_args = plugin["args"]; if (!json_args.isObject()) { return ret; } for (const auto& key : json_args.getMemberNames()) { const auto& value = json_args[key]; // Value has to be a string, number, or bool if (!value.isString() && !value.isNumeric() && !value.isBool()) { return ret; } ret.args[key] = value.asString(); } return ret; } Oomd::Config2::IR::DetectorGroup parseDetectorGroup( const Json::Value& detector_group) { Oomd::Config2::IR::DetectorGroup ir_detectorgroup; if (!detector_group.isArray()) { return {}; } for (Json::ArrayIndex i = 0; i < detector_group.size(); ++i) { if (i == 0 && detector_group[i].isString()) { ir_detectorgroup.name = detector_group[i].asString(); continue; } // else: all other indicies (ie detectors) const auto& detector = detector_group[i]; ir_detectorgroup.detectors.emplace_back( parsePlugin(detector)); } return ir_detectorgroup; } Oomd::Config2::IR::DropIn parseDropIn(const Json::Value& dropin) { Oomd::Config2::IR::DropIn ir_dropin; ir_dropin.disable_on_drop_in = dropin.get("disable-on-drop-in", false).asBool(); ir_dropin.detectorgroups_enabled = dropin.get("detectors", false).asBool(); ir_dropin.actiongroup_enabled = dropin.get("actions", false).asBool(); return ir_dropin; } Oomd::Config2::IR::Ruleset parseRuleset(const Json::Value& ruleset) { Oomd::Config2::IR::Ruleset ir_ruleset; ir_ruleset.name = ruleset.get("name", "").asString(); ir_ruleset.dropin = parseDropIn(ruleset.get("drop-in", {})); ir_ruleset.silence_logs = ruleset.get("silence-logs", {}).asString(); ir_ruleset.post_action_delay = ruleset.get("post_action_delay", {}).asString(); ir_ruleset.prekill_hook_timeout = ruleset.get("prekill_hook_timeout", {}).asString(); for (const auto& detector_group : ruleset.get("detectors", {})) { ir_ruleset.dgs.emplace_back(parseDetectorGroup(detector_group)); } for (const auto& action : ruleset.get("actions", {})) { ir_ruleset.acts.emplace_back( parsePlugin(action)); } return ir_ruleset; } } // namespace namespace Oomd { namespace Config2 { std::unique_ptr JsonConfigParser::parse(const std::string& input) { Json::Value json_root; getJson(json_root, input); auto ir_root = std::make_unique(); for (const auto& ruleset : json_root.get("rulesets", {})) { ir_root->rulesets.emplace_back(parseRuleset(ruleset)); } for (const auto& prekill_hook : json_root.get("prekill_hooks", {})) { ir_root->prekill_hooks.emplace_back( parsePlugin(prekill_hook)); } IR::dumpIR(*ir_root); return ir_root; } } // namespace Config2 } // namespace Oomd oomd-0.5.0/src/oomd/config/JsonConfigParser.h000066400000000000000000000017201406470533700210420ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include "oomd/config/ConfigTypes.h" namespace Oomd { namespace Config2 { class JsonConfigParser { public: std::unique_ptr parse(const std::string& input); }; } // namespace Config2 } // namespace Oomd oomd-0.5.0/src/oomd/config/JsonConfigParserTest.cpp000066400000000000000000000076031406470533700222430ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include "oomd/config/JsonConfigParser.h" using namespace Oomd::Config2; constexpr auto kConfig_1_0_0 = "oomd/fixtures/oomd.json"; TEST(JsonConfigParserTest, LoadIR) { std::ifstream file(kConfig_1_0_0, std::ios::in); ASSERT_TRUE(file.is_open()); std::ostringstream buffer; buffer << file.rdbuf(); // Make sure the IR was generated JsonConfigParser parser; auto root = parser.parse(buffer.str()); ASSERT_TRUE(root); // Check root values ASSERT_EQ(root->rulesets.size(), 2); // Check first ruleset const auto& first = root->rulesets[0]; EXPECT_EQ(first.name, "my first ruleset"); ASSERT_EQ(first.dgs.size(), 1); EXPECT_EQ(first.silence_logs, "engine,plugins"); EXPECT_EQ(first.post_action_delay, "10"); EXPECT_EQ(first.prekill_hook_timeout, "40"); // Check first ruleset drop in config EXPECT_TRUE(first.dropin.detectorgroups_enabled); EXPECT_FALSE(first.dropin.actiongroup_enabled); EXPECT_TRUE(first.dropin.disable_on_drop_in); // Check first ruleset's first DetectorGroup const auto& first_dg = first.dgs[0]; EXPECT_EQ(first_dg.name, "group1"); ASSERT_EQ(first_dg.detectors.size(), 2); EXPECT_EQ(first_dg.detectors[0].name, "pressure_rising_beyond"); EXPECT_EQ(first_dg.detectors[0].args.at("cgroup"), "workload.slice"); EXPECT_EQ(first_dg.detectors[0].args.at("resource"), "memory"); EXPECT_EQ(first_dg.detectors[0].args.at("threshold"), "5"); EXPECT_EQ(first_dg.detectors[1].name, "pressure_rising_beyond"); EXPECT_EQ(first_dg.detectors[1].args.at("cgroup"), "system.slice"); EXPECT_EQ(first_dg.detectors[1].args.at("resource"), "memory"); EXPECT_EQ(first_dg.detectors[1].args.at("threshold"), "40"); // Check first ruleset's actions ASSERT_EQ(first.acts.size(), 1); EXPECT_EQ(first.acts[0].name, "kill_by_memory_size_or_growth"); EXPECT_EQ(first.acts[0].args.at("cgroup"), "system.slice"); // Check second ruleset const auto& second = root->rulesets[1]; EXPECT_EQ(second.name, "low swap ruleset"); ASSERT_EQ(second.dgs.size(), 1); // Check second ruleset's first DetectorGroup const auto& second_dg = second.dgs[0]; EXPECT_EQ(second_dg.name, "my other group"); ASSERT_EQ(second_dg.detectors.size(), 1); EXPECT_EQ(second_dg.detectors[0].name, "swap_free"); EXPECT_EQ(second_dg.detectors[0].args.at("threshold_pct"), "15"); // Check second ruleset's actions ASSERT_EQ(second.acts.size(), 3); EXPECT_EQ(second.acts[0].name, "kill_by_swap_usage"); EXPECT_EQ(second.acts[0].args.at("cgroup"), "system.slice"); EXPECT_EQ(second.acts[1].name, "kill_by_swap_usage"); EXPECT_EQ( second.acts[1].args.at("cgroup"), "workload.slice/workload-wdb.slice"); EXPECT_EQ(second.acts[2].name, "kill_by_swap_usage"); EXPECT_EQ( second.acts[2].args.at("cgroup"), "workload.slice/workload-tw.slice"); // Check prekill hooks ASSERT_EQ(root->prekill_hooks.size(), 1); ASSERT_EQ(root->prekill_hooks[0].name, "hypothetical_prekill_hook"); ASSERT_EQ(root->prekill_hooks[0].args.size(), 0); } TEST(JsonConfigParserTest, LoadIRBadInput) { JsonConfigParser parser; ASSERT_THROW(parser.parse("not a json string"), std::runtime_error); } oomd-0.5.0/src/oomd/dropin/000077500000000000000000000000001406470533700155035ustar00rootroot00000000000000oomd-0.5.0/src/oomd/dropin/DropInServiceAdaptor.cpp000066400000000000000000000044751406470533700222500ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/dropin/DropInServiceAdaptor.h" #include "oomd/PluginConstructionContext.h" #include "oomd/config/ConfigTypes.h" #include "oomd/engine/Engine.h" namespace Oomd { void DropInServiceAdaptor::updateDropIns() { decltype(drop_in_queue_) drop_in_queue; tick(); { std::lock_guard lock(queue_mutex_); drop_in_queue = std::move(drop_in_queue_); } for (auto&& [tag, unit] : drop_in_queue) { // First remove then re-add. We don't do in place modifications as it'll // be complicated for the code and it probably wouldn't be what the user // expects. The user probably expects the entire drop in config is reset // and added to the front of the LIFO queue. engine_.removeDropInConfig(tag); if (!unit) { // If unit is nullopt, we just need to remove it handleDropInRemoveResult(tag, true); } else { bool drop_in_add_ok = engine_.addDropInConfig(tag, std::move(*unit)); handleDropInAddResult(tag, drop_in_add_ok); } } } bool DropInServiceAdaptor::scheduleDropInAdd( const std::string& tag, const Config2::IR::Root& drop_in) { const PluginConstructionContext compile_context(cgroup_fs_); auto unit = Config2::compileDropIn(root_, drop_in, compile_context); if (!unit.has_value()) { return false; } std::lock_guard lock(queue_mutex_); drop_in_queue_.emplace_back(tag, std::move(unit.value())); return true; } void DropInServiceAdaptor::scheduleDropInRemove(const std::string& tag) { std::lock_guard lock(queue_mutex_); drop_in_queue_.emplace_back(tag, std::nullopt); } } // namespace Oomd oomd-0.5.0/src/oomd/dropin/DropInServiceAdaptor.h000066400000000000000000000047261406470533700217140ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include "oomd/config/ConfigCompiler.h" namespace Oomd { namespace Config2::IR { struct Root; } namespace Engine { class Engine; } /** * Adaptor between services that handles oomd drop in configs and the oomd core. */ class DropInServiceAdaptor { public: /** * Both root and engine are from the oomd core and stored as references. They * are guaranteed to be valid when accessed as they are only used in * updateDropIns() which is called by the oomd core event loop. */ DropInServiceAdaptor( const std::string& cgroup_fs, const Config2::IR::Root& root, Engine::Engine& engine) : cgroup_fs_(cgroup_fs), root_(root), engine_(engine) {} virtual ~DropInServiceAdaptor() = default; /** * Called by Oomd core in event loop every interval. Drop in config changes * received by the service will be applied to the engine. Also update service * internal states with virtual functions below. */ void updateDropIns(); protected: virtual void tick() = 0; virtual void handleDropInAddResult(const std::string& tag, bool ok) = 0; virtual void handleDropInRemoveResult(const std::string& tag, bool ok) = 0; /** * For drop in service to add or remove configs from core asynchronously. * Adding drop in may fail if materialization fails. */ bool scheduleDropInAdd( const std::string& tag, const Config2::IR::Root& drop_in); void scheduleDropInRemove(const std::string& tag); private: std::string cgroup_fs_; const Config2::IR::Root& root_; Engine::Engine& engine_; std::mutex queue_mutex_; std::vector>> drop_in_queue_; }; } // namespace Oomd oomd-0.5.0/src/oomd/dropin/DropInServiceAdaptorTest.cpp000066400000000000000000000165711406470533700231100ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "oomd/PluginConstructionContext.h" #include "oomd/config/ConfigCompiler.h" #include "oomd/dropin/DropInServiceAdaptor.h" #include "oomd/util/TestHelper.h" namespace Oomd { namespace { // Defines MockPlugin class registered with name "AdaptorTest" DEFINE_MOCK_PLUGIN(AdaptorTest) using namespace Config2::IR; const Root root{ .rulesets = {Ruleset{ .name = "drop in ruleset", .dgs = {DetectorGroup{ .name = "detector group 0", .detectors = {{MockPlugin::createIR("RegularDetector")}}}}, .acts = {{MockPlugin::createIR("RegularAction")}}, .dropin = DropIn{ .disable_on_drop_in = true, .detectorgroups_enabled = true, .actiongroup_enabled = true}}}}; const Root drop_in_detector{ .rulesets = {Ruleset{ .name = "drop in ruleset", .dgs = {DetectorGroup{ .name = "drop in detector group 0", .detectors = {{MockPlugin::createIR("DropInDetector")}}}}}}}; const Root drop_in_action{ .rulesets = {Ruleset{ .name = "drop in ruleset", .acts = {{MockPlugin::createIR("DropInAction")}}}}}; class MockAdaptor : public DropInServiceAdaptor { public: MockAdaptor( const std::string& cgroup_fs, const Root& root, Engine::Engine& engine) : DropInServiceAdaptor(cgroup_fs, root, engine) {} bool scheduleDropInAdd(const std::string& tag, const Root& drop_in) { return DropInServiceAdaptor::scheduleDropInAdd(tag, drop_in); } void scheduleDropInRemove(const std::string& tag) { DropInServiceAdaptor::scheduleDropInRemove(tag); } MOCK_METHOD0(tick, void()); MOCK_METHOD2(handleDropInAddResult, void(const std::string&, bool)); MOCK_METHOD2(handleDropInRemoveResult, void(const std::string&, bool)); }; } // namespace class DropInServiceAdaptorTest : public ::testing::Test { protected: void SetUp() override { MockPlugin::runCounts().clear(); expectedRunCounts_.clear(); PluginConstructionContext ctx("/sys/fs/cgroup"); ctx_ = OomdContext(); engine_ = Config2::compile(root, ctx); ASSERT_TRUE(engine_ != nullptr); adaptor_ = std::make_unique("/sys/fs/cgroup", root, *engine_); } OomdContext ctx_; std::unique_ptr engine_; std::unique_ptr adaptor_; std::unordered_map expectedRunCounts_; }; TEST_F(DropInServiceAdaptorTest, AddRemove) { // Add drop in detector EXPECT_TRUE( adaptor_->scheduleDropInAdd("drop_in_detector.json", drop_in_detector)); EXPECT_CALL(*adaptor_, tick()); EXPECT_CALL(*adaptor_, handleDropInAddResult("drop_in_detector.json", true)); adaptor_->updateDropIns(); ::testing::Mock::VerifyAndClearExpectations(&*adaptor_); engine_->runOnce(ctx_); expectedRunCounts_ = {{"DropInDetector", 1}, {"RegularAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); MockPlugin::runCounts().clear(); // Add drop in action (now with two drop ins) EXPECT_TRUE( adaptor_->scheduleDropInAdd("drop_in_action.json", drop_in_action)); EXPECT_CALL(*adaptor_, tick()); EXPECT_CALL(*adaptor_, handleDropInAddResult("drop_in_action.json", true)); adaptor_->updateDropIns(); ::testing::Mock::VerifyAndClearExpectations(&*adaptor_); engine_->runOnce(ctx_); expectedRunCounts_ = { {"DropInDetector", 1}, {"RegularAction", 1}, {"RegularDetector", 1}, {"DropInAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); MockPlugin::runCounts().clear(); // Remove drop in detector (with drop in action left) adaptor_->scheduleDropInRemove("drop_in_detector.json"); EXPECT_CALL(*adaptor_, tick()); EXPECT_CALL( *adaptor_, handleDropInRemoveResult("drop_in_detector.json", true)); adaptor_->updateDropIns(); ::testing::Mock::VerifyAndClearExpectations(&*adaptor_); engine_->runOnce(ctx_); expectedRunCounts_ = {{"RegularDetector", 1}, {"DropInAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); MockPlugin::runCounts().clear(); // Remove drop in action (no drop in left) adaptor_->scheduleDropInRemove("drop_in_action.json"); EXPECT_CALL(*adaptor_, tick()); EXPECT_CALL(*adaptor_, handleDropInRemoveResult("drop_in_action.json", true)); adaptor_->updateDropIns(); ::testing::Mock::VerifyAndClearExpectations(&*adaptor_); engine_->runOnce(ctx_); expectedRunCounts_ = {{"RegularDetector", 1}, {"RegularAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); } TEST_F(DropInServiceAdaptorTest, QueuedAddRemove) { // Add and then remove processed in order EXPECT_TRUE( adaptor_->scheduleDropInAdd("drop_in_action.json", drop_in_action)); adaptor_->scheduleDropInRemove("drop_in_action.json"); EXPECT_CALL(*adaptor_, tick()); EXPECT_CALL(*adaptor_, handleDropInAddResult("drop_in_action.json", true)); EXPECT_CALL(*adaptor_, handleDropInRemoveResult("drop_in_action.json", true)); adaptor_->updateDropIns(); ::testing::Mock::VerifyAndClearExpectations(&*adaptor_); engine_->runOnce(ctx_); // No drop in expectedRunCounts_ = {{"RegularDetector", 1}, {"RegularAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); MockPlugin::runCounts().clear(); // Remove and then add processed in order adaptor_->scheduleDropInRemove("drop_in_action.json"); EXPECT_TRUE( adaptor_->scheduleDropInAdd("drop_in_action.json", drop_in_action)); EXPECT_CALL(*adaptor_, tick()); EXPECT_CALL(*adaptor_, handleDropInAddResult("drop_in_action.json", true)); // Currently remove never fails, even if it's no-op EXPECT_CALL(*adaptor_, handleDropInRemoveResult("drop_in_action.json", true)); adaptor_->updateDropIns(); ::testing::Mock::VerifyAndClearExpectations(&*adaptor_); engine_->runOnce(ctx_); // Drop in action injected expectedRunCounts_ = {{"RegularDetector", 1}, {"DropInAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); } TEST_F(DropInServiceAdaptorTest, AddFail) { Root bad_drop_in_action{ .rulesets = {Ruleset{ .name = "drop in ruleset", .acts = {Action{Plugin{.name = "BadPluginName"}}}}}}; EXPECT_FALSE( adaptor_->scheduleDropInAdd("drop_in_action.json", bad_drop_in_action)); Root bad_drop_in_ruleset{ .rulesets = {Ruleset{ .name = "bad drop in ruleset", .acts = {Action{Plugin{.name = "AdaptorTest"}}}}}}; EXPECT_FALSE( adaptor_->scheduleDropInAdd("drop_in_action.json", bad_drop_in_ruleset)); adaptor_->updateDropIns(); engine_->runOnce(ctx_); // No drop in added expectedRunCounts_ = {{"RegularDetector", 1}, {"RegularAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); } } // namespace Oomd oomd-0.5.0/src/oomd/dropin/FsDropInService.cpp000066400000000000000000000233101406470533700212130ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/dropin/FsDropInService.h" #include #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/config/JsonConfigParser.h" #include "oomd/include/Assert.h" #include "oomd/util/Fs.h" #include "oomd/util/Util.h" static constexpr auto kMaxEvents = 10; namespace Oomd { std::unique_ptr FsDropInService::create( const std::string& cgroup_fs, const Config2::IR::Root& root, Engine::Engine& engine, const std::string& drop_in_dir) { if (drop_in_dir.size() == 0) { return nullptr; } Fs::Fd epollfd(::epoll_create1(EPOLL_CLOEXEC)); if (epollfd.fd() < 0) { OLOG << "epoll_create1: " << Util::strerror_r(); return nullptr; } Fs::Fd terminatefd(::eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)); if (terminatefd.fd() < 0) { OLOG << "eventfd: " << Util::strerror_r(); return nullptr; } struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = terminatefd.fd(); if (::epoll_ctl(epollfd.fd(), EPOLL_CTL_ADD, terminatefd.fd(), &ev) < 0) { OLOG << "epoll_ctl: " << Util::strerror_r(); return nullptr; } return std::make_unique( Tag{}, cgroup_fs, root, engine, drop_in_dir, std::move(epollfd).fd(), std::move(terminatefd).fd()); } FsDropInService::FsDropInService( const Tag&, const std::string& cgroup_fs, const Config2::IR::Root& root, Engine::Engine& engine, const std::string& drop_in_dir, int epollfd, int terminatefd) : DropInServiceAdaptor(cgroup_fs, root, engine), epollfd_(epollfd), terminatefd_(terminatefd), drop_in_dir_(drop_in_dir) { // Sanitize the drop in dir a little if (drop_in_dir_.size() && drop_in_dir_.back() == '/') { // Delete the trailing '/' drop_in_dir_.pop_back(); } if (prepDropInWatcher(drop_in_dir_)) { drop_in_dir_deleted_ = true; } event_loop_ = std::thread(&FsDropInService::run, this); } FsDropInService::~FsDropInService() { uint64_t val = 1; if (sizeof(val) == ::write(terminatefd_, &val, sizeof(val))) { event_loop_.join(); } else { OLOG << "Failed to join event loop thread"; } ::close(terminatefd_); if (!drop_in_dir_deleted_) { // May race but we don't really care deregisterDropInWatcherFromEventLoop(); } ::close(epollfd_); } void FsDropInService::tick() { if (drop_in_dir_deleted_) { if (prepDropInWatcher(drop_in_dir_) == 0) { drop_in_dir_deleted_ = false; } } } void FsDropInService::handleDropInAddResult(const std::string& tag, bool ok) { if (ok) { OLOG << "Drop in config=" << tag << " injected into engine"; } else { OLOG << "Failed to inject drop in config=" << tag << " into engine"; } } void FsDropInService::handleDropInRemoveResult( const std::string& tag, bool ok) { if (ok) { OLOG << "Drop in config=" << tag << " removed from engine"; } else { OLOG << "Failed to remove drop in config=" << tag << " from engine"; } } int FsDropInService::deregisterDropInWatcherFromEventLoop() { int ret = 0; if (::epoll_ctl(epollfd_, EPOLL_CTL_DEL, inotifyfd_, nullptr) < 0) { OLOG << "epoll_ctl: " << Util::strerror_r(); ret = 1; } ::close(inotifyfd_); inotifyfd_ = -1; inotifywd_ = -1; return ret; } int FsDropInService::prepDropInWatcherEventLoop(const std::string& dir) { inotifyfd_ = ::inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (inotifyfd_ < 0) { OLOG << "inotify_init1: " << Util::strerror_r(); return 1; } uint32_t mask = IN_DELETE | IN_MODIFY | IN_MOVE | IN_ONLYDIR | IN_MOVE_SELF | IN_DELETE_SELF; if ((inotifywd_ = ::inotify_add_watch(inotifyfd_, dir.c_str(), mask)) < 0) { OLOG << "inotify_add_watch: " << Util::strerror_r(); return 1; } // Add inotifyfd to epoll set struct epoll_event ev; std::memset(&ev, 0, sizeof(ev)); ev.events = EPOLLIN; ev.data.fd = inotifyfd_; if (::epoll_ctl(epollfd_, EPOLL_CTL_ADD, inotifyfd_, &ev) < 0) { OLOG << "epoll_ctl: " << Util::strerror_r(); return 1; } return 0; } int FsDropInService::prepDropInWatcher(const std::string& dir) { if (!Fs::isDir(dir)) { OLOG << "Error: " << dir << " is not a directory"; return 1; } /* * This lock blocks the event loop from processing incoming watcher events, * but allows them to become pending. Then we add all existing drop in configs * in the directory. This ensures us not missing any drop in event after we * add the existing ones, although some may overlap, i.e. file A added to * directory triggers inotify and we also consider it an "existing" file. This * causes us to add the same file twice, but the net effect is the same. * Drop in removal is fine too, as both add and remove are idempotent. */ std::lock_guard lock(event_loop_mutex_); if (prepDropInWatcherEventLoop(dir)) { return 1; } auto de = Fs::readDir(dir, Fs::DE_FILE); // TODO(dschatzberg): Report error if (de) { std::sort(de->files.begin(), de->files.end()); // Provide some determinism for (const auto& config : de->files) { processDropInAdd(config); } } return 0; } void FsDropInService::processDropInRemove(const std::string& file) { // Ignore dot files if (file.empty() || (file.size() && file.at(0) == '.')) { return; } OLOG << "Removing drop in config=" << file; scheduleDropInRemove(file); } void FsDropInService::processDropInAdd(const std::string& file) { // Ignore dot files if (file.empty() || (file.size() && file.at(0) == '.')) { return; } OLOG << "Adding drop in config=" << file; std::ifstream dropin_file(drop_in_dir_ + '/' + file, std::ios::in); if (!dropin_file.is_open()) { OLOG << "Could not open drop in config=" << file; return; } std::stringstream buf; buf << dropin_file.rdbuf(); Config2::JsonConfigParser json_parser; std::unique_ptr dropin_root; try { dropin_root = json_parser.parse(buf.str()); } catch (const std::exception& e) { OLOG << "Caught: " << e.what(); OLOG << "Failed to inject drop in config into engine"; return; } if (!dropin_root) { OLOG << "Could not parse drop in config=" << file; OLOG << "Failed to inject drop in config into engine"; return; } if (!scheduleDropInAdd(file, *dropin_root)) { OLOG << "Could not compile drop in config"; OLOG << "Failed to inject drop in config into engine"; } } int FsDropInService::processDropInWatcher(int fd) { const struct inotify_event* event; alignas(struct inotify_event) std::array buf; while (true) { int len = ::read(fd, buf.data(), sizeof(buf)); if (len < 0 && errno != EAGAIN) { OLOG << "read: " << Util::strerror_r(); return 1; } if (len <= 0) { break; } for (char* ptr = buf.data(); ptr < (buf.data() + len); ptr += sizeof(struct inotify_event) + event->len) { event = reinterpret_cast(ptr); if (event->mask & (IN_MOVED_TO | IN_MODIFY)) { // Remove and re-add drop in if a file has been added to the // watched directory processDropInAdd(event->name); } else if (event->mask & (IN_DELETE | IN_MOVED_FROM)) { // Remove drop in if file has been moved from or removed from // the watched directory processDropInRemove(event->name); } else if (event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)) { // Remove stale watch descriptor for drop in if watched file or // directory itself is moved or deleted if (deregisterDropInWatcherFromEventLoop()) { return 1; } drop_in_dir_deleted_ = true; return 0; } } } return 0; } int FsDropInService::processEventLoop() { std::array events; int n; do { n = ::epoll_wait(epollfd_, events.data(), kMaxEvents, -1); } while (n < 0 && errno == EINTR); if (n < 0) { OLOG << "epoll_wait: " << Util::strerror_r(); return 1; } // This will only contend when drop in dir is recreated and some event fires, // which is very rare. See comment above in prepDropInWatcher(). std::lock_guard lock(event_loop_mutex_); for (int i = 0; i < n; ++i) { int fd = events[i].data.fd; if (fd == terminatefd_) { uint64_t val; ssize_t len = ::read(fd, &val, sizeof(val)); if (len != sizeof(val)) { OLOG << "read: " << Util::strerror_r(); return 1; } if (val != 1) { OLOG << "Unexpected terminatefd value=" << val; return 1; } // Special value for termination return 2; } else if (fd == inotifyfd_) { if (processDropInWatcher(fd)) { return 1; } } else { OLOG << "Unknown fd=" << fd << " in event loop"; return 1; } } return 0; } void FsDropInService::run() { int ret = 0; while (ret != 2) { ret = processEventLoop(); // Crash for unrecoverable errors OCHECK(ret != 1); } } } // namespace Oomd oomd-0.5.0/src/oomd/dropin/FsDropInService.h000066400000000000000000000045321406470533700206650ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include "oomd/dropin/DropInServiceAdaptor.h" namespace Oomd { class FsDropInService : public DropInServiceAdaptor { private: // Hide constructor struct Tag {}; public: // Return constructed service object or nullptr if anything fails static std::unique_ptr create( const std::string& cgroup_fs, const Config2::IR::Root& root, Engine::Engine& engine, const std::string& drop_in_dir); // Public constructor hidden by Tag, so make_unique works FsDropInService( const Tag& tag, const std::string& cgroup_fs, const Config2::IR::Root& root, Engine::Engine& engine, const std::string& drop_in_dir, int epollfd, int terminatefd); ~FsDropInService() override; protected: virtual void tick() override; virtual void handleDropInAddResult(const std::string& tag, bool ok) override; virtual void handleDropInRemoveResult(const std::string& tag, bool ok) override; private: int prepDropInWatcher(const std::string& dir); int prepDropInWatcherEventLoop(const std::string& dir); int deregisterDropInWatcherFromEventLoop(); int prepEventLoop(const std::chrono::seconds& interval); void processDropInRemove(const std::string& file); void processDropInAdd(const std::string& file); int processDropInWatcher(int fd); int processEventLoop(); void run(); int epollfd_{-1}; int terminatefd_{-1}; int inotifyfd_{-1}; int inotifywd_{-1}; std::atomic_bool drop_in_dir_deleted_{false}; std::string drop_in_dir_; std::thread event_loop_; std::mutex event_loop_mutex_; }; } // namespace Oomd oomd-0.5.0/src/oomd/dropin/FsDropInServiceTest.cpp000066400000000000000000000115061406470533700220570ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include "oomd/dropin/FsDropInService.h" #include "oomd/util/Fixture.h" #include "oomd/util/TestHelper.h" namespace Oomd { namespace { // Defines MockPlugin class registered with name "FsDropInTest" DEFINE_MOCK_PLUGIN(FsDropInTest); using namespace Config2::IR; const Root root{ .rulesets = {Ruleset{ .name = "drop in ruleset", .dgs = {DetectorGroup{ .name = "detector group 0", .detectors = {{MockPlugin::createIR("RegularDetector")}}}}, .acts = {{MockPlugin::createIR("RegularAction")}}, .dropin = DropIn{ .disable_on_drop_in = true, .detectorgroups_enabled = true, .actiongroup_enabled = true}}}}; constexpr auto drop_in_action = R"JSON({ "rulesets": [ { "name": "drop in ruleset", "actions": [ { "name": "FsDropInTest", "args": { "id": "DropInAction" } } ] } ] })JSON"; } // namespace class FsDropInServiceTest : public ::testing::Test { protected: void SetUp() override { MockPlugin::runCounts().clear(); expectedRunCounts_.clear(); PluginConstructionContext ctx("/sys/fs/cgroup"); ctx_ = OomdContext(); engine_ = Config2::compile(root, ctx); ASSERT_TRUE(engine_ != nullptr); drop_in_dir_ = Fixture::mkdtempChecked(); drop_in_service_ = FsDropInService::create("/sys/fs/cgroup", root, *engine_, drop_in_dir_); ASSERT_TRUE(drop_in_service_ != nullptr); } void TearDown() override { if (drop_in_dir_.size()) { Fixture::rmrChecked(drop_in_dir_); } } /* * FsDropInService uses epoll_wait on inotify to monitor drop in configs. * There is no easy way to tell if FS operations have been processed, so let's * sleep for a few jiffies. */ void wait_for_inotify() { /* sleep override */ std::this_thread::sleep_for(std::chrono::milliseconds(50)); } OomdContext ctx_; std::unique_ptr engine_; std::string drop_in_dir_; std::unique_ptr drop_in_service_; std::unordered_map expectedRunCounts_; }; TEST_F(FsDropInServiceTest, AddRemove) { Fixture::materialize( Fixture::makeFile("drop_in_action.json", drop_in_action), drop_in_dir_); wait_for_inotify(); drop_in_service_->updateDropIns(); engine_->runOnce(ctx_); expectedRunCounts_ = {{"RegularDetector", 1}, {"DropInAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); MockPlugin::runCounts().clear(); Fixture::rmrChecked(drop_in_dir_ + "/drop_in_action.json"); wait_for_inotify(); drop_in_service_->updateDropIns(); engine_->runOnce(ctx_); expectedRunCounts_ = {{"RegularDetector", 1}, {"RegularAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); MockPlugin::runCounts().clear(); } TEST_F(FsDropInServiceTest, LoadExisting) { // Recreate drop in service after creating drop in configs to simulate oomd // restarts. drop_in_service_ = nullptr; Fixture::materialize( Fixture::makeFile("drop_in_action.json", drop_in_action), drop_in_dir_); drop_in_service_ = FsDropInService::create("/sys/fs/cgroup", root, *engine_, drop_in_dir_); ASSERT_TRUE(drop_in_service_ != nullptr); drop_in_service_->updateDropIns(); engine_->runOnce(ctx_); expectedRunCounts_ = {{"RegularDetector", 1}, {"DropInAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); MockPlugin::runCounts().clear(); } TEST_F(FsDropInServiceTest, RecreateDir) { // Remove and recreate drop in dir Fixture::rmrChecked(drop_in_dir_); Fixture::materialize(Fixture::makeDir(drop_in_dir_)); wait_for_inotify(); // tick() should setup watcher for drop in dir again drop_in_service_->updateDropIns(); // Adding drop in config to recreated drop in dir should work Fixture::materialize( Fixture::makeFile("drop_in_action.json", drop_in_action), drop_in_dir_); wait_for_inotify(); drop_in_service_->updateDropIns(); engine_->runOnce(ctx_); expectedRunCounts_ = {{"RegularDetector", 1}, {"DropInAction", 1}}; EXPECT_EQ(MockPlugin::runCounts(), expectedRunCounts_); } } // namespace Oomd oomd-0.5.0/src/oomd/engine/000077500000000000000000000000001406470533700154555ustar00rootroot00000000000000oomd-0.5.0/src/oomd/engine/BasePlugin.h000066400000000000000000000053571406470533700176710ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include "oomd/OomdContext.h" #include "oomd/PluginConstructionContext.h" #include "oomd/include/CgroupPath.h" #include "oomd/include/Types.h" #include "oomd/util/PluginArgParser.h" namespace Oomd { namespace Engine { enum class PluginRet { CONTINUE = 0, STOP, ASYNC_PAUSED, }; class BasePlugin { public: /* * Config arguments are passed via @param args. If some required args are * missing, the plugin can return a non-zero value and oomd will abort * initialization. * * * @return 0 on successful initialization. */ virtual int init( const PluginArgs& args, const PluginConstructionContext& context) = 0; /* * This is always run at the beginning of the event loop before run() has * been called on any plugin. * * This is the ideal place to generate and store state inside object instance * because it is guaranteed to be called each interval, but run() may not. * Therefore, this function should be lightweight, i.e. no sleep() inside. */ virtual void prerun(OomdContext& context){}; /* * This is the main work method every plugin will implement. * * If part of a detector chain, a PluginRet::STOP will terminate and fail * the detector chain. If the entire detector chain returns * PluginRet::CONTINUE, the detector chain will succeed. * * If part of an action chain, a PluginRet::STOP will terminate further * execution of the action chain. PluginRet::Continue continues execution. * * Plugins may store state between invocations inside the object instance. * * @return @class PluginRet */ virtual PluginRet run(OomdContext& context) = 0; virtual void setName(const std::string& name) { name_ = name; argParser_.setName(name_); } virtual const std::string& getName() const { return name_; } virtual ~BasePlugin() = default; protected: PluginArgParser argParser_; private: std::string name_; }; } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/DetectorGroup.cpp000066400000000000000000000041661406470533700207560ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/engine/DetectorGroup.h" #include "oomd/Log.h" #include "oomd/OomdContext.h" #include "oomd/engine/EngineTypes.h" namespace Oomd { namespace Engine { DetectorGroup::DetectorGroup( const std::string& name, std::vector> detectors) : name_(name), detectors_(std::move(detectors)) {} void DetectorGroup::prerun(OomdContext& context) { for (const auto& detector : detectors_) { detector->prerun(context); } } bool DetectorGroup::check(OomdContext& context, uint32_t silenced_logs) { // Note we're running all Detectors so that any detectors keeping sliding // windows can update their window bool triggered = true; for (const auto& detector : detectors_) { if (silenced_logs & LogSources::PLUGINS) { OLOG << LogStream::Control::DISABLE; } PluginRet ret = detector->run(context); if (silenced_logs & LogSources::PLUGINS) { OLOG << LogStream::Control::ENABLE; } switch (ret) { case PluginRet::CONTINUE: continue; case PluginRet::STOP: triggered = false; break; case PluginRet::ASYNC_PAUSED: // ASYNC_PAUSED is not supported for detectors. Treat as no-op continue; // missing default to protect against future PluginRet vals } } return triggered; } const std::string& DetectorGroup::name() const { return name_; } } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/DetectorGroup.h000066400000000000000000000026751406470533700204260ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include "oomd/OomdContext.h" #include "oomd/engine/BasePlugin.h" namespace Oomd { namespace Engine { class DetectorGroup { public: DetectorGroup( const std::string& name, std::vector> detectors); ~DetectorGroup() = default; /* * Prerun all plugins in this detector group. */ void prerun(OomdContext& context); /* * @return true if no @class Detector returns PluginRet::STOP. */ bool check(OomdContext& context, uint32_t silenced_logs); const std::string& name() const; private: std::string name_; std::vector> detectors_; }; } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/Engine.cpp000066400000000000000000000123651406470533700173750ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/engine/Engine.h" #include #include #include "oomd/Log.h" #include "oomd/Stats.h" #include "oomd/include/CoreStats.h" namespace Oomd { namespace Engine { Engine::Engine( std::vector> rulesets, std::vector> prekill_hooks) { for (auto& rs : rulesets) { if (rs) { rulesets_.emplace_back(BaseRuleset{.ruleset = std::move(rs)}); } } // add base config hooks in reverse order so they'll be tried in forward order for (auto it = prekill_hooks.rbegin(); it != prekill_hooks.rend(); ++it) { prekill_hooks_in_reverse_order_.emplace_back( TaggedPrekillHook{.dropin_tag = std::nullopt, .hook = std::move(*it)}); } } bool Engine::addDropInConfig(const std::string& tag, DropInUnit unit) { for (auto& drop_in : unit.rulesets) { if (!addDropInRuleset(tag, std::move(drop_in))) { // Clean up partial added drop ins removeDropInConfig(tag); return false; } } // add dropin hooks in reverse order to the end of // prekill_hooks_in_reverse_order_ so they'll be tried in forward order, // before the base hooks. for (auto it = unit.prekill_hooks.rbegin(); it != unit.prekill_hooks.rend(); ++it) { prekill_hooks_in_reverse_order_.emplace_back( TaggedPrekillHook{.dropin_tag = tag, .hook = std::move(*it)}); } return true; } bool Engine::addDropInRuleset( const std::string& tag, std::unique_ptr ruleset) { if (!ruleset) { return false; } // First located the targeted ruleset auto it = std::find_if( rulesets_.begin(), rulesets_.end(), [&](const BaseRuleset& b) { return b.ruleset->getName() == ruleset->getName(); }); if (it == rulesets_.end()) { OLOG << "Error: could not locate targeted ruleset: " << ruleset->getName(); return false; } // Add drop in ruleset DropInRuleset dir; dir.tag = tag; dir.ruleset = std::move(ruleset); // NB: the drop in rulesets must be added/executed LIFO order. it->dropins.emplace_front(std::move(dir)); // Mark base ruleset at targeted it->ruleset->markDropInTargeted(); Oomd::incrementStat(CoreStats::kNumDropInAdds, 1); return true; } void Engine::removeDropInConfig(const std::string& tag) { auto pred = [&](const DropInRuleset& dir) { return dir.tag == tag; }; for (auto& base : rulesets_) { auto new_end = std::remove_if(base.dropins.begin(), base.dropins.end(), pred); int n = base.dropins.cend() - new_end; if (!n) { continue; } // Delete properly tagged drop in rulesets as requested base.dropins.erase(new_end, base.dropins.end()); // Mark base ruleset as untargeted for (int i = 0; i < n; ++i) { base.ruleset->markDropInUntargeted(); } // Make sure to decrement counter if there's a remove. This is to // normalize the count in case the same drop-in config is added/ // removed a bunch for some reason. Oomd::incrementStat(CoreStats::kNumDropInAdds, -n); } auto new_hooks_end = std::remove_if( prekill_hooks_in_reverse_order_.begin(), prekill_hooks_in_reverse_order_.end(), [&](const TaggedPrekillHook& tagged_hook) { return tagged_hook.dropin_tag == tag; }); prekill_hooks_in_reverse_order_.erase( new_hooks_end, prekill_hooks_in_reverse_order_.end()); } void Engine::prerun(OomdContext& context) { for (const auto& base : rulesets_) { for (const auto& dropin : base.dropins) { if (dropin.ruleset) { dropin.ruleset->prerun(context); } } base.ruleset->prerun(context); } } void Engine::runOnce(OomdContext& context) { uint32_t nr_dropins_run = 0; for (const auto& base : rulesets_) { // Run all the drop in rulesets first for (const auto& dropin : base.dropins) { if (dropin.ruleset) { nr_dropins_run += dropin.ruleset->runOnce(context); } } // Now run the base ruleset base.ruleset->runOnce(context); } Oomd::incrementStat(CoreStats::kNumDropInFired, nr_dropins_run); } std::optional> Engine::firePrekillHook( const CgroupContext& cgroup_ctx, const OomdContext& oomd_context) { // try hooks in reverse order so dropins come first for (auto it = prekill_hooks_in_reverse_order_.rbegin(); it != prekill_hooks_in_reverse_order_.rend(); ++it) { if (it->hook->canRunOnCgroup(cgroup_ctx)) { return it->hook->fire(cgroup_ctx, oomd_context.getActionContext()); } } return std::nullopt; } } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/Engine.h000066400000000000000000000054271406470533700170430ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include "oomd/OomdContext.h" #include "oomd/engine/BasePlugin.h" #include "oomd/engine/PrekillHook.h" #include "oomd/engine/Ruleset.h" namespace Oomd { namespace Engine { struct DropInUnit { std::vector> prekill_hooks; std::vector> rulesets; }; class Engine { public: explicit Engine( std::vector> rulesets, std::vector> prekill_hooks); ~Engine() = default; /* * Adds a drop in config to the running engine. * * @param tag is a unique tag to be associated with the drop * in config. Removing the dropped in config requires the original tag. * * @returns false if @param ruleset's target is not found. true otherwise. */ bool addDropInConfig(const std::string& tag, DropInUnit unit); bool addDropInRuleset( const std::string& tag, std::unique_ptr ruleset); /* * Removes drop in configs associated with @param tag */ void removeDropInConfig(const std::string& tag); /* * Preruns every @class Ruleset once. */ void prerun(OomdContext& context); /* * Runs every @class Ruleset once. */ void runOnce(OomdContext& context); std::optional> firePrekillHook( const CgroupContext& cgroup_ctx, const OomdContext& oomd_context); private: struct DropInRuleset { std::string tag; // required field std::unique_ptr ruleset; }; struct BaseRuleset { std::unique_ptr ruleset; std::deque dropins; }; std::vector rulesets_; struct TaggedPrekillHook { // dropin_tag is nullopt if hook is not a dropin std::optional dropin_tag; std::unique_ptr hook; }; // stored in reverse order so that dropins, which are pushed to the back, // are run first std::vector prekill_hooks_in_reverse_order_; }; } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/EngineTypes.h000066400000000000000000000015571406470533700200700ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once namespace Oomd { namespace Engine { enum LogSources { ENGINE = 1 << 0, PLUGINS = 1 << 1, }; } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/PrekillHook.h000066400000000000000000000056201406470533700200540ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include "oomd/CgroupContext.h" #include "oomd/PluginConstructionContext.h" #include "oomd/include/CgroupPath.h" #include "oomd/include/Types.h" #include "oomd/util/PluginArgParser.h" #include "oomd/util/Util.h" namespace Oomd { struct ActionContext; } namespace Oomd { namespace Engine { /* * A PrekillHook subclass's fire() is expected to start work that takes multiple * ticks of the main loop to finish and should not block. Instead, do the work * asynchronously and return an instance of PrekillHookInvocation whose * didFinish() will be polled. * All functions will only be called from main thread, including fire(), * didFinish(), and ~PrekillHookInvocation. None should block for long times. * See docs/prekill_hooks.md for details. */ class PrekillHookInvocation { public: virtual bool didFinish() = 0; virtual ~PrekillHookInvocation() = default; }; class PrekillHook { public: PrekillHook() = default; virtual ~PrekillHook() = default; PrekillHook(const PrekillHook&) = delete; PrekillHook& operator=(const PrekillHook&) = delete; virtual int init( const PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroup_patterns_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); if (!argParser_.parse(args)) { return 1; } return 0; } virtual std::unique_ptr fire( const CgroupContext& cgroup_ctx, const ActionContext& action_ctx) = 0; virtual bool canRunOnCgroup(const CgroupContext& cgroup_ctx) { for (auto pattern : cgroup_patterns_) { if (cgroup_ctx.cgroup().hasDescendantWithPrefixMatching(pattern)) { return true; } } return false; } virtual void setName(const std::string& name) { name_ = name; } virtual const std::string& getName() const { return name_; } protected: PluginArgParser argParser_; private: std::string name_; std::unordered_set cgroup_patterns_{}; }; } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/Ruleset.cpp000066400000000000000000000157771406470533700176250ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/engine/Ruleset.h" #include "oomd/Log.h" #include "oomd/engine/EngineTypes.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace Oomd { namespace Engine { Ruleset::Ruleset( const std::string& name, std::vector> detector_groups, std::vector> action_group, bool disable_on_drop_in, bool detectorgroups_dropin_enabled, bool actiongroup_dropin_enabled, uint32_t silence_logs, int post_action_delay, int prekill_hook_timeout) : name_(name), detector_groups_(std::move(detector_groups)), action_group_(std::move(action_group)), post_action_delay_(post_action_delay), prekill_hook_timeout_(prekill_hook_timeout), disable_on_drop_in_(disable_on_drop_in), detectorgroups_dropin_enabled_(detectorgroups_dropin_enabled), actiongroup_dropin_enabled_(actiongroup_dropin_enabled), silenced_logs_(silence_logs) {} bool Ruleset::mergeWithDropIn(std::unique_ptr ruleset) { if (!ruleset) { OLOG << "Error: merging with null ruleset"; return false; } if (ruleset->detector_groups_.size()) { if (!detectorgroups_dropin_enabled_) { OLOG << "Error: DetectorGroup drop-in configs disabled"; return false; } detector_groups_ = std::move(ruleset->detector_groups_); } if (ruleset->action_group_.size()) { if (!actiongroup_dropin_enabled_) { OLOG << "Error: Action drop-in configs disabled"; return false; } action_group_ = std::move(ruleset->action_group_); } return true; } void Ruleset::markDropInTargeted() { ++numTargeted_; if (disable_on_drop_in_ && numTargeted_) { enabled_ = false; } } void Ruleset::markDropInUntargeted() { --numTargeted_; if (numTargeted_ <= 0) { enabled_ = true; } } void Ruleset::prerun(OomdContext& context) { if (!enabled_) { return; } for (const auto& dg : detector_groups_) { dg->prerun(context); } for (const auto& action : action_group_) { action->prerun(context); } } uint32_t Ruleset::runOnce(OomdContext& context) { if (!enabled_) { return 0; } // If any DetectorGroup fires, then begin running action chain // // Note we're still check()'ing the detector groups so that any detectors // keeping sliding windows can update their window // // TODO(lnyng): Use prerun() for detector plugins to generate states, i.e. // store sliding window metrics, so we can break early in this loop. Need to // make sure time between prerun() and run() is short. bool run_actions = false; for (const auto& dg : detector_groups_) { if (dg->check(context, silenced_logs_) && !run_actions) { run_actions = true; context.setActionContext( {name_, dg->name(), Util::generateUuid(), std::chrono::steady_clock::now() + std::chrono::seconds(prekill_hook_timeout_)}); context.setInvokingRuleset(this); } } OOMD_SCOPE_EXIT { context.setActionContext({"", "", "", std::nullopt}); context.setInvokingRuleset(std::nullopt); }; // run actions if now() == pause_actions_until_ because a delay of 0 should // not cause a pause. if (std::chrono::steady_clock::now() < pause_actions_until_) { return 0; } if (active_action_chain_state_ != std::nullopt) { // resume the action context from when the action chain was fired context.setActionContext( std::move(active_action_chain_state_->action_context)); // clear active_async_plugin_ and save it to a temp BasePlugin& target = active_action_chain_state_->active_plugin; active_action_chain_state_ = std::nullopt; for (auto&& it = action_group_.begin(); it != action_group_.end(); ++it) { if (it->get() == &target) { return run_action_chain(it, action_group_.end(), context); } } } if (!run_actions) { return 0; } if (!(silenced_logs_ & LogSources::ENGINE)) { OLOG << "DetectorGroup=" << context.getActionContext().detectorgroup << " has fired for Ruleset=" << name_ << ". Running action chain."; } // Begin running action chain return run_action_chain(action_group_.begin(), action_group_.end(), context); } int Ruleset::run_action_chain( std::vector>::iterator action_chain_start, std::vector>::iterator action_chain_end, OomdContext& context) { for (; action_chain_start != action_chain_end; ++action_chain_start) { const std::unique_ptr& action = *action_chain_start; if (!(silenced_logs_ & LogSources::ENGINE)) { OLOG << "Running Action=" << action->getName(); } if (silenced_logs_ & LogSources::PLUGINS) { OLOG << LogStream::Control::DISABLE; } PluginRet ret = action->run(context); if (silenced_logs_ & LogSources::PLUGINS) { OLOG << LogStream::Control::ENABLE; } switch (ret) { case PluginRet::CONTINUE: if (!(silenced_logs_ & LogSources::ENGINE)) { OLOG << "Action=" << action->getName() << " returned CONTINUE. Continuing action chain."; } continue; case PluginRet::STOP: if (!(silenced_logs_ & LogSources::ENGINE)) { OLOG << "Action=" << action->getName() << " returned STOP. Terminating action chain."; } if (!plugin_overrode_post_action_delay_) { pause_actions_until_ = std::chrono::steady_clock::now() + std::chrono::seconds(post_action_delay_); } plugin_overrode_post_action_delay_ = false; break; // break out of switch case PluginRet::ASYNC_PAUSED: active_action_chain_state_ = std::make_optional(AsyncActionChainState{ .active_plugin = std::ref(*action.get()), .action_context = context.getActionContext()}); OLOG << "Action=" << action->getName() << " returned ASYNC. Yielding action chain."; return 0; // don't return 1 until action returns STOP // missing default to protect against future PluginRet vals } break; } return 1; } void Ruleset::pause_actions(std::chrono::seconds duration) { pause_actions_until_ = std::chrono::steady_clock::now() + duration; plugin_overrode_post_action_delay_ = true; } } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/engine/Ruleset.h000066400000000000000000000070371406470533700172600ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include "oomd/OomdContext.h" #include "oomd/engine/BasePlugin.h" #include "oomd/engine/DetectorGroup.h" namespace Oomd { namespace Engine { #define DEFAULT_POST_ACTION_DELAY 15 #define DEFAULT_PREKILL_HOOK_TIMEOUT 5 class Ruleset { public: Ruleset( const std::string& name, std::vector> detector_groups, std::vector> action_group, bool disable_on_drop_in = false, bool detectorgroups_dropin_enabled = false, bool actiongroup_dropin_enabled = false, uint32_t silenced_logs = 0, int post_action_delay = DEFAULT_POST_ACTION_DELAY, int prekill_hook_timeout = DEFAULT_PREKILL_HOOK_TIMEOUT); ~Ruleset() = default; /* * Merges this ruleset with @param ruleset with priority given to @param * ruleset. * * @returns false if @param ruleset tries to override configs to ruleset * members which do not have drop in turned on. true otherwise. */ [[nodiscard]] bool mergeWithDropIn(std::unique_ptr ruleset); /* * Mark/unmark this ruleset as being targeted by an active drop in. */ void markDropInTargeted(); void markDropInUntargeted(); /* * Prerun all plugins in this ruleset. */ void prerun(OomdContext& context); /* * Runs the all the DetectorGroup's. If any of them fires, then begin * running the action chain. * * @returns 1 if we attempted to run the action chain. 0 otherwise. */ uint32_t runOnce(OomdContext& context); const std::string& getName() const { return name_; } /* * for the next @param duration seconds, runOnce wont run the action chain, * even if the DetectorGroups fire. */ void pause_actions(std::chrono::seconds duration); private: std::string name_; std::vector> detector_groups_; std::vector> action_group_; int post_action_delay_{DEFAULT_POST_ACTION_DELAY}; int prekill_hook_timeout_{DEFAULT_PREKILL_HOOK_TIMEOUT}; bool enabled_{true}; bool disable_on_drop_in_{false}; bool detectorgroups_dropin_enabled_{false}; bool actiongroup_dropin_enabled_{false}; uint32_t silenced_logs_{0}; int32_t numTargeted_{0}; struct AsyncActionChainState { public: std::reference_wrapper active_plugin; ActionContext action_context; }; std::optional active_action_chain_state_{std::nullopt}; int run_action_chain( std::vector>::iterator action_chain_start, std::vector>::iterator action_chain_end, OomdContext& context); std::chrono::steady_clock::time_point pause_actions_until_ = std::chrono::steady_clock::time_point(); bool plugin_overrode_post_action_delay_{false}; }; } // namespace Engine } // namespace Oomd oomd-0.5.0/src/oomd/etc/000077500000000000000000000000001406470533700147635ustar00rootroot00000000000000oomd-0.5.0/src/oomd/etc/desktop.json000066400000000000000000000061771406470533700173420ustar00rootroot00000000000000// // Basic configuration for a desktop linux machine // { "rulesets": [ { "name": "system overview", "silence-logs": "engine", "detectors": [ [ "records system stats", { "name": "dump_cgroup_overview", "args": { "cgroup": "system.slice" } } ] ], "actions": [ { "name": "continue", "args": { } } ] }, { "name": "user session protection", "detectors": [ [ "user pressure above 60 for 30s", { "name": "pressure_above", "args": { "cgroup": "user.slice", "resource": "memory", "threshold": "60", "duration": "30" } }, { "name": "memory_reclaim", "args": { "cgroup": "user.slice", "duration": "10" } } ], [ "system pressure above 80 for 60s", { "name": "pressure_above", "args": { "cgroup": "system.slice", "resource": "memory", "threshold": "80", "duration": "60" } }, { "name": "memory_reclaim", "args": { "cgroup": "system.slice", "duration": "10" } } ] ], "actions": [ { "name": "kill_by_memory_size_or_growth", "args": { "cgroup": "user.slice/user-*.slice/session-*.scope,user.slice/user-*.slice/user@*.service/*,system.slice/*" } } ] }, { "name": "protection against low swap", "detectors": [ [ "free swap goes below 10 percent", { "name": "swap_free", "args": { "threshold_pct": "10" } } ] ], "actions": [ { "name": "kill_by_swap_usage", "args": { "cgroup": "user.slice/user-*.slice/session-*.scope,user.slice/user-*.slice/user@*.service/*,system.slice/*" } } ] } ] } oomd-0.5.0/src/oomd/etc/oomd.service.in000066400000000000000000000003651406470533700177140ustar00rootroot00000000000000[Unit] Description=Userland out-of-memory killer daemon After=system.slice [Service] ExecStart=@bindir@/oomd --interval 1 --config @oomdconfdir@/oomd.json Restart=always SyslogIdentifier=oomd MemoryLow=64M [Install] WantedBy=multi-user.target oomd-0.5.0/src/oomd/fixtures/000077500000000000000000000000001406470533700160615ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/FsFixture.cpp000066400000000000000000000255031406470533700205110ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/fixtures/FsFixture.h" namespace Oomd { constexpr auto kDataMeminfo = R"(MemTotal: 58616708 kB MemFree: 2955848 kB MemAvailable: 49878948 kB Buffers: 4970160 kB Cached: 35172508 kB SwapCached: 0 kB Active: 27822776 kB Inactive: 19032824 kB Active(anon): 4024064 kB Inactive(anon): 2822876 kB Active(file): 23798712 kB Inactive(file): 16209948 kB Unevictable: 11684 kB Mlocked: 11668 kB SwapTotal: 2097148 kB SwapFree: 1097041 kB Dirty: 24576 kB Writeback: 0 kB AnonPages: 6725380 kB Mapped: 657992 kB Shmem: 130124 kB Slab: 8238080 kB SReclaimable: 7334616 kB SUnreclaim: 903464 kB KernelStack: 45984 kB PageTables: 75756 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 31405500 kB Committed_AS: 21265672 kB VmallocTotal: 34359738367 kB VmallocUsed: 0 kB VmallocChunk: 0 kB HardwareCorrupted: 0 kB AnonHugePages: 71680 kB ShmemHugePages: 0 kB ShmemPmdMapped: 0 kB CmaTotal: 0 kB CmaFree: 0 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB Hugetlb: 0 kB DirectMap4k: 4544368 kB DirectMap2M: 53127168 kB DirectMap1G: 4194304 kB )"; constexpr auto kDataMounts = R"(sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0 proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 devtmpfs /dev devtmpfs rw,seclabel,nosuid,size=58710812k,nr_inodes=14677703,mode=755 0 0 securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0 tmpfs /dev/shm tmpfs rw,seclabel,nosuid,nodev 0 0 devpts /dev/pts devpts rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0 tmpfs /run tmpfs rw,seclabel,nosuid,nodev,mode=755 0 0 cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate 0 0 pstore /sys/fs/pstore pstore rw,seclabel,nosuid,nodev,noexec,relatime 0 0 bpf /sys/fs/bpf bpf rw,relatime,mode=700 0 0 configfs /sys/kernel/config configfs rw,relatime 0 0 /dev/vda3 / btrfs rw,seclabel,relatime,compress-force=zstd,discard,space_cache,subvolid=5,subvol=/ 0 0 selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0 systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct 0 0 hugetlbfs /dev/hugepages hugetlbfs rw,seclabel,relatime,pagesize=2M 0 0 debugfs /sys/kernel/debug debugfs rw,seclabel,relatime,mode=755 0 0 mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0 sunrpc /var/lib/nfs/rpc_pipefs rpc_pipefs rw,relatime 0 0 nfsd /proc/fs/nfsd nfsd rw,relatime 0 0 fusectl /sys/fs/fuse/connections fusectl rw,relatime 0 0 /dev/vda1 /boot ext4 rw,seclabel,relatime,data=ordered 0 0 none-tw-channel /run/facebook/tw_logging_channel tmpfs rw,seclabel,relatime,size=10240k,nr_inodes=4096 0 0 tracefs /sys/kernel/debug/tracing tracefs rw,seclabel,relatime 0 0 /etc/auto.home.override /home autofs rw,relatime,fd=5,pgrp=2182594,timeout=600,minproto=5,maxproto=5,indirect 0 0 binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc rw,relatime 0 0 tmpfs /run/user/30015 tmpfs rw,seclabel,nosuid,nodev,relatime,size=11744436k,mode=700,uid=30015,gid=30015 0 0 tmpfs /run/user/0 tmpfs rw,seclabel,nosuid,nodev,relatime,size=11744436k,mode=700 0 0 tmpfs /run/user/180401 tmpfs rw,seclabel,nosuid,nodev,relatime,size=11744436k,mode=700,uid=180401,gid=100 0 0 )"; constexpr auto kDataCgMemStat = R"(anon 1294168064 file 3870687232 kernel_stack 7225344 slab 296456192 sock 1228800 shmem 135168 file_mapped 199778304 file_dirty 405504 file_writeback 811008 inactive_anon 296529920 active_anon 997982208 inactive_file 1284907008 active_file 2586988544 unevictable 28672 slab_reclaimable 262471680 slab_unreclaimable 33984512 pgfault 734452455 pgmajfault 4627590 pgrefill 243154 pgscan 787288 pgsteal 770913 pgactivate 13992 pgdeactivate 233504 pglazyfree 0 pglazyfreed 0 workingset_refault 510510 workingset_activate 25608 workingset_restore 3927 workingset_nodereclaim 0 )"; constexpr auto kDataVmstat = R"(first_key 12345 second_key 678910 thirdkey 999999 )"; namespace { using F = Fixture; const auto kEntCgroup = F::makeDir( "cgroup", {F::makeDir( "system.slice", { F::makeDir( "service1.service", {F::makeFile( "cgroup.procs", "456\n" "789\n")}), F::makeDir( "service2.service", {F::makeFile( "memory.pressure", "aggr 128544748770\n" "some 1.11 2.22 3.33\n" "full 4.44 5.55 6.66\n")}), F::makeDir( "service3.service", { F::makeFile( "memory.pressure", "aggr 128544748770\n" "some 1.11 2.22 3.33\n" "full 4.44 5.55 6.66\n" "0 0 0\n" "0 0 0\n" "0 0 0\n" "0 0 0\n"), F::makeFile( "cgroup.events", "populated 0\n" "frozen 0\n"), }), F::makeDir( "slice1.slice", {F::makeFile("memory.oom.group", "1\n"), F::makeDir( "service1.service", {F::makeFile("memory.oom.group", "0\n"), F::makeFile( "cgroup.procs", "456\n" "789\n")}), F::makeDir( "service2.service", {F::makeFile( "cgroup.procs", "12\n" "34\n")})}), F::makeFile("cgroup.controllers", "cpu io memory pids\n"), F::makeFile("cgroup.procs", "123\n"), F::makeFile( "cgroup.events", "populated 1\n" "frozen 0\n"), F::makeFile( "cgroup.stat", "nr_descendants 34\n" "nr_dying_descendants 27\n"), F::makeFile( "io.pressure", "some avg10=1.12 avg60=2.23 avg300=3.34 total=134829384401\n" "full avg10=4.45 avg60=5.56 avg300=6.67 total=128544748771\n"), F::makeFile( "io.stat", "1:10 rbytes=1111111 wbytes=2222222 rios=33 wios=44 dbytes=5555555555 dios=6\n" "1:11 rbytes=2222222 wbytes=3333333 rios=44 wios=55 dbytes=6666666666 dios=7\n"), F::makeFile("memory.current", "987654321\n"), F::makeFile("memory.max", "654\n"), F::makeFile("memory.high", "1000\n"), F::makeFile("memory.high.tmp", "2000 20000\n"), F::makeFile("memory.low", "333333\n"), F::makeFile("memory.min", "666\n"), F::makeFile( "memory.pressure", "some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400\n" "full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770\n"), F::makeFile("memory.stat", kDataCgMemStat), F::makeFile("memory.swap.current", "321321\n"), F::makeFile("memory.swap.max", "12345\n"), })}); const auto kEntFsData = F::makeDir( "fs_data", { F::makeDir( "dir1", { F::makeFile( "stuff", "hello world\n" "my good man\n" "\n" "1\n"), }), F::makeDir("dir2", {F::makeFile("empty")}), F::makeDir("dir3", {F::makeFile("empty")}), F::makeDir( "wildcard", { F::makeDir("dir1", {F::makeFile("file")}), F::makeDir("dir2", {F::makeFile("file")}), F::makeDir("different_dir", {F::makeFile("file")}), F::makeFile("file"), }), F::makeFile("file1"), F::makeFile("file2"), F::makeFile("file3"), F::makeFile("file4"), }); const auto kEntProc = F::makeDir( "proc", { F::makeFile("meminfo", kDataMeminfo), F::makeFile("mounts", kDataMounts), F::makeFile("vmstat", kDataVmstat), }); const auto kEntSysDevBlock = F::makeDir( "sys_dev_block", { F::makeDir( "1:0", {F::makeDir("queue", {F::makeFile("rotational", "0\n")})}), F::makeDir( "1:1", {F::makeDir("queue", {F::makeFile("rotational", "1\n")})}), F::makeDir( "1:2", {F::makeDir("queue", {F::makeFile("rotational", "\n")})}), }); const auto kEntFsFixture = F::makeDir( "fs_test", { kEntCgroup, kEntFsData, kEntProc, kEntSysDevBlock, }); } // namespace constexpr auto kCgroupDataDir = "fs_test/cgroup/system.slice"; constexpr auto kFsDataDir = "fs_test/fs_data"; constexpr auto kFsVmstatFile = "fs_test/proc/vmstat"; constexpr auto kFsMeminfoFile = "fs_test/proc/meminfo"; constexpr auto kFsMountsFile = "fs_test/proc/mounts"; constexpr auto kFsDeviceDir = "fs_test/sys_dev_block"; void FsFixture::materialize() { tempFixtureDir_ = F::mkdtempChecked(); kEntFsFixture.second.materialize(tempFixtureDir_, kEntFsFixture.first); } void FsFixture::teardown() { F::rmrChecked(tempFixtureDir_); tempFixtureDir_ = ""; } std::string FsFixture::cgroupDataDir() { return tempFixtureDir_ + "/" + kCgroupDataDir; } std::string FsFixture::fsDataDir() { return tempFixtureDir_ + "/" + kFsDataDir; } std::string FsFixture::fsVmstatFile() { return tempFixtureDir_ + "/" + kFsVmstatFile; } std::string FsFixture::fsMeminfoFile() { return tempFixtureDir_ + "/" + kFsMeminfoFile; } std::string FsFixture::fsMountsFile() { return tempFixtureDir_ + "/" + kFsMountsFile; } std::string FsFixture::fsDeviceDir() { return tempFixtureDir_ + "/" + kFsDeviceDir; } } // namespace Oomd oomd-0.5.0/src/oomd/fixtures/FsFixture.h000066400000000000000000000021171406470533700201520ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/util/Fixture.h" namespace Oomd { class FsFixture { public: void materialize(); void teardown(); std::string cgroupDataDir(); std::string fsDataDir(); std::string fsVmstatFile(); std::string fsMeminfoFile(); std::string fsMountsFile(); std::string fsDeviceDir(); private: std::string tempFixtureDir_{}; }; } // namespace Oomd oomd-0.5.0/src/oomd/fixtures/cgroup/000077500000000000000000000000001406470533700173605ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/missing_control_files.slice/000077500000000000000000000000001406470533700250515ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/missing_control_files.slice/cgroup.controllers000066400000000000000000000000071406470533700306350ustar00rootroot00000000000000memory oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/000077500000000000000000000000001406470533700250675ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/000077500000000000000000000000001406470533700252475ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/000077500000000000000000000000001406470533700255105ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/cgroup.controllers000066400000000000000000000000231406470533700312720ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/memory.current000066400000000000000000000000131406470533700304160ustar00rootroot000000000000002147483648 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/memory.high000066400000000000000000000000041406470533700276530ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/memory.low000066400000000000000000000000131406470533700275350ustar00rootroot000000000000001073741824 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/memory.max000066400000000000000000000000041406470533700275210ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/memory.min000066400000000000000000000000021406470533700275150ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A1/memory.pressure000066400000000000000000000001641406470533700306130ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/000077500000000000000000000000001406470533700255115ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/cgroup.controllers000066400000000000000000000000231406470533700312730ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/memory.current000066400000000000000000000000131406470533700304170ustar00rootroot000000000000002147483648 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/memory.high000066400000000000000000000000041406470533700276540ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/memory.low000066400000000000000000000000131406470533700275360ustar00rootroot000000000000004294967296 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/memory.max000066400000000000000000000000041406470533700275220ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/memory.min000066400000000000000000000000021406470533700275160ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/A2/memory.pressure000066400000000000000000000001641406470533700306140ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/cgroup.controllers000066400000000000000000000000231406470533700310310ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/memory.current000066400000000000000000000000131406470533700301550ustar00rootroot000000000000004294967296 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/memory.high000066400000000000000000000000041406470533700274120ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/memory.low000066400000000000000000000000131406470533700272740ustar00rootroot000000000000002147483648 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/memory.max000066400000000000000000000000041406470533700272600ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/memory.min000066400000000000000000000000021406470533700272540ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/A/memory.pressure000066400000000000000000000001641406470533700303520ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/000077500000000000000000000000001406470533700252505ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/000077500000000000000000000000001406470533700255125ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/cgroup.controllers000066400000000000000000000000231406470533700312740ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/memory.current000066400000000000000000000000131406470533700304200ustar00rootroot000000000000002147483648 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/memory.high000066400000000000000000000000041406470533700276550ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/memory.low000066400000000000000000000000131406470533700275370ustar00rootroot000000000000001073741824 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/memory.max000066400000000000000000000000041406470533700275230ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/memory.min000066400000000000000000000000021406470533700275170ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B1/memory.pressure000066400000000000000000000001641406470533700306150ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/000077500000000000000000000000001406470533700255135ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/cgroup.controllers000066400000000000000000000000231406470533700312750ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/memory.current000066400000000000000000000000131406470533700304210ustar00rootroot000000000000002147483648 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/memory.high000066400000000000000000000000041406470533700276560ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/memory.low000066400000000000000000000000131406470533700275400ustar00rootroot000000000000004294967296 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/memory.max000066400000000000000000000000041406470533700275240ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/memory.min000066400000000000000000000000021406470533700275200ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/B2/memory.pressure000066400000000000000000000001641406470533700306160ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/cgroup.controllers000066400000000000000000000000231406470533700310320ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/memory.current000066400000000000000000000000131406470533700301560ustar00rootroot000000000000004294967296 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/memory.high000066400000000000000000000000041406470533700274130ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/memory.low000066400000000000000000000000131406470533700272750ustar00rootroot000000000000001073741824 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/memory.max000066400000000000000000000000041406470533700272610ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/memory.min000066400000000000000000000000021406470533700272550ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/B/memory.pressure000066400000000000000000000001641406470533700303530ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/000077500000000000000000000000001406470533700252515ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/000077500000000000000000000000001406470533700255145ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/cgroup.controllers000066400000000000000000000000231406470533700312760ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/memory.current000066400000000000000000000000131406470533700304220ustar00rootroot000000000000006442450944 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/memory.high000066400000000000000000000000041406470533700276570ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/memory.low000066400000000000000000000000131406470533700275410ustar00rootroot000000000000005368709120 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/memory.max000066400000000000000000000000041406470533700275250ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/memory.min000066400000000000000000000000021406470533700275210ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/C1/memory.pressure000066400000000000000000000001641406470533700306170ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/cgroup.controllers000066400000000000000000000000231406470533700310330ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/memory.current000066400000000000000000000000131406470533700301570ustar00rootroot000000000000006442450944 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/memory.high000066400000000000000000000000041406470533700274140ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/memory.low000066400000000000000000000000131406470533700272760ustar00rootroot000000000000005368709120 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/memory.max000066400000000000000000000000041406470533700272620ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/memory.min000066400000000000000000000000021406470533700272560ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/C/memory.pressure000066400000000000000000000001641406470533700303540ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/memory.high000066400000000000000000000000041406470533700272320ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/protection_overage.fakeroot/memory.max000066400000000000000000000000041406470533700271000ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/000077500000000000000000000000001406470533700220025ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/cgroup.controllers000066400000000000000000000000231406470533700255640ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/cgroup.procs000066400000000000000000000000041406470533700243430ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/cgroup.subtree_control000066400000000000000000000000231406470533700264270ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/io.pressure000066400000000000000000000001641406470533700242040ustar00rootroot00000000000000some avg10=1.12 avg60=2.23 avg300=3.34 total=134829384401 full avg10=4.45 avg60=5.56 avg300=6.67 total=128544748771 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/io.stat000066400000000000000000000002301406470533700233010ustar00rootroot000000000000001:10 rbytes=1111111 wbytes=2222222 rios=33 wios=44 dbytes=5555555555 dios=6 1:11 rbytes=2222222 wbytes=3333333 rios=44 wios=55 dbytes=6666666666 dios=7 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.current000066400000000000000000000000121406470533700247070ustar00rootroot00000000000000987654321 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.high000066400000000000000000000000051406470533700241460ustar00rootroot000000000000001000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.high.tmp000066400000000000000000000000131406470533700247440ustar00rootroot000000000000002000 20000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.low000066400000000000000000000000071406470533700240320ustar00rootroot00000000000000333333 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.max000066400000000000000000000000041406470533700240130ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.min000066400000000000000000000000041406470533700240110ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.pressure000066400000000000000000000001641406470533700251050ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.stat000066400000000000000000000010721406470533700242070ustar00rootroot00000000000000anon 1294168064 file 3870687232 kernel_stack 7225344 slab 296456192 sock 1228800 shmem 135168 file_mapped 199778304 file_dirty 405504 file_writeback 811008 inactive_anon 296529920 active_anon 997982208 inactive_file 1284907008 active_file 2586988544 unevictable 28672 slab_reclaimable 262471680 slab_unreclaimable 33984512 pgfault 734452455 pgmajfault 4627590 pgrefill 243154 pgscan 787288 pgsteal 770913 pgactivate 13992 pgdeactivate 233504 pglazyfree 0 pglazyfreed 0 workingset_refault 510510 workingset_activate 25608 workingset_restore 3927 workingset_nodereclaim 0 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/memory.swap.current000066400000000000000000000000071406470533700256640ustar00rootroot00000000000000321321 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/000077500000000000000000000000001406470533700251625ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/cgroup.controllers000066400000000000000000000000231406470533700307440ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/cgroup.procs000066400000000000000000000000101406470533700275200ustar00rootroot00000000000000456 789 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/memory.current000066400000000000000000000000071406470533700300730ustar00rootroot00000000000000111111 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/memory.high000066400000000000000000000000051406470533700273260ustar00rootroot000000000000001000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/memory.low000066400000000000000000000000041406470533700272070ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/memory.max000066400000000000000000000000041406470533700271730ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/memory.min000066400000000000000000000000041406470533700271710ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/memory.pressure000066400000000000000000000001641406470533700302650ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service1.service/memory.swap.current000066400000000000000000000000021406470533700310370ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/000077500000000000000000000000001406470533700251635ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/cgroup.controllers000066400000000000000000000000231406470533700307450ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/memory.current000066400000000000000000000000121406470533700300700ustar00rootroot00000000000000987654321 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/memory.high000066400000000000000000000000051406470533700273270ustar00rootroot000000000000001000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/memory.low000066400000000000000000000000041406470533700272100ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/memory.max000066400000000000000000000000041406470533700271740ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/memory.min000066400000000000000000000000041406470533700271720ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/memory.pressure000066400000000000000000000000721406470533700302640ustar00rootroot00000000000000aggr 128544748770 some 1.11 2.22 3.33 full 4.44 5.55 6.66 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service2.service/memory.swap.current000066400000000000000000000000021406470533700310400ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/000077500000000000000000000000001406470533700251645ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/cgroup.controllers000066400000000000000000000000231406470533700307460ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/memory.current000066400000000000000000000000121406470533700300710ustar00rootroot00000000000000987654321 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/memory.high000066400000000000000000000000051406470533700273300ustar00rootroot000000000000001000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/memory.low000066400000000000000000000000041406470533700272110ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/memory.max000066400000000000000000000000041406470533700271750ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/memory.min000066400000000000000000000000041406470533700271730ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/memory.pressure000066400000000000000000000001221406470533700302610ustar00rootroot00000000000000aggr 128544748770 some 1.11 2.22 3.33 full 4.44 5.55 6.66 0 0 0 0 0 0 0 0 0 0 0 0 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service3.service/memory.swap.current000066400000000000000000000000021406470533700310410ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/000077500000000000000000000000001406470533700251655ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/cgroup.controllers000066400000000000000000000000231406470533700307470ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/memory.current000066400000000000000000000000121406470533700300720ustar00rootroot00000000000000987654321 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/memory.high000066400000000000000000000000051406470533700273310ustar00rootroot000000000000001000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/memory.low000066400000000000000000000000041406470533700272120ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/memory.max000066400000000000000000000000041406470533700271760ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/memory.min000066400000000000000000000000041406470533700271740ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/memory.pressure000066400000000000000000000000721406470533700302660ustar00rootroot00000000000000aggr 128544748770 some 1.11 2.22 3.33 full 4.44 5.55 6.66 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/service4.service/memory.swap.current000066400000000000000000000000021406470533700310420ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/000077500000000000000000000000001406470533700242605ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/cgroup.controllers000066400000000000000000000000231406470533700300420ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/cgroup.procs000066400000000000000000000000001406470533700266150ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/memory.current000066400000000000000000000000121406470533700271650ustar00rootroot00000000000000987654321 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/memory.high000066400000000000000000000000051406470533700264240ustar00rootroot000000000000001000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/memory.low000066400000000000000000000000041406470533700263050ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/memory.max000066400000000000000000000000041406470533700262710ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/memory.min000066400000000000000000000000041406470533700262670ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/memory.pressure000066400000000000000000000000721406470533700273610ustar00rootroot00000000000000aggr 128544748770 some 1.11 2.22 3.33 full 4.44 5.55 6.66 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/memory.swap.current000066400000000000000000000000021406470533700301350ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service1.service/000077500000000000000000000000001406470533700274405ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service1.service/cgroup.procs000066400000000000000000000000101406470533700317760ustar00rootroot00000000000000456 789 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service1.service/memory.current000066400000000000000000000000071406470533700323510ustar00rootroot00000000000000111111 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service1.service/memory.low000066400000000000000000000000041406470533700314650ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service1.service/memory.pressure000066400000000000000000000001641406470533700325430ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service1.service/memory.swap.current000066400000000000000000000000021406470533700333150ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service2.service/000077500000000000000000000000001406470533700274415ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service2.service/cgroup.procs000066400000000000000000000000061406470533700320040ustar00rootroot0000000000000012 34 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service2.service/memory.current000066400000000000000000000000071406470533700323520ustar00rootroot00000000000000111111 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service2.service/memory.low000066400000000000000000000000041406470533700314660ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service2.service/memory.pressure000066400000000000000000000001641406470533700325440ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/system.slice/slice1.slice/service2.service/memory.swap.current000066400000000000000000000000021406470533700333160ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/000077500000000000000000000000001406470533700223005ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/cgroup.controllers000066400000000000000000000000231406470533700260620ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/cgroup.procs000066400000000000000000000000041406470533700246410ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/cgroup.subtree_control000066400000000000000000000000231406470533700267250ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/io.pressure000066400000000000000000000001641406470533700245020ustar00rootroot00000000000000some avg10=1.12 avg60=2.23 avg300=3.34 total=134829384401 full avg10=4.45 avg60=5.56 avg300=6.67 total=128544748771 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/memory.current000066400000000000000000000000121406470533700252050ustar00rootroot00000000000000987654321 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/memory.high000066400000000000000000000000041406470533700244430ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/memory.low000066400000000000000000000000071406470533700243300ustar00rootroot00000000000000333333 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/memory.max000066400000000000000000000000041406470533700243110ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/memory.min000066400000000000000000000000041406470533700243070ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/memory.pressure000066400000000000000000000001641406470533700254030ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/memory.swap.current000066400000000000000000000000071406470533700261620ustar00rootroot00000000000000321321 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/000077500000000000000000000000001406470533700254605ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/cgroup.controllers000066400000000000000000000000231406470533700312420ustar00rootroot00000000000000cpu io memory pids oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/cgroup.procs000066400000000000000000000000101406470533700300160ustar00rootroot00000000000000456 789 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/memory.current000066400000000000000000000000071406470533700303710ustar00rootroot00000000000000111111 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/memory.high000066400000000000000000000000041406470533700276230ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/memory.low000066400000000000000000000000041406470533700275050ustar00rootroot00000000000000123 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/memory.max000066400000000000000000000000041406470533700274710ustar00rootroot00000000000000max oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/memory.min000066400000000000000000000000041406470533700274670ustar00rootroot00000000000000666 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/memory.pressure000066400000000000000000000001641406470533700305630ustar00rootroot00000000000000some avg10=1.11 avg60=2.22 avg300=3.33 total=134829384400 full avg10=4.44 avg60=5.55 avg300=6.66 total=128544748770 oomd-0.5.0/src/oomd/fixtures/cgroup/workload.slice/service1.service/memory.swap.current000066400000000000000000000000021406470533700313350ustar00rootroot000000000000000 oomd-0.5.0/src/oomd/fixtures/fs_data/000077500000000000000000000000001406470533700174625ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/dir1/000077500000000000000000000000001406470533700203215ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/dir1/stuff000066400000000000000000000000331406470533700213670ustar00rootroot00000000000000hello world my good man 1 oomd-0.5.0/src/oomd/fixtures/fs_data/dir2/000077500000000000000000000000001406470533700203225ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/dir2/empty000066400000000000000000000000001406470533700213710ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/dir3/000077500000000000000000000000001406470533700203235ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/dir3/empty000066400000000000000000000000001406470533700213720ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/file1000066400000000000000000000000001406470533700203730ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/file2000066400000000000000000000000001406470533700203740ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/file3000066400000000000000000000000001406470533700203750ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/file4000066400000000000000000000000001406470533700203760ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/000077500000000000000000000000001406470533700212535ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/000077500000000000000000000000001406470533700222225ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/000077500000000000000000000000001406470533700231565ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/WAH/000077500000000000000000000000001406470533700235755ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/WAH/going/000077500000000000000000000000001406470533700247005ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/WAH/going/to/000077500000000000000000000000001406470533700253225ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/WAH/going/to/be/000077500000000000000000000000001406470533700257105ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/WAH/going/to/be/long/000077500000000000000000000000001406470533700266475ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/WAH/going/to/be/long/file000066400000000000000000000000001406470533700274770ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/is/000077500000000000000000000000001406470533700235715ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/is/going/000077500000000000000000000000001406470533700246745ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/is/going/to/000077500000000000000000000000001406470533700253165ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/is/going/to/be/000077500000000000000000000000001406470533700257045ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/is/going/to/be/long/000077500000000000000000000000001406470533700266435ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/is/going/to/be/long/file000066400000000000000000000000001406470533700274730ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/isNOT/000077500000000000000000000000001406470533700241525ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/isNOT/going/000077500000000000000000000000001406470533700252555ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/isNOT/going/to/000077500000000000000000000000001406470533700256775ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/isNOT/going/to/be/000077500000000000000000000000001406470533700262655ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/isNOT/going/to/be/long/000077500000000000000000000000001406470533700272245ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/fs_data/wildcard/this/path/isNOT/going/to/be/long/file000066400000000000000000000000001406470533700300540ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/oomd.json000066400000000000000000000046571406470533700177260ustar00rootroot00000000000000{ "rulesets": [ { "name": "my first ruleset", "drop-in": { "detectors": true, "actions": false, "disable-on-drop-in": true }, "silence-logs": "engine,plugins", "post_action_delay": "10", "prekill_hook_timeout": "40", "detectors": [ [ "group1", { "name": "pressure_rising_beyond", "args": { "cgroup": "workload.slice", "resource": "memory", "threshold": "5" } }, { "name": "pressure_rising_beyond", "args": { "cgroup": "system.slice", "resource": "memory", "threshold": "40" } } ] ], "actions": [ { "name": "kill_by_memory_size_or_growth", "args": { "cgroup": "system.slice" } } ] }, { "name": "low swap ruleset", "detectors": [ [ "my other group", { "name": "swap_free", "args": { "threshold_pct": "15" } } ] ], "actions": [ { "name": "kill_by_swap_usage", "args": { "cgroup": "system.slice" } }, { "name": "kill_by_swap_usage", "args": { "cgroup": "workload.slice/workload-wdb.slice" } }, { "name": "kill_by_swap_usage", "args": { "cgroup": "workload.slice/workload-tw.slice" } } ] } ], "prekill_hooks": [ { "name": "hypothetical_prekill_hook", "args": {} } ] } oomd-0.5.0/src/oomd/fixtures/plugins/000077500000000000000000000000001406470533700175425ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/base_kill_plugin/000077500000000000000000000000001406470533700230455ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/base_kill_plugin/one_big/000077500000000000000000000000001406470533700244475ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/base_kill_plugin/one_big/cgroup.procs000077500000000000000000000001211406470533700270130ustar00rootroot000000000000001 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 oomd-0.5.0/src/oomd/fixtures/plugins/base_kill_plugin/one_big/child/000077500000000000000000000000001406470533700255325ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/base_kill_plugin/one_big/child/cgroup.procs000066400000000000000000000000051406470533700300740ustar00rootroot000000000000001234 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/000077500000000000000000000000001406470533700227065ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/one_high/000077500000000000000000000000001406470533700244665ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/one_high/cgroup1/000077500000000000000000000000001406470533700260465ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/one_high/cgroup1/cgroup.procs000066400000000000000000000000101406470533700304040ustar00rootroot00000000000000123 456 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/one_high/cgroup2/000077500000000000000000000000001406470533700260475ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/one_high/cgroup2/cgroup.procs000066400000000000000000000000041406470533700304100ustar00rootroot00000000000000789 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/one_high/cgroup3/000077500000000000000000000000001406470533700260505ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/one_high/cgroup3/cgroup.procs000066400000000000000000000000041406470533700304110ustar00rootroot00000000000000111 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/sibling/000077500000000000000000000000001406470533700243355ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/sibling/cgroup1/000077500000000000000000000000001406470533700257155ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_io_cost/sibling/cgroup1/cgroup.procs000066400000000000000000000000041406470533700302560ustar00rootroot00000000000000888 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/000077500000000000000000000000001406470533700257035ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/000077500000000000000000000000001406470533700300365ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/cgroup1/000077500000000000000000000000001406470533700314165ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/cgroup1/cgroup.procs000066400000000000000000000000101406470533700337540ustar00rootroot00000000000000123 456 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/cgroup2/000077500000000000000000000000001406470533700314175ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/cgroup2/cgroup.procs000066400000000000000000000000041406470533700337600ustar00rootroot00000000000000789 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/cgroup3/000077500000000000000000000000001406470533700314205ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/cgroup3/cgroup.procs000066400000000000000000000000041406470533700337610ustar00rootroot00000000000000111 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/growth_big/memory.pressure000066400000000000000000000001721406470533700331400ustar00rootroot00000000000000some avg10=99.99 avg60=99.99 avg300=99.99 total=134829384400 full avg10=99.99 avg60=99.99 avg300=99.99 total=128544748770 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/000077500000000000000000000000001406470533700273055ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/cgroup1/000077500000000000000000000000001406470533700306655ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/cgroup1/cgroup.procs000066400000000000000000000000101406470533700332230ustar00rootroot00000000000000123 456 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/cgroup2/000077500000000000000000000000001406470533700306665ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/cgroup2/cgroup.procs000066400000000000000000000000041406470533700332270ustar00rootroot00000000000000789 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/cgroup3/000077500000000000000000000000001406470533700306675ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/cgroup3/cgroup.procs000066400000000000000000000000041406470533700332300ustar00rootroot00000000000000111 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/one_big/memory.pressure000066400000000000000000000001721406470533700324070ustar00rootroot00000000000000some avg10=99.99 avg60=99.99 avg300=99.99 total=134829384400 full avg10=99.99 avg60=99.99 avg300=99.99 total=128544748770 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/sibling/000077500000000000000000000000001406470533700273325ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/sibling/cgroup1/000077500000000000000000000000001406470533700307125ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_memory_size_or_growth/sibling/cgroup1/cgroup.procs000066400000000000000000000000041406470533700332530ustar00rootroot00000000000000888 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/000077500000000000000000000000001406470533700226615ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/000077500000000000000000000000001406470533700244415ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup1/000077500000000000000000000000001406470533700260215ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup1/cgroup.controllers000066400000000000000000000000161406470533700316050ustar00rootroot00000000000000cpu memory io oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup1/cgroup.procs000066400000000000000000000000101406470533700303570ustar00rootroot00000000000000123 456 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup2/000077500000000000000000000000001406470533700260225ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup2/cgroup.controllers000066400000000000000000000000161406470533700316060ustar00rootroot00000000000000cpu memory io oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup2/cgroup.procs000066400000000000000000000000041406470533700303630ustar00rootroot00000000000000789 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup3/000077500000000000000000000000001406470533700260235ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup3/cgroup.controllers000066400000000000000000000000161406470533700316070ustar00rootroot00000000000000cpu memory io oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/one_high/cgroup3/cgroup.procs000066400000000000000000000000041406470533700303640ustar00rootroot00000000000000111 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/sibling/000077500000000000000000000000001406470533700243105ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/sibling/cgroup1/000077500000000000000000000000001406470533700256705ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/sibling/cgroup1/cgroup.controllers000066400000000000000000000000161406470533700314540ustar00rootroot00000000000000cpu memory io oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pg_scan/sibling/cgroup1/cgroup.procs000066400000000000000000000000041406470533700302310ustar00rootroot00000000000000888 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/000077500000000000000000000000001406470533700231175ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/one_high/000077500000000000000000000000001406470533700246775ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/one_high/cgroup1/000077500000000000000000000000001406470533700262575ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/one_high/cgroup1/cgroup.procs000066400000000000000000000000101406470533700306150ustar00rootroot00000000000000123 456 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/one_high/cgroup2/000077500000000000000000000000001406470533700262605ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/one_high/cgroup2/cgroup.procs000066400000000000000000000000041406470533700306210ustar00rootroot00000000000000789 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/one_high/cgroup3/000077500000000000000000000000001406470533700262615ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/one_high/cgroup3/cgroup.procs000066400000000000000000000000041406470533700306220ustar00rootroot00000000000000111 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/sibling/000077500000000000000000000000001406470533700245465ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/sibling/cgroup1/000077500000000000000000000000001406470533700261265ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_pressure/sibling/cgroup1/cgroup.procs000066400000000000000000000000041406470533700304670ustar00rootroot00000000000000888 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/000077500000000000000000000000001406470533700234055ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/meminfo000066400000000000000000000000301406470533700247530ustar00rootroot00000000000000SwapTotal: 100 kB oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/one_big/000077500000000000000000000000001406470533700250075ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/one_big/cgroup1/000077500000000000000000000000001406470533700263675ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/one_big/cgroup1/cgroup.procs000066400000000000000000000000101406470533700307250ustar00rootroot00000000000000123 456 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/one_big/cgroup2/000077500000000000000000000000001406470533700263705ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/one_big/cgroup2/cgroup.procs000066400000000000000000000000041406470533700307310ustar00rootroot00000000000000789 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/one_big/cgroup3/000077500000000000000000000000001406470533700263715ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/one_big/cgroup3/cgroup.procs000066400000000000000000000000041406470533700307320ustar00rootroot00000000000000111 oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/sibling/000077500000000000000000000000001406470533700250345ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/sibling/cgroup1/000077500000000000000000000000001406470533700264145ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/kill_by_swap_usage/sibling/cgroup1/cgroup.procs000066400000000000000000000000041406470533700307550ustar00rootroot00000000000000555 oomd-0.5.0/src/oomd/fixtures/plugins/memory_above/000077500000000000000000000000001406470533700222265ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/memory_above/meminfo000066400000000000000000000000331406470533700235770ustar00rootroot00000000000000MemTotal: 8388608 kB oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/000077500000000000000000000000001406470533700225465ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/multi_cgroup/000077500000000000000000000000001406470533700252575ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/multi_cgroup/cgroup1/000077500000000000000000000000001406470533700266375ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/multi_cgroup/cgroup1/memory.stat000066400000000000000000000010361406470533700310440ustar00rootroot00000000000000anon 1056014336 file 11877441536 kernel_stack 158109696 slab 1440972800 sock 13357056 shmem 12419072 file_mapped 1073233920 file_dirty 28925952 file_writeback 0 inactive_anon 12128256 active_anon 1006006272 inactive_file 6990409728 active_file 4981571584 unevictable 8138752 slab_reclaimable 815513600 slab_unreclaimable 625459200 pgfault 1225796781 pgmajfault 259380 pgrefill 0 pgscan 0 pgsteal 0 pgactivate 0 pgdeactivate 0 pglazyfree 0 pglazyfreed 0 workingset_refault 0 workingset_activate 0 workingset_restore 0 workingset_nodereclaim 0 oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/multi_cgroup/cgroup2/000077500000000000000000000000001406470533700266405ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/multi_cgroup/cgroup2/memory.stat000066400000000000000000000010361406470533700310450ustar00rootroot00000000000000anon 1056014336 file 11877441536 kernel_stack 158109696 slab 1440972800 sock 13357056 shmem 12419072 file_mapped 1073233920 file_dirty 28925952 file_writeback 0 inactive_anon 12128256 active_anon 1006006272 inactive_file 6990409728 active_file 4981571584 unevictable 8138752 slab_reclaimable 815513600 slab_unreclaimable 625459200 pgfault 1225796781 pgmajfault 259380 pgrefill 0 pgscan 5 pgsteal 0 pgactivate 0 pgdeactivate 0 pglazyfree 0 pglazyfreed 0 workingset_refault 0 workingset_activate 0 workingset_restore 0 workingset_nodereclaim 0 oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/single_cgroup/000077500000000000000000000000001406470533700254065ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/single_cgroup/cgroup1/000077500000000000000000000000001406470533700267665ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/plugins/memory_reclaim/single_cgroup/cgroup1/memory.stat000066400000000000000000000010361406470533700311730ustar00rootroot00000000000000anon 1056014336 file 11877441536 kernel_stack 158109696 slab 1440972800 sock 13357056 shmem 12419072 file_mapped 1073233920 file_dirty 28925952 file_writeback 0 inactive_anon 12128256 active_anon 1006006272 inactive_file 6990409728 active_file 4981571584 unevictable 8138752 slab_reclaimable 815513600 slab_unreclaimable 625459200 pgfault 1225796781 pgmajfault 259380 pgrefill 0 pgscan 5 pgsteal 0 pgactivate 0 pgdeactivate 0 pglazyfree 0 pglazyfreed 0 workingset_refault 0 workingset_activate 0 workingset_restore 0 workingset_nodereclaim 0 oomd-0.5.0/src/oomd/fixtures/proc/000077500000000000000000000000001406470533700170245ustar00rootroot00000000000000oomd-0.5.0/src/oomd/fixtures/proc/meminfo000066400000000000000000000025231406470533700204030ustar00rootroot00000000000000MemTotal: 58616708 kB MemFree: 2955848 kB MemAvailable: 49878948 kB Buffers: 4970160 kB Cached: 35172508 kB SwapCached: 0 kB Active: 27822776 kB Inactive: 19032824 kB Active(anon): 4024064 kB Inactive(anon): 2822876 kB Active(file): 23798712 kB Inactive(file): 16209948 kB Unevictable: 11684 kB Mlocked: 11668 kB SwapTotal: 2097148 kB SwapFree: 1097041 kB Dirty: 24576 kB Writeback: 0 kB AnonPages: 6725380 kB Mapped: 657992 kB Shmem: 130124 kB Slab: 8238080 kB SReclaimable: 7334616 kB SUnreclaim: 903464 kB KernelStack: 45984 kB PageTables: 75756 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 31405500 kB Committed_AS: 21265672 kB VmallocTotal: 34359738367 kB VmallocUsed: 0 kB VmallocChunk: 0 kB HardwareCorrupted: 0 kB AnonHugePages: 71680 kB ShmemHugePages: 0 kB ShmemPmdMapped: 0 kB CmaTotal: 0 kB CmaFree: 0 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB Hugetlb: 0 kB DirectMap4k: 4544368 kB DirectMap2M: 53127168 kB DirectMap1G: 4194304 kB oomd-0.5.0/src/oomd/fixtures/proc/mounts000066400000000000000000000040101406470533700202670ustar00rootroot00000000000000sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0 proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 devtmpfs /dev devtmpfs rw,seclabel,nosuid,size=58710812k,nr_inodes=14677703,mode=755 0 0 securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0 tmpfs /dev/shm tmpfs rw,seclabel,nosuid,nodev 0 0 devpts /dev/pts devpts rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0 tmpfs /run tmpfs rw,seclabel,nosuid,nodev,mode=755 0 0 cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate 0 0 pstore /sys/fs/pstore pstore rw,seclabel,nosuid,nodev,noexec,relatime 0 0 bpf /sys/fs/bpf bpf rw,relatime,mode=700 0 0 configfs /sys/kernel/config configfs rw,relatime 0 0 /dev/vda3 / btrfs rw,seclabel,relatime,compress-force=zstd,discard,space_cache,subvolid=5,subvol=/ 0 0 selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0 systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct 0 0 hugetlbfs /dev/hugepages hugetlbfs rw,seclabel,relatime,pagesize=2M 0 0 debugfs /sys/kernel/debug debugfs rw,seclabel,relatime,mode=755 0 0 mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0 sunrpc /var/lib/nfs/rpc_pipefs rpc_pipefs rw,relatime 0 0 nfsd /proc/fs/nfsd nfsd rw,relatime 0 0 fusectl /sys/fs/fuse/connections fusectl rw,relatime 0 0 /dev/vda1 /boot ext4 rw,seclabel,relatime,data=ordered 0 0 none-tw-channel /run/facebook/tw_logging_channel tmpfs rw,seclabel,relatime,size=10240k,nr_inodes=4096 0 0 tracefs /sys/kernel/debug/tracing tracefs rw,seclabel,relatime 0 0 /etc/auto.home.override /home autofs rw,relatime,fd=5,pgrp=2182594,timeout=600,minproto=5,maxproto=5,indirect 0 0 binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc rw,relatime 0 0 tmpfs /run/user/30015 tmpfs rw,seclabel,nosuid,nodev,relatime,size=11744436k,mode=700,uid=30015,gid=30015 0 0 tmpfs /run/user/0 tmpfs rw,seclabel,nosuid,nodev,relatime,size=11744436k,mode=700 0 0 tmpfs /run/user/180401 tmpfs rw,seclabel,nosuid,nodev,relatime,size=11744436k,mode=700,uid=180401,gid=100 0 0 oomd-0.5.0/src/oomd/fixtures/proc/vmstat000066400000000000000000000000621406470533700202630ustar00rootroot00000000000000first_key 12345 second_key 678910 thirdkey 999999 oomd-0.5.0/src/oomd/include/000077500000000000000000000000001406470533700156335ustar00rootroot00000000000000oomd-0.5.0/src/oomd/include/Assert.cpp000066400000000000000000000027221406470533700176030ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include // backtrace(3) is a GNU extension #ifdef __GNU_LIBRARY__ #include #include #endif // __GNU_LIBRARY__ #include #include #include "oomd/include/Assert.h" void __bt() { #ifdef __GNU_LIBRARY__ static const int num = 100; void* buffer[num]; int n = backtrace(buffer, num); std::cerr << "Backtrace (most recent call first):" << std::endl; backtrace_symbols_fd(buffer, n, STDERR_FILENO); #endif // __GNU_LIBRARY__ } [[noreturn]] void __OCHECK_FAIL(const char* expr, const char* file, int line, const char* func) { std::cerr << file << ":" << line << ": " << func << ": Assertion \'" << expr << "\' failed." << std::endl << std::flush; __bt(); std::abort(); } oomd-0.5.0/src/oomd/include/Assert.h000066400000000000000000000020661406470533700172510ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once [[noreturn]] void __OCHECK_FAIL(const char* expr, const char* file, int line, const char* func); #define OCHECK(expr) \ (static_cast(expr) \ ? void(0) \ : __OCHECK_FAIL(#expr, __FILE__, __LINE__, __PRETTY_FUNCTION__)) #define OCHECK_EXCEPT(expr, exc) (static_cast(expr)) ? void(0) : throw exc oomd-0.5.0/src/oomd/include/AssertTest.cpp000066400000000000000000000025461406470533700204470ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include "oomd/include/Assert.h" TEST(OomdAssertTest, Death) { // Shouldn't die OCHECK(true); OCHECK(1 == 1); int x = 1; int y = x; OCHECK(x == y); ASSERT_DEATH(OCHECK(false), "Assertion.*failed"); } TEST(OomdAssertTest, ExprDeath) { ASSERT_DEATH(OCHECK(true == false), "Assertion.*failed"); } TEST(OomdAssertExceptionTest, Throws) { ASSERT_NO_THROW(OCHECK_EXCEPT(true, std::runtime_error("x"))); ASSERT_THROW( OCHECK_EXCEPT(false, std::runtime_error("x")), std::runtime_error); ASSERT_THROW(OCHECK_EXCEPT(false, std::exception()), std::exception); } oomd-0.5.0/src/oomd/include/CgroupPath.cpp000066400000000000000000000101071406470533700204120ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/include/CgroupPath.h" #include #include #include "oomd/util/Fs.h" #include "oomd/util/Util.h" namespace Oomd { CgroupPath::CgroupPath( const std::string& cgroup_fs, const std::string& cgroup_path) : cgroup_fs_(cgroup_fs) { // Strip trailing '/' if (cgroup_fs_.at(cgroup_fs_.size() - 1) == '/' && cgroup_fs.size() > 1) { cgroup_fs_.pop_back(); } cgroup_path_ = Util::split(cgroup_path, '/'); recomputeReadCache(); } const std::string& CgroupPath::absolutePath() const { return absolute_cache_; } const std::string& CgroupPath::relativePath() const { return relative_cache_; } const std::vector& CgroupPath::relativePathParts() const { return cgroup_path_; } const std::string& CgroupPath::cgroupFs() const { return cgroup_fs_; } CgroupPath CgroupPath::getParent() const { if (this->isRoot()) { throw std::invalid_argument("Cannot get parent of root"); } CgroupPath parent(*this); parent.cgroup_path_.pop_back(); parent.recomputeReadCache(); return parent; } CgroupPath CgroupPath::getChild(const std::string& path) const { CgroupPath child(*this); auto pieces = Util::split(path, '/'); child.cgroup_path_.reserve(child.cgroup_path_.size() + pieces.size()); for (auto& piece : pieces) { child.cgroup_path_.emplace_back(std::move(piece)); } child.recomputeReadCache(); return child; } std::vector CgroupPath::resolveWildcard() const { std::vector ret; auto glob = Fs::glob(absolutePath(), /* dir_only */ true); // TODO(dschatzberg): Report error if (!glob) { return ret; } for (const auto& path : *glob) { if (path.find(cgroup_fs_) == 0) { if (path.size() == cgroup_fs_.size()) { ret.emplace_back(cgroup_fs_, ""); } else if (path[cgroup_fs_.size()] == '/') { ret.emplace_back(cgroup_fs_, path.substr(cgroup_fs_.size() + 1)); } } } return ret; } bool CgroupPath::hasDescendantWithPrefixMatching( const CgroupPath& pattern) const { unsigned int prefix_len = std::min(cgroup_path_.size(), pattern.cgroup_path_.size()); for (unsigned int i = 0; i < prefix_len; i++) { if (!(cgroup_path_[i] == pattern.cgroup_path_[i] || pattern.cgroup_path_[i] == "*")) { return false; } } return true; } bool CgroupPath::operator==(const CgroupPath& other) const { return this->absolutePath() == other.absolutePath(); } bool CgroupPath::operator!=(const CgroupPath& other) const { return !operator==(other); } bool CgroupPath::isRoot() const { return cgroup_path_.size() == 0; } void CgroupPath::recomputeReadCache() { // Recreate relative_cache_ relative_cache_.clear(); size_t s = 0; for (size_t i = 0; i < cgroup_path_.size(); ++i) { s += cgroup_path_.at(i).size(); if (i == cgroup_path_.size() - 1) { break; } ++s; // for the path separator } relative_cache_.reserve(s); for (size_t i = 0; i < cgroup_path_.size(); ++i) { relative_cache_ += cgroup_path_.at(i); if (i == cgroup_path_.size() - 1) { break; } relative_cache_ += '/'; } // Recreate absolute_cache_ absolute_cache_.clear(); absolute_cache_.reserve(cgroup_fs_.size() + 1 + relative_cache_.size()); absolute_cache_ += cgroup_fs_; if (relative_cache_.size()) { absolute_cache_ += '/'; absolute_cache_ += relative_cache_; } } } // namespace Oomd oomd-0.5.0/src/oomd/include/CgroupPath.h000066400000000000000000000060231406470533700200610ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include namespace Oomd { class CgroupPath { public: CgroupPath(const std::string& cgroup_fs, const std::string& cgroup_path); ~CgroupPath() = default; CgroupPath(const CgroupPath& other) = default; CgroupPath(CgroupPath&& other) = default; CgroupPath& operator=(const CgroupPath& other) = default; CgroupPath& operator=(CgroupPath&& other) = default; static void setCgroupFs(const std::string& cgroup_fs); const std::string& absolutePath() const; // cgroup path without the cgroup fs const std::string& relativePath() const; const std::vector& relativePathParts() const; const std::string& cgroupFs() const; CgroupPath getParent() const; CgroupPath getChild(const std::string& path) const; /* * Resolve the glob pattern in this CgroupPath to a vector of CgroupPaths that * exist, share the same cgroup fs, and are directories. */ std::vector resolveWildcard() const; // hasDescendantWithPrefixMatching is true if any descendant of relativePath() // has a prefix that matches @param pattern. // @param pattern is a glob-like pattern, supporting only wildcard path // components. /foo/*/baz/ matches relativePath() /foo/bar/baz, but the // pattern /foo/b*/baz only matches the exact path "/foo/b*/baz". Leading and // trailing slashes are ignored. // @returns true if // 1. relativePath() matches a prefix of pattern, in which case there may // exist descendants matching the full pattern, // 2. if pattern matches a prefix of relativePath(), in which case // relativePath() is a descendant of a path matching pattern, or // 3. if pattern matches relativePath() itself. bool hasDescendantWithPrefixMatching(const CgroupPath& pattern) const; bool operator==(const CgroupPath& other) const; bool operator!=(const CgroupPath& other) const; // Do we represent the root cgroup? bool isRoot() const; private: void recomputeReadCache(); std::string cgroup_fs_; std::vector cgroup_path_; std::string absolute_cache_; std::string relative_cache_; }; } // namespace Oomd namespace std { template <> struct hash { size_t operator()(const Oomd::CgroupPath& path) const { return hash()(path.absolutePath()); } }; } // namespace std oomd-0.5.0/src/oomd/include/CgroupPathTest.cpp000066400000000000000000000136661406470533700212670ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include "oomd/include/CgroupPath.h" #include "oomd/util/Fixture.h" using namespace Oomd; TEST(CgroupPathTest, ConstructorsTest) { CgroupPath path1("/sys/fs/cgroup", "myservice"); CgroupPath path2(path1); EXPECT_EQ(path1.absolutePath(), path2.absolutePath()); path1 = path2; EXPECT_EQ(path1.absolutePath(), path2.absolutePath()); CgroupPath path3("/sys/fs/cgroup", "myservice"); CgroupPath path4(std::move(path3)); EXPECT_EQ(path4.absolutePath(), "/sys/fs/cgroup/myservice"); CgroupPath path5 = std::move(path4); EXPECT_EQ(path5.absolutePath(), "/sys/fs/cgroup/myservice"); // Test root token maps to root cgroup CgroupPath path("/sys/fs/cgroup", "/"); EXPECT_TRUE(path.isRoot()); } TEST(CgroupPathTest, GettersTest) { CgroupPath path("/sys/fs/cgroup", "system.slice/myservice.slice"); EXPECT_EQ(path.absolutePath(), "/sys/fs/cgroup/system.slice/myservice.slice"); EXPECT_EQ(path.relativePath(), "system.slice/myservice.slice"); EXPECT_EQ(path.cgroupFs(), "/sys/fs/cgroup"); ASSERT_EQ(path.relativePathParts().size(), 2); EXPECT_EQ(path.relativePathParts()[0], "system.slice"); EXPECT_EQ(path.relativePathParts()[1], "myservice.slice"); CgroupPath pathEmpty("/sys/fs/cgroup", ""); EXPECT_EQ(pathEmpty.relativePath().size(), 0); EXPECT_EQ(pathEmpty.absolutePath(), "/sys/fs/cgroup"); CgroupPath pathSame("/sys/fs/cgroup", "////////"); EXPECT_EQ(pathSame.relativePath().size(), 0); EXPECT_EQ(pathSame.absolutePath(), "/sys/fs/cgroup"); } TEST(CgroupPathTest, ModifiersTest) { CgroupPath path("/sys/fs/cgroup", "system.slice"); path = path.getChild("myservice"); path = path.getChild("/one/"); path = path.getChild("two/"); path = path.getChild("three"); EXPECT_EQ(path.relativePath(), "system.slice/myservice/one/two/three"); EXPECT_EQ( path.getParent().getParent().relativePath(), "system.slice/myservice/one"); CgroupPath root("/sys/fs/cgroup", ""); EXPECT_THROW(root.getParent(), std::invalid_argument); } TEST(CgroupPathTest, ComparisonsTest) { CgroupPath path1("/sys/fs/cgroup", "system.slice"); CgroupPath path2("/sys/fs/cgroup", "system.slice"); EXPECT_EQ(path1, path2); CgroupPath path3("/sys/fs/cgroup", "asdf.slice"); EXPECT_NE(path1, path3); EXPECT_FALSE(path3.isRoot()); EXPECT_TRUE(path3.getParent().isRoot()); } TEST(CgroupPathTest, HashTest) { std::unordered_map m; CgroupPath p1("/sys/fs/cgroup", "system.slice"); CgroupPath p2("/sys/fs/cgroup", "workload.slice"); m[p1] = 1; m[p2] = 2; EXPECT_EQ(m[p1], 1); EXPECT_EQ(m[p2], 2); CgroupPath p3("/sys/fs/cgroup", "system.slice"); EXPECT_EQ(m[p3], 1); } TEST(CgroupPathTest, ResolveWildcardTest) { using F = Fixture; auto tempDir = F::mkdtempChecked(); auto [name, fixture] = F::makeDir( "wildcard_root", {F::makeFile("a.txt", "content of a\n"), F::makeDir("b_dir", {}), F::makeFile("c.txt", "content of c\n"), F::makeDir( "d_dir", { F::makeFile("e.txt", "content of e\n"), F::makeDir("e_dir", {}), })}); fixture.materialize(tempDir, name); CgroupPath p1(tempDir, "wildcard_root/**"); auto resolved = p1.resolveWildcard(); EXPECT_EQ(resolved.size(), 2); using ::testing::Contains; EXPECT_THAT(resolved, Contains(CgroupPath(tempDir, "wildcard_root/b_dir"))); EXPECT_THAT(resolved, Contains(CgroupPath(tempDir, "wildcard_root/d_dir"))); } TEST(CgroupPathTest, HasDescendantWithPrefixMatchingTest) { const std::string fs = "/cgroup2fs"; auto matches = [&](const std::string& path, const std::string& pattern) { return CgroupPath(fs, path).hasDescendantWithPrefixMatching( CgroupPath(fs, pattern)); }; // basic pattern matching EXPECT_TRUE(matches("/foo/bar/baz", "/foo/*/baz")); // leading and trailing slashes are ignored EXPECT_TRUE(matches("foo/bar/baz", "/foo/")); EXPECT_TRUE(matches("foo/bar/baz", "foo/")); EXPECT_TRUE(matches("foo/bar/baz/", "foo/")); EXPECT_TRUE(matches("/foo/bar/baz/", "foo/")); EXPECT_TRUE(matches("/foo/bar/baz/", "foo/")); // patterns other than simple "*" per path segment are not supported EXPECT_FALSE(matches("foo/bar", "foo/b*")); EXPECT_TRUE(matches("foo/bar", "foo/*")); EXPECT_TRUE(matches("foo/bar", "foo/bar")); // paths which are parents of potential matches match EXPECT_TRUE(matches("foo/", "foo/bar/*/qoux/*")); EXPECT_TRUE(matches("foo/bar/baz", "foo/bar/*/qoux/*")); // star matches exactly one level of hierarchy EXPECT_FALSE(matches("foo/bar/baz/nux/qoux/muh", "foo/bar/*/qoux/*")); // paths which are not on the same lineage as any match do not match EXPECT_FALSE(matches("foo/nux", "foo/bar/*/qoux/*")); // paths which whose parents match pattern match pattern EXPECT_TRUE(matches("foo/bar/baz/qoux/nux/", "/foo/bar")); EXPECT_TRUE(matches("/qoux/nux/blah", "/*/nux")); EXPECT_TRUE(matches("/one/two/three", "/one/*")); // "/" matches everything EXPECT_TRUE(matches("/foo/bar/baz/", "/")); EXPECT_TRUE(matches("/foo/bar/baz/qoux/nux", "/")); EXPECT_TRUE(matches("/", "/")); EXPECT_TRUE(matches("/toplevel", "/")); EXPECT_TRUE(matches("toplevel/secondlevel", "/")); EXPECT_TRUE(matches("toplevel", "/")); } oomd-0.5.0/src/oomd/include/CoreStats.h000066400000000000000000000023231406470533700177130ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include namespace Oomd { class CoreStats { public: static constexpr auto kKillsKey = "oomd.kills"; static constexpr auto kNumDropInAdds = "oomd.dropin.added"; static constexpr auto kNumDropInFired = "oomd.dropin.fired"; // List of all the stats keys. Useful for operations that need to know // all the available core keys. static constexpr std::array kAllKeys = { kKillsKey, kNumDropInAdds, kNumDropInFired, }; }; } // namespace Oomd oomd-0.5.0/src/oomd/include/Defines.h000066400000000000000000000013771406470533700173710ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #define EXIT_CANT_RECOVER 3 oomd-0.5.0/src/oomd/include/Types.h000066400000000000000000000051421406470533700171120ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include #include namespace Oomd { namespace Engine { using PluginArgs = std::unordered_map; } enum struct ResourceType { MEMORY, IO, }; enum struct DeviceType { HDD, SSD, }; struct DeviceIOStat { std::string dev_id{""}; int64_t rbytes{0}; int64_t wbytes{0}; int64_t rios{0}; int64_t wios{0}; int64_t dbytes{0}; int64_t dios{0}; bool operator==(const DeviceIOStat& rhs) const { return dev_id == rhs.dev_id && rbytes == rhs.rbytes && wbytes == rhs.wbytes && rios == rhs.rios && wios == rhs.wios && dbytes == rhs.dbytes && dios == rhs.dios; } }; using IOStat = std::vector; struct IOCostCoeffs { double read_iops{0}; double readbw{0}; double write_iops{0}; double writebw{0}; double trim_iops{0}; double trimbw{0}; }; struct ResourcePressure { float sec_10{0}; float sec_60{0}; float sec_300{0}; std::optional total{std::nullopt}; bool operator==(const ResourcePressure& rhs) const { return sec_10 == rhs.sec_10 && sec_60 == rhs.sec_60 && sec_300 == rhs.sec_300 && total == rhs.total; } }; struct SystemContext { uint64_t swaptotal{0}; uint64_t swapused{0}; int swappiness{0}; std::unordered_map vmstat{}; // moving avg swap out rate derived from vmstat[pswpout] double swapout_bps_60{0}; double swapout_bps_300{0}; }; enum struct KillPreference { PREFER = 1, NORMAL = 0, AVOID = -1, }; inline std::ostream& operator<<(std::ostream& os, KillPreference kp) { switch (kp) { case KillPreference::PREFER: os << "PREFER"; break; case KillPreference::NORMAL: os << "NORMAL"; break; case KillPreference::AVOID: os << "AVOID"; break; } return os; } } // namespace Oomd oomd-0.5.0/src/oomd/include/Version.h.in000066400000000000000000000000561406470533700200370ustar00rootroot00000000000000#pragma once #define GIT_VERSION "@VCS_TAG@" oomd-0.5.0/src/oomd/plugins/000077500000000000000000000000001406470533700156715ustar00rootroot00000000000000oomd-0.5.0/src/oomd/plugins/BaseKillPlugin.cpp000066400000000000000000000460301406470533700212450ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/BaseKillPlugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/OomdContext.h" #include "oomd/Stats.h" #include "oomd/engine/Ruleset.h" #include "oomd/include/Assert.h" #include "oomd/include/CgroupPath.h" #include "oomd/include/CoreStats.h" #include "oomd/include/Types.h" #include "oomd/util/Fs.h" #include "oomd/util/Util.h" static auto constexpr kOomdKillInitiationXattr = "trusted.oomd_ooms"; static auto constexpr kOomdKillCompletionXattr = "trusted.oomd_kill"; static auto constexpr kOomdKillUuidXattr = "trusted.oomd_kill_uuid"; namespace Oomd { int BaseKillPlugin::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgument("recursive", recursive_); argParser_.addArgumentCustom( "post_action_delay", post_action_delay_, PluginArgParser::parseUnsignedInt); argParser_.addArgument("dry", dry_); argParser_.addArgument("always_continue", always_continue_); argParser_.addArgument("debug", debug_); if (!argParser_.parse(args)) { return 1; } // Success return 0; } Engine::PluginRet BaseKillPlugin::run(OomdContext& ctx) { KillResult ret; if (prekill_hook_state_) { ret = resumeFromPrekillHook(ctx); } else { ret = tryToKillSomething(ctx, ctx.addToCacheAndGet(cgroups_)); } if (ret == KillResult::DEFER) { return Engine::PluginRet::ASYNC_PAUSED; } if (prekill_hook_state_ != std::nullopt) { OLOG << "Error: there shouldn't be a running prekill hook" " once we're done with a kill cycle"; prekill_hook_state_ = std::nullopt; } if (ret == KillResult::FAILED || always_continue_) { return Engine::PluginRet::CONTINUE; } auto ruleset = ctx.getInvokingRuleset(); if (ruleset && post_action_delay_) { (*ruleset)->pause_actions(std::chrono::seconds(*post_action_delay_)); } return Engine::PluginRet::STOP; } BaseKillPlugin::KillResult BaseKillPlugin::resumeFromPrekillHook( OomdContext& ctx) { OCHECK(prekill_hook_state_ != std::nullopt); if (prekill_hook_state_->hook_invocation->didFinish()) { OLOG << "pre-kill hook finished for Ruleset=" << ctx.getActionContext().ruleset_name; } else if (pastPrekillHookTimeout(ctx)) { OLOG << "pre-kill hook timed out for Ruleset=" << ctx.getActionContext().ruleset_name; } else { OLOG << "Still running pre-kill hook for Ruleset=" << ctx.getActionContext().ruleset_name; return KillResult::DEFER; } auto deserialize_cgroup_ref = [&](const SerializedCgroupRef& sc) -> std::optional { if (auto cgroup_ctx = ctx.addToCacheAndGet(sc.path)) { // Check inodes match and not a re-created cgroup. nullopt ids mean // deleted cgroups, which are never equal to each other. auto id = cgroup_ctx->get().id(); if (id.has_value() && sc.id.has_value() && *id == *sc.id) { return cgroup_ctx; } } return std::nullopt; }; // memoize deserialize_peer_group by serialized peer group pointer std::map< std::vector*, std::shared_ptr>> memoized_peer_groups; auto deserialize_peer_group = [&](std::vector* serialized_peers) { auto it = memoized_peer_groups.find(serialized_peers); if (it != memoized_peer_groups.end()) { return it->second; } auto deserialized_peers = std::make_shared>(); for (const auto& peer : *serialized_peers) { if (auto peer_cgroup_ctx = deserialize_cgroup_ref(peer)) { deserialized_peers->emplace_back(*peer_cgroup_ctx); } } memoized_peer_groups[serialized_peers] = deserialized_peers; return deserialized_peers; }; auto deserialize_kill_candidate = [&](const SerializedKillCandidate& skc) -> std::optional { if (auto candidate_ctx = deserialize_cgroup_ref(skc.target)) { if (auto kill_root_ctx = deserialize_cgroup_ref(skc.kill_root)) { return KillCandidate{ .cgroup_ctx = *candidate_ctx, .kill_root = *kill_root_ctx, .peers = deserialize_peer_group(skc.peers.get())}; } } return std::nullopt; }; // pull state out of prekill_hook_state and clear it to delete the invocation auto intended_victim = std::move(prekill_hook_state_->intended_victim); auto serialized_next_best_option_stack = std::move(prekill_hook_state_->next_best_option_stack); prekill_hook_state_ = std::nullopt; // Try to kill intended victim if (auto intended_candidate = deserialize_kill_candidate(intended_victim)) { if (tryToLogAndKillCgroup(ctx, *intended_candidate)) { return KillResult::SUCCESS; } } else { // intended_candidate isn't deserializable means someone else removed it // before we could. Consider that they did our job for us. If we still // need to kill something detectors will fire again in the next interval and // start a fresh kill cycle. return KillResult::FAILED; } std::vector next_best_option_stack; for (const auto& skc : serialized_next_best_option_stack) { if (auto candidate = deserialize_kill_candidate(skc)) { next_best_option_stack.emplace_back(*candidate); } else { // candidate isn't deserializable means someone else killed it before we // got a chance to. resumeTryingToKillSomething gets to the point where it // would have killed candidate, consider the other folks to have done our // job for us. Don't keep going further down the DFS stack looking for // another cgroup to kill. This means removing all candidates *lower* in // the stack, or *earlier* in the vector. next_best_option_stack.clear(); } } serialized_next_best_option_stack.clear(); return resumeTryingToKillSomething(ctx, std::move(next_best_option_stack)); } BaseKillPlugin::KillResult BaseKillPlugin::tryToKillSomething( OomdContext& ctx, const std::vector& initial_cgroups) { std::vector next_best_option_stack; auto sorted = std::make_shared>( rankForKilling(ctx, initial_cgroups)); OomdContext::dump(*sorted, !debug_); // push the lowest ranked sibling onto the next_best_option_stack first, so // the highest ranked sibling is on top reverse(sorted->begin(), sorted->end()); for (const auto& cgroup_ctx : *sorted) { next_best_option_stack.emplace_back(KillCandidate{ .cgroup_ctx = cgroup_ctx, // kill_roots are the initial_cgroups .kill_root = cgroup_ctx, .peers = sorted}); } return resumeTryingToKillSomething(ctx, std::move(next_best_option_stack)); } // DFS down tree looking for best kill target. Keep a next_best_option_stack // (instead of just the current target) because if killing fails, we try the // next-best target. This may involve backtracking up the tree. The // next_best_option_stack tracks (cgroup, siblings) because ologKillTarget needs // to know what peers a cgroup was compared to when it was picked. // initial_cgroups are treated as siblings. BaseKillPlugin::KillResult BaseKillPlugin::resumeTryingToKillSomething( OomdContext& ctx, std::vector next_best_option_stack) { OCHECK_EXCEPT( prekill_hook_state_ == std::nullopt, std::runtime_error("Shouldn't be trying to kill anything while pre-kill" " hook is still running")); while (!next_best_option_stack.empty()) { const auto candidate = next_best_option_stack.back(); next_best_option_stack.pop_back(); bool may_recurse = recursive_ && !candidate.cgroup_ctx.oom_group().value_or(false); if (may_recurse) { auto children = ctx.addChildrenToCacheAndGet(candidate.cgroup_ctx); if (children.size() > 0) { ologKillTarget(ctx, candidate.cgroup_ctx, *candidate.peers); auto sorted = std::make_shared>( rankForKilling(ctx, children)); OomdContext::dump(*sorted, !debug_); // push the lowest ranked sibling onto the next_best_option_stack first, // so the highest ranked sibling is on top reverse(sorted->begin(), sorted->end()); for (const auto& cgroup_ctx : *sorted) { next_best_option_stack.emplace_back(KillCandidate{ .cgroup_ctx = cgroup_ctx, // kill_root is nullopt when peers are themselves // the roots, in the first call. Each cgroup is then // its own kill_root. .kill_root = candidate.kill_root, .peers = sorted}); } continue; } } // Skip trying to kill an empty cgroup, which would unfairly increment the // empty cgroup's kill counters and pollute the logs. We get into a // situation where we try to kill empty cgroups when a cgroup marked // PREFER is not the source of pressure: KillMemoryGrowth will kill the // PREFER cgroup first, but that won't fix the problem so it will kill // again; on the second time around, it first targets the now-empty PREFER // cgroup before moving on to a better victim. if (!candidate.cgroup_ctx.is_populated().value_or(true)) { continue; } ologKillTarget(ctx, candidate.cgroup_ctx, *candidate.peers); if (!pastPrekillHookTimeout(ctx)) { auto hook_invocation = ctx.firePrekillHook(candidate.cgroup_ctx); if (hook_invocation && !(*hook_invocation)->didFinish()) { auto serialize_cgroup_ref = [&](const CgroupContext& cgroup_ctx) { // cgroup_ctx.id() may be nullopt, which means the cgroup is deleted return SerializedCgroupRef{ .path = cgroup_ctx.cgroup(), .id = cgroup_ctx.id()}; }; // memoize serialize_peer_group by unserialized peer group pointer std::map< const std::vector*, std::shared_ptr>> memoized_peer_groups; auto serialize_peer_group = [&](const std::vector* peers) { auto it = memoized_peer_groups.find(peers); if (it != memoized_peer_groups.end()) { return it->second; } auto serialized_peers = std::make_shared>(); for (const auto& peer : *peers) { serialized_peers->emplace_back(serialize_cgroup_ref(peer)); } memoized_peer_groups[peers] = serialized_peers; return serialized_peers; }; auto serialize_kill_candidate = [&](const KillCandidate& kc) { return SerializedKillCandidate{ .target = serialize_cgroup_ref(kc.cgroup_ctx), .kill_root = serialize_cgroup_ref(kc.cgroup_ctx), .peers = serialize_peer_group(kc.peers.get())}; }; prekill_hook_state_ = ActivePrekillHook{ .hook_invocation = std::move(*hook_invocation), .intended_victim = serialize_kill_candidate(candidate)}; for (KillCandidate& kc : next_best_option_stack) { prekill_hook_state_->next_best_option_stack.emplace_back( serialize_kill_candidate(kc)); } return KillResult::DEFER; } } if (tryToLogAndKillCgroup(ctx, candidate)) { return KillResult::SUCCESS; } } return KillResult::FAILED; } bool BaseKillPlugin::pastPrekillHookTimeout(const OomdContext& ctx) const { auto timeout = ctx.getActionContext().prekill_hook_timeout_ts; return timeout.has_value() && std::chrono::steady_clock::now() > timeout; } BaseKillPlugin::BaseKillPlugin() { /* * Initializes kKillsKey in stats for immediate reporting, * rather than waiting for first occurrence */ Oomd::setStat(CoreStats::kKillsKey, 0); } int BaseKillPlugin::getAndTryToKillPids(const CgroupContext& target) { static constexpr size_t stream_size = 20; int nr_killed = 0; const auto fd = ::openat(target.fd().fd(), Fs::kProcsFile, O_RDONLY); if (fd == -1) { return 0; } auto fp = ::fdopen(fd, "r"); if (fp == nullptr) { ::close(fd); return 0; } std::vector pids; char* line = nullptr; size_t len = 0; ssize_t read; errno = 0; while ((read = ::getline(&line, &len, fp)) != -1) { OCHECK(line != nullptr); pids.push_back(std::stoi(line)); if (pids.size() == stream_size) { nr_killed += tryToKillPids(pids); pids.clear(); } } nr_killed += tryToKillPids(pids); ::free(line); ::fclose(fp); // target.children is cached, and may be stale if (const auto& children = target.children()) { for (const auto& child_name : *children) { if (auto child_ctx = target.oomd_ctx().addChildToCacheAndGet(target, child_name)) { nr_killed += getAndTryToKillPids(*child_ctx); } } } return nr_killed; } bool BaseKillPlugin::tryToKillCgroup( const CgroupContext& target, const KillUuid& kill_uuid, bool dry) { using namespace std::chrono_literals; const std::string& cgroup_path = target.cgroup().absolutePath(); int last_nr_killed = 0; int nr_killed = 0; int tries = 10; if (dry) { OLOG << "OOMD: In dry-run mode; would have tried to kill " << cgroup_path; return true; } OLOG << "Trying to kill " << cgroup_path; reportKillUuidToXattr(cgroup_path, kill_uuid); reportKillInitiationToXattr(cgroup_path); while (tries--) { // Descendent cgroups created during killing will be missed because // getAndTryToKillPids reads cgroup children from OomdContext's cache nr_killed += getAndTryToKillPids(target); if (nr_killed == last_nr_killed) { break; } // Give it a breather before killing again // // Don't sleep after the first round of kills b/c the majority of the // time the sleep isn't necessary. The system responds fast enough. if (last_nr_killed) { std::this_thread::sleep_for(1s); } last_nr_killed = nr_killed; } reportKillCompletionToXattr(cgroup_path, nr_killed); return nr_killed > 0; } int BaseKillPlugin::tryToKillPids(const std::vector& pids) { std::ostringstream buf; int nr_killed = 0; for (int pid : pids) { auto comm_path = std::string("/proc/") + std::to_string(pid) + "/comm"; auto comm = Fs::readFileByLine(comm_path); if (comm && comm->size()) { buf << " " << pid << "(" << comm.value()[0] << ")"; } else { buf << " " << pid; } if (::kill(static_cast(pid), SIGKILL) == 0) { nr_killed++; } else { buf << "[E" << errno << "]"; } } if (buf.tellp()) { OLOG << "Killed " << nr_killed << ":" << buf.str(); } return nr_killed; } BaseKillPlugin::KillUuid BaseKillPlugin::generateKillUuid() const { return Util::generateUuid(); } std::string BaseKillPlugin::getxattr( const std::string& path, const std::string& attr) { auto ret = Fs::getxattr(path, attr); // TODO(dschatzberg): Report error if (!ret) { return ""; } return *ret; } bool BaseKillPlugin::setxattr( const std::string& path, const std::string& attr, const std::string& val) { // TODO(dschatzberg): Report error if (!Fs::setxattr(path, attr, val)) { return false; } return true; } void BaseKillPlugin::reportKillInitiationToXattr( const std::string& cgroup_path) { auto prev_xattr_str = getxattr(cgroup_path, kOomdKillInitiationXattr); const int prev_xattr = std::stoi(prev_xattr_str != "" ? prev_xattr_str : "0"); std::string new_xattr_str = std::to_string(prev_xattr + 1); if (setxattr(cgroup_path, kOomdKillInitiationXattr, new_xattr_str)) { OLOG << "Set xattr " << kOomdKillInitiationXattr << "=" << new_xattr_str << " on " << cgroup_path; } } void BaseKillPlugin::reportKillCompletionToXattr( const std::string& cgroup_path, int num_procs_killed) { auto prev_xattr_str = getxattr(cgroup_path, kOomdKillCompletionXattr); const int prev_xattr = std::stoi(prev_xattr_str != "" ? prev_xattr_str : "0"); std::string new_xattr_str = std::to_string(prev_xattr + num_procs_killed); if (setxattr(cgroup_path, kOomdKillCompletionXattr, new_xattr_str)) { OLOG << "Set xattr " << kOomdKillCompletionXattr << "=" << new_xattr_str << " on " << cgroup_path; } } void BaseKillPlugin::reportKillUuidToXattr( const std::string& cgroup_path, const std::string& kill_uuid) { if (setxattr(cgroup_path, kOomdKillUuidXattr, kill_uuid)) { OLOG << "Set xattr " << kOomdKillUuidXattr << "=" << kill_uuid << " on " << cgroup_path; } } bool BaseKillPlugin::tryToLogAndKillCgroup( const OomdContext& ctx, const KillCandidate& candidate) { KillUuid kill_uuid = generateKillUuid(); auto action_context = ctx.getActionContext(); bool success = tryToKillCgroup(candidate.cgroup_ctx, kill_uuid, dry_); if (success) { auto mem_pressure = candidate.cgroup_ctx.mem_pressure().value_or(ResourcePressure{}); std::ostringstream oss; oss << std::setprecision(2) << std::fixed; oss << mem_pressure.sec_10 << " " << mem_pressure.sec_60 << " " << mem_pressure.sec_300 << " " << candidate.cgroup_ctx.cgroup().relativePath() << " " << candidate.cgroup_ctx.current_usage().value_or(0) << " " << "ruleset:[" << action_context.ruleset_name << "] " << "detectorgroup:[" << action_context.detectorgroup << "] " << "killer:" << (dry_ ? "(dry)" : "") << getName() << " v2"; if (!dry_) { Oomd::incrementStat(CoreStats::kKillsKey, 1); } OOMD_KMSG_LOG(oss.str(), "oomd kill"); } dumpKillInfo( candidate.cgroup_ctx.cgroup(), candidate.cgroup_ctx, candidate.kill_root, action_context, kill_uuid, success, dry_); return success; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/BaseKillPlugin.h000066400000000000000000000211411406470533700207060ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include "oomd/CgroupContext.h" #include "oomd/OomdContext.h" #include "oomd/engine/BasePlugin.h" #include "oomd/engine/PrekillHook.h" #include "oomd/include/CgroupPath.h" namespace Oomd { /* * This abstract base class provides an overridable set of methods that * enables reuse of kill mechanism code. All plugins that kill processes * need code that can traverse a cgroup directory and start killing PIDs. * There is no reason for every plugin to rewrite that code. * * Ideally all killing plugins should inherit from this base class. * If customized behavior is desired, then simply override the relevant * methods. */ class BaseKillPlugin : public Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& ctx) override; /* * Runs @param fn on every cgroup in cgroups_ and their descendants * * Useful when implementing prerun() to track historical stats of cgroups * plugin will use to rank for killing. * Respects `recursive` config automatically. */ template void prerunOnCgroups(OomdContext& ctx, Functor&& fn) { // Order doesn't matter. Use DFS instead of BFS because we expect tree to be // shallower than wide. std::vector unvisited; const auto& root_cgroups = ctx.addToCacheAndGet(cgroups_); std::move( root_cgroups.begin(), root_cgroups.end(), std::back_inserter(unvisited)); while (!unvisited.empty()) { const CgroupContext& cgroup_ctx = unvisited.back(); unvisited.pop_back(); if (recursive_ && !cgroup_ctx.oom_group().value_or(false)) { const auto& children = ctx.addChildrenToCacheAndGet(cgroup_ctx); std::move( children.begin(), children.end(), std::back_inserter(unvisited)); } fn(cgroup_ctx); } } protected: /* * Required implementation point for kill plugins * * @return cgroups in the order of worthiness to kill, best target first. * @param cgroups is an unordered set of cgroups to rank. * * rankForKilling() will typically @return its input in a different order, but * doesn't strictly have to. It may eg. return a subset of its input. * * @return a vector instead of an * optional because if killing the best-choice cgroup * fails, we'll try to kill the next-best, and so on down the list. * * The caller, BaseKillPlugin::run, implements "recurse", "dry", and "debug" * config and memory.oom.group support, so rankForKilling() doesn't have to. * What it passes as @param cgroups is the resolved "cgroup" config, or if * "recursive" is set, the children of the highest-ranked cgroup from the last * iteration. */ virtual std::vector rankForKilling( OomdContext& ctx, const std::vector& cgroups) = 0; /* * Override point to OLOG why plugin chose @param target to die. * * @param target is the cgroup to be killed * @param peers are the set of cgroups from which target was chosen. This may * be useful for logging eg. target's memory as a fraction of total memory * usage among the peer group. peers includes target. If recursive_ is set, * peers are often a target's siblings. * * @param peers is the same vec as was passed to rankForKilling() as @param * cgroups in the call when rankForKilling() returned @param target. * See KillMemoryGrowth for an example using siblings. */ virtual void ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) = 0; BaseKillPlugin(); using KillUuid = std::string; /* * Kills a cgroup * * @param target is the cgroup to kill * @param kill_uuid is the name of this kill to use in logs * @param dry sets whether or not we should actually issue SIGKILLs * @returns true if successfully killed anything */ virtual bool tryToKillCgroup( const CgroupContext& target, const KillUuid& kill_uuid, bool dry); /* * Sends SIGKILL to every PID in @param procs */ virtual int tryToKillPids(const std::vector& procs); virtual KillUuid generateKillUuid() const; /* * get/set methods for xattrs values. Since manipulating extended attributes * requires root permission, we can't use ::get/setxattr in unit tests. */ virtual std::string getxattr( const std::string& path, const std::string& attr); virtual bool setxattr( const std::string& path, const std::string& attr, const std::string& val); /* * Increments the "trusted.oomd_ooms" extended attribute key on @param * cgroup_path */ virtual void reportKillInitiationToXattr(const std::string& cgroup_path); /* * Increments the "trusted.oomd_kill" extended attribute key by @param * num_procs_killed on @param cgroup_path */ virtual void reportKillCompletionToXattr( const std::string& cgroup_path, int num_procs_killed); /* * Sets the "trusted.oomd_kill_uuid" extended attribute key to @param * kill_uuid on @param cgroup_path */ virtual void reportKillUuidToXattr( const std::string& cgroup_path, const std::string& kill_uuid); virtual void dumpKillInfo( const CgroupPath& killed_group, const CgroupContext& context, const CgroupContext& kill_root, const ActionContext& action_context, const std::string& kill_uuid, bool success, bool dry) const; /* * Override point for tests to control the clock */ virtual bool pastPrekillHookTimeout(const OomdContext& ctx) const; private: virtual int getAndTryToKillPids(const CgroupContext& target); enum class KillResult { SUCCESS, FAILED, DEFER, }; KillResult tryToKillSomething( OomdContext& ctx, const std::vector& initial_cgroups); struct KillCandidate { public: const CgroupContext& cgroup_ctx; // .kill_root and .peers are for logging // .kill_root is for when recursive targeting is enabled. It is the ancestor // cgroup of .cgroup_ctx that was targeted in the plugin's "cgroup" arg. // When recursive targeting is disabled, .kill_root == .cgroup_ctx const CgroupContext& kill_root; std::shared_ptr> peers; }; KillResult resumeTryingToKillSomething( OomdContext& ctx, std::vector next_best_option_stack); /* * Kills cgroup and logs a structured kill message to kmsg and stderr. * Returns false on failure. */ bool tryToLogAndKillCgroup( const OomdContext& ctx, const KillCandidate& candidate); // SerializedKillCandidates may be held across intervals because unlike // KillCandidates, Serialized* versions do not hold CgroupContext refs. struct SerializedCgroupRef { public: CgroupPath path; // id=nullopt represents a deleted cgroup which is undeserializable std::optional id; }; struct SerializedKillCandidate { public: SerializedCgroupRef target; SerializedCgroupRef kill_root; std::shared_ptr> peers; }; KillResult resumeFromPrekillHook(OomdContext& ctx); std::unordered_set cgroups_; bool recursive_{false}; std::optional post_action_delay_{std::nullopt}; bool dry_{false}; bool always_continue_{false}; bool debug_{false}; struct ActivePrekillHook { public: std::unique_ptr hook_invocation; SerializedKillCandidate intended_victim; std::vector next_best_option_stack; }; std::optional prekill_hook_state_{std::nullopt}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/ContinuePlugin.cpp000066400000000000000000000015731406470533700213460ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/ContinuePlugin.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(continue, ContinuePlugin::create); } // namespace Oomd oomd-0.5.0/src/oomd/plugins/ContinuePlugin.h000066400000000000000000000023171406470533700210100ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/engine/BasePlugin.h" namespace Oomd { class ContinuePlugin : public Engine::BasePlugin { public: int init( const Engine::PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } Engine::PluginRet run(OomdContext& /* unused */) override { return Engine::PluginRet::CONTINUE; } static ContinuePlugin* create() { return new ContinuePlugin(); } ~ContinuePlugin() = default; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/CorePluginsTest.cpp000066400000000000000000002776031406470533700215060ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include "oomd/OomdContext.h" #include "oomd/PluginRegistry.h" #include "oomd/engine/BasePlugin.h" #include "oomd/plugins/BaseKillPlugin.h" #include "oomd/plugins/KillIOCost.h" #include "oomd/plugins/KillMemoryGrowth.h" #include "oomd/plugins/KillPgScan.h" #include "oomd/plugins/KillPressure.h" #include "oomd/plugins/KillSwapUsage.h" #include "oomd/util/Fixture.h" #include "oomd/util/Fs.h" #include "oomd/util/TestHelper.h" using namespace Oomd; using namespace testing; namespace { std::unique_ptr createPlugin(const std::string& name) { return std::unique_ptr( Oomd::getPluginRegistry().create(name)); } } // namespace namespace Oomd { class BaseKillPluginMock : public BaseKillPlugin { public: int tryToKillPids(const std::vector& pids) override { int ret = 0; killed.reserve(pids.size()); for (int pid : pids) { if (killed.find(pid) == killed.end()) { killed.emplace(pid); ++ret; } } return ret; } bool tryToKillCgroup( const CgroupContext& target, const KillUuid& kill_uuid, bool dry) override { if (unkillable_cgroups.count(target.cgroup().absolutePath()) > 0) { OLOG << "tried to kill " << target.cgroup().absolutePath() << ", failed b/c it's in unkillable_cgroups"; return false; } OLOG << "killed " << target.cgroup().absolutePath(); killed_cgroup = target.cgroup().absolutePath(); return BaseKillPlugin::tryToKillCgroup(target, kill_uuid, dry); } std::optional killed_cgroup{std::nullopt}; std::unordered_set unkillable_cgroups; std::unordered_set killed; }; /* * Use this concrete class to test BaseKillPlugin methods */ class BaseKillPluginShim : public BaseKillPluginMock { public: int init( const Engine::PluginArgs& /* unused */, const PluginConstructionContext& context) override { return 0; } Engine::PluginRet run(OomdContext& /* unused */) override { return Engine::PluginRet::CONTINUE; } std::vector rankForKilling( OomdContext& /* unused */, const std::vector& /* unused */) override { return std::vector{}; } void ologKillTarget( OomdContext& /* unused */, const CgroupContext& target, const std::vector& /* unused */) override { OLOG << "Picked \"" << target.cgroup().relativePath() << "\""; } }; } // namespace Oomd class CorePluginsTest : public ::testing::Test { protected: using CgroupData = TestHelper::CgroupData; using memory_stat_t = decltype(CgroupData::memory_stat)::value_type; using F = Fixture; void SetUp() override { tempdir_ = F::mkdtempChecked(); } void TearDown() override { F::rmrChecked(tempdir_); } std::string tempdir_; OomdContext ctx_; }; class BaseKillPluginTest : public CorePluginsTest {}; TEST_F(BaseKillPluginTest, TryToKillCgroupKillsRecursive) { auto target = ASSERT_EXISTS(CgroupContext::make( ctx_, CgroupPath("oomd/fixtures/plugins/base_kill_plugin", "one_big"))); BaseKillPluginShim plugin; EXPECT_EQ(plugin.tryToKillCgroup(target, "fake_kill_uuid", false), true); int expected_total = 0; for (int i = 1; i <= 30; ++i) { expected_total += i; } expected_total += 1234; int received_total = 0; for (int i : plugin.killed) { received_total += i; } EXPECT_EQ(expected_total, received_total); } class BaseKillPluginXattrTest : public ::testing::Test, public BaseKillPluginShim { protected: std::string getxattr(const std::string& path, const std::string& attr) override { return xattrs_[path][attr]; } bool setxattr( const std::string& path, const std::string& attr, const std::string& val) override { xattrs_[path][attr] = val; return true; } private: std::unordered_map> xattrs_; }; TEST_F(BaseKillPluginXattrTest, XattrSetts) { auto cgroup_path = "/sys/fs/cgroup/test/test"; static auto constexpr kOomdKillInitiationXattr = "trusted.oomd_ooms"; static auto constexpr kOomdKillCompletionXattr = "trusted.oomd_kill"; static auto constexpr kOomdKillUuidXattr = "trusted.oomd_kill_uuid"; static auto constexpr kKillUuid1 = "8c774f00-8202-4893-a58d-74bd1515660e"; static auto constexpr kKillUuid2 = "9c774f00-8202-4893-a58d-74bd1515660e"; // Kill Initiation increments on each kill EXPECT_EQ(getxattr(cgroup_path, kOomdKillInitiationXattr), ""); reportKillInitiationToXattr(cgroup_path); EXPECT_EQ(getxattr(cgroup_path, kOomdKillInitiationXattr), "1"); reportKillInitiationToXattr(cgroup_path); EXPECT_EQ(getxattr(cgroup_path, kOomdKillInitiationXattr), "2"); // Kill Completion sums up for each kill EXPECT_EQ(getxattr(cgroup_path, kOomdKillCompletionXattr), ""); reportKillCompletionToXattr(cgroup_path, 10); EXPECT_EQ(getxattr(cgroup_path, kOomdKillCompletionXattr), "10"); reportKillCompletionToXattr(cgroup_path, 10); EXPECT_EQ(getxattr(cgroup_path, kOomdKillCompletionXattr), "20"); // Kill Uuid resets on each kill EXPECT_EQ(getxattr(cgroup_path, kOomdKillUuidXattr), ""); reportKillUuidToXattr(cgroup_path, kKillUuid1); EXPECT_EQ(getxattr(cgroup_path, kOomdKillUuidXattr), kKillUuid1); reportKillUuidToXattr(cgroup_path, kKillUuid2); EXPECT_EQ(getxattr(cgroup_path, kOomdKillUuidXattr), kKillUuid2); } // Test BaseKillPlugin's cgroup traversal with a subclass that chooses // cgroups the simplest way: by name, alphabetically (descending). class AlphabeticStandardKillPlugin : public BaseKillPluginMock { public: std::vector rankForKilling( OomdContext& /* unused */, const std::vector& cgroups) override { return OomdContext::sortDescWithKillPrefs( cgroups, [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.cgroup().relativePathParts().back(); }); } void ologKillTarget( OomdContext& /* unused */, const CgroupContext& target, const std::vector& /* unused */) override { OLOG << "Picked \"" << target.cgroup().relativePath() << "\""; } }; class StandardKillRecursionTest : public CorePluginsTest {}; TEST_F(StandardKillRecursionTest, Recurses) { F::materialize(F::makeDir( tempdir_, {Fixture::makeDir( "A", { // Note "Z" and "X" are higher than the "F" in "A" below. // This tests that decisions are made locally: first choose B // from root's {A, B}, then choose F from B's {F}. If leaves // are compared across subtrees Z will win and break the // test. Fixture::makeDir("Z", {}), Fixture::makeDir("X", {}), }), Fixture::makeDir( "B", { Fixture::makeDir("F", {}), })})); auto plugin = std::make_shared(); ASSERT_NE(plugin, nullptr); const PluginConstructionContext compile_context(tempdir_); Engine::PluginArgs args; args["cgroup"] = "*"; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(*plugin->killed_cgroup, CgroupPath(tempdir_, "B/F").absolutePath()); } TEST_F(StandardKillRecursionTest, ConfigurableToNotRecurse) { // Same as StandardKillRecursionTest.Recurses but without args["recursive"] F::materialize(F::makeDir( tempdir_, {Fixture::makeDir( "A", { // Note "Z" and "X" are higher than the "F" in "A" below. // This tests that decisions are made locally: first choose B // from root's {A, B}, then choose F from B's {F}. If leaves // are compared across subtrees Z will win and break the // test. Fixture::makeDir("Z", {}), Fixture::makeDir("X", {}), }), Fixture::makeDir( "B", { Fixture::makeDir("F", {}), })})); auto plugin = std::make_shared(); ASSERT_NE(plugin, nullptr); const PluginConstructionContext compile_context(tempdir_); Engine::PluginArgs args; args["cgroup"] = "*"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(*plugin->killed_cgroup, CgroupPath(tempdir_, "B").absolutePath()); } TEST_F(StandardKillRecursionTest, BacktracksUpTreeOnFail) { /* Consider A - B - C \ D - E where C has no active processes. We should pick A, B, C, then fail. We should - backtrack to B, - finding no other children of B, backtrack to A - pick D, A's only other child - pick E and ultimately kill E. */ auto plugin = std::make_shared(); ASSERT_NE(plugin, nullptr); F::materialize(F::makeDir( tempdir_, {F::makeDir( "test.slice", {Fixture::makeDir( "A", { Fixture::makeDir("Z", {}), }), Fixture::makeDir( "P", { Fixture::makeDir("Z", {}), Fixture::makeDir("X", {}), }), Fixture::makeDir( "Q", { Fixture::makeDir( "F", { Fixture::makeDir("P", {}), }), })})})); plugin->unkillable_cgroups.emplace( CgroupPath(tempdir_, "test.slice/Q/F/P").absolutePath()); const PluginConstructionContext compile_context(tempdir_); Engine::PluginArgs args; args["cgroup"] = "*"; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ( *plugin->killed_cgroup, CgroupPath(tempdir_, "test.slice/P/Z").absolutePath()); } TEST_F(StandardKillRecursionTest, RespectsMemoryOomGroup) { auto plugin = std::make_shared(); ASSERT_NE(plugin, nullptr); F::materialize(F::makeDir( tempdir_, {F::makeDir( "test.slice", {Fixture::makeDir( "A", { Fixture::makeDir("Z", {}), }), Fixture::makeDir( "Y", {Fixture::makeDir( "M", { // Without oom.group here, child Y/M/Z would be killed. // With this, recursion will stop here, and Y/M will be // killed. Fixture::makeFile("memory.oom.group", "1\n"), Fixture::makeDir("Z", {}), Fixture::makeDir("X", {}), })}), Fixture::makeDir( "Q", { Fixture::makeDir( "F", { Fixture::makeDir("P", {}), }), })})})); const PluginConstructionContext compile_context(tempdir_); Engine::PluginArgs args; args["cgroup"] = "*"; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ( *plugin->killed_cgroup, CgroupPath(tempdir_, "test.slice/Y/M").absolutePath()); } TEST_F(StandardKillRecursionTest, RespectsPreferAvoid) { // Technically not an aspect of BaseKillPlugin, and implemented // separately in each subclass with OomdContext::sortDescWithKillPrefs in // the subclass' rankForKilling. It's expected that all subclasses will // use OomdContext::sortDescWithKillPrefs in the same way, so we test an ex // of it here. F::materialize(F::makeDir( tempdir_, {F::makeDir( "test.slice", {Fixture::makeDir( "A", { Fixture::makeDir("Z", {}), }), Fixture::makeDir( "B", {Fixture::makeDir( "M", { Fixture::makeDir("Z", {}), Fixture::makeDir("V", {}), })}), Fixture::makeDir( "Q", { Fixture::makeDir( "F", { Fixture::makeDir("P", {}), }), })})})); Engine::PluginArgs args; args["cgroup"] = "*"; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["dry"] = "true"; const auto& expect_to_kill = [&](const std::string& expected_victim, std::function customizer) { OomdContext ctx; customizer(ctx); const PluginConstructionContext compile_context(tempdir_); auto plugin = std::make_shared(); ASSERT_NE(plugin, nullptr); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx), Engine::PluginRet::STOP); EXPECT_EQ( *plugin->killed_cgroup, CgroupPath(tempdir_, expected_victim).absolutePath()); }; expect_to_kill("test.slice/B/M/V", [&](auto& ctx) { TestHelper::setCgroupData( ctx, CgroupPath(tempdir_, "test.slice/Q"), CgroupData{.kill_preference = KillPreference::AVOID}); TestHelper::setCgroupData( ctx, CgroupPath(tempdir_, "test.slice/B/M/V"), CgroupData{.kill_preference = KillPreference::PREFER}); }); // Test locality of prefs. Even though Y/M/X is PREFER, it's not chosen // because Q is chosen over Y in the first level of the tree. // Q/F/P is selected despite being the only AVOID in a tree with a PREFER. expect_to_kill("test.slice/Q/F/P", [&](auto& ctx) { TestHelper::setCgroupData( ctx, CgroupPath(tempdir_, "test.slice/Q/F/P"), CgroupData{.kill_preference = KillPreference::AVOID}); TestHelper::setCgroupData( ctx, CgroupPath(tempdir_, "test.slice/B/M/V"), CgroupData{.kill_preference = KillPreference::PREFER}); }); // Test multiple prefs in a path expect_to_kill("test.slice/B/M/V", [&](auto& ctx) { TestHelper::setCgroupData( ctx, CgroupPath(tempdir_, "test.slice/B"), CgroupData{.kill_preference = KillPreference::PREFER}); TestHelper::setCgroupData( ctx, CgroupPath(tempdir_, "test.slice/B/M/Z"), CgroupData{.kill_preference = KillPreference::AVOID}); }); } TEST_F(StandardKillRecursionTest, IgnoresDeadCgroup) { auto plugin = std::make_shared(); ASSERT_NE(plugin, nullptr); F::materialize(F::makeDir( tempdir_, {Fixture::makeDir( "A", { Fixture::makeDir("Z", {}), Fixture::makeDir("X", {}), }), Fixture::makeDir( "B", { Fixture::makeDir( "F", { // Same as StandardKillRecursionTest.Recurses above, // except cgroup.events' populated=0 F::makeFile( "cgroup.events", "populated 0\n" "frozen 0\n"), }), })})); const PluginConstructionContext compile_context(tempdir_); Engine::PluginArgs args; args["cgroup"] = "*"; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(*plugin->killed_cgroup, CgroupPath(tempdir_, "A/Z").absolutePath()); } TEST_F(StandardKillRecursionTest, IgnoresOutsideConfiguredCgroup) { F::materialize(F::makeDir( tempdir_, {Fixture::makeDir( "A", { Fixture::makeDir( "Y", { Fixture::makeDir("P", {}), Fixture::makeDir("Q", {}), }), Fixture::makeDir("X", {}), }), Fixture::makeDir( "B", { Fixture::makeDir("F", {}), }), Fixture::makeDir( "C", { Fixture::makeDir("F", {}), }), Fixture::makeDir( "Z", { Fixture::makeDir("F", {}), })})); const auto& target_when_plugin_cgroup_arg_is = [&](const std::string& cgroup_arg) -> std::string { auto plugin = std::make_shared(); EXPECT_NE(plugin, nullptr); const PluginConstructionContext compile_context(tempdir_); Engine::PluginArgs args; args["cgroup"] = cgroup_arg; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["dry"] = "true"; args["debug"] = "true"; EXPECT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); return *plugin->killed_cgroup; }; EXPECT_EQ( target_when_plugin_cgroup_arg_is("A,B"), CgroupPath(tempdir_, "B/F").absolutePath()); EXPECT_EQ( target_when_plugin_cgroup_arg_is("A/*,B,C"), CgroupPath(tempdir_, "A/Y/Q").absolutePath()); EXPECT_EQ( target_when_plugin_cgroup_arg_is("A/*,Z,B,C"), CgroupPath(tempdir_, "Z/F").absolutePath()); EXPECT_EQ( target_when_plugin_cgroup_arg_is("*"), CgroupPath(tempdir_, "Z/F").absolutePath()); } TEST_F(StandardKillRecursionTest, PrerunsRecursively) { F::materialize(F::makeDir( tempdir_, {Fixture::makeDir( "A", { Fixture::makeDir( "Y", { // oom.group prevents prerun from running on P and Q, // even if "recursive" is set Fixture::makeFile("memory.oom.group", "1\n"), Fixture::makeDir("P", {}), Fixture::makeDir("Q", {}), }), Fixture::makeDir("X", {}), }), Fixture::makeDir( "B", { Fixture::makeDir( "F", { Fixture::makeDir("P", {}), Fixture::makeDir("Q", {}), }), Fixture::makeDir("X", {}), }), Fixture::makeDir( "C", { Fixture::makeDir("F", {}), }), Fixture::makeDir( "Z", { Fixture::makeDir("F", {}), })})); const auto& get_touched_cgroups = [&](bool recurse) -> std::unordered_set { auto plugin = std::make_shared(); EXPECT_NE(plugin, nullptr); const PluginConstructionContext compile_context(tempdir_); Engine::PluginArgs args; args["cgroup"] = "B,Z,A/*"; args["post_action_delay"] = "0"; args["dry"] = "true"; if (recurse) { args["recursive"] = "true"; } EXPECT_EQ(plugin->init(std::move(args), compile_context), 0); std::unordered_set touched_cgroup_paths; plugin->prerunOnCgroups(ctx_, [&](auto& cgroup_ctx) { touched_cgroup_paths.emplace(cgroup_ctx.cgroup().relativePath()); }); return touched_cgroup_paths; }; EXPECT_EQ( get_touched_cgroups(true), (std::unordered_set{ "A/Y", "A/X", "B", "B/F", "B/F/P", "B/F/Q", "B/X", "Z", "Z/F"})); // Don't waste CPU walking down the cgroup tree in prerun if // recursive_=false EXPECT_EQ( get_touched_cgroups(false), (std::unordered_set{"A/Y", "A/X", "B", "Z"})); } class PressureRisingBeyondTest : public CorePluginsTest {}; TEST_F(PressureRisingBeyondTest, DetectsHighMemPressure) { F::materialize(F::makeDir(tempdir_, {F::makeDir("high_pressure")})); auto plugin = createPlugin("pressure_rising_beyond"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "high_pressure"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; args["fast_fall_ratio"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_pressure"), CgroupData{ .mem_pressure = ResourcePressure{ .sec_10 = 99.99, .sec_60 = 99.99, .sec_300 = 99.99}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(PressureRisingBeyondTest, NoDetectLowMemPressure) { F::materialize(F::makeDir(tempdir_, {F::makeDir("low_pressure")})); auto plugin = createPlugin("pressure_rising_beyond"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "low_pressure"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; args["fast_fall_ratio"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_pressure"), CgroupData{ .mem_pressure = ResourcePressure{.sec_10 = 1.11, .sec_60 = 1.11, .sec_300 = 1.11}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(PressureRisingBeyondTest, DetectsHighMemPressureMultiCgroup) { F::materialize(F::makeDir( tempdir_, {F::makeDir("high_pressure"), F::makeDir("low_pressure")})); auto plugin = createPlugin("pressure_rising_beyond"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "low_pressure,high_pressure"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; args["fast_fall_ratio"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_pressure"), CgroupData{ .mem_pressure = ResourcePressure{ .sec_10 = 99.99, .sec_60 = 99.99, .sec_300 = 99.99}, .current_usage = 987654321}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_pressure"), CgroupData{ .mem_pressure = ResourcePressure{.sec_10 = 1.11, .sec_60 = 1.11, .sec_300 = 1.11}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(PressureRisingBeyondTest, DetectsHighMemPressureWildcard) { F::materialize(F::makeDir( tempdir_, {F::makeDir("high_pressure"), F::makeDir("low_pressure")})); auto plugin = createPlugin("pressure_rising_beyond"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "*_*"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; args["fast_fall_ratio"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_pressure"), CgroupData{ .mem_pressure = ResourcePressure{ .sec_10 = 99.99, .sec_60 = 99.99, .sec_300 = 99.99}, .current_usage = 987654321}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_pressure"), CgroupData{ .mem_pressure = ResourcePressure{.sec_10 = 1.11, .sec_60 = 1.11, .sec_300 = 1.11}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } class PressureAboveTest : public CorePluginsTest {}; TEST_F(PressureAboveTest, DetectsHighMemPressure) { F::materialize(F::makeDir(tempdir_, {F::makeDir("high_pressure")})); auto plugin = createPlugin("pressure_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "high_pressure"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_pressure"), CgroupData{ .mem_pressure = ResourcePressure{ .sec_10 = 99.99, .sec_60 = 99.99, .sec_300 = 99.99}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(PressureAboveTest, NoDetectLowMemPressure) { F::materialize(F::makeDir(tempdir_, {F::makeDir("low_pressure")})); auto plugin = createPlugin("pressure_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "low_pressure"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_pressure"), CgroupData{ .mem_pressure = ResourcePressure{.sec_10 = 1.11, .sec_60 = 1.11, .sec_300 = 1.11}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(PressureAboveTest, DetectsHighMemPressureMultiCgroup) { F::materialize(F::makeDir( tempdir_, {F::makeDir("high_pressure"), F::makeDir("low_pressure")})); auto plugin = createPlugin("pressure_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "high_pressure,low_pressure"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_pressure"), CgroupData{ .mem_pressure = ResourcePressure{ .sec_10 = 99.99, .sec_60 = 99.99, .sec_300 = 99.99}, .current_usage = 987654321}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_pressure"), CgroupData{ .mem_pressure = ResourcePressure{.sec_10 = 1.11, .sec_60 = 1.11, .sec_300 = 1.11}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(PressureAboveTest, DetectsHighMemPressureWildcard) { F::materialize(F::makeDir( tempdir_, {F::makeDir("high_pressure"), F::makeDir("low_pressure")})); auto plugin = createPlugin("pressure_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "*"; args["resource"] = "memory"; args["threshold"] = "80"; args["duration"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_pressure"), CgroupData{ .mem_pressure = ResourcePressure{ .sec_10 = 99.99, .sec_60 = 99.99, .sec_300 = 99.99}, .current_usage = 987654321}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_pressure"), CgroupData{ .mem_pressure = ResourcePressure{.sec_10 = 1.11, .sec_60 = 1.11, .sec_300 = 1.11}, .current_usage = 987654321}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } class MemoryAboveTest : public CorePluginsTest {}; TEST_F(MemoryAboveTest, DetectsHighMemUsage) { F::materialize(F::makeDir(tempdir_, {F::makeDir("high_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "high_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "1536M"; args["duration"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_memory"), CgroupData{.current_usage = 2147483648}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(MemoryAboveTest, NoDetectLowMemUsage) { F::materialize(F::makeDir(tempdir_, {F::makeDir("low_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["cgroup"] = "low_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "1536M"; args["duration"] = "0"; const PluginConstructionContext compile_context(tempdir_); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_memory"), CgroupData{.current_usage = 1073741824}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(MemoryAboveTest, DetectsHighMemUsageCompat) { F::materialize(F::makeDir(tempdir_, {F::makeDir("high_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "high_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "1536"; // Should be interpreted as MB args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_memory"), CgroupData{.current_usage = 2147483648}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(MemoryAboveTest, NoDetectLowMemUsageCompat) { F::materialize(F::makeDir(tempdir_, {F::makeDir("low_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "low_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "1536"; // Should be interpreted as MB args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_memory"), CgroupData{.current_usage = 1073741824}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(MemoryAboveTest, DetectsHighMemUsagePercent) { F::materialize(F::makeDir(tempdir_, {F::makeDir("high_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "high_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "10%"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_memory"), CgroupData{.current_usage = 2147483648}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(MemoryAboveTest, NoDetectLowMemUsageMultiple) { F::materialize(F::makeDir( tempdir_, {F::makeDir("high_memory"), F::makeDir("low_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "low_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "1536M"; args["duration"] = "0"; args["debug"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_memory"), CgroupData{.current_usage = 1073741824}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_memory"), CgroupData{.current_usage = 2147483648}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(MemoryAboveTest, DetectsHighMemUsageMultiple) { F::materialize(F::makeDir( tempdir_, {F::makeDir("high_memory"), F::makeDir("low_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "high_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "1536M"; args["duration"] = "0"; args["debug"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_memory"), CgroupData{.current_usage = 1073741824}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_memory"), CgroupData{.current_usage = 2147483648}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(MemoryAboveTest, NoDetectLowMemUsagePercent) { F::materialize(F::makeDir(tempdir_, {F::makeDir("low_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "low_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold"] = "80%"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(MemoryAboveTest, DetectsHighAnonUsage) { F::materialize(F::makeDir(tempdir_, {F::makeDir("high_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "high_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold_anon"] = "1536M"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_memory"), CgroupData{ .memory_stat = memory_stat_t{{"anon", 2147483648}}, .swap_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(MemoryAboveTest, NoDetectLowAnonUsage) { F::materialize(F::makeDir(tempdir_, {F::makeDir("low_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "low_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold_anon"] = "1536M"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_memory"), CgroupData{ .memory_stat = memory_stat_t{{"anon", 1073741824}}, .swap_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(MemoryAboveTest, DetectsHighAnonUsageIgnoreLowMemUsage) { F::materialize(F::makeDir(tempdir_, {F::makeDir("high_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "high_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold_anon"] = "1536M"; args["threshold"] = "1536M"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "high_memory"), CgroupData{ .memory_stat = memory_stat_t{{"anon", 2147483648}}, .current_usage = 1073741824, .swap_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(MemoryAboveTest, NoDetectLowAnonUsageIgnoreHighMemUsage) { F::materialize(F::makeDir(tempdir_, {F::makeDir("low_memory")})); auto plugin = createPlugin("memory_above"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "low_memory"; args["meminfo_location"] = "oomd/fixtures/plugins/memory_above/meminfo"; args["threshold_anon"] = "1536M"; args["threshold"] = "1536M"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "low_memory"), CgroupData{ .memory_stat = memory_stat_t{{"anon", 1073741824}}, .current_usage = 2147483648, .swap_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } class MemoryReclaimTest : public CorePluginsTest {}; TEST_F(MemoryReclaimTest, SingleCgroupReclaimSuccess) { auto plugin = createPlugin("memory_reclaim"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/memory_reclaim/single_cgroup"); args["cgroup"] = "cgroup1"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "cgroup1"), {}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(MemoryReclaimTest, MultiCgroupReclaimSuccess) { auto plugin = createPlugin("memory_reclaim"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/memory_reclaim/multi_cgroup"); args["cgroup"] = "cgroup1,cgroup2"; args["duration"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "cgroup1"), {}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "cgroup2"), {}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } class SwapFreeTest : public CorePluginsTest {}; TEST_F(SwapFreeTest, LowSwap) { auto plugin = createPlugin("swap_free"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["threshold_pct"] = "20"; const PluginConstructionContext compile_context("/sys/fs/cgroup"); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); SystemContext system_ctx; system_ctx.swaptotal = static_cast(20971512) * 1024; system_ctx.swapused = static_cast(20971440) * 1024; ctx_.setSystemContext(system_ctx); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(SwapFreeTest, EnoughSwap) { auto plugin = createPlugin("swap_free"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["threshold_pct"] = "20"; const PluginConstructionContext compile_context("/sys/fs/cgroup"); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); SystemContext system_ctx; system_ctx.swaptotal = static_cast(20971512) * 1024; system_ctx.swapused = static_cast(3310136) * 1024; ctx_.setSystemContext(system_ctx); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(SwapFreeTest, SwapOff) { auto plugin = createPlugin("swap_free"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["threshold_pct"] = "20"; const PluginConstructionContext compile_context("/sys/fs/cgroup"); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } class ExistsTest : public CorePluginsTest {}; TEST_F(ExistsTest, Exists) { auto plugin = createPlugin("exists"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "cgroup_A,cgroup_B,cgroup_C"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_D")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_C")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(ExistsTest, NotExists) { auto plugin = createPlugin("exists"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "cgroup_A,cgroup_B,cgroup_C"; args["negate"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_D")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_C")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } class KillIOCostTest : public CorePluginsTest {}; TEST_F(KillIOCostTest, TemporalCounter) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_io_cost"); args["cgroup"] = "one_high/cgroup1"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); CgroupPath cgroup(compile_context.cgroupFs(), "one_high/cgroup1"); TestHelper::setCgroupData( ctx_, cgroup, CgroupData{.io_cost_cumulative = 10000}); plugin->prerun(ctx_); EXPECT_TRUE( TestHelper::getDataRef(*ctx_.addToCacheAndGet(cgroup)).io_cost_rate); } TEST_F(KillIOCostTest, KillsHighestIOCost) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_io_cost"); args["cgroup"] = "one_high/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.io_cost_cumulative = 10000, .io_cost_rate = 10}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.io_cost_cumulative = 5000, .io_cost_rate = 30}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.io_cost_cumulative = 6000, .io_cost_rate = 50}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.io_cost_cumulative = 20000, .io_cost_rate = 100}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(111)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(888))); } TEST_F(KillIOCostTest, KillsHighestIOCostMultiCgroup) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_io_cost"); args["cgroup"] = "one_high/*,sibling/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.io_cost_cumulative = 10000, .io_cost_rate = 10}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.io_cost_cumulative = 5000, .io_cost_rate = 30}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.io_cost_cumulative = 6000, .io_cost_rate = 50}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.io_cost_cumulative = 20000, .io_cost_rate = 100}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(888)); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); } TEST_F(KillIOCostTest, DoesntKillsHighestIOCostDry) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_pressure"); args["cgroup"] = "one_high/*"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.io_cost_cumulative = 10000, .io_cost_rate = 10}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.io_cost_cumulative = 5000, .io_cost_rate = 30}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.io_cost_cumulative = 6000, .io_cost_rate = 50}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.io_cost_cumulative = 20000, .io_cost_rate = 100}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(plugin->killed.size(), 0); } class KillPgScanTest : public CorePluginsTest {}; TEST_F(KillPgScanTest, KillsHighestPgScan) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_pg_scan"); args["cgroup"] = "one_high/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.pg_scan_cumulative = 10000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.pg_scan_cumulative = 5000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.pg_scan_cumulative = 6000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.pg_scan_cumulative = 20000}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::ASYNC_PAUSED); ctx_.refresh(); ctx_.bumpCurrentTick(); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.pg_scan_cumulative = 10010}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.pg_scan_cumulative = 5030}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.pg_scan_cumulative = 6050}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.pg_scan_cumulative = 20100}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(111)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(888))); } TEST_F(KillPgScanTest, KillsHighestPgScanMultiCgroup) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_pg_scan"); args["cgroup"] = "one_high/*,sibling/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.pg_scan_cumulative = 10000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.pg_scan_cumulative = 5000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.pg_scan_cumulative = 6000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.pg_scan_cumulative = 20000}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::ASYNC_PAUSED); ctx_.refresh(); ctx_.bumpCurrentTick(); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.pg_scan_cumulative = 10010}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.pg_scan_cumulative = 5030}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.pg_scan_cumulative = 6050}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.pg_scan_cumulative = 20100}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(888)); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); } TEST_F(KillPgScanTest, DoesntKillsHighestPgScanDry) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_pg_scan"); args["cgroup"] = "one_high/*"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.pg_scan_cumulative = 10000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.pg_scan_cumulative = 5000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.pg_scan_cumulative = 6000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.pg_scan_cumulative = 20000}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::ASYNC_PAUSED); ctx_.refresh(); ctx_.bumpCurrentTick(); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{.pg_scan_cumulative = 10010}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{.pg_scan_cumulative = 5030}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{.pg_scan_cumulative = 6050}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.pg_scan_cumulative = 20100}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(plugin->killed.size(), 0); } TEST_F(KillPgScanTest, CanTargetRecursively) { // without cgroup.controllers, CgroupContext::refresh() thinks the cgroup was // removed, and gets itself removed from OomdContext's cache. auto controllers = F::makeFile("cgroup.controllers", "memory"); F::materialize(F::makeDir( tempdir_, {controllers, Fixture::makeDir( "A", { controllers, Fixture::makeDir("Z", {controllers}), Fixture::makeDir("X", {controllers}), }), Fixture::makeDir( "B", { controllers, Fixture::makeDir("F", {controllers}), }), Fixture::makeDir( "sibling", { controllers, Fixture::makeDir("F", {controllers}), })})); auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "A,B"; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["debug"] = "true"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A"), CgroupData{.pg_scan_cumulative = 20}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A/Z"), CgroupData{.pg_scan_cumulative = 10}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A/X"), CgroupData{.pg_scan_cumulative = 10}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "B"), CgroupData{.pg_scan_cumulative = 30}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling"), CgroupData{.pg_scan_cumulative = 50}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::ASYNC_PAUSED); ctx_.refresh(); ctx_.bumpCurrentTick(); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A"), CgroupData{.pg_scan_cumulative = 1100}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A/Z"), CgroupData{.pg_scan_cumulative = 600}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A/X"), CgroupData{.pg_scan_cumulative = 500}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "B"), CgroupData{.pg_scan_cumulative = 1000}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling"), CgroupData{.pg_scan_cumulative = 30000}); plugin->prerun(ctx_); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(*plugin->killed_cgroup, CgroupPath(tempdir_, "A/Z").absolutePath()); } TEST_F(ExistsTest, ExistsWildcard) { auto plugin = createPlugin("exists"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "cgroup_PREFIX*"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_SOMETHING")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_PREFIXhere")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(ExistsTest, NotExistsWildcard) { auto plugin = createPlugin("exists"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "cgroup_PREFIX*"; args["negate"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_SOMETHING")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); F::materialize(F::makeDir(tempdir_, {F::makeDir("cgroup_PREFIXhere")})); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } class NrDyingDescendantsTest : public CorePluginsTest {}; TEST_F(NrDyingDescendantsTest, SingleCgroupLte) { F::materialize(F::makeDir(tempdir_, {F::makeDir("cg")})); auto plugin = createPlugin("nr_dying_descendants"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "cg"; args["debug"] = "true"; args["lte"] = "true"; args["count"] = "100"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "cg"), CgroupData{.nr_dying_descendants = 123}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "cg"), CgroupData{.nr_dying_descendants = 90}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(NrDyingDescendantsTest, SingleCgroupGt) { F::materialize(F::makeDir(tempdir_, {F::makeDir("cg")})); auto plugin = createPlugin("nr_dying_descendants"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "cg"; args["debug"] = "true"; args["lte"] = "false"; args["count"] = "100"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "cg"), CgroupData{.nr_dying_descendants = 123}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "cg"), CgroupData{.nr_dying_descendants = 90}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } TEST_F(NrDyingDescendantsTest, RootCgroup) { auto plugin = createPlugin("nr_dying_descendants"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "/"; args["debug"] = "true"; args["lte"] = "false"; // Greater than args["count"] = "29"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), ""), CgroupData{.nr_dying_descendants = 30}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } TEST_F(NrDyingDescendantsTest, MultiCgroupGt) { F::materialize(F::makeDir( tempdir_, {F::makeDir("above"), F::makeDir("above1"), F::makeDir("below")})); auto plugin = createPlugin("nr_dying_descendants"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "above,above1,below"; args["debug"] = "true"; args["lte"] = "true"; args["count"] = "100"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "above"), CgroupData{.nr_dying_descendants = 200}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "above1"), CgroupData{.nr_dying_descendants = 300}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "below"), CgroupData{.nr_dying_descendants = 90}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); } class KillMemoryGrowthTest : public CorePluginsTest {}; TEST_F(KillMemoryGrowthTest, TemporalCounter) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/cgroup1"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); CgroupPath cgroup(compile_context.cgroupFs(), "one_big/cgroup1"); TestHelper::setCgroupData(ctx_, cgroup, CgroupData{.current_usage = 60}); plugin->prerun(ctx_); EXPECT_TRUE( TestHelper::getDataRef(*ctx_.addToCacheAndGet(cgroup)).average_usage); } TEST_F(KillMemoryGrowthTest, KillsBigCgroup) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 20, .average_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(123)); EXPECT_THAT(plugin->killed, Contains(456)); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT( plugin->killed, Not(Contains(888))); // make sure there's no siblings } TEST_F(KillMemoryGrowthTest, PreferredOverridesSize) { // Same as KillsBigCgroup, but with .kill_preferences set auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 20, .kill_preference = KillPreference::PREFER, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 20, .average_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(789)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT( plugin->killed, Not(Contains(888))); // make sure there's no siblings } TEST_F(KillMemoryGrowthTest, AvoidedOverridesSize) { // Same as KillsBigCgroup, but with .kill_preferences set auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .kill_preference = KillPreference::AVOID, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 30, .average_usage = 30, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 20, .average_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(789)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT( plugin->killed, Not(Contains(888))); // make sure there's no siblings } TEST_F(KillMemoryGrowthTest, AvoidedNoEffect) { // Same as KillsBigCgroup, but with .kill_preferences set. Avoiding cgroups // we would not have killed anyway has no effect. auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 30, .kill_preference = KillPreference::AVOID, .average_usage = 30, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .kill_preference = KillPreference::AVOID, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 20, .average_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(123)); EXPECT_THAT(plugin->killed, Contains(456)); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT( plugin->killed, Not(Contains(888))); // make sure there's no siblings } TEST_F(KillMemoryGrowthTest, PreferredNoEffect) { // Same as KillsBigCgroup, but with .kill_preferences set. Preferring all // cgroups is the same as preferring none of them. auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .kill_preference = KillPreference::PREFER, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 30, .kill_preference = KillPreference::PREFER, .average_usage = 30, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .kill_preference = KillPreference::PREFER, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 20, .kill_preference = KillPreference::PREFER, .average_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(123)); EXPECT_THAT(plugin->killed, Contains(456)); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT( plugin->killed, Not(Contains(888))); // make sure there's no siblings } TEST_F(KillMemoryGrowthTest, KillsOneOfPreferred) { // Same as KillsBigCgroup, but with .kill_preferences set. When multiple // cgroups are preferred, we choose between them with the same rules we use // when no cgroups are preferred. auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .kill_preference = KillPreference::PREFER, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 30, .kill_preference = KillPreference::AVOID, .average_usage = 30, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .kill_preference = KillPreference::PREFER, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 20, .kill_preference = KillPreference::PREFER, .average_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(123)); EXPECT_THAT(plugin->killed, Contains(456)); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT( plugin->killed, Not(Contains(888))); // make sure there's no siblings } TEST_F(KillMemoryGrowthTest, KillsBigCgroupGrowth) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "growth_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); // First test that we do the last ditch size killing. // // cgroup3 should be killed even though (30 / (21+20+30) < .5) TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup1"), CgroupData{ .current_usage = 21, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup2"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup3"), CgroupData{ .current_usage = 30, .average_usage = 30, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(111)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); // Now lower average usage to artificially "boost" growth rate to trigger // growth kill TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup1"), CgroupData{ .current_usage = 21, .average_usage = 5, }); // Do the same thing for a sibling cgroup, but set the growth higher. This // tests that sibling removal occurs for growth kills too. TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 99, .average_usage = 5, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Not(Contains(888))); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Contains(123)); EXPECT_THAT(plugin->killed, Contains(456)); } TEST_F(KillMemoryGrowthTest, DoesntGrowthKillBelowUsageThreshold) { const auto& target_with_args = [&](Engine::PluginArgs&& extra_args) { Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "growth_big/*"; args["post_action_delay"] = "0"; args["size_threshold"] = "50"; args.merge(extra_args); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup1"), CgroupData{ .current_usage = 40, .average_usage = 7, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup2"), CgroupData{ .current_usage = 50, .average_usage = 30, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup3"), CgroupData{ .current_usage = 60, .average_usage = 60, }); auto plugin = std::make_shared>(); EXPECT_NE(plugin, nullptr); EXPECT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); return plugin->killed; }; // None are eligible for size kill at size_threshold=50, which translates to // current_usage > (60+50+40)*0.5 = 75. // At growing_size_percentile=50, cgroup2 and cgroup3 are qualified, and // at min_growth_ratio=1.25 cgroup1 and cgroup2 are qualified, so only // cgroup2 is eligible for growth kill. EXPECT_EQ( target_with_args(Engine::PluginArgs{ {"growing_size_percentile", "50"}, {"min_growth_ratio", "1.25"}}), std::unordered_set({789})); // cgroup2 // At growing_size_percentile=20 all cgroups are eligible, and cgroup1 has // the highest growth. EXPECT_EQ( target_with_args(Engine::PluginArgs{ {"growing_size_percentile", "20"}, {"min_growth_ratio", "1.25"}}), std::unordered_set({123, 456})); // cgroup1 // At growing_size_percentile=67 only cgroup3 is qualified, but it does not // meet the min_growth_ratio requirement. We skip the growth kill phase, and // fall through to kill by size (no threshold), which picks cgroup3. EXPECT_EQ( target_with_args(Engine::PluginArgs{ {"growing_size_percentile", "67"}, {"min_growth_ratio", "1.25"}}), std::unordered_set({111})); // cgroup3 // All cgroups pass growing_size_percentile=20, but none pass // min_growth_ratio=10. We skip the growth kill phase, and fall through to // kill by size (no threshold), which picks cgroup3. EXPECT_EQ( target_with_args(Engine::PluginArgs{ {"growing_size_percentile", "20"}, {"min_growth_ratio", "10"}}), std::unordered_set({111})); // cgroup3 } TEST_F(KillMemoryGrowthTest, KillsByPreferredGrowth) { // Preferred cgroups that wont be targeted until the growth kill phase are // killed before non-prefer cgroups targeted in the size w/ threshold phase. auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "growth_big/*"; args["post_action_delay"] = "0"; args["debug"] = "1"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup1"), CgroupData{ .current_usage = 21 << 20, .kill_preference = KillPreference::PREFER, .average_usage = 5 << 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup2"), CgroupData{ .current_usage = 99 << 20, .kill_preference = KillPreference::PREFER, .average_usage = 5 << 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup3"), CgroupData{ .current_usage = 1000 << 20, .average_usage = 1000 << 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(789)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(888))); } // If without preferences we'd kill A from {A, B, C}, any additional // preferences where A is PREFER should still target A. // For example, if we PREFER A and B, we would not expect to start killing B, // since A would have been killed without preferences, and A is not lower // preference than anyone else. class KillMemoryGrowthConsistentWithPreference : public CorePluginsTest, public WithParamInterface {}; TEST_P(KillMemoryGrowthConsistentWithPreference, SizeThreshold) { KillPreference maybe_prefer = GetParam(); auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "growth_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup1"), CgroupData{ .current_usage = 21, .kill_preference = maybe_prefer, .average_usage = 5, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup2"), CgroupData{ .current_usage = 99, .kill_preference = maybe_prefer, .average_usage = 5, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "growth_big/cgroup3"), CgroupData{ .current_usage = 30, .average_usage = 30, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(789)); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(888))); } INSTANTIATE_TEST_CASE_P( NoPrefVsPrefer, KillMemoryGrowthConsistentWithPreference, Values(KillPreference::NORMAL, KillPreference::PREFER)); TEST_F(KillMemoryGrowthTest, KillsBigCgroupMultiCgroup) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*,sibling/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .current_usage = 100, .average_usage = 100, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(888)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(111))); } TEST_F(KillMemoryGrowthTest, CanTargetRecursively) { F::materialize(F::makeDir( tempdir_, {Fixture::makeDir( "A", { Fixture::makeDir("Z", {}), Fixture::makeDir("X", {}), }), Fixture::makeDir( "B", { Fixture::makeDir("F", {}), }), Fixture::makeDir( "sibling", { Fixture::makeDir("F", {}), })})); auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context(tempdir_); args["cgroup"] = "A,B"; args["recursive"] = "true"; args["post_action_delay"] = "0"; args["debug"] = "true"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A"), CgroupData{ .current_usage = 60, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A/Z"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "A/X"), CgroupData{ .current_usage = 40, .average_usage = 40, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "B"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling"), CgroupData{ .current_usage = 100, .average_usage = 100, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(*plugin->killed_cgroup, CgroupPath(tempdir_, "A/X").absolutePath()); } TEST_F(KillMemoryGrowthTest, DoesntKillBigCgroupInDry) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_memory_size_or_growth"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{ .current_usage = 60, .average_usage = 60, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{ .current_usage = 20, .average_usage = 20, }); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{ .current_usage = 20, .average_usage = 20, }); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(plugin->killed.size(), 0); } class KillSwapUsageTest : public CorePluginsTest {}; TEST_F(KillSwapUsageTest, KillsBigSwapCgroup) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_swap_usage"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{.swap_usage = 20}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{.swap_usage = 60}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{.swap_usage = 40}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(789)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(111))); } TEST_F(KillSwapUsageTest, ThresholdTest) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_swap_usage"); args["meminfo_location"] = "oomd/fixtures/plugins/kill_by_swap_usage/meminfo"; args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; args["threshold"] = "20%"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{.swap_usage = 1}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{.swap_usage = 2}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{.swap_usage = 3}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{.swap_usage = 20 << 10}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{.swap_usage = 60 << 10}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{.swap_usage = 40 << 10}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(789)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(111))); } TEST_F(KillSwapUsageTest, KillsBigSwapCgroupMultiCgroup) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_swap_usage"); args["cgroup"] = "one_big/*,sibling/*"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{.swap_usage = 20}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{.swap_usage = 60}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{.swap_usage = 40}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{.swap_usage = 70}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(555)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(111))); } TEST_F(KillSwapUsageTest, DoesntKillBigSwapCgroupDry) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_swap_usage"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{.swap_usage = 20}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{.swap_usage = 60}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{.swap_usage = 40}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(plugin->killed.size(), 0); } TEST_F(KillSwapUsageTest, DoesntKillNoSwap) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_swap_usage"); args["cgroup"] = "one_big/*"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup1"), CgroupData{}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup2"), CgroupData{}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_big/cgroup3"), CgroupData{}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::CONTINUE); EXPECT_EQ(plugin->killed.size(), 0); } class KillPressureTest : public CorePluginsTest {}; TEST_F(KillPressureTest, KillsHighestPressure) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_pressure"); args["cgroup"] = "one_high/*"; args["resource"] = "io"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 60, .sec_60 = 60, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 50, .sec_60 = 70, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 80, .sec_60 = 80, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 99, .sec_60 = 99, .sec_300 = 99, }}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(111)); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); EXPECT_THAT(plugin->killed, Not(Contains(888))); } TEST_F(KillPressureTest, KillsHighestPressureMultiCgroup) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_pressure"); args["cgroup"] = "one_high/*,sibling/*"; args["resource"] = "io"; args["post_action_delay"] = "0"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 60, .sec_60 = 60, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 50, .sec_60 = 70, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 80, .sec_60 = 80, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 99, .sec_60 = 99, .sec_300 = 99, }}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_THAT(plugin->killed, Contains(888)); EXPECT_THAT(plugin->killed, Not(Contains(111))); EXPECT_THAT(plugin->killed, Not(Contains(123))); EXPECT_THAT(plugin->killed, Not(Contains(456))); EXPECT_THAT(plugin->killed, Not(Contains(789))); } TEST_F(KillPressureTest, DoesntKillsHighestPressureDry) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context( "oomd/fixtures/plugins/kill_by_pressure"); args["cgroup"] = "one_high/*"; args["resource"] = "io"; args["post_action_delay"] = "0"; args["dry"] = "true"; ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup1"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 60, .sec_60 = 60, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup2"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 50, .sec_60 = 70, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "one_high/cgroup3"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 80, .sec_60 = 80, }}); TestHelper::setCgroupData( ctx_, CgroupPath(compile_context.cgroupFs(), "sibling/cgroup1"), CgroupData{ .io_pressure = ResourcePressure{ .sec_10 = 99, .sec_60 = 99, .sec_300 = 99, }}); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); EXPECT_EQ(plugin->killed.size(), 0); } class StopTest : public CorePluginsTest {}; TEST_F(StopTest, Stops) { auto plugin = createPlugin("stop"); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; const PluginConstructionContext compile_context("/sys/fs/cgroup"); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); EXPECT_EQ(plugin->run(ctx_), Engine::PluginRet::STOP); } oomd-0.5.0/src/oomd/plugins/DummyPrekillHook.cpp000066400000000000000000000032711406470533700216370ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/DummyPrekillHook.h" #include "oomd/Log.h" #include "oomd/OomdContext.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PREKILL_HOOK(dummy_prekill_hook, DummyPrekillHook::create); int DummyPrekillHook::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { // parse any args you want to be configurable here return PrekillHook::init(args, context); } std::unique_ptr DummyPrekillHook::fire( const CgroupContext& cgroup_ctx, const ActionContext& /* unused */) { OLOG << "Prekill hook fired on " << cgroup_ctx.cgroup().relativePath(); // this allocation is a waste, but it simplifies the prekill hook mechanism // and all real prekill hooks should need it. return std::unique_ptr( new DummyPrekillHookInvocation()); } bool DummyPrekillHookInvocation::didFinish() { // always finish immediately return true; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/DummyPrekillHook.h000066400000000000000000000030301406470533700212750ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/engine/PrekillHook.h" namespace Oomd { /* * A simple prekill hook useful for e2e testing and as a prekill hook * implementation template. */ class DummyPrekillHook : public Engine::PrekillHook { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; static DummyPrekillHook* create() { return new DummyPrekillHook(); } virtual ~DummyPrekillHook() override = default; virtual std::unique_ptr fire( const CgroupContext& cgroup_ctx, const ActionContext& action_ctx) override; }; class DummyPrekillHookInvocation : public Engine::PrekillHookInvocation { public: virtual bool didFinish() override; virtual ~DummyPrekillHookInvocation() override = default; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/DumpCgroupOverview.cpp000066400000000000000000000060101406470533700222060ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/DumpCgroupOverview.h" #include #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/include/CgroupPath.h" #include "oomd/util/Fs.h" #include "oomd/util/Util.h" namespace { auto constexpr kPgscanSwap = "pgscan_kswapd"; auto constexpr kPgscanDirect = "pgscan_direct"; void dumpCgroupOverview(const Oomd::CgroupContext& cgroup_ctx, bool always) { // Only log on exceptional cases auto pressure = cgroup_ctx.mem_pressure().value_or(Oomd::ResourcePressure{}); bool should_dump = (always || (pressure.sec_10 >= 1 && pressure.sec_60 > 0)); if (!should_dump) { return; } const auto& path = cgroup_ctx.cgroup(); const int64_t current = cgroup_ctx.current_usage().value_or(0); // TODO(dschatzberg): Report error auto meminfo = Oomd::Fs::getMeminfo(); int64_t swapfree = 0; int64_t swaptotal = 0; if (meminfo) { swapfree = (*meminfo)["SwapFree"]; swaptotal = (*meminfo)["SwapTotal"]; } int64_t pgscan = 0; // TODO(dschatzberg): Report error auto vmstat = Oomd::Fs::getVmstat(); if (vmstat) { pgscan = (*vmstat)[kPgscanSwap] + (*vmstat)[kPgscanDirect]; } std::ostringstream oss; oss << std::setprecision(2) << std::fixed; oss << "cgroup=" << path.relativePath() << " total=" << current / 1024 / 1024 << "MB pressure=" << pressure.sec_10 << ":" << pressure.sec_60 << ":" << pressure.sec_300 << " swapfree=" << swapfree / 1024 / 1024 << "MB/" << swaptotal / 1024 / 1024 << "MB pgscan=" << pgscan; OLOG << oss.str(); } } // namespace namespace Oomd { REGISTER_PLUGIN(dump_cgroup_overview, DumpCgroupOverview::create); int DumpCgroupOverview::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgument("always", always_); if (!argParser_.parse(args)) { return 1; } return 0; } Engine::PluginRet DumpCgroupOverview::run(OomdContext& ctx) { for (const CgroupContext& cgroup_ctx : ctx.addToCacheAndGet(cgroups_)) { dumpCgroupOverview(cgroup_ctx, always_); } return Engine::PluginRet::CONTINUE; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/DumpCgroupOverview.h000066400000000000000000000024131406470533700216560ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/engine/BasePlugin.h" #include #include namespace Oomd { class DumpCgroupOverview : public Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& /* unused */) override; static DumpCgroupOverview* create() { return new DumpCgroupOverview(); } ~DumpCgroupOverview() = default; private: std::unordered_set cgroups_; bool always_{false}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/DumpKillInfoNoOp.cpp000066400000000000000000000020771406470533700215340ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/BaseKillPlugin.h" namespace Oomd { // No-op implementation void BaseKillPlugin::dumpKillInfo( const CgroupPath& killed_group, const CgroupContext& context, const CgroupContext& kill_root, const ActionContext& action_context, const std::string& kill_uuid, bool success, bool dry) const {} } // namespace Oomd oomd-0.5.0/src/oomd/plugins/Exists.cpp000066400000000000000000000034461406470533700176630ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/Exists.h" #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace Oomd { REGISTER_PLUGIN(exists, Exists::create); int Exists::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgument("negate", negate_); argParser_.addArgument("debug", debug_); if (!argParser_.parse(args)) { return 1; } // Success return 0; } Engine::PluginRet Exists::run(OomdContext& ctx) { bool exists = false; for (const auto& cgroup : cgroups_) { if (cgroup.resolveWildcard().size()) { exists = true; goto out; } } out: if (negate_) { exists = !exists; } if (exists) { return Engine::PluginRet::CONTINUE; } else { return Engine::PluginRet::STOP; } } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/Exists.h000066400000000000000000000023451406470533700173250ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include "oomd/engine/BasePlugin.h" namespace Oomd { class Exists : public Oomd::Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& /* unused */) override; static Exists* create() { return new Exists(); } ~Exists() = default; private: std::unordered_set cgroups_; bool negate_{false}; bool debug_{false}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillIOCost-inl.h000066400000000000000000000036741406470533700206100ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/include/Types.h" #include "oomd/util/Util.h" namespace Oomd { template void KillIOCost::prerun(OomdContext& ctx) { // Make sure temporal counters be available when run() is invoked Base::prerunOnCgroups( ctx, [](const auto& cgroup_ctx) { cgroup_ctx.io_cost_rate(); }); } template std::vector KillIOCost::rankForKilling( OomdContext& ctx, const std::vector& cgroups) { return OomdContext::sortDescWithKillPrefs( cgroups, [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.io_cost_rate().value_or(0); }); } template void KillIOCost::ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& /* unused */) { OLOG << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on io cost generation at " << target.io_cost_rate().value_or(0); } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillIOCost.cpp000066400000000000000000000015721406470533700203560ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/KillIOCost.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(kill_by_io_cost, KillIOCost<>::create); } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillIOCost.h000066400000000000000000000027121406470533700200200ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include "oomd/plugins/BaseKillPlugin.h" namespace Oomd { template class KillIOCost : public Base { public: void prerun(OomdContext& ctx) override; static KillIOCost* create() { return new KillIOCost(); } ~KillIOCost() override = default; protected: std::vector rankForKilling( OomdContext& ctx, const std::vector& cgroups) override; void ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) override; }; } // namespace Oomd #include "oomd/plugins/KillIOCost-inl.h" oomd-0.5.0/src/oomd/plugins/KillMemoryGrowth-inl.h000066400000000000000000000157621406470533700221140ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/include/Types.h" #include "oomd/util/Util.h" namespace Oomd { template int KillMemoryGrowth::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { this->argParser_.addArgumentCustom( "size_threshold", size_threshold_, PluginArgParser::parseUnsignedInt); this->argParser_.addArgumentCustom( "growing_size_percentile", growing_size_percentile_, PluginArgParser::parseUnsignedInt); this->argParser_.addArgumentCustom( "min_growth_ratio", min_growth_ratio_, PluginArgParser::parseUnsignedInt); return Base::init(args, context); } template void KillMemoryGrowth::prerun(OomdContext& ctx) { // Make sure temporal counters be available when run() is invoked Base::prerunOnCgroups( ctx, [](const auto& cgroup_ctx) { cgroup_ctx.average_usage(); }); } template std::function>( const CgroupContext&)> KillMemoryGrowth::get_ranking_fn( OomdContext& ctx, const std::vector& cgroups) { // First, compute respective thresholds for inclusion in the first 2 // phases, size_threshold_in_bytes and // growth_kill_min_effective_usage_threshold. int64_t cur_memcurrent = 0; for (const CgroupContext& cgroup_ctx : cgroups) { cur_memcurrent += cgroup_ctx.current_usage().value_or(0); } int64_t size_threshold_in_bytes = cur_memcurrent * (static_cast(size_threshold_) / 100); // Only the top P(growing_size_percentile_) cgroups by usage are eligible // for killing by growth. nth is the index of the idx of the cgroup w/ // smallest usage in the top P(growing_size_percentile_) int64_t growth_kill_min_effective_usage_threshold = 0; if (cgroups.size() > 0) { const size_t nth = std::ceil( cgroups.size() * (100 - static_cast(growing_size_percentile_)) / 100) - 1; auto cgroups_mutable_copy = cgroups; std::nth_element( cgroups_mutable_copy.begin(), cgroups_mutable_copy.begin() + nth, cgroups_mutable_copy.end(), [](const auto& a, const auto& b) { // order by effective_usage desc return a.get().effective_usage().value_or(0) > b.get().effective_usage().value_or(0); }); growth_kill_min_effective_usage_threshold = cgroups_mutable_copy[nth].get().effective_usage().value_or(0); } return [=](const CgroupContext& cgroup_ctx) { int64_t current_usage = cgroup_ctx.current_usage().value_or(0); int64_t effective_usage = cgroup_ctx.effective_usage().value_or(0); float growth_ratio = cgroup_ctx.memory_growth().value_or(0); bool size_phase_eligible = current_usage >= size_threshold_in_bytes; auto growth_phase_eligible = growth_ratio >= min_growth_ratio_ && effective_usage >= growth_kill_min_effective_usage_threshold; // KillMemoryGrowth has 3 phases: cgroups above a usage threshold are // targeted first, then cgroups above a growth threshold, and finally // the rest. KMGPhase phase = size_phase_eligible ? KMGPhase::SIZE_THRESHOLD : growth_phase_eligible ? KMGPhase::GROWTH : KMGPhase::SIZE_NO_THRESHOLD; // All cgroups in SIZE_THRESHOLD phase rank first, then groups in // KMGPhase::GROWTH, then KMGPhase::SIZE_NO_THRESHOLD because tuples // sort lexicographically and cgroups ineligible for a phase get a 0 for // that phase. auto rank = std::make_tuple( size_phase_eligible ? effective_usage : 0, growth_phase_eligible ? growth_ratio : 0, effective_usage); return std::make_pair(phase, rank); }; } template std::vector KillMemoryGrowth::rankForKilling( OomdContext& ctx, const std::vector& cgroups) { auto rank_cgroup = get_ranking_fn(ctx, cgroups); // Note kill_preference take priority over phase, which is // handled automatically by sortDescWithKillPrefs. return OomdContext::sortDescWithKillPrefs( cgroups, [&](const CgroupContext& cgroup_ctx) { return rank_cgroup(cgroup_ctx).second; }); } template void KillMemoryGrowth::ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) { auto rank_cgroup = get_ranking_fn(ctx, peers); int64_t sib_memcurrent = 0; for (const CgroupContext& cgroup_ctx : peers) { sib_memcurrent += cgroup_ctx.current_usage().value_or(0); } switch (rank_cgroup(target).first) { case KMGPhase::SIZE_THRESHOLD: { OLOG << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on size > " << size_threshold_ << "% of total " << sib_memcurrent / 1024 / 1024 << "MB" << " with kill preference " << target.kill_preference().value_or(KillPreference::NORMAL); break; } case KMGPhase::GROWTH: { std::ostringstream oss; oss << std::setprecision(2) << std::fixed; oss << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on growth rate " << target.memory_growth().value_or(0) << " (min growth rate " << min_growth_ratio_ << ")" << " among P" << growing_size_percentile_ << " largest," << " with kill preference " << target.kill_preference().value_or(KillPreference::NORMAL); OLOG << oss.str(); break; } case KMGPhase::SIZE_NO_THRESHOLD: { OLOG << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on size > " << size_threshold_ << "% of total " << sib_memcurrent / 1024 / 1024 << "MB (size threshold overridden)" << " with kill preference " << target.kill_preference().value_or(KillPreference::NORMAL); break; } } } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillMemoryGrowth.cpp000066400000000000000000000015621406470533700216600ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/KillMemoryGrowth.h" namespace Oomd { REGISTER_PLUGIN(kill_by_memory_size_or_growth, KillMemoryGrowth<>::create); } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillMemoryGrowth.h000066400000000000000000000041601406470533700213220ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include "oomd/plugins/BaseKillPlugin.h" namespace Oomd { // KillMemoryGrowh kills in 3 phases, as described in docs/core_plugins.md // KMGPhase is only for internal use in KillMemoryGrowth ranking. enum struct KMGPhase { SIZE_THRESHOLD, GROWTH, SIZE_NO_THRESHOLD, }; template class KillMemoryGrowth : public Base { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; void prerun(OomdContext& ctx) override; static KillMemoryGrowth* create() { return new KillMemoryGrowth(); } ~KillMemoryGrowth() = default; protected: std::vector rankForKilling( OomdContext& ctx, const std::vector& cgroups) override; void ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) override; std::function>( const CgroupContext&)> get_ranking_fn( OomdContext& ctx, const std::vector& cgroups); int size_threshold_{50}; int growing_size_percentile_{80}; float min_growth_ratio_{1.25}; }; } // namespace Oomd #include "oomd/plugins/KillMemoryGrowth-inl.h" oomd-0.5.0/src/oomd/plugins/KillPgScan-inl.h000066400000000000000000000062531406470533700206170ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/engine/BasePlugin.h" #include "oomd/include/Types.h" #include "oomd/util/Fs.h" #include "oomd/util/Util.h" namespace Oomd { template Engine::PluginRet KillPgScan::run(OomdContext& ctx) { bool has_prev_tick_data = (last_tick_data_was_collected_ == ctx.getCurrentTick() - 1); // always collect pg scan data Base::prerunOnCgroups( ctx, [](const auto& cgroup_ctx) { cgroup_ctx.pg_scan_rate(); }); last_tick_data_was_collected_ = ctx.getCurrentTick(); if (!has_prev_tick_data) { // wait until we have 2 ticks of data to compare return Engine::PluginRet::ASYNC_PAUSED; } // run the kill like normal return Base::run(ctx); } template std::vector KillPgScan::rankForKilling( OomdContext& ctx, const std::vector& cgroups) { int64_t num_missing_pg_scan = 0, num_invalid = 0; for (const CgroupContext& cgroup : cgroups) { if (!cgroup.pg_scan_rate().has_value()) { num_missing_pg_scan += 1; // assume all invalid cgroups will also fail to fetch pg_scan_rate if (!Fs::isCgroupValid(cgroup.fd())) { num_invalid += 1; } } if (num_missing_pg_scan > 0) { if (num_invalid == 0) { OLOG << "couldn't read pgscan data in " << num_missing_pg_scan << "/" << cgroups.size() << " cgroups"; } else { OLOG << "couldn't read pgscan data in " << num_missing_pg_scan << "/" << cgroups.size() << " cgroups where " << (cgroups.size() - num_invalid) << "/" << cgroups.size() << " are still valid"; } } } return OomdContext::sortDescWithKillPrefs( cgroups, [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.pg_scan_rate().value_or(0); }); } template void KillPgScan::ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& /* unused */) { OLOG << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on pg scan rate at " << target.pg_scan_rate().value_or(0) << " with kill preference " << target.kill_preference().value_or(KillPreference::NORMAL); } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillPgScan.cpp000066400000000000000000000015721406470533700203710ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/KillPgScan.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(kill_by_pg_scan, KillPgScan<>::create); } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillPgScan.h000066400000000000000000000042621406470533700200350ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include "oomd/plugins/BaseKillPlugin.h" namespace Oomd { template class KillPgScan : public Base { public: Engine::PluginRet run(OomdContext& ctx) override; static KillPgScan* create() { return new KillPgScan(); } ~KillPgScan() override = default; protected: std::vector rankForKilling( OomdContext& ctx, const std::vector& cgroups) override; void ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) override; /* * KillPgScan has a 2-tick kill. It needs to collect pgscan data over time * so it can pick the cgroup whose pgscan grew the most over the last tick. * Because this data collection is mildly costly, we do it only on kill. * On first run(), we collect data and return ASYNC_PAUSED * On second run(), collect data a second time and do the actual kill. * We collect data every tick until the kill finishes, because a kill can take * multiple ticks (thanks to async prekill hooks) and we don't want * accidentally stale-ish data. * If we haven't collected data in 2 ticks, consider it dropped to be safe. */ std::optional last_tick_data_was_collected_{std::nullopt}; }; } // namespace Oomd #include "oomd/plugins/KillPgScan-inl.h" oomd-0.5.0/src/oomd/plugins/KillPressure-inl.h000066400000000000000000000055071406470533700212550ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/include/Types.h" #include "oomd/util/Util.h" namespace Oomd { template int KillPressure::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { this->argParser_.addArgument("resource", resource_, true); return Base::init(args, context); } template std::vector KillPressure::rankForKilling( OomdContext& ctx, const std::vector& cgroups) { return OomdContext::sortDescWithKillPrefs( cgroups, [&](const CgroupContext& cgroup_ctx) { int average = 0; switch (resource_) { case ResourceType::IO: if (const auto& pressure = cgroup_ctx.io_pressure()) { average = pressure->sec_10 / 2 + pressure->sec_60 / 2; } break; case ResourceType::MEMORY: if (const auto& pressure = cgroup_ctx.mem_pressure()) { average = pressure->sec_10 / 2 + pressure->sec_60 / 2; } break; } return average; }); } template void KillPressure::ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& /* unused */) { float pressure10 = 0; float pressure60 = 0; switch (resource_) { case ResourceType::IO: if (const auto& pressure = target.io_pressure()) { pressure10 = pressure->sec_10; pressure60 = pressure->sec_60; } break; case ResourceType::MEMORY: if (const auto& pressure = target.mem_pressure()) { pressure10 = pressure->sec_10; pressure60 = pressure->sec_60; } break; } OLOG << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on pressure generation at " << "10s=" << pressure10 << " 60s=" << pressure60; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillPressure.cpp000066400000000000000000000015771406470533700210330ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/KillPressure.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(kill_by_pressure, KillPressure<>::create); } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillPressure.h000066400000000000000000000030501406470533700204640ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include "oomd/plugins/BaseKillPlugin.h" namespace Oomd { template class KillPressure : public Base { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; static KillPressure* create() { return new KillPressure(); } ~KillPressure() = default; protected: std::vector rankForKilling( OomdContext& ctx, const std::vector& cgroups) override; void ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) override; ResourceType resource_; }; } // namespace Oomd #include "oomd/plugins/KillPressure-inl.h" oomd-0.5.0/src/oomd/plugins/KillSwapUsage-inl.h000066400000000000000000000054451406470533700213450ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/include/Types.h" #include "oomd/util/Fs.h" #include "oomd/util/Util.h" namespace Oomd { template int KillSwapUsage::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { auto meminfo = args.find("meminfo_location") != args.end() ? Fs::getMeminfo(args.at("meminfo_location")) : Fs::getMeminfo(); auto swapTotal = 0; // TODO(dschatzberg): Report Error if (meminfo && meminfo->count("SwapTotal")) { swapTotal = (*meminfo)["SwapTotal"]; } // erase meminfo_location since we already loaded it auto argsCopy = args; argsCopy.erase("meminfo_location"); this->argParser_.addArgumentCustom( "threshold", threshold_, [swapTotal](const std::string& str) { int64_t res = 0; if (Util::parseSizeOrPercent(str, &res, swapTotal) != 0) { throw std::invalid_argument("Failed to parse threshold: " + str); } return res; }); return Base::init(argsCopy, context); } template std::vector KillSwapUsage::rankForKilling( OomdContext& ctx, const std::vector& cgroups) { return OomdContext::sortDescWithKillPrefs( Util::filter( cgroups, [=](const CgroupContext& cgroup_ctx) { return cgroup_ctx.swap_usage().value_or(0) >= threshold_; }), [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.swap_usage().value_or(0); }); } template void KillSwapUsage::ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& /* unused */) { OLOG << "Picked \"" << target.cgroup().relativePath() << "\" (" << target.current_usage().value_or(0) / 1024 / 1024 << "MB) based on swap usage at " << target.swap_usage().value_or(0) / 1024 / 1024 << "MB"; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillSwapUsage.cpp000066400000000000000000000016031406470533700211100ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/KillSwapUsage.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(kill_by_swap_usage, KillSwapUsage<>::create); } // namespace Oomd oomd-0.5.0/src/oomd/plugins/KillSwapUsage.h000066400000000000000000000031301406470533700205520ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include "oomd/plugins/BaseKillPlugin.h" namespace Oomd { template class KillSwapUsage : public Base { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; static KillSwapUsage* create() { return new KillSwapUsage(); } ~KillSwapUsage() = default; protected: std::vector rankForKilling( OomdContext& ctx, const std::vector& cgroups) override; void ologKillTarget( OomdContext& ctx, const CgroupContext& target, const std::vector& peers) override; // Default threshold is to kill something with non-zero swap usage int64_t threshold_{1}; }; } // namespace Oomd #include "oomd/plugins/KillSwapUsage-inl.h" oomd-0.5.0/src/oomd/plugins/MemoryAbove.cpp000066400000000000000000000121351406470533700206240ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/MemoryAbove.h" #include #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/util/Fs.h" #include "oomd/util/PluginArgParser.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace Oomd { REGISTER_PLUGIN(memory_above, MemoryAbove::create); int MemoryAbove::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { // load `meminfo` outside of the PluginArgParser. // Because the threshold arg parsing depends on meminfo, to avoid making the // parser over complicated for this specific case. We handle it as a special // case and make a copy of args with `meminfo` removed. auto meminfoMaybe = args.find("meminfo_location") != args.end() ? Fs::getMeminfo(args.at("meminfo_location")) : Fs::getMeminfo(); if (!meminfoMaybe) { OLOG << "Could not read meminfo " << meminfoMaybe.error().what(); return 1; } else if (!meminfoMaybe->count("MemTotal")) { OLOG << "meminfo does not contain MemTotal"; return 1; } auto memTotal = (*meminfoMaybe)["MemTotal"]; // erase meminfo_location since we already loaded it auto argsCopy = args; argsCopy.erase("meminfo_location"); // by default we're looking for `threshold`. // if `threshold_anon` is passed in, use it instead auto thresholdArgName = "threshold"; if (argsCopy.find("threshold_anon") != argsCopy.end()) { thresholdArgName = "threshold_anon"; // remove threshold so that we don't need to handle it in the parser argsCopy.erase("threshold"); is_anon_ = true; } argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgumentCustom( thresholdArgName, threshold_, [memTotal](const std::string& str) { int64_t res = 0; if (Util::parseSizeOrPercent(str, &res, memTotal) != 0) { throw std::invalid_argument("Failed to parse threshold: " + str); } return res; }, true); argParser_.addArgument("duration", duration_, true); argParser_.addArgument("debug", debug_); if (!argParser_.parse(argsCopy)) { return 1; } // Success return 0; } Engine::PluginRet MemoryAbove::run(OomdContext& ctx) { using std::chrono::steady_clock; int64_t current_memory_usage = 0; std::string current_cgroup; for (const CgroupContext& cgroup_ctx : ctx.addToCacheAndGet(cgroups_)) { if (debug_) { OLOG << "cgroup \"" << cgroup_ctx.cgroup().relativePath() << "\" " << "memory.current=" << cgroup_ctx.current_usage().value_or(0) << "memory.stat (anon)=" << cgroup_ctx.anon_usage().value_or(0); } auto usage = is_anon_ ? cgroup_ctx.anon_usage().value_or(0) : cgroup_ctx.current_usage().value_or(0); if (current_memory_usage < usage) { current_memory_usage = usage; current_cgroup = cgroup_ctx.cgroup().relativePath(); } } const auto now = steady_clock::now(); if (current_memory_usage > threshold_) { // Logging this on every positive match is too verbose. Daniel is // fixing it properly but let's shut it up for the time being. if (debug_) { OLOG << "cgroup \"" << current_cgroup << "\" " << (is_anon_ ? "anon usage=" : "memory usage=") << current_memory_usage << " hit threshold=" << threshold_; } if (hit_thres_at_ == steady_clock::time_point()) { hit_thres_at_ = now; } const auto diff = std::chrono::duration_cast(now - hit_thres_at_) .count(); if (diff >= duration_) { // Logging this on every positive match is too verbose. Daniel is // fixing it properly but let's shut it up for the time being. if (debug_) { std::ostringstream oss; oss << std::setprecision(2) << std::fixed; oss << "cgroup \"" << current_cgroup << "\" " << "current memory usage " << current_memory_usage / 1024 / 1024 << "MB is over the threshold of " << threshold_ / 1024 / 1024 << "MB for " << duration_ << " seconds"; OLOG << oss.str(); } return Engine::PluginRet::CONTINUE; } } else { hit_thres_at_ = steady_clock::time_point(); } return Engine::PluginRet::STOP; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/MemoryAbove.h000066400000000000000000000026651406470533700203000ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include "oomd/engine/BasePlugin.h" namespace Oomd { class MemoryAbove : public Oomd::Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& /* unused */) override; static MemoryAbove* create() { return new MemoryAbove(); } ~MemoryAbove() = default; private: std::unordered_set cgroups_; // Initialized to bogus values; init() will crash oomd if non-0 return int64_t threshold_; int duration_; bool is_anon_{false}; bool debug_{false}; std::chrono::steady_clock::time_point hit_thres_at_{}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/MemoryReclaim.cpp000066400000000000000000000042721406470533700211470ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/MemoryReclaim.h" #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" static constexpr auto kPgscan = "pgscan"; namespace Oomd { REGISTER_PLUGIN(memory_reclaim, MemoryReclaim::create); int MemoryReclaim::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgument("duration", duration_, true); if (!argParser_.parse(args)) { return 1; } // Success return 0; } Engine::PluginRet MemoryReclaim::run(OomdContext& ctx) { using std::chrono::steady_clock; int64_t pgscan = 0; for (const CgroupContext& cgroup_ctx : ctx.addToCacheAndGet(cgroups_)) { if (const auto& memstat = cgroup_ctx.memory_stat()) { if (auto pos = memstat->find(kPgscan); pos != memstat->end()) { pgscan += pos->second; } } } OOMD_SCOPE_EXIT { last_pgscan_ = pgscan; }; const auto now = steady_clock::now(); if (pgscan > last_pgscan_) { last_reclaim_at_ = now; } const auto diff = std::chrono::duration_cast(now - last_reclaim_at_) .count(); if (diff <= duration_) { return Engine::PluginRet::CONTINUE; } else { return Engine::PluginRet::STOP; } } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/MemoryReclaim.h000066400000000000000000000025111406470533700206060ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include "oomd/engine/BasePlugin.h" namespace Oomd { class MemoryReclaim : public Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& /* unused */) override; static MemoryReclaim* create() { return new MemoryReclaim(); } ~MemoryReclaim() = default; private: std::unordered_set cgroups_; int duration_; int64_t last_pgscan_{0}; std::chrono::steady_clock::time_point last_reclaim_at_{}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/NrDyingDescendants.cpp000066400000000000000000000040341406470533700221240ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/NrDyingDescendants.h" #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/util/PluginArgParser.h" #include "oomd/util/Util.h" namespace Oomd { REGISTER_PLUGIN(nr_dying_descendants, NrDyingDescendants::create); int NrDyingDescendants::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgumentCustom( "count", count_, PluginArgParser::parseUnsignedInt, true); argParser_.addArgument("lte", lte_); argParser_.addArgument("debug", debug_); if (!argParser_.parse(args)) { return 1; } // Success return 0; } Engine::PluginRet NrDyingDescendants::run(OomdContext& ctx) { for (const CgroupContext& cgroup_ctx : ctx.addToCacheAndGet(cgroups_)) { if (auto nr = cgroup_ctx.nr_dying_descendants()) { if ((lte_ && *nr <= count_) || (!lte_ && *nr > count_)) { if (debug_) { OLOG << "nr_dying_descendants=" << *nr << (lte_ ? " <= " : " > ") << "count=" << count_; } return Engine::PluginRet::CONTINUE; } } } return Engine::PluginRet::STOP; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/NrDyingDescendants.h000066400000000000000000000024741406470533700215770ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include "oomd/engine/BasePlugin.h" namespace Oomd { class NrDyingDescendants : public Oomd::Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& ctx) override; static NrDyingDescendants* create() { return new NrDyingDescendants(); } ~NrDyingDescendants() override = default; private: std::unordered_set cgroups_; int64_t count_{0}; bool lte_{true}; // less than or equal bool debug_{false}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/PressureAbove.cpp000066400000000000000000000065421406470533700211710ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/PressureAbove.h" #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace Oomd { REGISTER_PLUGIN(pressure_above, PressureAbove::create); int PressureAbove::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgument("resource", resource_, true); argParser_.addArgument("threshold", threshold_, true); argParser_.addArgument("duration", duration_, true); if (!argParser_.parse(args)) { return 1; } // Success return 0; } Engine::PluginRet PressureAbove::run(OomdContext& ctx) { using std::chrono::steady_clock; ResourcePressure current_pressure; int64_t current_memory_usage = 0; for (const CgroupContext& cgroup_ctx : ctx.addToCacheAndGet(cgroups_)) { ResourcePressure rp; switch (resource_) { case ResourceType::IO: rp = cgroup_ctx.io_pressure().value_or(rp); break; case ResourceType::MEMORY: rp = cgroup_ctx.mem_pressure().value_or(rp); break; // No default to catch new additions in ResourceType } // Do a weighted comparison (we care more about 10s, then 60s, then 300s) if (rp.sec_10 * 3 + rp.sec_60 * 2 + rp.sec_300 > current_pressure.sec_10 * 3 + current_pressure.sec_60 * 2 + current_pressure.sec_300) { current_pressure = rp; current_memory_usage = cgroup_ctx.current_usage().value_or(0); } } OOMD_SCOPE_EXIT { last_pressure_ = current_pressure; }; const auto now = steady_clock::now(); // Check if the 10s pressure is above threshold_ for duration_ if (current_pressure.sec_10 > threshold_) { if (hit_thres_at_ == steady_clock::time_point()) { hit_thres_at_ = now; } const auto diff = std::chrono::duration_cast(now - hit_thres_at_) .count(); if (diff >= duration_) { std::ostringstream oss; oss << std::setprecision(2) << std::fixed; oss << "10s pressure " << current_pressure.sec_10 << " is over the threshold of " << threshold_ << " for " << duration_ << " seconds , total usage is " << current_memory_usage / 1024 / 1024 << "MB"; OLOG << oss.str(); return Engine::PluginRet::CONTINUE; } } else { hit_thres_at_ = steady_clock::time_point(); } return Engine::PluginRet::STOP; } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/PressureAbove.h000066400000000000000000000027301406470533700206310ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include "oomd/engine/BasePlugin.h" namespace Oomd { class PressureAbove : public Oomd::Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& /* unused */) override; static PressureAbove* create() { return new PressureAbove(); } ~PressureAbove() = default; private: std::unordered_set cgroups_; ResourceType resource_; // Initialized to bogus values; init() will crash oomd if non-0 return int threshold_; int duration_; ResourcePressure last_pressure_{100, 100, 100}; std::chrono::steady_clock::time_point hit_thres_at_{}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/PressureRisingBeyond.cpp000066400000000000000000000075331406470533700225320ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/PressureRisingBeyond.h" #include #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/include/Types.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace Oomd { REGISTER_PLUGIN(pressure_rising_beyond, PressureRisingBeyond::create); int PressureRisingBeyond::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgument("resource", resource_, true); argParser_.addArgument("threshold", threshold_, true); argParser_.addArgument("duration", duration_, true); argParser_.addArgument("fast_fall_ratio", fast_fall_ratio_); if (!argParser_.parse(args)) { return 1; } // Success return 0; } Engine::PluginRet PressureRisingBeyond::run(OomdContext& ctx) { using std::chrono::steady_clock; ResourcePressure current_pressure; int64_t current_memory_usage = 0; for (const CgroupContext& cgroup_ctx : ctx.addToCacheAndGet(cgroups_)) { ResourcePressure rp; switch (resource_) { case ResourceType::IO: rp = cgroup_ctx.io_pressure().value_or(rp); break; case ResourceType::MEMORY: rp = cgroup_ctx.mem_pressure().value_or(rp); break; // No default case to catch for future additions to ResourceType } // Do a weighted comparison (we care more about 10s, then 60s, then 300s) if (rp.sec_10 * 3 + rp.sec_60 * 2 + rp.sec_300 > current_pressure.sec_10 * 3 + current_pressure.sec_60 * 2 + current_pressure.sec_300) { current_pressure = rp; current_memory_usage = cgroup_ctx.current_usage().value_or(0); } } OOMD_SCOPE_EXIT { last_pressure_ = current_pressure; }; const auto now = steady_clock::now(); // Check if the 60s pressure is above threshold_ for duration_ bool pressure_duration_met_60s = false; if (current_pressure.sec_60 > threshold_) { if (hit_thres_at_ == steady_clock::time_point()) { hit_thres_at_ = now; } const auto diff = std::chrono::duration_cast(now - hit_thres_at_) .count(); if (diff >= duration_) { pressure_duration_met_60s = true; } } else { hit_thres_at_ = steady_clock::time_point(); } bool above_threshold_10s = current_pressure.sec_10 > threshold_; bool falling_rapidly_10s = current_pressure.sec_10 < last_pressure_.sec_10 * fast_fall_ratio_; if (pressure_duration_met_60s && above_threshold_10s && !falling_rapidly_10s) { std::ostringstream oss; oss << std::setprecision(2) << std::fixed; oss << "1m pressure " << current_pressure.sec_60 << " is over the threshold of " << threshold_ << " for " << duration_ << " seconds , total usage is " << current_memory_usage / 1024 / 1024 << "MB"; OLOG << oss.str(); return Engine::PluginRet::CONTINUE; } else { return Engine::PluginRet::STOP; } } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/PressureRisingBeyond.h000066400000000000000000000030241406470533700221660ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include "oomd/engine/BasePlugin.h" namespace Oomd { class PressureRisingBeyond : public Oomd::Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& /* unused */) override; static PressureRisingBeyond* create() { return new PressureRisingBeyond(); } ~PressureRisingBeyond() = default; private: std::unordered_set cgroups_; ResourceType resource_; // Initialized to bogus values; init() will crash oomd if non-0 return int threshold_; int duration_; float fast_fall_ratio_{0.85}; ResourcePressure last_pressure_{100, 100, 100}; std::chrono::steady_clock::time_point hit_thres_at_{}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/Senpai.cpp000066400000000000000000000602451406470533700176230ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/Senpai.h" #include #include #include #include #include #include #include "oomd/Log.h" #include "oomd/PluginRegistry.h" #include "oomd/util/Fs.h" #include "oomd/util/PluginArgParser.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace Oomd { REGISTER_PLUGIN(senpai, Senpai::create); int Senpai::init( const Engine::PluginArgs& args, const PluginConstructionContext& context) { argParser_.addArgumentCustom( "cgroup", cgroups_, [context](const std::string& cgroupStr) { return PluginArgParser::parseCgroup(context, cgroupStr); }, true); argParser_.addArgument("limit_min_bytes", limit_min_bytes_); argParser_.addArgument("limit_max_bytes", limit_max_bytes_); argParser_.addArgument("interval", interval_); argParser_.addArgument("pressure_ms", pressure_ms_); argParser_.addArgument("pressure_pct", mem_pressure_pct_); argParser_.addArgument("io_pressure_pct", io_pressure_pct_); argParser_.addArgument("max_probe", max_probe_); argParser_.addArgument("max_backoff", max_backoff_); argParser_.addArgument("coeff_probe", coeff_probe_); argParser_.addArgument("coeff_backoff", coeff_backoff_); argParser_.addArgument("immediate_backoff", immediate_backoff_); argParser_.addArgument("memory_high_timeout_ms", memory_high_timeout_); argParser_.addArgument("swap_threshold", swap_threshold_); argParser_.addArgument("swapout_bps_threshold", swapout_bps_threshold_); argParser_.addArgument("swap_validation", swap_validation_); argParser_.addArgument("modulate_swappiness", modulate_swappiness_); argParser_.addArgument("log_interval", log_interval_); if (!argParser_.parse(args)) { return 1; } auto meminfo = Fs::getMeminfo(); // TODO(dschatzberg): Report Error if (meminfo) { if (auto pos = meminfo->find("MemTotal"); pos != meminfo->end()) { host_mem_total_ = pos->second; } } else { OLOG << "Cannot read host MemTotal"; return 1; } return 0; } Engine::PluginRet Senpai::run(OomdContext& ctx) { auto resolved_cgroups = ctx.reverseSort( cgroups_, [](const CgroupContext& cgroup_ctx) { return cgroup_ctx.id(); }); // Use reverse iterator after reverseSort to make it normal order auto resolvedIt = resolved_cgroups.crbegin(); auto trackedIt = tracked_cgroups_.begin(); bool do_aggregate_log = false; if (++log_ticks_ >= log_interval_) { log_ticks_ = 0; do_aggregate_log = true; } // Iterate both tracked cgroups and resolved cgroups in increasing id order while (resolvedIt != resolved_cgroups.crend()) { const CgroupContext& cgroup_ctx = *resolvedIt; // Use id to identify CgroupContext across intervals, as path, dir_fd, and // memory address could all be recycled upon cgroup recreation. auto id_opt = cgroup_ctx.id(); if (!id_opt) { continue; } if (trackedIt == tracked_cgroups_.end() || *id_opt < trackedIt->first) { // Resolved cgroup not in tracked map, track it // New cgroups will be polled after a "tick" has elapsed if (auto new_cgroup_state_opt = initializeCgroup(cgroup_ctx)) { tracked_cgroups_.emplace_hint( trackedIt, *id_opt, *new_cgroup_state_opt); } ++resolvedIt; } else if (*cgroup_ctx.id() > trackedIt->first) { trackedIt = tracked_cgroups_.erase(trackedIt); } else { bool tick_result = immediate_backoff_ ? tick_immediate_backoff(cgroup_ctx, trackedIt->second) : tick(cgroup_ctx, trackedIt->second); if (do_aggregate_log && tick_result) { auto& state = trackedIt->second; std::ostringstream oss; oss << "cgroup " << cgroup_ctx.cgroup().relativePath() << " " << state.probe_count << " probe attempts (" << std::setprecision(3) << std::fixed << state.probe_bytes / (double)(1 << 30UL) << " gb)"; OLOG << oss.str(); // Reset stats state.probe_count = 0; state.probe_bytes = 0; } // Keep the tracked cgroups if they are still valid after tick trackedIt = tick_result ? std::next(trackedIt) : tracked_cgroups_.erase(trackedIt); ++resolvedIt; } } tracked_cgroups_.erase(trackedIt, tracked_cgroups_.end()); return Engine::PluginRet::CONTINUE; } Senpai::CgroupState::CgroupState( int64_t start_limit, std::chrono::microseconds total, int64_t start_ticks) : limit{start_limit}, last_total{total}, ticks{start_ticks} {} namespace { // Get the total pressure (some) from a cgroup, or nullopt if cgroup is invalid std::optional getPressureTotalSome( const CgroupContext& cgroup_ctx) { // Senpai reads pressure.some to get early notice that a workload // may be under resource pressure if (const auto pressure = Oomd::Fs::readMempressureAt( cgroup_ctx.fd(), Oomd::Fs::PressureType::SOME)) { if (const auto total = pressure.value().total) { return total.value(); } throw std::runtime_error("Senpai enabled but no total pressure info"); } return std::nullopt; } } // namespace // Check if the system support memory.reclaim cgroup control file. If the given // cgroup supports it, the system supports it. The result is then stored and // further calls won't access filesystem. If no stored result exists and the // cgroup does not has memory controller enabled or is no longer valid, nullopt // is returned. std::optional Senpai::hasMemoryReclaim(const CgroupContext& cgroup_ctx) { if (!has_memory_reclaim_.has_value()) { if (auto controllers_maybe = Fs::readControllersAt(cgroup_ctx.fd()); controllers_maybe) { for (const auto& ctrl : *controllers_maybe) { if (ctrl == "memory") { has_memory_reclaim_ = (bool)Fs::checkExistAt(cgroup_ctx.fd(), Fs::kMemReclaimFile); break; } } } } return has_memory_reclaim_; } // Check if the system support memory.high.tmp cgroup control file. If the given // cgroup supports it, the system supports it. The result is then stored and // further calls won't access filesystem. If the cgroup is no longer valid and // no stored result exists, nullopt is returned. std::optional Senpai::hasMemoryHighTmp(const CgroupContext& cgroup_ctx) { if (!has_memory_high_tmp_.has_value()) { if (auto memhightmp = cgroup_ctx.memory_high_tmp()) { has_memory_high_tmp_ = true; } else if (auto memhigh = cgroup_ctx.memory_high()) { // If memory.high exists but memory.high.tmp doesn't, it's not supported has_memory_high_tmp_ = false; } // If neither exist, cgroup is invalid. Nothing changed. } return has_memory_high_tmp_; } // Read from memory.high.tmp (preferred) or memory.high of a given cgroup. // Return nullopt if cgroup is no longer valid. std::optional Senpai::readMemhigh(const CgroupContext& cgroup_ctx) { if (auto has_memory_high_tmp = hasMemoryHighTmp(cgroup_ctx)) { return *has_memory_high_tmp ? cgroup_ctx.memory_high_tmp() : cgroup_ctx.memory_high(); } return std::nullopt; } // Write to memory.high.tmp (preferred) or memory.high of a given cgroup. // Return if the cgroup is still valid. bool Senpai::writeMemhigh(const CgroupContext& cgroup_ctx, int64_t value) { if (auto has_memory_high_tmp = hasMemoryHighTmp(cgroup_ctx)) { if (*has_memory_high_tmp) { if (!Oomd::Fs::writeMemhightmpAt( cgroup_ctx.fd(), value, std::chrono::seconds(20))) { return false; } } else if (!Oomd::Fs::writeMemhighAt(cgroup_ctx.fd(), value)) { return false; } return true; } return false; } /* * Invoke functor with some timeout. If functor does not return after timeout, * a signal is sent to the thread running functor to interrupt the running * syscall every second. Won't help if functor is uninterruptable or spinning. */ template SystemMaybe::type> timed_invoke( Functor&& fn, Duration timeout) { // ensure signal handler is setup before waiting on functor execution std::promise barrier; auto barrier_future = barrier.get_future(); std::promise::type> result; auto future = result.get_future(); std::thread t( [](auto&& barrier, auto&& result, Functor&& fn) { // Empty signal handler to interrupt syscall in fn std::signal(SIGUSR1, [](int) {}); barrier.set_value(); result.set_value(fn()); }, std::move(barrier), std::move(result), std::forward(fn)); barrier_future.wait(); if (future.wait_for(timeout) == std::future_status::timeout) { // Send signal to interrupt every second until we hear back from thread do { if (auto rc = ::pthread_kill(t.native_handle(), SIGUSR1); rc != 0) { // thread already gone if (rc == ESRCH) { break; } // Something very wrong... OLOG << systemError(rc, "pthread_kill failed").error().what(); std::terminate(); } } while (future.wait_for(std::chrono::seconds(1)) == std::future_status::timeout); t.join(); return systemError(ETIMEDOUT, "Timed out waiting execution"); } else { t.join(); return future.get(); } } // Call writeMemhigh in a different thread and send signal to interrupt write // after timeout. Workaround for a kernel "feature" that blocks such write // indefinitely if reclaim target is too low. bool Senpai::writeMemhighTimeout( const CgroupContext& cgroup_ctx, int64_t value, std::chrono::milliseconds timeout) { auto valid_maybe = timed_invoke([&]() { return writeMemhigh(cgroup_ctx, value); }, timeout); if (!valid_maybe) { // Most likely write timed out. Assume cgroup still valid and verify later. OLOG << "Failed to write memory limit for " << cgroup_ctx.cgroup().relativePath() << ": " << valid_maybe.error().what(); return true; } else { return valid_maybe.value(); } } // Reset memory.high.tmp (preferred) or memory.high of a given cgroup to max. // Return if the cgroup is still valid. bool Senpai::resetMemhigh(const CgroupContext& cgroup_ctx) { if (auto has_memory_high_tmp = hasMemoryHighTmp(cgroup_ctx)) { auto value = std::numeric_limits::max(); if (*has_memory_high_tmp) { if (!Oomd::Fs::writeMemhightmpAt( cgroup_ctx.fd(), value, std::chrono::seconds(0))) { return false; } } else if (!Oomd::Fs::writeMemhighAt(cgroup_ctx.fd(), value)) { return false; } return true; } return false; } // Reclaim some number of bytes from the given cgroup. bool Senpai::reclaim(const CgroupContext& cgroup_ctx, int64_t size) { auto has_memory_reclaim_opt = hasMemoryReclaim(cgroup_ctx); if (has_memory_reclaim_opt && *has_memory_reclaim_opt) { return (bool)Fs::writeMemReclaimAt(cgroup_ctx.fd(), size); } auto current_opt = cgroup_ctx.current_usage(); if (!current_opt) { return false; } int64_t limit = *current_opt - size; // Poking by setting memory limit and immediately resetting it, which // prevents sudden allocation later from triggering thrashing if (memory_high_timeout_.count() > 0) { if (!writeMemhighTimeout(cgroup_ctx, limit, memory_high_timeout_)) { return false; } } else { if (!writeMemhigh(cgroup_ctx, limit)) { return false; } } if (!resetMemhigh(cgroup_ctx)) { return false; } return true; } /** Returns file cache + swappable anon. */ SystemMaybe Senpai::getReclaimableBytes( const CgroupContext& cgroup_ctx) { const auto& stat_opt = cgroup_ctx.memory_stat(); if (!stat_opt) { return SYSTEM_ERROR(ENOENT); } auto active_file_pos = stat_opt->find("active_file"); auto inactive_file_pos = stat_opt->find("inactive_file"); if (active_file_pos == stat_opt->end() || inactive_file_pos == stat_opt->end()) { throw std::runtime_error("Invalid memory.stat cgroup file"); } auto file_cache = active_file_pos->second + inactive_file_pos->second; int64_t swappable = 0; const auto& system_ctx = cgroup_ctx.oomd_ctx().getSystemContext(); if (system_ctx.swaptotal > 0 && system_ctx.swappiness > 0) { auto effective_swap_free_opt = cgroup_ctx.effective_swap_free(); if (!effective_swap_free_opt) { return SYSTEM_ERROR(ENOENT); } else if (*effective_swap_free_opt > 0) { auto active_anon_pos = stat_opt->find("active_anon"); auto inactive_anon_pos = stat_opt->find("inactive_anon"); if (active_anon_pos == stat_opt->end() || inactive_anon_pos == stat_opt->end()) { return SYSTEM_ERROR(EINVAL); } auto anon_size = active_anon_pos->second + inactive_anon_pos->second; swappable = std::min(*effective_swap_free_opt, anon_size); } } return file_cache + swappable; } /** Returns unreclaimable + limit_min_bytes. */ std::optional Senpai::getLimitMinBytes( const CgroupContext& cgroup_ctx) { auto memcurr_opt = cgroup_ctx.current_usage(); if (!memcurr_opt) { return std::nullopt; } auto reclaimable_maybe = getReclaimableBytes(cgroup_ctx); if (!reclaimable_maybe) { return std::nullopt; } auto unreclaimable = *memcurr_opt - *reclaimable_maybe; auto limit_min_bytes = limit_min_bytes_ + unreclaimable; auto memmin_opt = cgroup_ctx.memory_min(); if (!memmin_opt) { return std::nullopt; } // Make sure memory.high don't go below memory.min limit_min_bytes = std::max(limit_min_bytes, *memmin_opt); return limit_min_bytes; } /** * Return the minimum of the following: * /proc/meminfo[MemTotal] * memory.current + limit_max_bytes (default: 10G) * memory.high (only if memory.high.tmp exist) * memory.max */ std::optional Senpai::getLimitMaxBytes( const CgroupContext& cgroup_ctx) { auto memcurr_opt = cgroup_ctx.current_usage(); if (!memcurr_opt) { return std::nullopt; } auto limit_max_bytes = std::min(host_mem_total_, limit_max_bytes_ + *memcurr_opt); // Don't let memory.high.tmp go above memory.high as kernel ignores the // latter when the former is set. auto has_memory_high_tmp_opt = hasMemoryHighTmp(cgroup_ctx); if (!has_memory_high_tmp_opt) { return std::nullopt; } if (*has_memory_high_tmp_opt) { auto memhigh_opt = cgroup_ctx.memory_high(); if (!memhigh_opt) { return std::nullopt; } limit_max_bytes = std::min(limit_max_bytes, *memhigh_opt); } auto memmax_opt = cgroup_ctx.memory_max(); if (!memmax_opt) { return std::nullopt; } limit_max_bytes = std::min(limit_max_bytes, *memmax_opt); return limit_max_bytes; } // Update state of a cgroup. Return if the cgroup is still valid. bool Senpai::tick(const CgroupContext& cgroup_ctx, CgroupState& state) { auto name = cgroup_ctx.cgroup().absolutePath(); auto limit_opt = readMemhigh(cgroup_ctx); if (!limit_opt) { return false; } auto factor = 0.0; if (*limit_opt != state.limit) { // Something else changed limits on this cgroup or it was // recreated in-between ticks - reset the state and return, // unfortuantely, the rest of this logic is still racy after this // point std::ostringstream oss; oss << "cgroup " << name << " memory.high " << *limit_opt << " does not match recorded state " << state.limit << ". Resetting cgroup"; OLOG << oss.str(); if (auto state_opt = initializeCgroup(cgroup_ctx)) { state = *state_opt; return true; } return false; } // Adjust cgroup limit by factor auto adjust = [&](double factor) { auto limit_min_bytes_opt = getLimitMinBytes(cgroup_ctx); if (!limit_min_bytes_opt) { return false; } auto limit_max_bytes_opt = getLimitMaxBytes(cgroup_ctx); if (!limit_max_bytes_opt) { return false; } state.limit += state.limit * factor; state.limit = std::max( *limit_min_bytes_opt, std::min(*limit_max_bytes_opt, state.limit)); // Memory high is always a multiple of 4K state.limit &= ~0xFFF; state.ticks = interval_; state.cumulative = std::chrono::microseconds{0}; return writeMemhigh(cgroup_ctx, state.limit); }; auto total_opt = getPressureTotalSome(cgroup_ctx); if (!total_opt) { return false; } auto total = *total_opt; auto delta = total - state.last_total; state.last_total = total; state.cumulative += delta; auto cumulative = state.cumulative.count(); if (state.cumulative >= pressure_ms_) { // Excessive pressure, back off. The rate scales exponentially // with pressure deviation. The coefficient defines how sensitive // we are to fluctuations around the target pressure: when the // coefficient is 10, the adjustment curve reaches the backoff // limit when observed pressure is ten times the target pressure. double error = state.cumulative / pressure_ms_; factor = error / coeff_backoff_; factor *= factor; factor = std::min(factor * max_backoff_, max_backoff_); if (!adjust(factor)) { return false; } std::ostringstream oss; oss << "cgroup " << name << std::setprecision(3) << std::fixed << " limitgb " << *limit_opt / (double)(1 << 30UL) << " totalus " << total.count() << " deltaus " << delta.count() << " cumus " << cumulative << " ticks " << state.ticks << std::defaultfloat << " adjust " << factor; OLOG << oss.str(); } else if (state.ticks) { --state.ticks; } else { // Pressure too low, tighten the limit. Like when backing off, the // adjustment becomes exponentially more aggressive as observed // pressure falls below the target pressure. The adjustment limit // is reached when stall time falls through pressure/coeff_probe_. auto one = std::chrono::microseconds{1}; double error = pressure_ms_ / std::max(state.cumulative, one); factor = error / coeff_probe_; factor *= factor; factor = std::min(factor * max_probe_, max_probe_); factor = -factor; if (!adjust(factor)) { return false; } if (*limit_opt > state.limit) { state.probe_count++; state.probe_bytes += *limit_opt - state.limit; } } return true; } // Update state of a cgroup. Return if the cgroup is still valid. bool Senpai::tick_immediate_backoff( const CgroupContext& cgroup_ctx, CgroupState& state) { // Wait for interval to prevent making senpai too aggressive // May wait longer if pressures are too high if (state.ticks) { state.ticks--; return true; } auto validate_pressure_maybe = validatePressure(cgroup_ctx); if (!validate_pressure_maybe) { return false; } auto validate = *validate_pressure_maybe; if (swap_validation_) { auto validate_swap_maybe = validateSwap(cgroup_ctx); if (!validate_swap_maybe) { return false; } validate = validate && *validate_swap_maybe; } if (validate) { auto limit_min_bytes_opt = getLimitMinBytes(cgroup_ctx); if (!limit_min_bytes_opt) { return false; } auto current_opt = cgroup_ctx.current_usage(); if (!current_opt) { return false; } if (*current_opt > *limit_min_bytes_opt) { int original_swappiness; if (modulate_swappiness_) { original_swappiness = cgroup_ctx.oomd_ctx().getSystemContext().swappiness; auto swappiness_factor_maybe = calculateSwappinessFactor(cgroup_ctx); if (!swappiness_factor_maybe) { return false; } Fs::setSwappiness(original_swappiness * (*swappiness_factor_maybe)); } OOMD_SCOPE_EXIT { if (modulate_swappiness_) { Fs::setSwappiness(original_swappiness); } }; // Reclaim slowly towards limit_min_bytes int64_t reclaim_size = (*current_opt - *limit_min_bytes_opt) * max_probe_; // Reclaim in number of 4k pages reclaim_size &= ~0xFFF; if (!reclaim(cgroup_ctx, reclaim_size)) { return false; } state.probe_count++; state.probe_bytes += reclaim_size; state.ticks = interval_; } } return true; } // Initialize a CgroupState. Return nullopt if cgroup no longer valid. std::optional Senpai::initializeCgroup( const CgroupContext& cgroup_ctx) { int64_t start_limit = 0; // Immediate backoff does not use limit as a state. if (!immediate_backoff_) { auto current_opt = cgroup_ctx.current_usage(); if (!current_opt) { return std::nullopt; } if (!writeMemhigh(cgroup_ctx, *current_opt)) { return std::nullopt; } start_limit = *current_opt; } auto total_opt = getPressureTotalSome(cgroup_ctx); if (!total_opt) { return std::nullopt; } return CgroupState(start_limit, *total_opt, interval_); } // Validate that pressure is low enough to drive Senpai SystemMaybe Senpai::validatePressure( const CgroupContext& cgroup_ctx) const { auto mem_pressure_opt = cgroup_ctx.mem_pressure_some(); if (!mem_pressure_opt) { return SYSTEM_ERROR(ENOENT); } auto io_pressure_opt = cgroup_ctx.io_pressure_some(); if (!io_pressure_opt) { return SYSTEM_ERROR(ENOENT); } // Only drive senpai if both short and long term pressure from memory and I/O // are lower than target return std::max(mem_pressure_opt->sec_10, mem_pressure_opt->sec_60) < mem_pressure_pct_ && std::max(io_pressure_opt->sec_10, io_pressure_opt->sec_60) < io_pressure_pct_; } // Validate that swap is sufficient to run Senpai SystemMaybe Senpai::validateSwap(const CgroupContext& cgroup_ctx) const { const auto& system_ctx = cgroup_ctx.oomd_ctx().getSystemContext(); // If there's no swap at all, then there's nothing to validate if (system_ctx.swaptotal == 0 || system_ctx.swappiness == 0) { return true; } // Similarly if effective swap.max is zero, nothing to validate auto effective_swap_max_opt = cgroup_ctx.effective_swap_max(); if (!effective_swap_max_opt) { return SYSTEM_ERROR(ENOENT); } if (*effective_swap_max_opt == 0) { return true; } // We validate that the effective swap usage is below the defined // threshold. This is useful to prevent OOM killing due to swap // depletion. auto effective_swap_util_pct_opt = cgroup_ctx.effective_swap_util_pct(); if (!effective_swap_util_pct_opt) { return SYSTEM_ERROR(ENOENT); } return *effective_swap_util_pct_opt >= swap_threshold_; } // Calculate swappiness factor (between 0 and 1) for a cgroup to modulate swap // behavior. SystemMaybe Senpai::calculateSwappinessFactor( const CgroupContext& cgroup_ctx) const { if (swap_threshold_ <= 0) { return 0; } auto swapout_bps_60 = cgroup_ctx.oomd_ctx().getSystemContext().swapout_bps_60; auto swapout_bps_300 = cgroup_ctx.oomd_ctx().getSystemContext().swapout_bps_300; auto swapout_bps = std::max(swapout_bps_60, swapout_bps_300); if (swapout_bps >= swapout_bps_threshold_) { return 0; } // If system has swapout bps close to or above threshold, factor will be close // to or equal to 0. If instead rate is close to 0, factor approaches 1. auto limit_by_rate = 1.0 - swapout_bps / swapout_bps_threshold_; auto effective_swap_util_pct_opt = cgroup_ctx.effective_swap_util_pct(); if (!effective_swap_util_pct_opt) { return SYSTEM_ERROR(ENOENT); } if (*effective_swap_util_pct_opt >= swap_threshold_) { return 0; } // If cgroup has swap usage close to or above threshold, factor will be close // to or equal to 0. If instead usage is close to 0, factor approaches 1. auto limit_by_size = 1.0 - *effective_swap_util_pct_opt / swap_threshold_; return std::min(limit_by_rate, limit_by_size); } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/Senpai.h000066400000000000000000000107571406470533700172730ustar00rootroot00000000000000/* * Copyright (C) 2019-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/engine/BasePlugin.h" #include "oomd/util/SystemMaybe.h" #include #include #include #include #include namespace Oomd { /* * A plugin which adjusts memory.high on a cgroup in order to create a * light amount of memory pressure. This allows memory.current to more * accurately represent the amount of memory required by the cgroup. */ class Senpai : public Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& context) override; Engine::PluginRet run(OomdContext& ctx) override; static Senpai* create() { return new Senpai(); } ~Senpai() = default; private: struct CgroupState { CgroupState( int64_t start_limit, std::chrono::microseconds total, int64_t start_ticks); // Current memory limit int64_t limit; // Last recorded total memory pressure std::chrono::microseconds last_total; // Cumulative memory pressure since last adjustment std::chrono::microseconds cumulative{0}; // Count-down to decision to probe/backoff int64_t ticks; // Probe statistics for logging uint64_t probe_bytes{0}; uint64_t probe_count{0}; }; std::optional hasMemoryReclaim(const CgroupContext& cgroup_ctx); std::optional hasMemoryHighTmp(const CgroupContext& cgroup_ctx); std::optional readMemhigh(const CgroupContext& cgroup_ctx); bool writeMemhigh(const CgroupContext& cgroup_ctx, int64_t value); bool writeMemhighTimeout( const CgroupContext& cgroup_ctx, int64_t value, std::chrono::milliseconds timeout); bool resetMemhigh(const CgroupContext& cgroup_ctx); bool reclaim(const CgroupContext& cgroup_ctx, int64_t size); SystemMaybe getReclaimableBytes(const CgroupContext& cgroup_ctx); std::optional getLimitMinBytes(const CgroupContext& cgroup_ctx); std::optional getLimitMaxBytes(const CgroupContext& cgroup_ctx); void checkAndLogHighPressure(const CgroupContext& cgroup_ctx) const; SystemMaybe validatePressure(const CgroupContext& cgroup_ctx) const; SystemMaybe validateSwap(const CgroupContext& cgroup_ctx) const; SystemMaybe calculateSwappinessFactor( const CgroupContext& cgroup_ctx) const; bool tick(const CgroupContext& cgroup_ctx, CgroupState& state); bool tick_immediate_backoff( const CgroupContext& cgroup_ctx, CgroupState& state); std::optional initializeCgroup(const CgroupContext& cgroup_ctx); int64_t host_mem_total_{0}; std::optional has_memory_reclaim_{}; std::optional has_memory_high_tmp_{}; std::unordered_set cgroups_; std::map tracked_cgroups_; // cgroup size limits int64_t limit_min_bytes_{100ull << 20}; int64_t limit_max_bytes_{10ull << 30}; // pressure target - stall time over sampling period int64_t interval_{6}; // interval between aggregation logging; only for immediate_backoff int64_t log_interval_{60}; int64_t log_ticks_{0}; std::chrono::milliseconds pressure_ms_{10}; // Currently only used for immediate backoff double mem_pressure_pct_{0.1}; double io_pressure_pct_{0.1}; // translate observed target deviation to cgroup adjustment rate // - max_probe is reached when stalling falls below pressure / coeff_probe // - max_backoff is reached when stalling exceeds pressure * coeff_backoff double max_probe_{0.01}; double max_backoff_{1.0}; double coeff_probe_{10}; double coeff_backoff_{20}; double swap_threshold_{0.8}; int64_t swapout_bps_threshold_{1ull << 20}; std::chrono::milliseconds memory_high_timeout_{}; bool swap_validation_{false}; bool immediate_backoff_{false}; bool modulate_swappiness_{false}; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/StopPlugin.cpp000066400000000000000000000015571406470533700205110ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/StopPlugin.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(stop, StopPlugin::create); } // namespace Oomd oomd-0.5.0/src/oomd/plugins/StopPlugin.h000066400000000000000000000022731406470533700201520ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/engine/BasePlugin.h" namespace Oomd { class StopPlugin : public Engine::BasePlugin { public: int init( const Engine::PluginArgs& /* unused */, const PluginConstructionContext& /* unused */) override { return 0; } Engine::PluginRet run(OomdContext& /* unused */) override { return Engine::PluginRet::STOP; } static StopPlugin* create() { return new StopPlugin(); } ~StopPlugin() = default; }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/SwapFree.cpp000066400000000000000000000033321406470533700201120ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/SwapFree.h" #include "oomd/Log.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(swap_free, SwapFree::create); int SwapFree::init( const Engine::PluginArgs& args, const PluginConstructionContext& /* unused */) { argParser_.addArgument("threshold_pct", threshold_pct_, true); if (!argParser_.parse(args)) { return 1; } // Success return 0; } Engine::PluginRet SwapFree::run(OomdContext& ctx) { auto& system_ctx = ctx.getSystemContext(); uint64_t swaptotal = system_ctx.swaptotal; uint64_t swapused = system_ctx.swapused; const uint64_t swapthres = swaptotal * threshold_pct_ / 100; if ((swaptotal - swapused) < swapthres) { OLOG << "SwapFree " << (swaptotal - swapused) / 1024 / 1024 << "MB is smaller than the threshold of " << swapthres / 1024 / 1024 << "MB, total swap is " << swaptotal / 1024 / 1024 << "MB"; return Engine::PluginRet::CONTINUE; } else { return Engine::PluginRet::STOP; } } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/SwapFree.h000066400000000000000000000023071406470533700175600ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include "oomd/engine/BasePlugin.h" namespace Oomd { class SwapFree : public Oomd::Engine::BasePlugin { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& /* unused */) override; Engine::PluginRet run(OomdContext& /* unused */) override; static SwapFree* create() { return new SwapFree(); } ~SwapFree() = default; private: int threshold_pct_; // will be assigned in init() }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/systemd/000077500000000000000000000000001406470533700173615ustar00rootroot00000000000000oomd-0.5.0/src/oomd/plugins/systemd/BaseSystemdPlugin.cpp000066400000000000000000000061241406470533700234720ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/systemd/BaseSystemdPlugin.h" #include #include "oomd/Log.h" #include "oomd/util/ScopeGuard.h" namespace Oomd { /* Communicate with systemd over DBUS interface. Supports only methods with "ss" signature. see https://www.freedesktop.org/wiki/Software/systemd/dbus/ for details. returns false in case of any errors. */ bool BaseSystemdPlugin::talkToSystemdManager( const std::string& method, const std::string& service, const std::string& mode) { ::sd_bus_error error = SD_BUS_ERROR_NULL; ::sd_bus_message* m = nullptr; ::sd_bus* bus = nullptr; const char* path; int r; OOMD_SCOPE_EXIT { ::sd_bus_error_free(&error); ::sd_bus_message_unref(m); ::sd_bus_close(bus); ::sd_bus_unref(bus); }; /* Connect to the system bus */ r = ::sd_bus_open_system(&bus); if (r < 0) { OLOG << "Failed to connect to system bus: " << strerror(-r); return false; } if (bus == nullptr) { OLOG << "Failed to connect to system bus: bus is null"; return false; }; /* Issue the method call and store the respons message in m */ r = ::sd_bus_call_method( bus, "org.freedesktop.systemd1", /* service to contact */ "/org/freedesktop/systemd1", /* object path */ "org.freedesktop.systemd1.Manager", /* interface name */ method.c_str(), /* method name */ &error, /* object to return error in */ &m, /* return message on success */ "ss", /* input signature */ service.c_str(), /* first argument */ mode.c_str()); /* second argument */ if (r < 0) { OLOG << "Failed to issue method call: " << error.message; return false; } if (m == nullptr) { OLOG << "Failed to issue method call: return message is null"; return false; } /* Parse the response message */ r = ::sd_bus_message_read(m, "o", &path); if (r < 0) { OLOG << "Failed to parse response message: " << strerror(-r); return false; } OLOG << "Queued service job as " << path; return true; } bool BaseSystemdPlugin::restartService(const std::string& service) { OLOG << "Restarting systemd service \"" << service << "\""; return talkToSystemdManager("RestartUnit", service); } bool BaseSystemdPlugin::stopService(const std::string& service) { OLOG << "Stopping systemd service \"" << service << "\""; return talkToSystemdManager("StopUnit", service); } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/systemd/BaseSystemdPlugin.h000066400000000000000000000025121406470533700231340ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/engine/BasePlugin.h" namespace Oomd { /* * This abstract base class provides an overridable set of methods that * allow us to request some actions from systemd over DBUS. * Main reason this exists is mocking things for unittests. */ class BaseSystemdPlugin : public Oomd::Engine::BasePlugin { protected: virtual bool talkToSystemdManager( const std::string& method, const std::string& service, const std::string& mode = "replace"); virtual bool restartService(const std::string& service); virtual bool stopService(const std::string& service); }; } // namespace Oomd oomd-0.5.0/src/oomd/plugins/systemd/SystemdPluginsTest.cpp000066400000000000000000000047361406470533700237310ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "oomd/OomdContext.h" #include "oomd/plugins/systemd/BaseSystemdPlugin.h" #include "oomd/plugins/systemd/SystemdRestart.h" using namespace Oomd; namespace Oomd { class BaseSystemdPluginMock : public BaseSystemdPlugin { public: bool restartService(const std::string& service) override { restarted = service; return true; } bool stopService(const std::string& service) override { stopped = service; return true; } bool talkToSystemdManager( const std::string& /* unused */, const std::string& /* unused */, const std::string& /* unused */) override { return true; } std::string restarted; std::string stopped; }; } // namespace Oomd TEST(SystemdRestart, RestartService) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["service"] = "some.service"; args["post_action_delay"] = "0"; args["dry"] = "false"; const PluginConstructionContext compile_context("/sys/fs/cgroup"); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); OomdContext ctx; EXPECT_EQ(plugin->run(ctx), Engine::PluginRet::STOP); EXPECT_EQ(plugin->restarted, "some.service"); } TEST(SystemdRestart, RestartServiceDry) { auto plugin = std::make_shared>(); ASSERT_NE(plugin, nullptr); Engine::PluginArgs args; args["service"] = "some.service"; args["post_action_delay"] = "0"; args["dry"] = "true"; const PluginConstructionContext compile_context("/sys/fs/cgroup"); ASSERT_EQ(plugin->init(std::move(args), compile_context), 0); OomdContext ctx; EXPECT_EQ(plugin->run(ctx), Engine::PluginRet::STOP); EXPECT_EQ(plugin->restarted.size(), 0); } oomd-0.5.0/src/oomd/plugins/systemd/SystemdRestart-inl.h000066400000000000000000000042541406470533700233140ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/systemd/SystemdRestart.h" #include #include #include "oomd/Log.h" #include "oomd/Stats.h" namespace Oomd { template int SystemdRestart::init( const Engine::PluginArgs& args, const PluginConstructionContext& /* unused */) { this->argParser_.addArgumentCustom( "service", service_, [](const std::string& str) { if (str.empty()) { throw std::invalid_argument("service is empty"); } return str; }, true); this->argParser_.addArgumentCustom( "post_action_delay", post_action_delay_, PluginArgParser::parseUnsignedInt); this->argParser_.addArgument("dry", dry_); if (!this->argParser_.parse(args)) { return 1; } Oomd::setStat(kRestartsKey, 0); // Success return 0; } template Engine::PluginRet SystemdRestart::run(OomdContext& /* unused */) { bool ret; if (dry_) { OLOG << "DRY-RUN: restarting service " << service_; ret = true; } else { ret = Base::restartService(service_); } if (ret) { std::ostringstream oss; oss << "restarted systemd service=" << service_ << (dry_ ? " (dry)" : ""); OOMD_KMSG_LOG(oss.str(), "oomd kill"); Oomd::incrementStat(kRestartsKey, 1); std::this_thread::sleep_for(std::chrono::seconds(post_action_delay_)); return Engine::PluginRet::STOP; } else { return Engine::PluginRet::CONTINUE; } } } // namespace Oomd oomd-0.5.0/src/oomd/plugins/systemd/SystemdRestart.cpp000066400000000000000000000015701406470533700230650ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/plugins/systemd/SystemdRestart.h" #include "oomd/PluginRegistry.h" namespace Oomd { REGISTER_PLUGIN(systemd_restart, SystemdRestart<>::create); } oomd-0.5.0/src/oomd/plugins/systemd/SystemdRestart.h000066400000000000000000000025711406470533700225340ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/plugins/systemd/BaseSystemdPlugin.h" namespace Oomd { template class SystemdRestart : public Base { public: int init( const Engine::PluginArgs& args, const PluginConstructionContext& /* unused */) override; Engine::PluginRet run(OomdContext& /* unused */) override; static SystemdRestart* create() { return new SystemdRestart(); } ~SystemdRestart() = default; private: std::string service_; int post_action_delay_{15}; bool dry_{false}; static constexpr auto kRestartsKey = "oomd.restarts"; }; } // namespace Oomd #include "oomd/plugins/systemd/SystemdRestart-inl.h" oomd-0.5.0/src/oomd/util/000077500000000000000000000000001406470533700151655ustar00rootroot00000000000000oomd-0.5.0/src/oomd/util/Fixture.cpp000066400000000000000000000106711406470533700173240ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include #include #include "oomd/util/Fixture.h" #include "oomd/util/Util.h" constexpr auto kFixturesDirTemplate = "__oomd_fixtures_XXXXXX"; namespace Oomd { // static Fixture::DirEntryPair Fixture::makeFile( const std::string& name, const std::string& content) { return { name, DirEntry([content](const std::string& path, const std::string& name) { writeChecked(path + "/" + name, content); })}; } // static Fixture::DirEntryPair Fixture::makeDir( const std::string& name, std::unordered_map entries) { return { name, DirEntry([entries](const std::string& path, const std::string& name) { mkdirsChecked(name, path); const auto newPath = path + "/" + name; for (const auto& kv : entries) { kv.second.materialize(newPath, kv.first); } })}; } const char* getTempDir() { if (std::getenv("TMPDIR") != nullptr) { return std::getenv("TMPDIR"); } else if (std::getenv("TMP") != nullptr) { return std::getenv("TMP"); } else if (std::getenv("TEMP") != nullptr) { return std::getenv("TEMP"); } else if (std::getenv("TEMPDIR") != nullptr) { return std::getenv("TEMPDIR"); } else { return "/tmp"; } } // static std::string Fixture::mkdtempChecked() { std::array temp; ::snprintf( temp.data(), temp.size(), "%s/%s", getTempDir(), kFixturesDirTemplate); if (::mkdtemp(temp.data()) == nullptr) { std::array buf; buf[0] = '\0'; throw std::runtime_error( std::string(temp.data()) + ": mkdtemp failed: " + ::strerror_r(errno, buf.data(), buf.size())); } return temp.data(); } // static void Fixture::mkdirsChecked( const std::string& path, const std::string& prefix) { auto dirs = Util::split(path, '/'); std::string prefix_path; // two slashes after each component prefix_path.reserve(path.size() + prefix.size() + 2); if (path.size() && path[0] == '/') { prefix_path += "/"; } else if (prefix != "") { prefix_path += prefix + "/"; } for (const auto& dir : dirs) { if (dir == "") { continue; } prefix_path += dir + "/"; if (::mkdir(prefix_path.c_str(), 0777) == -1 && errno != EEXIST) { std::array buf; buf[0] = '\0'; throw std::runtime_error( prefix_path + ": mkdir failed: " + ::strerror_r(errno, buf.data(), buf.size())); } } } // static void Fixture::writeChecked( const std::string& path, const std::string& content) { std::array buf; buf[0] = '\0'; auto fd = ::open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); if (fd < 0) { throw std::runtime_error( path + ": open failed: " + ::strerror_r(errno, buf.data(), buf.size())); } auto ret = Util::writeFull(fd, content.c_str(), content.size()); ::close(fd); if (ret < 0) { throw std::runtime_error( path + ": write failed: " + ::strerror_r(errno, buf.data(), buf.size())); } if ((size_t)ret != content.size()) { throw std::runtime_error( path + ": write failed: not all bytes are written"); } } int rm(const char* path, const struct stat*, int, struct FTW*) { return ::remove(path); } // static void Fixture::rmrChecked(const std::string& path) { std::array buf; buf[0] = '\0'; if (::nftw(path.c_str(), rm, 10, FTW_DEPTH | FTW_PHYS) == -1) { switch (errno) { case ENOENT: return; default: throw std::runtime_error( path + ": remove failed: " + ::strerror_r(errno, buf.data(), buf.size())); } } } } // namespace Oomd oomd-0.5.0/src/oomd/util/Fixture.h000066400000000000000000000046411406470533700167710ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include #include #include namespace Oomd { // Fixture is a utility class for unit tests to encode their fixture files and // materialize them in run time in some temporary directory. class Fixture { public: typedef std::function materializeFunc; // Dirent to be materialized into a file or directory class DirEntry { public: explicit DirEntry(materializeFunc materialize) : materialize_(materialize) {} void materialize(const std::string& path, const std::string& name) const { materialize_(path, name); } private: const materializeFunc materialize_; }; // Helper functions to create different named DirEntries so that we can // represent a directory tree with initializer_list in a single statement. typedef std::pair DirEntryPair; static DirEntryPair makeFile( const std::string& name, const std::string& content = ""); static DirEntryPair makeDir( const std::string& name, std::unordered_map entries = {}); static void materialize( const DirEntryPair& pair, const std::string& path = "") { pair.second.materialize(path, pair.first); } // utility functions that interact with file system static std::string mkdtempChecked(); static void mkdirsChecked( const std::string& path, const std::string& prefix = ""); static void writeChecked(const std::string& path, const std::string& content); static void rmrChecked(const std::string& path); }; } // namespace Oomd oomd-0.5.0/src/oomd/util/FixtureTest.cpp000066400000000000000000000125611406470533700201640ustar00rootroot00000000000000// Copyright 2004-present Facebook. All Rights Reserved. #include #include #include #include #include #include #include #include #include #include using namespace Oomd; class FixtureTest : public ::testing::Test { protected: // remove all temp directories void TearDown() override { DIR* dir; std::string failureMsg = ""; for (const auto& tempDir : tempDirs_) { dir = ::opendir(tempDir.c_str()); if (dir != nullptr) { ::closedir(dir); // call `rm -r '$tempDir'` to remove recursively const auto& cmd = "rm -r '" + tempDir + "'"; auto stream = ::popen(cmd.c_str(), "r"); if (stream != nullptr) { ::pclose(stream); } dir = ::opendir(tempDir.c_str()); if (dir != nullptr || errno != ENOENT) { failureMsg += " " + tempDir; } } } if (failureMsg != "") { FAIL() << "remove failed:" + failureMsg; } } std::unordered_set tempDirs_; }; TEST_F(FixtureTest, TestMkdtempChecked) { DIR* dir; std::string tempDir; for (int i = 0; i < 100; i++) { EXPECT_NO_THROW(tempDir = Fixture::mkdtempChecked()); // directory exists EXPECT_TRUE((dir = ::opendir(tempDir.c_str())) && ::closedir(dir) == 0); // directory not already seen EXPECT_TRUE(tempDirs_.find(tempDir) == tempDirs_.end()); tempDirs_.insert(tempDir); } } TEST_F(FixtureTest, TestMkdirsChecked) { DIR* dir; std::string tempDir; std::string path; EXPECT_NO_THROW(tempDir = Fixture::mkdtempChecked()); tempDirs_.insert(tempDir); // nothing should happen EXPECT_NO_THROW(Fixture::mkdirsChecked(tempDir)); // should ignore repeated slashes EXPECT_NO_THROW(Fixture::mkdirsChecked("A//B///C/", tempDir)); path = tempDir + "/A/B/C"; EXPECT_TRUE((dir = ::opendir(path.c_str())) && ::closedir(dir) == 0); // should create D without touching A EXPECT_NO_THROW(Fixture::mkdirsChecked("A/D", tempDir)); path = tempDir + "/A/B/C"; EXPECT_TRUE((dir = ::opendir(path.c_str())) && ::closedir(dir) == 0); path = tempDir + "/A/D"; EXPECT_TRUE((dir = ::opendir(path.c_str())) && ::closedir(dir) == 0); } TEST_F(FixtureTest, TestWriteChecked) { std::vector buf(1024); std::string tempDir; EXPECT_NO_THROW(tempDir = Fixture::mkdtempChecked()); tempDirs_.insert(tempDir); const auto filepath = tempDir + "/writetest.txt"; const std::string content = "Hello, Facebook!\nHello, world!\n"; const std::string newContent = "Bye, Facebook!\nHello again, world!\n"; // create and write EXPECT_NO_THROW(Fixture::writeChecked(filepath, content)); int fd = ::open(filepath.c_str(), O_RDONLY); EXPECT_GE(fd, 0); int bytes_read = Util::readFull(fd, buf.data(), content.length()); ::close(fd); EXPECT_EQ(bytes_read, content.length()); EXPECT_EQ(std::string(buf.data(), content.length()), content); // overwrite EXPECT_NO_THROW(Fixture::writeChecked(filepath, newContent)); fd = ::open(filepath.c_str(), O_RDONLY); EXPECT_GE(fd, 0); bytes_read = Util::readFull(fd, buf.data(), newContent.length()); ::close(fd); EXPECT_EQ(bytes_read, newContent.length()); EXPECT_EQ(std::string(buf.data(), newContent.length()), newContent); } TEST_F(FixtureTest, TestRmrChecked) { DIR* dir; std::string tempDir; EXPECT_NO_THROW({ // create directory tree tempDir = Fixture::mkdtempChecked(); tempDirs_.insert(tempDir); Fixture::mkdirsChecked("A/B/C", tempDir); Fixture::mkdirsChecked("D/E", tempDir); Fixture::writeChecked(tempDir + "/file1", "file1 content"); Fixture::writeChecked(tempDir + "/A/file2", "file2 content"); Fixture::writeChecked(tempDir + "/D/E/file3", "file3 content"); Fixture::rmrChecked(tempDir); }); tempDirs_.erase(tempDir); // check file does not exist dir = ::opendir(tempDir.c_str()); if (dir != nullptr) { ::closedir(dir); FAIL() << "Failed to remove tempDir: " + tempDir; } else { EXPECT_TRUE(errno == ENOENT); } } bool verifyFile(const std::string& filepath, const std::string& content) { int fd = ::open(filepath.c_str(), O_RDONLY); if (fd < 0) { return false; } std::vector buf(content.size()); ssize_t count = Util::readFull(fd, buf.data(), content.size()); ::close(fd); return std::string(buf.data(), count) == content; } TEST_F(FixtureTest, TestMaterialize) { DIR* dir; std::string tempDir; EXPECT_NO_THROW(tempDir = Fixture::mkdtempChecked()); tempDirs_.insert(tempDir); const auto fixture = Fixture::makeDir( "root", {Fixture::makeDir( "A", {Fixture::makeDir("B", {Fixture::makeDir("C")}), Fixture::makeFile("file2", "file2 content")}), Fixture::makeDir( "D", {Fixture::makeDir( "E", {Fixture::makeFile("file3", "file3 content")})}), Fixture::makeFile("file1", "file1 content")}); EXPECT_NO_THROW(fixture.second.materialize(tempDir, fixture.first)); auto root = tempDir + "/" + "root"; auto path = root + "/A/B/C"; EXPECT_TRUE((dir = ::opendir(path.c_str())) && ::closedir(dir) == 0); path = root + "/A/file2"; EXPECT_TRUE(verifyFile(path, "file2 content")); path = root + "/D/E/file3"; EXPECT_TRUE(verifyFile(path, "file3 content")); path = root + "/file1"; EXPECT_TRUE(verifyFile(path, "file1 content")); } oomd-0.5.0/src/oomd/util/Fs.cpp000066400000000000000000000603151406470533700162460ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include "oomd/util/Fs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "oomd/include/Assert.h" #include "oomd/util/ScopeGuard.h" #include "oomd/util/Util.h" namespace { enum class PsiFormat { MISSING = 0, // File is missing INVALID, // Don't recognize EXPERIMENTAL, // Experimental format UPSTREAM, // Upstream kernel format }; PsiFormat getPsiFormat(const std::vector& lines) { if (lines.size() == 0) { return PsiFormat::MISSING; } const auto& first = lines[0]; if (Oomd::Util::startsWith("some", first) && lines.size() >= 2) { return PsiFormat::UPSTREAM; } else if (Oomd::Util::startsWith("aggr", first) && lines.size() >= 3) { return PsiFormat::EXPERIMENTAL; } else { return PsiFormat::INVALID; } } }; // namespace namespace Oomd { SystemMaybe Fs::Fd::openat(const DirFd& dirfd, const std::string& path, bool read_only) { int flags = read_only ? O_RDONLY : O_WRONLY; const auto fd = ::openat(dirfd.fd(), path.c_str(), flags); if (fd == -1) { return SYSTEM_ERROR(errno); } return Fd(fd); } SystemMaybe Fs::Fd::open(const std::string& path, bool read_only) { int flags = read_only ? O_RDONLY : O_WRONLY; const auto fd = ::open(path.c_str(), flags); if (fd == -1) { return SYSTEM_ERROR(errno); } return Fd(fd); } SystemMaybe Fs::Fd::inode() const { struct ::stat buf; if (::fstat(fd_, &buf) == 0) { return static_cast(buf.st_ino); } return SYSTEM_ERROR(errno); } void Fs::Fd::close() { if (fd_ != -1) { ::close(fd_); fd_ = -1; } } SystemMaybe Fs::DirFd::open(const std::string& path) { int fd = ::open(path.c_str(), O_RDONLY | O_DIRECTORY); if (fd == -1) { return SYSTEM_ERROR(errno); } return DirFd(fd); } SystemMaybe Fs::DirFd::openChildDir(const std::string& path) const { int child_fd = ::openat(fd(), path.c_str(), O_RDONLY | O_DIRECTORY); if (child_fd == -1) { return SYSTEM_ERROR(errno); } return DirFd(child_fd); } bool Fs::isCgroupValid(const DirFd& dirfd) { // If cgroup.controllers file exists, the cgroup should still be valid return ::faccessat(dirfd.fd(), kControllersFile, F_OK, 0) == 0; } SystemMaybe Fs::readDirFromDIR(DIR* d, int flags) { Fs::DirEnts de; while (struct dirent* dir = ::readdir(d)) { if (dir->d_name[0] == '.') { continue; } /* * Optimisation: Avoid doing lstat calls if kernfs gives us back d_type. * This actually can be pretty useful, since avoiding lstat()ing everything * can reduce oomd CPU usage by ~10% on a reasonably sized cgroup * hierarchy. */ if ((flags & DirEntFlags::DE_FILE) && dir->d_type == DT_REG) { de.files.push_back(dir->d_name); continue; } if ((flags & DirEntFlags::DE_DIR) && dir->d_type == DT_DIR) { de.dirs.push_back(dir->d_name); continue; } struct stat buf; int ret = ::fstatat(dirfd(d), dir->d_name, &buf, AT_SYMLINK_NOFOLLOW); if (ret == -1) { return SYSTEM_ERROR(errno); } if ((flags & DirEntFlags::DE_FILE) && (buf.st_mode & S_IFREG)) { de.files.push_back(dir->d_name); } if ((flags & DirEntFlags::DE_DIR) && (buf.st_mode & S_IFDIR)) { de.files.push_back(dir->d_name); } } return de; } SystemMaybe Fs::readDirAt(const DirFd& dirfd, int flags) { // once an fd is passed to fdopendir, it is unusable except through that DIR int fresh_fd = ::dup(dirfd.fd()); DIR* d = ::fdopendir(fresh_fd); if (!d) { auto e = errno; ::close(fresh_fd); return SYSTEM_ERROR(e); } OOMD_SCOPE_EXIT { // even though we dup()ed dirfd.fd(), the copied fd shares state with // dirfd.fd(). For dirfd to be reusable again, we need to rewind it, which // we can do through d. ::rewinddir(d); ::closedir(d); }; return Fs::readDirFromDIR(d, flags); } SystemMaybe Fs::readDir(const std::string& path, int flags) { DIR* d = ::opendir(path.c_str()); if (!d) { return SYSTEM_ERROR(errno); } OOMD_SCOPE_EXIT { ::closedir(d); }; return readDirFromDIR(d, flags); } bool Fs::isDir(const std::string& path) { struct stat sb; if (!::stat(path.c_str(), &sb) && S_ISDIR(sb.st_mode)) { return true; } return false; } SystemMaybe> Fs::glob( const std::string& pattern, bool dir_only) { glob_t globbuf; std::vector ret; int flags = GLOB_NOSORT | GLOB_BRACE | GLOB_ERR; if (dir_only) { flags |= GLOB_ONLYDIR; } auto ec = ::glob(pattern.c_str(), flags, nullptr, &globbuf); OOMD_SCOPE_EXIT { ::globfree(&globbuf); }; if (!ec) { for (size_t i = 0; i < globbuf.gl_pathc; i++) { std::string path(globbuf.gl_pathv[i]); // GLOB_ONLYDIR is a hint and we need to double check if (dir_only && !isDir(path)) { continue; } ret.emplace_back(std::move(path)); } return ret; } else if (ec == GLOB_NOMATCH) { // Error with GLOB_NOMATCH just means nothing matched, return empty vec return ret; } else if (ec == GLOB_NOSPACE) { return SYSTEM_ERROR(ENOMEM); } else if (ec == GLOB_ABORTED) { return SYSTEM_ERROR(EINVAL); } else { // Shouldn't be possible, ENOSYS seems reasonable return SYSTEM_ERROR(ENOSYS); } } void Fs::removePrefix(std::string& str, const std::string& prefix) { if (str.find(prefix) != std::string::npos) { // Strip the leading './' if it exists and we haven't been explicitly // told to strip it if (str.find("./") == 0 && prefix.find("./") != 0) { str.erase(0, 2); } str.erase(0, prefix.size()); } } /* Reads a file and returns a newline separated vector of strings */ SystemMaybe> Fs::readFileByLine( const std::string& path, const char delim) { std::ifstream f(path, std::ios::in); if (!f.is_open()) { return SYSTEM_ERROR(ENOENT); } std::string s; std::vector v; while (std::getline(f, s, f.widen(delim))) { v.push_back(std::move(s)); } // Error when reading file, and thus content might be corrupted if (f.bad()) { return SYSTEM_ERROR(EINVAL); } return v; } SystemMaybe> Fs::readFileByLine(Fd&& fd) { auto fp = ::fdopen(std::move(fd).fd(), "r"); if (fp == nullptr) { return SYSTEM_ERROR(errno); } std::vector v; char* line = nullptr; size_t len = 0; ssize_t read; errno = 0; OOMD_SCOPE_EXIT { ::free(line); ::fclose(fp); }; while ((read = ::getline(&line, &len, fp)) != -1) { OCHECK(line != nullptr); if (read > 0 && line[read - 1] == '\n') { v.emplace_back(line, read - 1); } else { v.emplace_back(line); } } // Error when reading file, and thus content might be corrupted if (errno) { return SYSTEM_ERROR(errno); } return v; } SystemMaybe Fs::checkExistAt(const DirFd& dirfd, const char* name) { if (int rc = ::faccessat(dirfd.fd(), name, F_OK, 0); rc == 0) { return noSystemError(); } else { return SYSTEM_ERROR(rc); } } SystemMaybe> Fs::readControllersAt( const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kControllersFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } return Util::split((*lines)[0], ' '); } SystemMaybe> Fs::getPidsAt(const DirFd& dirfd) { auto str_pids = readFileByLine(Fs::Fd::openat(dirfd, kProcsFile)); if (!str_pids) { return SYSTEM_ERROR(str_pids.error()); } std::vector pids; for (const auto& sp : *str_pids) { pids.push_back(std::stoi(sp)); } return pids; } SystemMaybe Fs::readIsPopulatedAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kEventsFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } for (const auto& line : *lines) { std::vector toks = Util::split(line, ' '); if (toks.size() == 2 && toks[0] == "populated") { if (toks[1] == "1") { return true; } else if (toks[1] == "0") { return false; } else { return SYSTEM_ERROR(EINVAL); } } } return SYSTEM_ERROR(EINVAL); } std::string Fs::pressureTypeToString(PressureType type) { switch (type) { case PressureType::SOME: return "some"; case PressureType::FULL: return "full"; } __builtin_unreachable(); } SystemMaybe Fs::readRespressureFromLines( const std::vector& lines, PressureType type) { auto type_name = pressureTypeToString(type); size_t pressure_line_index = 0; switch (type) { case PressureType::SOME: pressure_line_index = 0; break; case PressureType::FULL: pressure_line_index = 1; break; } switch (getPsiFormat(lines)) { case PsiFormat::UPSTREAM: { // Upstream v4.16+ format // // some avg10=0.22 avg60=0.17 avg300=1.11 total=58761459 // full avg10=0.22 avg60=0.16 avg300=1.08 total=58464525 std::vector toks = Util::split(lines[pressure_line_index], ' '); if (toks[0] != type_name) { return SYSTEM_ERROR(EINVAL); } std::vector avg10 = Util::split(toks[1], '='); if (avg10[0] != "avg10") { return SYSTEM_ERROR(EINVAL); } std::vector avg60 = Util::split(toks[2], '='); if (avg60[0] != "avg60") { return SYSTEM_ERROR(EINVAL); } std::vector avg300 = Util::split(toks[3], '='); if (avg300[0] != "avg300") { return SYSTEM_ERROR(EINVAL); } std::vector total = Util::split(toks[4], '='); if (total[0] != "total") { return SYSTEM_ERROR(EINVAL); } return ResourcePressure{ std::stof(avg10[1]), std::stof(avg60[1]), std::stof(avg300[1]), std::chrono::microseconds(std::stoull(total[1])), }; } case PsiFormat::EXPERIMENTAL: { // Old experimental format // // aggr 316016073 // some 0.00 0.03 0.05 // full 0.00 0.03 0.05 std::vector toks = Util::split(lines[pressure_line_index + 1], ' '); if (toks[0] != type_name) { return SYSTEM_ERROR(EINVAL); } return ResourcePressure{ std::stof(toks[1]), std::stof(toks[2]), std::stof(toks[3]), std::nullopt, }; } case PsiFormat::MISSING: // Missing the control file return SYSTEM_ERROR(ENOENT); case PsiFormat::INVALID: return SYSTEM_ERROR(EINVAL); } __builtin_unreachable(); } SystemMaybe Fs::readRootMemcurrent() { auto meminfo = getMeminfo("/proc/meminfo"); if (!meminfo) { return SYSTEM_ERROR(meminfo.error()); } if (!meminfo->count("MemTotal") || !meminfo->count("MemFree")) { return SYSTEM_ERROR(EINVAL); } return (*meminfo)["MemTotal"] - (*meminfo)["MemFree"]; } SystemMaybe Fs::readMemcurrentAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemCurrentFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } return static_cast(std::stoll((*lines)[0])); } SystemMaybe Fs::readRootMempressure(PressureType type) { auto lines = readFileByLine("/proc/pressure/memory"); if (!lines) { lines = readFileByLine("/proc/mempressure"); } if (!lines) { return SYSTEM_ERROR(lines.error()); } return readRespressureFromLines(*lines, type); } SystemMaybe Fs::readMempressureAt( const DirFd& dirfd, PressureType type) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemPressureFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } return readRespressureFromLines(*lines, type); } SystemMaybe Fs::readMinMaxLowHighFromLines( const std::vector& lines) { if (lines.size() != 1) { return SYSTEM_ERROR(EINVAL); } if (lines[0] == "max") { return std::numeric_limits::max(); } return static_cast(std::stoll(lines[0])); } SystemMaybe Fs::readMemlowAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemLowFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } auto ret = Fs::readMinMaxLowHighFromLines(*lines); if (!ret) { return SYSTEM_ERROR(ret.error()); } return ret; } SystemMaybe Fs::readMemhighAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemHighFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } auto ret = Fs::readMinMaxLowHighFromLines(*lines); if (!ret) { return SYSTEM_ERROR(ret.error()); } return ret; } SystemMaybe Fs::readMemmaxAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemMaxFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } auto ret = Fs::readMinMaxLowHighFromLines(*lines); if (!ret) { return SYSTEM_ERROR(ret.error()); } return ret; } SystemMaybe Fs::readMemhightmpFromLines( const std::vector& lines) { if (lines.size() != 1) { return SYSTEM_ERROR(ENOENT); } auto tokens = Util::split(lines[0], ' '); if (tokens.size() != 2) { return SYSTEM_ERROR(EINVAL); } if (tokens[0] == "max") { return std::numeric_limits::max(); } return static_cast(std::stoll(tokens[0])); } SystemMaybe Fs::readMemhightmpAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemHighTmpFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } auto ret = readMemhightmpFromLines(*lines); if (!ret) { return SYSTEM_ERROR(ret.error()); } return ret; } SystemMaybe Fs::readMemminAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemMinFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } auto ret = Fs::readMinMaxLowHighFromLines(*lines); if (!ret) { return SYSTEM_ERROR(ret.error()); } return ret; } SystemMaybe Fs::readSwapCurrentAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemSwapCurrentFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } // The swap controller can be disabled via CONFIG_MEMCG_SWAP=n return std::stoll((*lines)[0]); } SystemMaybe Fs::readSwapMaxAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemSwapMaxFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } auto ret = Fs::readMinMaxLowHighFromLines(*lines); if (!ret) { return SYSTEM_ERROR(ret.error()); } return ret; } SystemMaybe> Fs::getVmstat( const std::string& path) { auto lines = readFileByLine(path); if (!lines) { return SYSTEM_ERROR(lines.error()); } std::unordered_map map; char space{' '}; for (auto& line : *lines) { std::stringstream ss(line); std::string item; // get key std::getline(ss, item, space); std::string key{item}; // insert value into map std::getline(ss, item, space); map[key] = static_cast(std::stoll(item)); } return map; } SystemMaybe> Fs::getMeminfo( const std::string& path) { char name[256] = {0}; uint64_t val; std::unordered_map map; auto lines = readFileByLine(path); if (!lines) { return SYSTEM_ERROR(lines.error()); } for (auto& line : *lines) { int ret = sscanf(line.c_str(), "%255[^:]:%*[ \t]%" SCNu64 "%*s\n", name, &val); if (ret == 2) { map[name] = val * 1024; } } return map; } std::unordered_map Fs::getMemstatLikeFromLines( const std::vector& lines) { char name[256] = {0}; uint64_t val; std::unordered_map map; for (const auto& line : lines) { int ret = sscanf(line.c_str(), "%255s %" SCNu64 "\n", name, &val); if (ret == 2) { map[name] = val; } } return map; } SystemMaybe> Fs::getMemstatAt( const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemStatFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } return getMemstatLikeFromLines(lines.value()); } SystemMaybe Fs::readRootIopressure(PressureType type) { auto lines = readFileByLine("/proc/pressure/io"); if (!lines) { return SYSTEM_ERROR(lines.error()); } return readRespressureFromLines(*lines, type); } SystemMaybe Fs::readIopressureAt( const DirFd& dirfd, PressureType type) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kIoPressureFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } return readRespressureFromLines(*lines, type); } SystemMaybe Fs::readIostatAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kIoStatFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } std::vector io_stat; io_stat.reserve(lines->size()); for (const auto& line : *lines) { // format // // 0:0 rbytes=0 wbytes=0 rios=0 wios=0 dbytes=0 dios=0 DeviceIOStat dev_io_stat; int major, minor; int ret = sscanf( line.c_str(), "%d:%d rbytes=%" SCNd64 " wbytes=%" SCNd64 " rios=%" SCNd64 " wios=%" SCNd64 " dbytes=%" SCNd64 " dios=%" SCNd64 "\n", &major, &minor, &dev_io_stat.rbytes, &dev_io_stat.wbytes, &dev_io_stat.rios, &dev_io_stat.wios, &dev_io_stat.dbytes, &dev_io_stat.dios); if (ret != 8) { return SYSTEM_ERROR(EINVAL); } dev_io_stat.dev_id = std::to_string(major) + ":" + std::to_string(minor); io_stat.push_back(dev_io_stat); } return io_stat; } SystemMaybe Fs::writeControlFileAt( SystemMaybe&& fd, const std::string& content) { if (!fd) { return SYSTEM_ERROR(fd.error()); } auto ret = Util::writeFull(fd->fd(), content.c_str(), content.size()); if (ret < 0) { return SYSTEM_ERROR(errno); } return noSystemError(); } SystemMaybe Fs::writeMemhighAt(const DirFd& dirfd, int64_t value) { auto val_str = std::to_string(value); auto ret = writeControlFileAt(Fs::Fd::openat(dirfd, kMemHighFile, false), val_str); if (!ret) { return SYSTEM_ERROR(ret.error()); } return noSystemError(); } SystemMaybe Fs::writeMemhightmpAt( const DirFd& dirfd, int64_t value, std::chrono::microseconds duration) { auto val_str = std::to_string(value) + " " + std::to_string(duration.count()); auto ret = writeControlFileAt( Fs::Fd::openat(dirfd, kMemHighTmpFile, false), val_str); if (!ret) { return SYSTEM_ERROR(ret.error()); } return noSystemError(); } SystemMaybe Fs::writeMemReclaimAt(const DirFd& dirfd, int64_t value) { auto val_str = std::to_string(value); auto ret = writeControlFileAt( Fs::Fd::openat(dirfd, kMemReclaimFile, false), val_str); if (!ret) { return SYSTEM_ERROR(ret.error()); } return noSystemError(); } SystemMaybe Fs::getNrDyingDescendantsAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kCgroupStatFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } auto map = getMemstatLikeFromLines(lines.value()); // Will return 0 for missing entries return map["nr_dying_descendants"]; } SystemMaybe Fs::readKillPreferenceAt(const DirFd& path) { auto maybe = Fs::hasxattrAt(path, kOomdSystemPreferXAttr); if (!maybe) { return SYSTEM_ERROR(maybe.error()); } if (*maybe) { return KillPreference::PREFER; } maybe = Fs::hasxattrAt(path, kOomdUserPreferXAttr); if (!maybe) { return SYSTEM_ERROR(maybe.error()); } if (*maybe) { return KillPreference::PREFER; } maybe = Fs::hasxattrAt(path, kOomdSystemAvoidXAttr); if (!maybe) { return SYSTEM_ERROR(maybe.error()); } if (*maybe) { return KillPreference::AVOID; } maybe = Fs::hasxattrAt(path, kOomdUserAvoidXAttr); if (!maybe) { return SYSTEM_ERROR(maybe.error()); } if (*maybe) { return KillPreference::AVOID; } return KillPreference::NORMAL; } SystemMaybe Fs::readMemoryOomGroupAt(const DirFd& dirfd) { auto lines = readFileByLine(Fs::Fd::openat(dirfd, kMemOomGroupFile)); if (!lines) { return SYSTEM_ERROR(lines.error()); } return *lines == std::vector({"1"}); } SystemMaybe Fs::setxattr( const std::string& path, const std::string& attr, const std::string& val) { int ret = ::setxattr(path.c_str(), attr.c_str(), val.c_str(), val.size(), 0); if (ret == -1) { return SYSTEM_ERROR(errno); } return noSystemError(); } SystemMaybe Fs::getxattr( const std::string& path, const std::string& attr) { std::string val; int size = ::getxattr(path.c_str(), attr.c_str(), nullptr, 0); if (size == -1) { if (errno == ENODATA) { return val; } return SYSTEM_ERROR(errno); } val.resize(size); size = ::getxattr(path.c_str(), attr.c_str(), &val[0], val.size()); if (size == -1) { // We checked above but this could have raced, so check again if (errno == ENODATA) { return std::string{}; } return SYSTEM_ERROR(errno); } return val; } SystemMaybe Fs::hasxattrAt(const DirFd& dirfd, const std::string& attr) { auto ret = ::fgetxattr(dirfd.fd(), attr.c_str(), nullptr, 0); if (ret == -1) { if (errno == ENODATA || errno == EOPNOTSUPP) { return false; } return SYSTEM_ERROR(errno); } return true; } bool Fs::isUnderParentPath( const std::string& parent_path, const std::string& path) { if (parent_path.empty() || path.empty()) { return false; } auto parent_parts = Util::split(parent_path, '/'); auto path_parts = Util::split(path, '/'); int i = 0; if (path_parts.size() < parent_parts.size()) { return false; } for (const auto& parts : parent_parts) { if (path_parts[i] != parts) { return false; } i++; } return true; } SystemMaybe Fs::getCgroup2MountPoint(const std::string& path) { auto lines = readFileByLine(path); if (!lines) { return SYSTEM_ERROR(lines.error()); } for (auto& line : *lines) { auto parts = Util::split(line, ' '); if (parts.size() > 2) { if (parts[2] == "cgroup2") { return parts[1] + '/'; } } } return SYSTEM_ERROR(EINVAL, path); } SystemMaybe Fs::getDeviceType( const std::string& dev_id, const std::string& path) { const auto deviceTypeFile = path + "/" + dev_id + "/" + kDeviceTypeDir + "/" + kDeviceTypeFile; auto lines = readFileByLine(deviceTypeFile); if (!lines) { return SYSTEM_ERROR(lines.error()); } if (lines->size() == 1) { if ((*lines)[0] == "1") { return DeviceType::HDD; } else if ((*lines)[0] == "0") { return DeviceType::SSD; } } return SYSTEM_ERROR(EINVAL, deviceTypeFile); } SystemMaybe Fs::getSwappiness(const std::string& path) { auto str_swappiness = readFileByLine(Fd::open(path)); if (!str_swappiness) { return SYSTEM_ERROR(str_swappiness.error()); } if (str_swappiness->size() != 1) { return SYSTEM_ERROR(EINVAL, path, " malformed"); } return std::stoi((*str_swappiness)[0]); } SystemMaybe Fs::setSwappiness(int swappiness, const std::string& path) { return writeControlFileAt(Fd::open(path, false), std::to_string(swappiness)); } } // namespace Oomd oomd-0.5.0/src/oomd/util/Fs.h000066400000000000000000000237441406470533700157200ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include #include #include #include #include #include "oomd/include/Types.h" #include "oomd/util/SystemMaybe.h" namespace Oomd { class Fs { public: static constexpr auto kControllersFile = "cgroup.controllers"; static constexpr auto kSubtreeControlFile = "cgroup.subtree_control"; static constexpr auto kProcsFile = "cgroup.procs"; static constexpr auto kEventsFile = "cgroup.events"; static constexpr auto kMemCurrentFile = "memory.current"; static constexpr auto kMemPressureFile = "memory.pressure"; static constexpr auto kMemLowFile = "memory.low"; static constexpr auto kMemHighFile = "memory.high"; static constexpr auto kMemMaxFile = "memory.max"; static constexpr auto kMemHighTmpFile = "memory.high.tmp"; static constexpr auto kMemReclaimFile = "memory.reclaim"; static constexpr auto kMemMinFile = "memory.min"; static constexpr auto kMemStatFile = "memory.stat"; static constexpr auto kCgroupStatFile = "cgroup.stat"; static constexpr auto kMemSwapCurrentFile = "memory.swap.current"; static constexpr auto kMemSwapMaxFile = "memory.swap.max"; static constexpr auto kMemOomGroupFile = "memory.oom.group"; static constexpr auto kIoPressureFile = "io.pressure"; static constexpr auto kIoStatFile = "io.stat"; static constexpr auto kDeviceTypeDir = "queue"; static constexpr auto kDeviceTypeFile = "rotational"; static constexpr auto kOomdSystemPreferXAttr = "trusted.oomd_prefer"; static constexpr auto kOomdUserPreferXAttr = "user.oomd_prefer"; static constexpr auto kOomdSystemAvoidXAttr = "trusted.oomd_avoid"; static constexpr auto kOomdUserAvoidXAttr = "user.oomd_avoid"; struct DirEnts { std::vector dirs; std::vector files; }; enum DirEntFlags { DE_FILE = 1, DE_DIR = (1 << 1), }; enum class PressureType { SOME = 0, FULL, }; class DirFd; /* * Wrapper class for file descriptor that supports auto closing. * Fd is opened read-only as currently that's the only use case. */ class Fd { public: static SystemMaybe openat(const DirFd& dirfd, const std::string& path, bool read_only = true); // Not safe for accessing cgroup control files. Use Openat instead. static SystemMaybe open(const std::string& path, bool read_only = true); explicit Fd(int fd) : fd_(fd) {} Fd() = default; Fd(const Fd& other) = delete; Fd(Fd&& other) noexcept { *this = std::move(other); } Fd& operator=(const Fd& other) = delete; Fd& operator=(Fd&& other) { fd_ = other.fd_; other.fd_ = -1; return *this; } ~Fd() { this->close(); } int fd() const& { return fd_; } /* * Take ownership of fd. This object is no longer valid. Caller is * responsible for closing the stolen fd. */ int fd() && { int stolen_fd = fd_; fd_ = -1; return stolen_fd; } // Return inode of the fd, or nullopt if anything fails SystemMaybe inode() const; protected: void close(); int fd_{-1}; }; /* * Wrapper class for directory file descriptor that supports auto closing. */ class DirFd : public Fd { public: static SystemMaybe open(const std::string& path); SystemMaybe openChildDir(const std::string& path) const; protected: explicit DirFd(int fd) : Fd(fd) {} }; static bool isCgroupValid(const DirFd& dirfd); /* * Reads a directory and returns the names of the requested entry types * Won't return any dotfiles (including ./ and ../) */ static SystemMaybe readDir(const std::string& path, int flags); /* * Like readDir, but takes a DirFd */ static SystemMaybe readDirAt(const DirFd& dirfd, int flags); /* * Checks if @param path is a directory */ static bool isDir(const std::string& path); /* * Takes a fully qualified and wildcarded path and returns a set of * resolved fully qualified paths. */ static SystemMaybe> glob( const std::string& pattern, bool dir_only = false); /* * Path aware prefix removal. * * If @param str begins with './', those two characters will be stripped. */ static void removePrefix(std::string& str, const std::string& prefix); /* Reads a file and returns a newline separated vector of strings */ static SystemMaybe> readFileByLine( const std::string& path, const char delim = '\n'); /* * Same as variant taking string as argument except rvalue Fd is used, which * will be closed right after the call as it is read as a whole and we don't * seek offsets on Fd. */ static SystemMaybe> readFileByLine(Fd&& fd); /* * Same as above, convenience method accepting the output of Fd::openat */ inline static SystemMaybe> readFileByLine( SystemMaybe&& fd) { if (fd) { return readFileByLine(std::move(*fd)); } return SYSTEM_ERROR(fd.error()); } static SystemMaybe checkExistAt(const DirFd& dirfd, const char* name); static SystemMaybe> readControllersAt( const DirFd& dirfd); static SystemMaybe> getPidsAt(const DirFd& dirfd); static SystemMaybe readIsPopulatedAt(const DirFd& dirfd); static std::string pressureTypeToString(PressureType type); /* Helpers to read PSI files */ static SystemMaybe readRespressureFromLines( const std::vector& lines, PressureType type = PressureType::FULL); static SystemMaybe readRootMemcurrent(); static SystemMaybe readMemcurrentAt(const DirFd& dirfd); static SystemMaybe readRootMempressure( PressureType type = PressureType::FULL); static SystemMaybe readMempressureAt( const DirFd& dirfd, PressureType type = PressureType::FULL); static SystemMaybe readMinMaxLowHighFromLines( const std::vector& lines); static SystemMaybe readMemhightmpFromLines( const std::vector& lines); static SystemMaybe readMemlowAt(const DirFd& dirfd); static SystemMaybe readMemhighAt(const DirFd& dirfd); static SystemMaybe readMemmaxAt(const DirFd& dirfd); static SystemMaybe readMemhightmpAt(const DirFd& dirfd); static SystemMaybe readMemminAt(const DirFd& dirfd); static SystemMaybe readSwapCurrentAt(const DirFd& dirfd); static SystemMaybe readSwapMaxAt(const DirFd& dirfd); static SystemMaybe readRootIopressure( PressureType type = PressureType::FULL); static SystemMaybe readIopressureAt( const DirFd& dirfd, PressureType type = PressureType::FULL); static SystemMaybe writeMemhighAt(const DirFd& dirfd, int64_t value); static SystemMaybe writeMemhightmpAt( const DirFd& dirfd, int64_t value, std::chrono::microseconds duration); static SystemMaybe writeMemReclaimAt(const DirFd& dirfd, int64_t value); static SystemMaybe getNrDyingDescendantsAt(const DirFd& dirfd); static SystemMaybe readKillPreferenceAt(const DirFd& path); static SystemMaybe readMemoryOomGroupAt(const DirFd& dirfd); static SystemMaybe readIostatAt(const DirFd& dirfd); static SystemMaybe> getVmstat( const std::string& path = "/proc/vmstat"); static SystemMaybe> getMeminfo( const std::string& path = "/proc/meminfo"); static SystemMaybe> getMemstatAt( const DirFd& dirfd); // Return root part of cgroup2 from /proc/mounts/ static SystemMaybe getCgroup2MountPoint( const std::string& path = "/proc/mounts"); // Check if path point to parent path or somewhere inside parent path static bool isUnderParentPath( const std::string& parent_path, const std::string& path); /* Getters and setters to set xattr values */ static SystemMaybe setxattr( const std::string& path, const std::string& attr, const std::string& val); static SystemMaybe getxattr( const std::string& path, const std::string& attr); static SystemMaybe hasxattrAt( const DirFd& dirfd, const std::string& attr); // Return if device is SSD or HDD given its id in : format static SystemMaybe getDeviceType( const std::string& dev_id, const std::string& path = "/sys/dev/block"); // Return system swappiness static SystemMaybe getSwappiness( const std::string& path = "/proc/sys/vm/swappiness"); static SystemMaybe setSwappiness( int swappiness, const std::string& path = "/proc/sys/vm/swappiness"); private: static std::unordered_map getMemstatLikeFromLines( const std::vector& lines); static SystemMaybe writeControlFileAt( SystemMaybe&& fd, const std::string& content); static SystemMaybe readDirFromDIR(DIR* dir, int flags); }; } // namespace Oomd oomd-0.5.0/src/oomd/util/FsTest.cpp000066400000000000000000000372421406470533700171110ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include "oomd/fixtures/FsFixture.h" #include "oomd/util/Fs.h" #include "oomd/util/TestHelper.h" using namespace Oomd; using namespace testing; class FsTest : public ::testing::Test { protected: void SetUp() override { try { fixture_.materialize(); } catch (const std::exception& e) { FAIL() << "SetUpTestSuite: " << e.what(); } } void TearDown() override { try { fixture_.teardown(); } catch (const std::exception& e) { FAIL() << "TearDownTestSuite: " << e.what(); } } FsFixture fixture_{}; }; TEST_F(FsTest, FindDirectories) { auto dir = fixture_.fsDataDir(); auto de = ASSERT_SYS_OK(Fs::readDir(dir, Fs::DE_DIR)); ASSERT_EQ(de.dirs.size(), 4); EXPECT_THAT(de.dirs, Contains(std::string("dir1"))); EXPECT_THAT(de.dirs, Contains(std::string("dir2"))); EXPECT_THAT(de.dirs, Contains(std::string("dir3"))); EXPECT_THAT(de.dirs, Contains(std::string("wildcard"))); EXPECT_THAT(de.dirs, Not(Contains(std::string("dir21")))); EXPECT_THAT(de.dirs, Not(Contains(std::string("dir22")))); } TEST_F(FsTest, IsDir) { auto dir = fixture_.fsDataDir(); EXPECT_TRUE(Fs::isDir(dir + "/dir1")); EXPECT_FALSE(Fs::isDir(dir + "/dir1/stuff")); EXPECT_FALSE(Fs::isDir(dir + "/NOTINFS")); } TEST_F(FsTest, RemovePrefix) { std::string s = "long string like this"; Fs::removePrefix(s, "long string "); EXPECT_EQ(s, "like this"); std::string ss = "random string"; Fs::removePrefix(ss, "asdf"); EXPECT_EQ(ss, "random string"); std::string sss = "asdf"; Fs::removePrefix(sss, "asdf"); EXPECT_EQ(sss, ""); std::string path = "./var/log/messages"; Fs::removePrefix(path, "var/log/"); EXPECT_EQ(path, "messages"); std::string path2 = "./var/log/messages"; Fs::removePrefix(path2, "./var/log/"); EXPECT_EQ(path2, "messages"); } TEST_F(FsTest, FindFiles) { auto dir = fixture_.fsDataDir(); auto de = ASSERT_SYS_OK(Fs::readDir(dir, Fs::DE_FILE)); ASSERT_EQ(de.files.size(), 4); EXPECT_THAT(de.files, Contains(std::string("file1"))); EXPECT_THAT(de.files, Contains(std::string("file2"))); EXPECT_THAT(de.files, Contains(std::string("file3"))); EXPECT_THAT(de.files, Contains(std::string("file4"))); EXPECT_THAT(de.files, Not(Contains(std::string("file5")))); } TEST_F(FsTest, Glob) { auto dir = fixture_.fsDataDir(); dir += "/wildcard"; auto wildcarded_path_some = dir + "/dir*"; auto resolved = ASSERT_SYS_OK(Fs::glob(wildcarded_path_some)); ASSERT_EQ(resolved.size(), 2); EXPECT_THAT(resolved, Contains(dir + "/dir1")); EXPECT_THAT(resolved, Contains(dir + "/dir2")); auto wildcarded_path_dir_only = dir + "/*"; resolved = ASSERT_SYS_OK(Fs::glob(wildcarded_path_dir_only, /* dir_only */ true)); ASSERT_EQ(resolved.size(), 3); EXPECT_THAT(resolved, Contains(dir + "/dir1")); EXPECT_THAT(resolved, Contains(dir + "/dir2")); EXPECT_THAT(resolved, Contains(dir + "/different_dir")); auto wildcarded_path_all = dir + "/*"; resolved = ASSERT_SYS_OK(Fs::glob(wildcarded_path_all)); ASSERT_EQ(resolved.size(), 4); EXPECT_THAT(resolved, Contains(dir + "/dir1")); EXPECT_THAT(resolved, Contains(dir + "/dir2")); EXPECT_THAT(resolved, Contains(dir + "/different_dir")); EXPECT_THAT(resolved, Contains(dir + "/file")); auto nonexistent_path = dir + "/not/a/valid/dir"; resolved = ASSERT_SYS_OK(Fs::glob(nonexistent_path)); ASSERT_EQ(resolved.size(), 0); } TEST_F(FsTest, ReadFile) { auto file = fixture_.fsDataDir() + "/dir1/stuff"; auto lines = ASSERT_SYS_OK(Fs::readFileByLine(file)); ASSERT_EQ(lines.size(), 4); EXPECT_EQ(lines[0], "hello world"); EXPECT_EQ(lines[1], "my good man"); EXPECT_EQ(lines[2], ""); EXPECT_EQ(lines[3], "1"); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(fixture_.fsDataDir() + "/dir1")); auto openat_lines = ASSERT_SYS_OK(Fs::readFileByLine(Fs::Fd::openat(dir, "stuff"))); ASSERT_EQ(openat_lines, lines); } TEST_F(FsTest, ReadFileBad) { auto file = fixture_.fsDataDir() + "/ksldjfksdlfdsjf"; EXPECT_FALSE(Fs::readFileByLine(file)); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(fixture_.fsDataDir())); EXPECT_FALSE(Fs::readFileByLine(Fs::Fd::openat(dir, "ksldjfksdlfdsjf"))); } TEST_F(FsTest, CheckExist) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); EXPECT_TRUE(Fs::checkExistAt(dir, Fs::kControllersFile)); EXPECT_FALSE(Fs::checkExistAt(dir, "not_exists")); } TEST_F(FsTest, GetPids) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto pids = ASSERT_SYS_OK(Fs::getPidsAt(dir)); EXPECT_EQ(pids.size(), 1); EXPECT_THAT(pids, Contains(123)); auto path2 = path + "/service1.service"; auto dir2 = ASSERT_SYS_OK(Fs::DirFd::open(path2)); auto pids2 = ASSERT_SYS_OK(Fs::getPidsAt(dir2)); EXPECT_EQ(pids2.size(), 2); EXPECT_THAT(pids2, Contains(456)); EXPECT_THAT(pids2, Contains(789)); } TEST_F(FsTest, ReadIsPopulated) { auto dir1 = ASSERT_SYS_OK(Fs::DirFd::open(fixture_.cgroupDataDir())); auto isPopulated1 = ASSERT_SYS_OK(Fs::readIsPopulatedAt(dir1)); EXPECT_TRUE(isPopulated1); auto dir2 = ASSERT_SYS_OK( Fs::DirFd::open(fixture_.cgroupDataDir() + "/service3.service")); auto isPopulated2 = ASSERT_SYS_OK(Fs::readIsPopulatedAt(dir2)); EXPECT_FALSE(isPopulated2); } TEST_F(FsTest, GetNrDying) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto nr_dying = ASSERT_SYS_OK(Fs::getNrDyingDescendantsAt(dir)); EXPECT_EQ(nr_dying, 27); } TEST_F(FsTest, ReadMemoryCurrent) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto memcurrent = ASSERT_SYS_OK(Fs::readMemcurrentAt(dir)); EXPECT_EQ(memcurrent, 987654321); } TEST_F(FsTest, ReadMemoryLow) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto memlow = ASSERT_SYS_OK(Fs::readMemlowAt(dir)); EXPECT_EQ(memlow, 333333); } TEST_F(FsTest, ReadMemoryMin) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto memmin = ASSERT_SYS_OK(Fs::readMemminAt(dir)); EXPECT_EQ(memmin, 666); } TEST_F(FsTest, ReadMemoryHigh) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto memhigh = ASSERT_SYS_OK(Fs::readMemhighAt(dir)); EXPECT_EQ(memhigh, 1000); } TEST_F(FsTest, ReadMemoryMax) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto memmax = ASSERT_SYS_OK(Fs::readMemmaxAt(dir)); EXPECT_EQ(memmax, 654); } TEST_F(FsTest, ReadMemoryHighTmp) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto memtmphigh = ASSERT_SYS_OK(Fs::readMemhightmpAt(dir)); EXPECT_EQ(memtmphigh, 2000); } TEST_F(FsTest, ReadSwapCurrent) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto swap_current = ASSERT_SYS_OK(Fs::readSwapCurrentAt(dir)); EXPECT_EQ(swap_current, 321321); } TEST_F(FsTest, ReadSwapMax) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto swap_max = ASSERT_SYS_OK(Fs::readSwapMaxAt(dir)); EXPECT_EQ(swap_max, 12345); } TEST_F(FsTest, ReadControllers) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto controllers = ASSERT_SYS_OK(Fs::readControllersAt(dir)); ASSERT_EQ(controllers.size(), 4); EXPECT_THAT(controllers, Contains(std::string("cpu"))); EXPECT_THAT(controllers, Contains(std::string("io"))); EXPECT_THAT(controllers, Contains(std::string("memory"))); EXPECT_THAT(controllers, Contains(std::string("pids"))); EXPECT_THAT(controllers, Not(Contains(std::string("block")))); } TEST_F(FsTest, ReadMemoryPressure) { // v4.16+ upstream format auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto pressure = ASSERT_SYS_OK(Fs::readMempressureAt(dir)); EXPECT_FLOAT_EQ(pressure.sec_10, 4.44); EXPECT_FLOAT_EQ(pressure.sec_60, 5.55); EXPECT_FLOAT_EQ(pressure.sec_300, 6.66); // old experimental format auto path2 = path + "/service2.service"; auto dir2 = ASSERT_SYS_OK(Fs::DirFd::open(path2)); auto pressure2 = ASSERT_SYS_OK(Fs::readMempressureAt(dir2)); EXPECT_FLOAT_EQ(pressure2.sec_10, 4.44); EXPECT_FLOAT_EQ(pressure2.sec_60, 5.55); EXPECT_FLOAT_EQ(pressure2.sec_300, 6.66); // old experimental format w/ debug info on auto path3 = path + "/service3.service"; auto dir3 = ASSERT_SYS_OK(Fs::DirFd::open(path3)); auto pressure3 = ASSERT_SYS_OK(Fs::readMempressureAt(dir3)); EXPECT_FLOAT_EQ(pressure3.sec_10, 4.44); EXPECT_FLOAT_EQ(pressure3.sec_60, 5.55); EXPECT_FLOAT_EQ(pressure3.sec_300, 6.66); } TEST_F(FsTest, ReadMemoryPressureSome) { // v4.16+ upstream format auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto pressure = ASSERT_SYS_OK(Fs::readMempressureAt(dir, Fs::PressureType::SOME)); EXPECT_FLOAT_EQ(pressure.sec_10, 1.11); EXPECT_FLOAT_EQ(pressure.sec_60, 2.22); EXPECT_FLOAT_EQ(pressure.sec_300, 3.33); // old experimental format auto path2 = path + "/service2.service"; auto dir2 = ASSERT_SYS_OK(Fs::DirFd::open(path2)); auto pressure2 = ASSERT_SYS_OK(Fs::readMempressureAt(dir2, Fs::PressureType::SOME)); EXPECT_FLOAT_EQ(pressure2.sec_10, 1.11); EXPECT_FLOAT_EQ(pressure2.sec_60, 2.22); EXPECT_FLOAT_EQ(pressure2.sec_300, 3.33); } TEST_F(FsTest, GetVmstat) { auto vmstatfile = fixture_.fsVmstatFile(); auto vmstat = ASSERT_SYS_OK(Fs::getVmstat(vmstatfile)); EXPECT_EQ(vmstat["first_key"], 12345); EXPECT_EQ(vmstat["second_key"], 678910); EXPECT_EQ(vmstat["thirdkey"], 999999); // we expect the key is missing (ie default val = 0) EXPECT_EQ(vmstat["asdf"], 0); } TEST_F(FsTest, GetMeminfo) { auto meminfofile = fixture_.fsMeminfoFile(); auto meminfo = ASSERT_SYS_OK(Fs::getMeminfo(meminfofile)); EXPECT_EQ(meminfo.size(), 49); EXPECT_EQ(meminfo["SwapTotal"], 2097148 * 1024); EXPECT_EQ(meminfo["SwapFree"], 1097041 * 1024); EXPECT_EQ(meminfo["HugePages_Total"], 0); // we expect the key is missing (ie default val = 0) EXPECT_EQ(meminfo["asdf"], 0); } TEST_F(FsTest, GetMemstat) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto meminfo = ASSERT_SYS_OK(Fs::getMemstatAt(dir)); EXPECT_EQ(meminfo.size(), 29); EXPECT_EQ(meminfo["anon"], 1294168064); EXPECT_EQ(meminfo["file"], 3870687232); EXPECT_EQ(meminfo["pglazyfree"], 0); // we expect the key is missing (ie default val = 0) EXPECT_EQ(meminfo["asdf"], 0); } TEST_F(FsTest, ReadIoPressure) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto pressure = ASSERT_SYS_OK(Fs::readIopressureAt(dir)); EXPECT_FLOAT_EQ(pressure.sec_10, 4.45); EXPECT_FLOAT_EQ(pressure.sec_60, 5.56); EXPECT_FLOAT_EQ(pressure.sec_300, 6.67); } TEST_F(FsTest, ReadIoPressureSome) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto pressure = ASSERT_SYS_OK(Fs::readIopressureAt(dir, Fs::PressureType::SOME)); EXPECT_FLOAT_EQ(pressure.sec_10, 1.12); EXPECT_FLOAT_EQ(pressure.sec_60, 2.23); EXPECT_FLOAT_EQ(pressure.sec_300, 3.34); } TEST_F(FsTest, ReadMemoryOomGroup) { auto path1 = fixture_.cgroupDataDir() + "/slice1.slice"; auto dir1 = ASSERT_SYS_OK(Fs::DirFd::open(path1)); auto oom_group = ASSERT_SYS_OK(Fs::readMemoryOomGroupAt(dir1)); EXPECT_EQ(oom_group, true); auto path2 = fixture_.cgroupDataDir() + "/slice1.slice/service1.service"; auto dir2 = ASSERT_SYS_OK(Fs::DirFd::open(path2)); auto oom_group2 = ASSERT_SYS_OK(Fs::readMemoryOomGroupAt(dir2)); EXPECT_EQ(oom_group2, false); } TEST_F(FsTest, IsUnderParentPath) { EXPECT_TRUE(Fs::isUnderParentPath("/sys/fs/cgroup/", "/sys/fs/cgroup/")); EXPECT_TRUE(Fs::isUnderParentPath("/sys/fs/cgroup/", "/sys/fs/cgroup/blkio")); EXPECT_FALSE(Fs::isUnderParentPath("/sys/fs/cgroup/", "/sys/fs/")); EXPECT_TRUE(Fs::isUnderParentPath("/", "/sys/")); EXPECT_FALSE(Fs::isUnderParentPath("/sys/", "/")); EXPECT_FALSE(Fs::isUnderParentPath("", "/sys/")); EXPECT_FALSE(Fs::isUnderParentPath("/sys/", "")); EXPECT_FALSE(Fs::isUnderParentPath("", "")); } TEST_F(FsTest, GetCgroup2MountPoint) { auto mountsfile = fixture_.fsMountsFile(); auto cgrouppath = ASSERT_SYS_OK(Fs::getCgroup2MountPoint(mountsfile)); EXPECT_EQ(cgrouppath, std::string("/sys/fs/cgroup/")); } TEST_F(FsTest, GetDeviceType) { auto fsDevDir = fixture_.fsDeviceDir(); auto ssd_type = ASSERT_SYS_OK(Fs::getDeviceType("1:0", fsDevDir)); EXPECT_EQ(ssd_type, DeviceType::SSD); auto hdd_type = ASSERT_SYS_OK(Fs::getDeviceType("1:1", fsDevDir)); EXPECT_EQ(hdd_type, DeviceType::HDD); auto ret = Fs::getDeviceType("1:2", fsDevDir); ASSERT_FALSE(ret); } TEST_F(FsTest, ReadIostat) { auto path = fixture_.cgroupDataDir(); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); auto io_stat = ASSERT_SYS_OK(Fs::readIostatAt(dir)); EXPECT_EQ(io_stat.size(), 2); auto stat0 = io_stat[0]; EXPECT_EQ(stat0.dev_id, "1:10"); EXPECT_EQ(stat0.rbytes, 1111111); EXPECT_EQ(stat0.wbytes, 2222222); EXPECT_EQ(stat0.rios, 33); EXPECT_EQ(stat0.wios, 44); EXPECT_EQ(stat0.dbytes, 5555555555); EXPECT_EQ(stat0.dios, 6); auto stat1 = io_stat[1]; EXPECT_EQ(stat1.dev_id, "1:11"); EXPECT_EQ(stat1.rbytes, 2222222); EXPECT_EQ(stat1.wbytes, 3333333); EXPECT_EQ(stat1.rios, 44); EXPECT_EQ(stat1.wios, 55); EXPECT_EQ(stat1.dbytes, 6666666666); EXPECT_EQ(stat1.dios, 7); } TEST_F(FsTest, WriteMemoryHigh) { using F = Fixture; auto path = fixture_.cgroupDataDir() + "/write_test"; F::materialize(F::makeDir(path, {F::makeFile("memory.high")})); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); ASSERT_SYS_OK(Fs::writeMemhighAt(dir, 54321)); auto memhigh = ASSERT_SYS_OK(Fs::readMemhighAt(dir)); EXPECT_EQ(memhigh, 54321); } TEST_F(FsTest, WriteMemoryHighTmp) { using F = Fixture; auto path = fixture_.cgroupDataDir() + "/write_test"; F::materialize(F::makeDir(path, {F::makeFile("memory.high.tmp")})); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); ASSERT_SYS_OK( Fs::writeMemhightmpAt(dir, 54321, std::chrono::microseconds{400000})); auto memhightmp = ASSERT_SYS_OK(Fs::readMemhightmpAt(dir)); EXPECT_EQ(memhightmp, 54321); } TEST_F(FsTest, WriteMemoryReclaim) { using F = Fixture; auto path = fixture_.cgroupDataDir() + "/write_test"; F::materialize(F::makeDir(path, {F::makeFile("memory.reclaim")})); auto dir = ASSERT_SYS_OK(Fs::DirFd::open(path)); ASSERT_SYS_OK(Fs::writeMemReclaimAt(dir, 54321)); auto lines = ASSERT_SYS_OK( Fs::readFileByLine(Fs::Fd::openat(dir, Fs::kMemReclaimFile))); EXPECT_EQ(lines, std::vector{std::string("54321")}); } TEST_F(FsTest, Swappiness) { using F = Fixture; auto path = fixture_.fsDataDir() + "/swappiness"; F::materialize(F::makeFile(path)); ASSERT_SYS_OK(Fs::setSwappiness(42, path)); auto swappiness = ASSERT_SYS_OK(Fs::getSwappiness(path)); EXPECT_EQ(swappiness, 42); } oomd-0.5.0/src/oomd/util/PluginArgParser.cpp000066400000000000000000000071601406470533700207420ustar00rootroot00000000000000#include #include "oomd/include/Types.h" #include "oomd/util/PluginArgParser.h" namespace Oomd { std::unordered_set PluginArgParser::parseCgroup( const PluginConstructionContext& context, const std::string& cgroupStr) { std::unordered_set res; const auto& cgroup_fs = context.cgroupFs(); auto cgroups = Util::split(cgroupStr, ','); for (const auto& c : cgroups) { res.emplace(cgroup_fs, c); } return res; } int PluginArgParser::parseUnsignedInt(const std::string& intStr) { int res = std::stoi(intStr); if (res < 0) { throw std::invalid_argument("must be non-negative"); } return res; } void PluginArgParser::setName(const std::string& pluginName) { pluginName_ = pluginName; } std::string PluginArgParser::getName() { return pluginName_; } SystemMaybe PluginArgParser::parse(const Engine::PluginArgs& args) { for (const auto& argName : requiredArgs_) { if (args.find(argName) == args.end()) { OLOG << "Required arg \"" << argName << "\" missing in plugin \"" << pluginName_ << "\""; return SYSTEM_ERROR( EINVAL, "Required arg \"", argName, "\" missing in plugin \"", pluginName_, "\""); } } for (const auto& entry : args) { if (argValueFillingFuncs_.find(entry.first) != argValueFillingFuncs_.end()) { auto funcRes = argValueFillingFuncs_.at(entry.first)(entry.second); if (!funcRes) { return SYSTEM_ERROR( EINVAL, "Failed parsing arg for plugin \"", pluginName_, "\", error: ", funcRes.error().what()); } } else { OLOG << "Unknown arg \"" << entry.first << "\" in plugin \"" << pluginName_ << "\""; return SYSTEM_ERROR( EINVAL, "Unknown arg \"", entry.first, "\" in plugin \"", pluginName_, "\""); } } return noSystemError(); } std::unordered_set PluginArgParser::validArgNames() { std::unordered_set res; for (const auto& entry : argValueFillingFuncs_) { res.emplace(entry.first); } return res; }; template <> int64_t PluginArgParser::parseValue(const std::string& valueString) { return std::stoull(valueString); } template <> int PluginArgParser::parseValue(const std::string& valueString) { return std::stoi(valueString); } template <> double PluginArgParser::parseValue(const std::string& valueString) { return std::stod(valueString); } template <> float PluginArgParser::parseValue(const std::string& valueString) { return std::stof(valueString); } template <> bool PluginArgParser::parseValue(const std::string& valueString) { if (valueString == "true" || valueString == "True" || valueString == "1") { return true; } if (valueString == "false" || valueString == "False" || valueString == "0") { return false; } throw std::invalid_argument( "Invalid resource value, must be true/false, True/False, 1/0."); } template <> std::string PluginArgParser::parseValue(const std::string& valueString) { return valueString; } template <> std::chrono::milliseconds PluginArgParser::parseValue( const std::string& valueString) { return std::chrono::milliseconds(std::stoll(valueString)); } template <> ResourceType PluginArgParser::parseValue(const std::string& valueString) { if (valueString == "io") { return ResourceType::IO; } else if (valueString == "memory") { return ResourceType::MEMORY; } throw std::invalid_argument( "Invalid resource value, must be either 'io' or 'memory'."); } } // namespace Oomd oomd-0.5.0/src/oomd/util/PluginArgParser.h000066400000000000000000000047041406470533700204100ustar00rootroot00000000000000#pragma once #include #include #include "oomd/Log.h" #include "oomd/PluginConstructionContext.h" #include "oomd/include/CgroupPath.h" #include "oomd/include/Types.h" #include "oomd/util/SystemMaybe.h" #include "oomd/util/Util.h" namespace Oomd { namespace { // this is to help deduce function template. // this is the same as std::type_identity in c++20 template struct Identity { using type = T; }; } // namespace class PluginArgParser { public: static std::unordered_set parseCgroup( const PluginConstructionContext& context, const std::string& cgroupStr); static int parseUnsignedInt(const std::string& intStr); PluginArgParser() {} explicit PluginArgParser(const std::string& pluginName) : pluginName_(pluginName) {} void setName(const std::string& pluginName); std::string getName(); // pasrse arg input, it fails (returns a error SystemMaybe) when: // 1. any required arg is missing // 2. any unknown arg passed in // 3. failed to parse value string of any arg SystemMaybe parse(const Engine::PluginArgs& args); // return a set of valid arg names registered std::unordered_set validArgNames(); template void addArgument(const std::string& argName, T& argDest, bool required = false) { addArgumentCustom(argName, argDest, parseValue, required); } template void addArgumentCustom( const std::string& argName, T& argDest, typename Identity>::type& func, bool required = false) { if (required) { requiredArgs_.emplace(argName); } argValueFillingFuncs_.emplace( argName, [&argDest, argName, func]( const std::string& valueStr) -> SystemMaybe { try { argDest = func(valueStr); } catch (std::exception& e) { return SYSTEM_ERROR( EINVAL, "Failed to parse argument \"", argName, "\", error: ", e.what()); } return noSystemError(); }); } template static T parseValue(const std::string& valueStr); private: std::unordered_map< std::string, std::function(const std::string&)>> argValueFillingFuncs_; std::unordered_set requiredArgs_; std::string pluginName_; }; } // namespace Oomd oomd-0.5.0/src/oomd/util/PluginArgParserTest.cpp000066400000000000000000000165321406470533700216050ustar00rootroot00000000000000// Copyright 2004-present Facebook. All Rights Reserved. #include #include #include #include #include #include "oomd/include/Types.h" #include "oomd/util/PluginArgParser.h" using namespace ::testing; namespace Oomd { TEST(ParseCGroup, testCgroupStringParsing) { PluginConstructionContext context("/root/cgroupfs"); auto cgroups = PluginArgParser::parseCgroup(context, "cg1,cg2"); std::unordered_set expected{ {CgroupPath("/root/cgroupfs", "cg1")}, {CgroupPath("/root/cgroupfs", "cg2")}}; EXPECT_EQ(expected, cgroups); } TEST(ParseCGroup, testUnsignedIntParsing) { auto res = PluginArgParser::parseUnsignedInt("123"); EXPECT_EQ(123, res); EXPECT_THROW( PluginArgParser::parseUnsignedInt("-123"), std::invalid_argument); } TEST(PluginArgParserTest, testPluginName) { PluginArgParser p("test_plugin"); EXPECT_EQ("test_plugin", p.getName()); p.setName("new_plugin_name"); EXPECT_EQ("new_plugin_name", p.getName()); } TEST(PluginArgParserTest, testArgParsingSuccess) { int64_t valueLongInt; int valueInt; double valueDouble; float valueFloat; bool valueBool; std::string valueString; std::chrono::milliseconds valueMillis; ResourceType valueResourceType; std::vector valueStrs; PluginArgParser p("test_plugin"); std::function(const std::string&)> valueStrsFunc = [](const std::string& valueStr) -> std::vector { return std::vector{{valueStr + "-1", valueStr + "-2"}}; }; p.addArgument("arg_long_int", valueLongInt); p.addArgument("arg_int", valueInt); p.addArgument("arg_double", valueDouble); p.addArgument("arg_float", valueFloat); p.addArgument("arg_bool", valueBool); p.addArgument("arg_string", valueString); p.addArgument("arg_milli_second", valueMillis); p.addArgument("arg_resource_type", valueResourceType); p.addArgumentCustom("arg_strs", valueStrs, valueStrsFunc); // assert correct registered arg names std::unordered_set expectedArgNames{ {"arg_long_int"}, {"arg_int"}, {"arg_double"}, {"arg_float"}, {"arg_bool"}, {"arg_string"}, {"arg_milli_second"}, {"arg_resource_type"}, {"arg_strs"}}; EXPECT_EQ(expectedArgNames, p.validArgNames()); Engine::PluginArgs inputArgs{ {"arg_long_int", "1234"}, {"arg_int", "4321"}, {"arg_double", "1.234"}, {"arg_float", "4.321"}, {"arg_bool", "true"}, {"arg_string", "foo"}, {"arg_milli_second", "456"}, {"arg_resource_type", "io"}, {"arg_strs", "some_str"}}; auto res = p.parse(inputArgs); EXPECT_TRUE(res); EXPECT_EQ(1234, valueLongInt); EXPECT_EQ(4321, valueInt); EXPECT_EQ(1.234, valueDouble); EXPECT_EQ(4.321f, valueFloat); EXPECT_EQ(true, valueBool); EXPECT_EQ("foo", valueString); EXPECT_EQ(std::chrono::milliseconds(456), valueMillis); EXPECT_EQ(ResourceType::IO, valueResourceType); std::vector expectedStrs{{"some_str-1"}, {"some_str-2"}}; EXPECT_EQ(expectedStrs, valueStrs); } TEST(PluginArgParserTest, testArgParsingSuccessOnResourceType) { ResourceType valueResourceTypeIO; ResourceType valueResourceTypeMem; PluginArgParser p("test_plugin"); p.addArgument("arg_resource_type_io", valueResourceTypeIO); p.addArgument("arg_resource_type_mem", valueResourceTypeMem); // assert correct registered arg names std::unordered_set expectedArgNames{ {"arg_resource_type_io"}, {"arg_resource_type_mem"}}; EXPECT_EQ(expectedArgNames, p.validArgNames()); Engine::PluginArgs inputArgs{ {"arg_resource_type_io", "io"}, {"arg_resource_type_mem", "memory"}}; auto res = p.parse(inputArgs); EXPECT_TRUE(res); EXPECT_EQ(ResourceType::IO, valueResourceTypeIO); EXPECT_EQ(ResourceType::MEMORY, valueResourceTypeMem); } TEST(PluginArgParserTest, testArgParsingFailedOnInvalidResourceType) { ResourceType valueResourceType; PluginArgParser p("test_plugin"); p.addArgument("arg_resource_type_io", valueResourceType); Engine::PluginArgs inputArgs{{"arg_resource_type_io", "kidding"}}; auto res = p.parse(inputArgs); EXPECT_FALSE(res); EXPECT_THAT( std::string(res.error().what()), HasSubstr("Failed to parse argument \"arg_resource_type_io\", error")); } TEST(PluginArgParserTest, testArgParsingSuccessOnBool) { bool value1 = false; bool value2 = false; bool value3 = false; bool value4 = true; bool value5 = true; bool value6 = true; PluginArgParser p("test_plugin"); p.addArgument("arg_1", value1); p.addArgument("arg_2", value2); p.addArgument("arg_3", value3); p.addArgument("arg_4", value4); p.addArgument("arg_5", value5); p.addArgument("arg_6", value6); Engine::PluginArgs inputArgs{ {"arg_1", "true"}, {"arg_2", "True"}, {"arg_3", "1"}, {"arg_4", "false"}, {"arg_5", "False"}, {"arg_6", "0"}}; auto res = p.parse(inputArgs); EXPECT_TRUE(res); EXPECT_EQ(true, value1); EXPECT_EQ(true, value2); EXPECT_EQ(true, value3); EXPECT_EQ(false, value4); EXPECT_EQ(false, value5); EXPECT_EQ(false, value6); } TEST(PluginArgParserTest, testArgParsingFailedOnInvalidBool) { bool valueResourceType; PluginArgParser p("test_plugin"); p.addArgument("arg_bool", valueResourceType); Engine::PluginArgs inputArgs{{"arg_bool", "kidding"}}; auto res = p.parse(inputArgs); EXPECT_FALSE(res); EXPECT_THAT( std::string(res.error().what()), HasSubstr("Failed to parse argument \"arg_bool\", error")); } TEST(PluginArgParserTest, testArgParsingFailedWithUnkownArg) { int64_t valueLongInt; PluginArgParser p("test_plugin"); p.addArgument("arg", valueLongInt); // assert correct registered arg names std::unordered_set expectedArgNames{{"arg"}}; EXPECT_EQ(expectedArgNames, p.validArgNames()); Engine::PluginArgs inputArgs{{"arg", "1234"}, {"unknown_arg", "4321"}}; auto res = p.parse(inputArgs); EXPECT_FALSE(res); EXPECT_THAT( std::string(res.error().what()), HasSubstr( "Unknown arg \"unknown_arg\" in plugin \"test_plugin\": Invalid argument")); } TEST(PluginArgParserTest, testArgParsingFailedWithMissingRequired) { int64_t valueLongInt; int64_t anotherLongInt; PluginArgParser p("test_plugin"); p.addArgument("arg1", valueLongInt); p.addArgument("arg2", anotherLongInt, true); // assert correct registered arg names std::unordered_set expectedArgNames{{"arg1"}, {"arg2"}}; EXPECT_EQ(expectedArgNames, p.validArgNames()); Engine::PluginArgs inputArgs{{"arg1", "1234"}}; auto res = p.parse(inputArgs); EXPECT_FALSE(res); EXPECT_THAT( std::string(res.error().what()), HasSubstr( "Required arg \"arg2\" missing in plugin \"test_plugin\": Invalid argument")); } TEST(PluginArgParserTest, testArgParsingFailedWithInvalidValueStr) { int64_t valueLongInt; PluginArgParser p("test_plugin"); p.addArgument("bad_arg", valueLongInt); // assert correct registered arg names std::unordered_set expectedArgNames{{"bad_arg"}}; EXPECT_EQ(expectedArgNames, p.validArgNames()); Engine::PluginArgs inputArgs{{"bad_arg", "abcdefg"}}; auto res = p.parse(inputArgs); EXPECT_FALSE(res); EXPECT_THAT( std::string(res.error().what()), HasSubstr("Failed to parse argument \"bad_arg\", error")); } } // namespace Oomd oomd-0.5.0/src/oomd/util/ScopeGuard.h000066400000000000000000000026231406470533700173750ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include // Need two levels of indirection here for __LINE__ to correctly expand #define OOMD_CONCAT2(a, b) a##b #define OOMD_CONCAT(a, b) OOMD_CONCAT2(a, b) #define OOMD_ANON_VAR(str) OOMD_CONCAT(str, __LINE__) namespace Oomd { enum class ScopeGuardExit {}; class ScopeGuard { public: explicit ScopeGuard(std::function fn) { fn_ = fn; }; ~ScopeGuard() { if (fn_) { fn_(); } }; private: std::function fn_; }; inline ScopeGuard operator+(ScopeGuardExit, std::function fn) { return ScopeGuard(fn); } } // namespace Oomd #define OOMD_SCOPE_EXIT \ auto OOMD_ANON_VAR(SCOPE_EXIT_STATE) = ScopeGuardExit() + [&]() oomd-0.5.0/src/oomd/util/ScopeGuardTest.cpp000066400000000000000000000027761406470533700206010ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "oomd/util/ScopeGuard.h" #include using namespace Oomd; TEST(ScopeGuardTest, InnerScope) { int x = 5; { OOMD_SCOPE_EXIT { x++; }; } EXPECT_EQ(x, 6); } TEST(ScopeGuardTest, FunctionReturn) { int x = 5; [&]() { OOMD_SCOPE_EXIT { x++; }; x++; return; }(); EXPECT_EQ(x, 7); } TEST(ScopeGuardTest, ExceptionContext) { int x = 5; try { [&]() { OOMD_SCOPE_EXIT { x++; }; throw std::runtime_error("exception"); }(); } catch (const std::exception&) { } EXPECT_EQ(x, 6); } TEST(ScopeGuardTest, MultipleGuards) { int x = 5; { OOMD_SCOPE_EXIT { x++; }; OOMD_SCOPE_EXIT { x++; }; } EXPECT_EQ(x, 7); } oomd-0.5.0/src/oomd/util/SystemMaybe.h000066400000000000000000000141021406470533700175760ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include // This header provides SystemMaybe - a type which holds either // Value or a std::system_error intended to be used for return values // of functions that can have errors. namespace Oomd { // A Unit type to represent no value (void isn't able to be a value, // so use this instead as SystemMaybe instead of // SystemMaybe struct Unit {}; // This is an error type implicitly convertible to SystemMaybe so // that we can create errors without specifying the Value type and // then they are converted at return class SystemError final { std::system_error err_; public: SystemError() = default; SystemError(const SystemError&) = default; SystemError(SystemError&&) = default; SystemError& operator=(const SystemError&) = default; SystemError& operator=(SystemError&&) = default; SystemError(const std::system_error& err) : err_(err) {} SystemError(std::system_error&& err) : err_(std::move(err)) {} std::system_error& error() & { return err_; } const std::system_error& error() const& { return err_; } std::system_error&& error() && { return std::move(err_); } }; template class SystemMaybe final { using base_type = std::variant; base_type base_; public: SystemMaybe() = default; SystemMaybe(const SystemMaybe& other) = default; SystemMaybe(SystemMaybe&& other) = default; ~SystemMaybe() = default; /* * Assignment operators */ SystemMaybe& operator=(const SystemMaybe& that) = default; SystemMaybe& operator=(SystemMaybe&& that) = default; // Value constructors - allows for implicit conversion to SystemMaybe template < class Dummy = Value, typename = std::enable_if_t::value>> SystemMaybe(const Value& val) noexcept(noexcept(Value(val))) : base_(std::in_place_index_t<0>{}, val) {} template < class Dummy = Value, typename = std::enable_if_t::value>> SystemMaybe(Value&& val) noexcept(noexcept(Value(std::move(val)))) : base_(std::in_place_index_t<0>{}, std::move(val)) {} // Implicit conversions from the SystemError type SystemMaybe(const SystemError& err) : base_(std::in_place_index_t<1>{}, err.error()) {} SystemMaybe(SystemError&& err) : base_(std::in_place_index_t<1>{}, std::move(err.error())) {} SystemMaybe& operator=(const SystemError& err) { base_ = base_type(std::in_place_index_t<1>{}, err.error()); return *this; } SystemMaybe& operator=(SystemError&& err) { base_ = base_type(std::in_place_index_t<1>{}, std::move(err.error())); return *this; } // Everything else is just various accessors for Value or error constexpr const Value& value() const& { return std::get<0>(base_); } constexpr Value& value() & { return std::get<0>(base_); } constexpr Value& value() && { return std::get<0>(std::move(base_)); } constexpr const std::system_error& error() const& { return std::get<1>(base_); } constexpr std::system_error& error() & { return std::get<1>(base_); } constexpr std::system_error&& error() && { return std::get<1>(std::move(base_)); } constexpr explicit operator bool() const noexcept { return !base_.index(); } constexpr const Value& operator*() const& { return this->value(); } constexpr Value& operator*() & { return this->value(); } constexpr Value&& operator*() && { return std::move(this->value()); } constexpr const Value* operator->() const { return std::addressof(this->value()); } constexpr Value* operator->() { return std::addressof(this->value()); } }; // This all provides a method concatStrs which concatenates a // variadic number of arguments into a single string. This supports // any type that supports output to a stringstream namespace detail { template std::string concatStrs(Strs&&... strs) { std::ostringstream oss; (oss << ... << std::forward(strs)); return oss.str(); } } // namespace detail // These functions are likely to be the most common way to construct errors - // just pass in the error code and the rest of the arguments will be // concatenated into a string for the message template auto systemError(int err, Msg&&... msg) { return SystemError(std::system_error( err, std::system_category(), detail::concatStrs(std::forward(msg)...))); } template auto systemError(std::error_code ec, Msg&&... msg) { return SystemError( std::system_error(ec, detail::concatStrs(std::forward(msg)...))); } // This allows for error "chaining" to add additional context to an error template auto systemError(std::system_error err, Msg&&... msg) { std::string w = err.what(); w += " -- "; w += detail::concatStrs(std::forward(msg)...); return SystemError(std::system_error(err.code(), std::move(w))); } #define SYSTEM_ERROR(c, ...) \ ::Oomd::systemError(c, "[", __FILE__, ":", __LINE__, "] ", ##__VA_ARGS__) namespace { [[maybe_unused]] auto noSystemError() { return SystemMaybe(); } } // namespace #define ASSERT_SYS_OK(maybe) \ ({ \ auto x = (maybe); \ ASSERT_TRUE(x) << x.error().what(); \ std::move(*x); \ }) } // namespace Oomd oomd-0.5.0/src/oomd/util/SystemMaybeTest.cpp000066400000000000000000000035271406470533700210020ustar00rootroot00000000000000/* * Copyright (C) 2018-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "oomd/util/SystemMaybe.h" using namespace Oomd; namespace { SystemMaybe someFailingFunction() { return systemError(EBUSY, "something ", "is busy"); } SystemMaybe someFailingFunctionMacro() { return SYSTEM_ERROR(EBUSY); } SystemMaybe someSuccessfulFunction() { return 42; } } // namespace TEST(SystemMaybeTest, ErrorTest) { auto maybe = someFailingFunction(); ASSERT_FALSE(maybe); ASSERT_EQ(maybe.error().code().value(), EBUSY); ASSERT_NE( std::string(maybe.error().what()).find("something is busy"), std::string::npos); } TEST(SystemMaybeTest, ValueTest) { auto maybe = ASSERT_SYS_OK(someSuccessfulFunction()); ASSERT_EQ(maybe, 42); } TEST(SystemMaybeTest, noSystemErrorTest) { ASSERT_SYS_OK(noSystemError()); } TEST(SystemMaybeTest, MacroTest) { auto maybe = someFailingFunctionMacro(); ASSERT_FALSE(maybe); ASSERT_EQ(maybe.error().code().value(), EBUSY); } TEST(SystemMaybeTest, NonCopyableType) { SystemMaybe> foo = std::make_unique(3); auto v = ASSERT_SYS_OK(std::move(foo)); ASSERT_EQ(*v, 3); } oomd-0.5.0/src/oomd/util/TestHelper.h000066400000000000000000000127701406470533700174240ustar00rootroot00000000000000/* * Copyright (C) 2019-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include "oomd/CgroupContext.h" #include "oomd/OomdContext.h" #include "oomd/PluginRegistry.h" #include "oomd/engine/BasePlugin.h" #define ASSERT_EXISTS(opt_expr) \ ({ \ auto x = (opt_expr); \ ASSERT_TRUE(x.has_value()); \ std::move(*x); \ }) #define EXPECT_EXISTS(opt_expr) \ ({ \ auto x = (opt_expr); \ EXPECT_TRUE(x.has_value()); \ std::move(*x); \ }) namespace Oomd { /* * Friend of data classes to access their private fields for test injection. * This class must only be included in tests and not the main binary. */ class TestHelper { public: using CgroupData = CgroupContext::CgroupData; using CgroupArchivedData = CgroupContext::CgroupArchivedData; static CgroupData& getDataRef(const CgroupContext& cgroup_ctx) { return *cgroup_ctx.data_; } static std::unordered_map& getCgroupsRef( OomdContext& ctx) { return ctx.cgroups_; } /* * Set the cgroup data of a CgroupContext in OomdContext. * This is a shortcut for setting up CgroupContext without creating control * file fixtures. However, retrieving CgroupContext from OomdContext via * addToCacheAndGet still requires the requested CgroupPath exists, which * could be done using the Fixture utils. */ static void setCgroupData( OomdContext& ctx, const CgroupPath& cgroup, const CgroupData& data, const std::optional& archive = std::nullopt) { auto cgroup_ctx = CgroupContext::make(ctx, cgroup); if (cgroup_ctx.has_value()) { auto& cached_ctx = ctx.cgroups_.emplace(cgroup, std::move(*cgroup_ctx)).first->second; *cached_ctx.data_ = data; if (archive) { cached_ctx.archive_ = *archive; } } } }; /* * Define helper plugin for testing configs. * DEFINE_MOCK_PLUGIN(Foo) defines plugin class MockPlugin in an anonymous * namespace, which will be registered with "Foo". Each time the plugin is * executed, it increments the runCount indicated by the "id" plugin argument. * Use MockPlugin::runCounts() to get the reference of the run count map. */ #define DEFINE_MOCK_PLUGIN(plugin_name) \ namespace { \ class MockPlugin : public Engine::BasePlugin { \ public: \ int init( \ const Engine::PluginArgs& args, \ const PluginConstructionContext& /* unused */) override { \ if (auto pos = args.find("id"); pos != args.end()) { \ id_ = pos->second; \ } \ return 0; \ } \ Engine::PluginRet run(OomdContext& /* unused */) override { \ if (id_.size()) { \ runCounts()[id_]++; \ } \ return Engine::PluginRet::CONTINUE; \ } \ static MockPlugin* create() { \ return new MockPlugin(); \ } \ static Config2::IR::Plugin createIR(const std::string& id) { \ return Config2::IR::Plugin{.name = #plugin_name, .args = {{"id", id}}}; \ } \ static std::unordered_map& runCounts() { \ static std::unordered_map runCounts_; \ return runCounts_; \ } \ \ private: \ std::string id_; \ }; \ REGISTER_PLUGIN(plugin_name, MockPlugin::create); \ } } // namespace Oomd oomd-0.5.0/src/oomd/util/Util.cpp000066400000000000000000000120551406470533700166110ustar00rootroot00000000000000/* * Copyright (C) 2019-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "oomd/util/Util.h" #include #include #include #include #include #include static constexpr auto kWhitespaceChars = " \t\n\r"; namespace { void ltrim(std::string& s) { s.erase(0, s.find_first_not_of(kWhitespaceChars)); } void rtrim(std::string& s) { s.erase(s.find_last_not_of(kWhitespaceChars) + 1); } template ssize_t wrapFull(F f, int fd, T buf, size_t count) { ssize_t totalBytes = 0; ssize_t r; do { r = f(fd, buf, count); if (r == -1) { if (errno == EINTR) { continue; } return r; } totalBytes += r; buf += r; count -= r; } while (r != 0 && count); // 0 means EOF return totalBytes; } } // namespace namespace Oomd { int Util::parseSize(const std::string& input, int64_t* output) { bool is_neg = false; uint64_t size = 0; size_t pos = 0; auto istr = input; // lower case and get rid of spaces transform(istr.begin(), istr.end(), istr.begin(), tolower); auto new_end = std::remove_if(istr.begin(), istr.end(), isspace); istr.erase(new_end - istr.begin()); // pop off leading sign if (istr[0] == '+') { pos++; } else if (istr[0] == '-') { is_neg = true; pos++; } while (pos < istr.length()) { size_t unit_pos; size_t end_pos; unit_pos = istr.find_first_of("kmgt", pos); if (unit_pos == pos) { return -1; } if (unit_pos == std::string::npos) { unit_pos = istr.length(); } auto num = istr.substr(pos, unit_pos - pos); auto unit = istr.c_str()[unit_pos]; double v; try { v = std::stold(num, &end_pos); } catch (...) { return -1; } if (end_pos != num.length() || v < 0) { return -1; } switch (unit) { case '\0': break; case 'k': v *= 1ULL << 10; break; case 'm': v *= 1ULL << 20; break; case 'g': v *= 1ULL << 30; break; case 't': v *= 1ULL << 40; break; default: return -1; } size += v; pos = unit_pos + 1; } *output = is_neg ? -size : size; return 0; } int Util::parseSizeOrPercent( const std::string& input, int64_t* output, int64_t total) { try { if (input.size() > 0 && input.at(input.size() - 1) == '%') { int64_t pct = std::stoi(input.substr(0, input.size() - 1)); if (pct < 0 || pct > 100) { return -1; } *output = total * pct / 100; return 0; } else { int64_t v; size_t end_pos; // compat - a bare number is interpreted as megabytes v = std::stoll(input, &end_pos); if (end_pos == input.length()) { *output = v << 20; return 0; } if (Util::parseSize(input, &v) == 0) { *output = v; return 0; } return -1; } } catch (...) { return -1; } } std::vector Util::split(const std::string& line, char delim) { std::istringstream iss(line); std::string item; std::vector ret; while (std::getline(iss, item, delim)) { if (item.size()) { ret.push_back(std::move(item)); } } return ret; } bool Util::startsWith(const std::string& prefix, const std::string& to_search) { if (prefix.size() > to_search.size()) { return false; } for (size_t i = 0; i < prefix.size(); ++i) { if (prefix[i] != to_search[i]) { return false; } } return true; } void Util::trim(std::string& s) { rtrim(s); ltrim(s); } ssize_t Util::readFull(int fd, char* msg_buf, size_t count) { return wrapFull(::read, fd, msg_buf, count); } ssize_t Util::writeFull(int fd, const char* msg_buf, size_t count) { return wrapFull(::write, fd, msg_buf, count); } std::string Util::generateUuid() { unsigned int seed; try { seed = std::random_device{}(); } catch (const std::runtime_error&) { seed = std::random_device("/dev/urandom")(); } static std::mt19937 gen(seed); static std::uniform_int_distribution dis; // Combine two 64-bit numbers std::stringstream ss; ss << std::hex << dis(gen) << dis(gen); return ss.str(); } std::string Util::strerror_r() { std::array buf; buf[0] = '\0'; int savedErrno = errno; std::string ret(::strerror_r(errno, buf.data(), buf.size())); errno = savedErrno; return ret; } } // namespace Oomd oomd-0.5.0/src/oomd/util/Util.h000066400000000000000000000061361406470533700162610ustar00rootroot00000000000000/* * Copyright (C) 2019-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once #include #include #include #include namespace Oomd { /* * Class to hold various utility functions. * * NB: This file *cannot* take any dependency other than standard library * otherwise you will cause circular dependencies elsewhere in the codebase. */ class Util { public: /* * Parsing rules: * * "1.5G" : 1.5 gigabytes, outputs 1610612736 bytes * "1G 128M" : 1 gigabyte and 128 megabytes, outputs 1207959552 bytes * "4K 2048" : 4 kilobytes and 2048 bytes, outputs 6144 bytes * * Supports [kmgt] suffixes. * * @returns 0 on success, -1 on failure. Parsed output is passed out * through @param output */ static int parseSize(const std::string& input, int64_t* output); /* * Handles the following 3 cases and returns the first valid result: * * 1) `%` and returns the % of @param total * 2) `` and returns the value in bytes (parsed as megabytes) * 3) `suffix` and acts the same as @function parseSize * * @returns 0 on success, -1 on failure. Parsed output is passed out * through @param output */ static int parseSizeOrPercent(const std::string& input, int64_t* output, int64_t total); /* Split string into tokens by delim */ static std::vector split(const std::string& line, char delim); static bool startsWith( const std::string& prefix, const std::string& to_search); /* Trim spaces from a string */ static void trim(std::string& s); template static std::vector filter(std::vector elems, Functor&& fn) { std::vector ret; std::copy_if( elems.begin(), elems.end(), std::back_inserter(ret), std::forward(fn)); return ret; } /* * Read and write helpers * * Note that this is not an atomic operation. Multiple syscalls may be * issued to complete this request, and if any syscall fails, the API * will return -1. * * @returns @param count on success, -1 on failure. Will never return * any values other than those two. */ static ssize_t readFull(int fd, char* msg_buf, size_t count); static ssize_t writeFull(int fd, const char* msg_buf, size_t count); /* * Returned uuids are random 32 char hex strings */ static std::string generateUuid(); static std::string strerror_r(); }; } // namespace Oomd oomd-0.5.0/src/oomd/util/UtilTest.cpp000066400000000000000000000074351406470533700174570ustar00rootroot00000000000000/* * Copyright (C) 2019-present, Facebook, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include "oomd/util/Util.h" using namespace Oomd; using namespace testing; TEST(UtilTest, ParseSizeTest) { int64_t v; EXPECT_EQ(Util::parseSize("8192", &v), 0); EXPECT_EQ(v, 8192); EXPECT_EQ(Util::parseSize("8K", &v), 0); EXPECT_EQ(v, 8192); EXPECT_EQ(Util::parseSize("1.5M 32K 512", &v), 0); EXPECT_EQ(v, (1 << 20) * 3 / 2 + (1 << 10) * 32 + 512); EXPECT_EQ(Util::parseSize("1.5MK", &v), -1); EXPECT_EQ(Util::parseSize("??", &v), -1); EXPECT_EQ(Util::parseSize("+-123M", &v), -1); } TEST(UtilTest, ParseSizeOrPercentTest) { int64_t v; EXPECT_EQ(Util::parseSizeOrPercent("1%", &v, 100), 0); EXPECT_EQ(v, 1); EXPECT_EQ(Util::parseSizeOrPercent("5M", &v, 100), 0); EXPECT_EQ(v, 5 << 20); EXPECT_EQ(Util::parseSizeOrPercent("5", &v, 100), 0); EXPECT_EQ(v, 5 << 20); EXPECT_EQ(Util::parseSizeOrPercent("5K", &v, 100), 0); EXPECT_EQ(v, 5 << 10); EXPECT_NE(Util::parseSizeOrPercent("5%z", &v, 100), 0); } TEST(UtilTest, Split) { auto toks = Util::split("one by two", ' '); ASSERT_EQ(toks.size(), 3); EXPECT_THAT(toks, Contains("one")); EXPECT_THAT(toks, Contains("by")); EXPECT_THAT(toks, Contains("two")); toks = Util::split(" by two", ' '); ASSERT_EQ(toks.size(), 2); EXPECT_THAT(toks, Contains("by")); EXPECT_THAT(toks, Contains("two")); toks = Util::split(" by two", ' '); ASSERT_EQ(toks.size(), 2); EXPECT_THAT(toks, Contains("by")); EXPECT_THAT(toks, Contains("two")); toks = Util::split("one two three", ','); ASSERT_EQ(toks.size(), 1); EXPECT_EQ(toks[0], "one two three"); } TEST(UtilTest, StartsWith) { EXPECT_TRUE(Util::startsWith("prefix", "prefixThis!")); EXPECT_TRUE(Util::startsWith("x", "xx")); EXPECT_TRUE(Util::startsWith("", "xx")); EXPECT_TRUE(Util::startsWith("", "")); EXPECT_FALSE(Util::startsWith("prefix", "prefiyThat!")); EXPECT_FALSE(Util::startsWith("xx", "x")); EXPECT_FALSE(Util::startsWith("x", "")); } TEST(UtilTest, Trim) { std::string s = " sdf "; Util::trim(s); EXPECT_EQ(s, "sdf"); std::string s1 = " as df "; Util::trim(s1); EXPECT_EQ(s1, "as df"); std::string s2 = " asdf"; Util::trim(s2); EXPECT_EQ(s2, "asdf"); std::string s3 = "asdf "; Util::trim(s3); EXPECT_EQ(s3, "asdf"); std::string s4 = "asdf"; Util::trim(s4); EXPECT_EQ(s4, "asdf"); std::string s5 = ""; Util::trim(s5); EXPECT_EQ(s5, ""); std::string s6 = " \t \n"; Util::trim(s6); EXPECT_EQ(s6, ""); } TEST(UtilTest, ReadWriteFull) { int fd = ::syscall(SYS_memfd_create, "myfile", MFD_CLOEXEC); ASSERT_GE(fd, 0); // Write a bunch of data in std::string start(1234567, 'z'); EXPECT_EQ(Util::writeFull(fd, start.data(), start.size()), start.size()); // Seek back to beginning ::lseek(fd, 0, SEEK_SET); // Read data back out std::string end(12345678, 'x'); // Note the extra 8 EXPECT_EQ(Util::readFull(fd, end.data(), start.size()), start.size()); EXPECT_EQ(std::memcmp(start.data(), end.data(), start.size()), 0); } oomd-0.5.0/vcs_tagger.sh000077500000000000000000000004061406470533700151460ustar00rootroot00000000000000#!/bin/bash set -euo pipefail dir="$1" fallback="$2" # Go into provided dir so builds started from outside the project # directory still generate the right tags cd "$dir" &>/dev/null && git describe --abbrev=7 --dirty --tags 2>/dev/null || echo "$fallback"