diff --git a/docs/TRANSIENT-SETTINGS.md b/docs/TRANSIENT-SETTINGS.md
index 5037060254..9f69a3162a 100644
--- a/docs/TRANSIENT-SETTINGS.md
+++ b/docs/TRANSIENT-SETTINGS.md
@@ -273,6 +273,7 @@ All cgroup/resource control settings are available for transient units
✓ ManagedOOMSwap=
✓ ManagedOOMMemoryPressure=
✓ ManagedOOMMemoryPressureLimit=
+✓ ManagedOOMPreference=
```
## Process Killing Settings
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index 7543a617b7..1d419ac495 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -2450,6 +2450,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
readonly s ManagedOOMMemoryPressure = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly u ManagedOOMMemoryPressureLimitPermyriad = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly s ManagedOOMPreference = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -2974,6 +2976,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
@@ -3538,6 +3542,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
@@ -4204,6 +4210,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
readonly s ManagedOOMMemoryPressure = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly u ManagedOOMMemoryPressureLimitPermyriad = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly s ManagedOOMPreference = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -4756,6 +4764,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -5318,6 +5328,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -5897,6 +5909,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
readonly s ManagedOOMMemoryPressure = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly u ManagedOOMMemoryPressureLimitPermyriad = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly s ManagedOOMPreference = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -6377,6 +6391,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
@@ -6857,6 +6873,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
@@ -7557,6 +7575,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
readonly s ManagedOOMMemoryPressure = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly u ManagedOOMMemoryPressureLimitPermyriad = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly s ManagedOOMPreference = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -8023,6 +8043,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
@@ -8489,6 +8511,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
@@ -9042,6 +9066,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
readonly s ManagedOOMMemoryPressure = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly u ManagedOOMMemoryPressureLimitPermyriad = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly s ManagedOOMPreference = '...';
};
interface org.freedesktop.DBus.Peer { ... };
interface org.freedesktop.DBus.Introspectable { ... };
@@ -9178,6 +9204,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
+
+
@@ -9318,6 +9346,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
+
+
@@ -9477,6 +9507,8 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope {
readonly s ManagedOOMMemoryPressure = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly u ManagedOOMMemoryPressureLimitPermyriad = ...;
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly s ManagedOOMPreference = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s KillMode = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -9629,6 +9661,8 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope {
+
+
@@ -9795,6 +9829,8 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope {
+
+
diff --git a/man/systemd.resource-control.xml b/man/systemd.resource-control.xml
index be9c35057d..6d15540377 100644
--- a/man/systemd.resource-control.xml
+++ b/man/systemd.resource-control.xml
@@ -913,6 +913,38 @@ DeviceAllow=/dev/loop-control
+
+
+ ManagedOOMPreference=none|avoid|omit
+
+
+ Allows deprioritizing or omitting this unit's cgroup as a candidate when systemd-oomd
+ needs to act. Requires support for extended attributes (see
+ xattr7)
+ in order to use or . Additionally, systemd-oomd
+ will ignore these extended attributes if the unit's cgroup is not owned by the root user.
+
+ If this property is set to , the service manager will set the
+ "user.oomd_avoid" extended attribute on the unit's cgroup to "1". If systemd-oomd sees
+ this extended attribute on a cgroup set to "1" when choosing between candidates, it will only select the
+ cgroup with "user.oomd_avoid" if there are no other viable candidates.
+
+ If this property is set to , the service manager will set the "user.oomd_omit"
+ extended attribute on the unit's cgroup to "1". If systemd-oomd sees the this extended
+ attribute on the cgroup set to "1", it will ignore the cgroup as a candidate and will not perform any actions
+ on the cgroup.
+
+ It is recommended to use and sparingly as it can adversely
+ affect systemd-oomd's kill behavior. Also note that these extended attributes are not
+ applied recursively to cgroups under this unit's cgroup.
+
+ Defaults to which means no extended attributes will be set and systemd-oomd will
+ sort this unit's cgroup as defined in
+ systemd-oomd.service8
+ and oomd.conf5 (if this
+ unit's cgroup becomes a candidate).
+
+
diff --git a/src/basic/cgroup-util.c b/src/basic/cgroup-util.c
index bb20a12294..3c59ca5e10 100644
--- a/src/basic/cgroup-util.c
+++ b/src/basic/cgroup-util.c
@@ -635,6 +635,20 @@ int cg_get_xattr_malloc(const char *controller, const char *path, const char *na
return r;
}
+int cg_get_xattr_bool(const char *controller, const char *path, const char *name) {
+ _cleanup_free_ char *val = NULL;
+ int r;
+
+ assert(path);
+ assert(name);
+
+ r = cg_get_xattr_malloc(controller, path, name, &val);
+ if (r < 0)
+ return r;
+
+ return parse_boolean(val);
+}
+
int cg_remove_xattr(const char *controller, const char *path, const char *name) {
_cleanup_free_ char *fs = NULL;
int r;
@@ -1703,6 +1717,25 @@ int cg_get_attribute_as_bool(const char *controller, const char *path, const cha
return 0;
}
+int cg_get_owner(const char *controller, const char *path, uid_t *ret_uid) {
+ _cleanup_free_ char *f = NULL;
+ struct stat stats;
+ int r;
+
+ assert(ret_uid);
+
+ r = cg_get_path(controller, path, NULL, &f);
+ if (r < 0)
+ return r;
+
+ r = stat(f, &stats);
+ if (r < 0)
+ return -errno;
+
+ *ret_uid = stats.st_uid;
+ return 0;
+}
+
int cg_get_keyed_attribute_full(
const char *controller,
const char *path,
@@ -2185,3 +2218,11 @@ static const char* const managed_oom_mode_table[_MANAGED_OOM_MODE_MAX] = {
};
DEFINE_STRING_TABLE_LOOKUP(managed_oom_mode, ManagedOOMMode);
+
+static const char* const managed_oom_preference_table[_MANAGED_OOM_PREFERENCE_MAX] = {
+ [MANAGED_OOM_PREFERENCE_NONE] = "none",
+ [MANAGED_OOM_PREFERENCE_AVOID] = "avoid",
+ [MANAGED_OOM_PREFERENCE_OMIT] = "omit",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(managed_oom_preference, ManagedOOMPreference);
diff --git a/src/basic/cgroup-util.h b/src/basic/cgroup-util.h
index c5f54aafea..420ddb59c0 100644
--- a/src/basic/cgroup-util.h
+++ b/src/basic/cgroup-util.h
@@ -212,10 +212,13 @@ int cg_get_attribute_as_uint64(const char *controller, const char *path, const c
int cg_get_attribute_as_bool(const char *controller, const char *path, const char *attribute, bool *ret);
int cg_set_access(const char *controller, const char *path, uid_t uid, gid_t gid);
+int cg_get_owner(const char *controller, const char *path, uid_t *ret_uid);
int cg_set_xattr(const char *controller, const char *path, const char *name, const void *value, size_t size, int flags);
int cg_get_xattr(const char *controller, const char *path, const char *name, void *value, size_t size);
int cg_get_xattr_malloc(const char *controller, const char *path, const char *name, char **ret);
+/* Returns negative on error, and 0 or 1 on success for the bool value */
+int cg_get_xattr_bool(const char *controller, const char *path, const char *name);
int cg_remove_xattr(const char *controller, const char *path, const char *name);
int cg_install_release_agent(const char *controller, const char *agent);
@@ -288,3 +291,14 @@ typedef enum ManagedOOMMode {
const char* managed_oom_mode_to_string(ManagedOOMMode m) _const_;
ManagedOOMMode managed_oom_mode_from_string(const char *s) _pure_;
+
+typedef enum ManagedOOMPreference {
+ MANAGED_OOM_PREFERENCE_NONE = 0,
+ MANAGED_OOM_PREFERENCE_AVOID = 1,
+ MANAGED_OOM_PREFERENCE_OMIT = 2,
+ _MANAGED_OOM_PREFERENCE_MAX,
+ _MANAGED_OOM_PREFERENCE_INVALID = -1
+} ManagedOOMPreference;
+
+const char* managed_oom_preference_to_string(ManagedOOMPreference a) _const_;
+ManagedOOMPreference managed_oom_preference_from_string(const char *s) _pure_;
diff --git a/src/core/cgroup.c b/src/core/cgroup.c
index c9cf7fb16c..b0fc67a125 100644
--- a/src/core/cgroup.c
+++ b/src/core/cgroup.c
@@ -131,6 +131,7 @@ void cgroup_context_init(CGroupContext *c) {
.moom_swap = MANAGED_OOM_AUTO,
.moom_mem_pressure = MANAGED_OOM_AUTO,
+ .moom_preference = MANAGED_OOM_PREFERENCE_NONE,
};
}
@@ -417,7 +418,8 @@ void cgroup_context_dump(Unit *u, FILE* f, const char *prefix) {
"%sDelegate: %s\n"
"%sManagedOOMSwap: %s\n"
"%sManagedOOMMemoryPressure: %s\n"
- "%sManagedOOMMemoryPressureLimit: %" PRIu32 ".%02" PRIu32 "%%\n",
+ "%sManagedOOMMemoryPressureLimit: %" PRIu32 ".%02" PRIu32 "%%\n"
+ "%sManagedOOMPreference: %s%%\n",
prefix, yes_no(c->cpu_accounting),
prefix, yes_no(c->io_accounting),
prefix, yes_no(c->blockio_accounting),
@@ -450,7 +452,8 @@ void cgroup_context_dump(Unit *u, FILE* f, const char *prefix) {
prefix, yes_no(c->delegate),
prefix, managed_oom_mode_to_string(c->moom_swap),
prefix, managed_oom_mode_to_string(c->moom_mem_pressure),
- prefix, c->moom_mem_pressure_limit_permyriad / 100, c->moom_mem_pressure_limit_permyriad % 100);
+ prefix, c->moom_mem_pressure_limit_permyriad / 100, c->moom_mem_pressure_limit_permyriad % 100,
+ prefix, managed_oom_preference_to_string(c->moom_preference));
if (c->delegate) {
_cleanup_free_ char *t = NULL;
@@ -600,6 +603,41 @@ int cgroup_add_device_allow(CGroupContext *c, const char *dev, const char *mode)
UNIT_DEFINE_ANCESTOR_MEMORY_LOOKUP(memory_low);
UNIT_DEFINE_ANCESTOR_MEMORY_LOOKUP(memory_min);
+void cgroup_oomd_xattr_apply(Unit *u, const char *cgroup_path) {
+ CGroupContext *c;
+ int r;
+
+ assert(u);
+
+ c = unit_get_cgroup_context(u);
+ if (!c)
+ return;
+
+ if (c->moom_preference == MANAGED_OOM_PREFERENCE_OMIT) {
+ r = cg_set_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup_path, "user.oomd_omit", "1", 1, 0);
+ if (r < 0)
+ log_unit_debug_errno(u, r, "Failed to set oomd_omit flag on control group %s, ignoring: %m", cgroup_path);
+ }
+
+ if (c->moom_preference == MANAGED_OOM_PREFERENCE_AVOID) {
+ r = cg_set_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup_path, "user.oomd_avoid", "1", 1, 0);
+ if (r < 0)
+ log_unit_debug_errno(u, r, "Failed to set oomd_avoid flag on control group %s, ignoring: %m", cgroup_path);
+ }
+
+ if (c->moom_preference != MANAGED_OOM_PREFERENCE_AVOID) {
+ r = cg_remove_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup_path, "user.oomd_avoid");
+ if (r != -ENODATA)
+ log_unit_debug_errno(u, r, "Failed to remove oomd_avoid flag on control group %s, ignoring: %m", cgroup_path);
+ }
+
+ if (c->moom_preference != MANAGED_OOM_PREFERENCE_OMIT) {
+ r = cg_remove_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup_path, "user.oomd_omit");
+ if (r != -ENODATA)
+ log_unit_debug_errno(u, r, "Failed to remove oomd_omit flag on control group %s, ignoring: %m", cgroup_path);
+ }
+}
+
static void cgroup_xattr_apply(Unit *u) {
char ids[SD_ID128_STRING_MAX];
int r;
@@ -630,6 +668,8 @@ static void cgroup_xattr_apply(Unit *u) {
if (r != -ENODATA)
log_unit_debug_errno(u, r, "Failed to remove delegate flag on control group %s, ignoring: %m", u->cgroup_path);
}
+
+ cgroup_oomd_xattr_apply(u, u->cgroup_path);
}
static int lookup_block_device(const char *p, dev_t *ret) {
@@ -2746,7 +2786,7 @@ int unit_check_oomd_kill(Unit *u) {
else if (r == 0)
return 0;
- r = cg_get_xattr_malloc(SYSTEMD_CGROUP_CONTROLLER, u->cgroup_path, "user.systemd_oomd_kill", &value);
+ r = cg_get_xattr_malloc(SYSTEMD_CGROUP_CONTROLLER, u->cgroup_path, "user.oomd_kill", &value);
if (r < 0 && r != -ENODATA)
return r;
@@ -3737,12 +3777,6 @@ int unit_cgroup_freezer_action(Unit *u, FreezerAction action) {
return 1;
}
-static const char* const cgroup_device_policy_table[_CGROUP_DEVICE_POLICY_MAX] = {
- [CGROUP_DEVICE_POLICY_AUTO] = "auto",
- [CGROUP_DEVICE_POLICY_CLOSED] = "closed",
- [CGROUP_DEVICE_POLICY_STRICT] = "strict",
-};
-
int unit_get_cpuset(Unit *u, CPUSet *cpus, const char *name) {
_cleanup_free_ char *v = NULL;
int r;
@@ -3771,6 +3805,12 @@ int unit_get_cpuset(Unit *u, CPUSet *cpus, const char *name) {
return parse_cpu_set_full(v, cpus, false, NULL, NULL, 0, NULL);
}
+static const char* const cgroup_device_policy_table[_CGROUP_DEVICE_POLICY_MAX] = {
+ [CGROUP_DEVICE_POLICY_AUTO] = "auto",
+ [CGROUP_DEVICE_POLICY_CLOSED] = "closed",
+ [CGROUP_DEVICE_POLICY_STRICT] = "strict",
+};
+
DEFINE_STRING_TABLE_LOOKUP(cgroup_device_policy, CGroupDevicePolicy);
static const char* const freezer_action_table[_FREEZER_ACTION_MAX] = {
diff --git a/src/core/cgroup.h b/src/core/cgroup.h
index 9f225d934b..8183551420 100644
--- a/src/core/cgroup.h
+++ b/src/core/cgroup.h
@@ -164,6 +164,7 @@ struct CGroupContext {
ManagedOOMMode moom_swap;
ManagedOOMMode moom_mem_pressure;
uint32_t moom_mem_pressure_limit_permyriad;
+ ManagedOOMPreference moom_preference;
};
/* Used when querying IP accounting data */
@@ -204,6 +205,8 @@ void cgroup_context_free_blockio_device_bandwidth(CGroupContext *c, CGroupBlockI
int cgroup_add_device_allow(CGroupContext *c, const char *dev, const char *mode);
+void cgroup_oomd_xattr_apply(Unit *u, const char *cgroup_path);
+
CGroupMask unit_get_own_mask(Unit *u);
CGroupMask unit_get_delegate_mask(Unit *u);
CGroupMask unit_get_members_mask(Unit *u);
diff --git a/src/core/dbus-cgroup.c b/src/core/dbus-cgroup.c
index a8b438cc82..2b8b833fe3 100644
--- a/src/core/dbus-cgroup.c
+++ b/src/core/dbus-cgroup.c
@@ -21,6 +21,7 @@ BUS_DEFINE_PROPERTY_GET(bus_property_get_tasks_max, "t", TasksMax, tasks_max_res
static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_cgroup_device_policy, cgroup_device_policy, CGroupDevicePolicy);
static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_managed_oom_mode, managed_oom_mode, ManagedOOMMode);
+static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_managed_oom_preference, managed_oom_preference, ManagedOOMPreference);
static int property_get_cgroup_mask(
sd_bus *bus,
@@ -395,6 +396,7 @@ const sd_bus_vtable bus_cgroup_vtable[] = {
SD_BUS_PROPERTY("ManagedOOMSwap", "s", property_get_managed_oom_mode, offsetof(CGroupContext, moom_swap), 0),
SD_BUS_PROPERTY("ManagedOOMMemoryPressure", "s", property_get_managed_oom_mode, offsetof(CGroupContext, moom_mem_pressure), 0),
SD_BUS_PROPERTY("ManagedOOMMemoryPressureLimitPermyriad", "u", NULL, offsetof(CGroupContext, moom_mem_pressure_limit_permyriad), 0),
+ SD_BUS_PROPERTY("ManagedOOMPreference", "s", property_get_managed_oom_preference, offsetof(CGroupContext, moom_preference), 0),
SD_BUS_VTABLE_END
};
@@ -1720,6 +1722,26 @@ int bus_cgroup_set_property(
return 1;
}
+ if (streq(name, "ManagedOOMPreference")) {
+ ManagedOOMPreference p;
+ const char *pref;
+
+ r = sd_bus_message_read(message, "s", &pref);
+ if (r < 0)
+ return r;
+
+ p = managed_oom_preference_from_string(pref);
+ if (p < 0)
+ return -EINVAL;
+
+ if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
+ c->moom_preference = p;
+ unit_write_settingf(u, flags, name, "ManagedOOMPreference=%s", pref);
+ }
+
+ return 1;
+ }
+
if (streq(name, "DisableControllers") || (u->transient && u->load_state == UNIT_STUB))
return bus_cgroup_set_transient_property(u, c, name, message, flags, error);
diff --git a/src/core/execute.c b/src/core/execute.c
index 29fe9f05b1..2c35a917f8 100644
--- a/src/core/execute.c
+++ b/src/core/execute.c
@@ -4706,6 +4706,10 @@ int exec_spawn(Unit *unit,
r = cg_create(SYSTEMD_CGROUP_CONTROLLER, subcgroup_path);
if (r < 0)
return log_unit_error_errno(unit, r, "Failed to create control group '%s': %m", subcgroup_path);
+
+ /* Normally we would not propagate the oomd xattrs to children but since we created this
+ * sub-cgroup internally we should do it. */
+ cgroup_oomd_xattr_apply(unit, subcgroup_path);
}
}
diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4
index be07c7604c..b0460e2764 100644
--- a/src/core/load-fragment-gperf.gperf.m4
+++ b/src/core/load-fragment-gperf.gperf.m4
@@ -230,6 +230,7 @@ $1.IPEgressFilterPath, config_parse_ip_filter_bpf_progs,
$1.ManagedOOMSwap, config_parse_managed_oom_mode, 0, offsetof($1, cgroup_context.moom_swap)
$1.ManagedOOMMemoryPressure, config_parse_managed_oom_mode, 0, offsetof($1, cgroup_context.moom_mem_pressure)
$1.ManagedOOMMemoryPressureLimit, config_parse_managed_oom_mem_pressure_limit, 0, offsetof($1, cgroup_context.moom_mem_pressure_limit_permyriad)
+$1.ManagedOOMPreference, config_parse_managed_oom_preference, 0, offsetof($1, cgroup_context.moom_preference)
$1.NetClass, config_parse_warn_compat, DISABLED_LEGACY, 0'
)m4_dnl
Unit.Description, config_parse_unit_string_printf, 0, offsetof(Unit, description)
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
index 410168d4d6..d0aeaf1c76 100644
--- a/src/core/load-fragment.c
+++ b/src/core/load-fragment.c
@@ -133,6 +133,7 @@ DEFINE_CONFIG_PARSE_ENUM(config_parse_service_restart, service_restart, ServiceR
DEFINE_CONFIG_PARSE_ENUM(config_parse_service_timeout_failure_mode, service_timeout_failure_mode, ServiceTimeoutFailureMode, "Failed to parse timeout failure mode");
DEFINE_CONFIG_PARSE_ENUM(config_parse_socket_bind, socket_address_bind_ipv6_only_or_bool, SocketAddressBindIPv6Only, "Failed to parse bind IPv6 only value");
DEFINE_CONFIG_PARSE_ENUM(config_parse_oom_policy, oom_policy, OOMPolicy, "Failed to parse OOM policy");
+DEFINE_CONFIG_PARSE_ENUM(config_parse_managed_oom_preference, managed_oom_preference, ManagedOOMPreference, "Failed to parse ManagedOOMPreference=");
DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(config_parse_ip_tos, ip_tos, int, -1, "Failed to parse IP TOS value");
DEFINE_CONFIG_PARSE_PTR(config_parse_blockio_weight, cg_blkio_weight_parse, uint64_t, "Invalid block IO weight");
DEFINE_CONFIG_PARSE_PTR(config_parse_cg_weight, cg_weight_parse, uint64_t, "Invalid weight");
diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h
index 6b2175cd2a..e4a5cb7986 100644
--- a/src/core/load-fragment.h
+++ b/src/core/load-fragment.h
@@ -78,6 +78,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_tasks_max);
CONFIG_PARSER_PROTOTYPE(config_parse_delegate);
CONFIG_PARSER_PROTOTYPE(config_parse_managed_oom_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_managed_oom_mem_pressure_limit);
+CONFIG_PARSER_PROTOTYPE(config_parse_managed_oom_preference);
CONFIG_PARSER_PROTOTYPE(config_parse_device_policy);
CONFIG_PARSER_PROTOTYPE(config_parse_device_allow);
CONFIG_PARSER_PROTOTYPE(config_parse_io_device_latency);
diff --git a/src/oom/oomd-manager.c b/src/oom/oomd-manager.c
index 52158a66e5..94b17dd07b 100644
--- a/src/oom/oomd-manager.c
+++ b/src/oom/oomd-manager.c
@@ -93,7 +93,7 @@ static int process_managed_oom_reply(
m->monitored_swap_cgroup_contexts : m->monitored_mem_pressure_cgroup_contexts;
if (reply.mode == MANAGED_OOM_AUTO) {
- (void) oomd_cgroup_context_free(hashmap_remove(monitor_hm, reply.path));
+ (void) oomd_cgroup_context_free(hashmap_remove(monitor_hm, empty_to_root(reply.path)));
continue;
}
@@ -109,7 +109,7 @@ static int process_managed_oom_reply(
}
}
- ret = oomd_insert_cgroup_context(NULL, monitor_hm, reply.path);
+ ret = oomd_insert_cgroup_context(NULL, monitor_hm, empty_to_root(reply.path));
if (ret == -ENOMEM) {
r = ret;
goto finish;
@@ -117,7 +117,7 @@ static int process_managed_oom_reply(
/* Always update the limit in case it was changed. For non-memory pressure detection the value is
* ignored so always updating it here is not a problem. */
- ctx = hashmap_get(monitor_hm, reply.path);
+ ctx = hashmap_get(monitor_hm, empty_to_root(reply.path));
if (ctx)
ctx->mem_pressure_limit = limit;
}
diff --git a/src/oom/oomd-util.c b/src/oom/oomd-util.c
index fcccddb92e..9dd9b17c6d 100644
--- a/src/oom/oomd-util.c
+++ b/src/oom/oomd-util.c
@@ -3,7 +3,6 @@
#include
#include
-#include "cgroup-util.h"
#include "fd-util.h"
#include "format-util.h"
#include "oomd-util.h"
@@ -159,7 +158,8 @@ int oomd_sort_cgroup_contexts(Hashmap *h, oomd_compare_t compare_func, const cha
return -ENOMEM;
HASHMAP_FOREACH(item, h) {
- if (item->path && prefix && !path_startswith(item->path, prefix))
+ /* Skip over cgroups that are not valid candidates or are explicitly marked for omission */
+ if ((item->path && prefix && !path_startswith(item->path, prefix)) || item->preference == MANAGED_OOM_PREFERENCE_OMIT)
continue;
sorted[k++] = item;
@@ -201,9 +201,9 @@ int oomd_cgroup_kill(const char *path, bool recurse, bool dry_run) {
if (r < 0)
return r;
- r = increment_oomd_xattr(path, "user.systemd_oomd_kill", set_size(pids_killed));
+ r = increment_oomd_xattr(path, "user.oomd_kill", set_size(pids_killed));
if (r < 0)
- log_debug_errno(r, "Failed to set user.systemd_oomd_kill on kill: %m");
+ log_debug_errno(r, "Failed to set user.oomd_kill on kill: %m");
return set_size(pids_killed) != 0;
}
@@ -214,13 +214,15 @@ int oomd_kill_by_pgscan(Hashmap *h, const char *prefix, bool dry_run) {
assert(h);
- r = oomd_sort_cgroup_contexts(h, compare_pgscan, prefix, &sorted);
+ r = oomd_sort_cgroup_contexts(h, compare_pgscan_and_memory_usage, prefix, &sorted);
if (r < 0)
return r;
for (int i = 0; i < r; i++) {
- if (sorted[i]->pgscan == 0)
- break;
+ /* Skip cgroups with no reclaim and memory usage; it won't alleviate pressure. */
+ /* Don't break since there might be "avoid" cgroups at the end. */
+ if (sorted[i]->pgscan == 0 && sorted[i]->current_memory_usage == 0)
+ continue;
r = oomd_cgroup_kill(sorted[i]->path, true, dry_run);
if (r > 0 || r == -ENOMEM)
@@ -243,8 +245,10 @@ int oomd_kill_by_swap_usage(Hashmap *h, bool dry_run) {
/* Try to kill cgroups with non-zero swap usage until we either succeed in
* killing or we get to a cgroup with no swap usage. */
for (int i = 0; i < r; i++) {
+ /* Skip over cgroups with no resource usage. Don't break since there might be "avoid"
+ * cgroups at the end. */
if (sorted[i]->swap_usage == 0)
- break;
+ continue;
r = oomd_cgroup_kill(sorted[i]->path, true, dry_run);
if (r > 0 || r == -ENOMEM)
@@ -258,6 +262,7 @@ int oomd_cgroup_context_acquire(const char *path, OomdCGroupContext **ret) {
_cleanup_(oomd_cgroup_context_freep) OomdCGroupContext *ctx = NULL;
_cleanup_free_ char *p = NULL, *val = NULL;
bool is_root;
+ uid_t uid;
int r;
assert(path);
@@ -268,6 +273,7 @@ int oomd_cgroup_context_acquire(const char *path, OomdCGroupContext **ret) {
return -ENOMEM;
is_root = empty_or_root(path);
+ ctx->preference = MANAGED_OOM_PREFERENCE_NONE;
r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, path, "memory.pressure", &p);
if (r < 0)
@@ -277,6 +283,23 @@ int oomd_cgroup_context_acquire(const char *path, OomdCGroupContext **ret) {
if (r < 0)
return log_debug_errno(r, "Error parsing memory pressure from %s: %m", p);
+ r = cg_get_owner(SYSTEMD_CGROUP_CONTROLLER, path, &uid);
+ if (r < 0)
+ log_debug_errno(r, "Failed to get owner/group from %s: %m", path);
+ else if (uid == 0) {
+ /* Ignore most errors when reading the xattr since it is usually unset and cgroup xattrs are only used
+ * as an optional feature of systemd-oomd (and the system might not even support them). */
+ r = cg_get_xattr_bool(SYSTEMD_CGROUP_CONTROLLER, path, "user.oomd_avoid");
+ if (r == -ENOMEM)
+ return r;
+ ctx->preference = r == 1 ? MANAGED_OOM_PREFERENCE_AVOID : ctx->preference;
+
+ r = cg_get_xattr_bool(SYSTEMD_CGROUP_CONTROLLER, path, "user.oomd_omit");
+ if (r == -ENOMEM)
+ return r;
+ ctx->preference = r == 1 ? MANAGED_OOM_PREFERENCE_OMIT : ctx->preference;
+ }
+
if (is_root) {
r = procfs_memory_get_used(&ctx->current_memory_usage);
if (r < 0)
diff --git a/src/oom/oomd-util.h b/src/oom/oomd-util.h
index d7a9890e7a..bffccf75da 100644
--- a/src/oom/oomd-util.h
+++ b/src/oom/oomd-util.h
@@ -3,6 +3,7 @@
#include
+#include "cgroup-util.h"
#include "hashmap.h"
#include "psi-util.h"
@@ -29,6 +30,8 @@ struct OomdCGroupContext {
uint64_t last_pgscan;
uint64_t pgscan;
+ ManagedOOMPreference preference;
+
/* These are only used by oomd_pressure_above for acting on high memory pressure. */
loadavg_t mem_pressure_limit;
usec_t mem_pressure_duration_usec;
@@ -61,17 +64,35 @@ bool oomd_memory_reclaim(Hashmap *h);
/* Returns true if the amount of swap free is below the percentage of swap specified by `threshold_percent`. */
bool oomd_swap_free_below(const OomdSystemContext *ctx, uint64_t threshold_percent);
-static inline int compare_pgscan(OomdCGroupContext * const *c1, OomdCGroupContext * const *c2) {
+/* The compare functions will sort from largest to smallest, putting all the contexts with "avoid" at the end
+ * (after the smallest values). */
+static inline int compare_pgscan_and_memory_usage(OomdCGroupContext * const *c1, OomdCGroupContext * const *c2) {
+ int r;
+
assert(c1);
assert(c2);
- return CMP((*c2)->pgscan, (*c1)->pgscan);
+ r = CMP((*c1)->preference, (*c2)->preference);
+ if (r != 0)
+ return r;
+
+ r = CMP((*c2)->pgscan, (*c1)->pgscan);
+ if (r != 0)
+ return r;
+
+ return CMP((*c2)->current_memory_usage, (*c1)->current_memory_usage);
}
static inline int compare_swap_usage(OomdCGroupContext * const *c1, OomdCGroupContext * const *c2) {
+ int r;
+
assert(c1);
assert(c2);
+ r = CMP((*c1)->preference, (*c2)->preference);
+ if (r != 0)
+ return r;
+
return CMP((*c2)->swap_usage, (*c1)->swap_usage);
}
diff --git a/src/oom/test-oomd-util.c b/src/oom/test-oomd-util.c
index 54fe2a03d1..49a02f9424 100644
--- a/src/oom/test-oomd-util.c
+++ b/src/oom/test-oomd-util.c
@@ -79,7 +79,7 @@ static void test_oomd_cgroup_kill(void) {
sleep(2);
assert_se(cg_is_empty(SYSTEMD_CGROUP_CONTROLLER, cgroup) == true);
- assert_se(cg_get_xattr_malloc(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.systemd_oomd_kill", &v) >= 0);
+ assert_se(cg_get_xattr_malloc(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.oomd_kill", &v) >= 0);
assert_se(memcmp(v, i == 0 ? "2" : "4", 2) == 0);
}
}
@@ -89,6 +89,8 @@ static void test_oomd_cgroup_context_acquire_and_insert(void) {
_cleanup_(oomd_cgroup_context_freep) OomdCGroupContext *ctx = NULL;
_cleanup_free_ char *cgroup = NULL;
OomdCGroupContext *c1, *c2;
+ bool test_xattrs;
+ int r;
if (geteuid() != 0)
return (void) log_tests_skipped("not root");
@@ -101,6 +103,16 @@ static void test_oomd_cgroup_context_acquire_and_insert(void) {
assert_se(cg_pid_get_path(NULL, 0, &cgroup) >= 0);
+ /* If we don't have permissions to set xattrs we're likely in a userns or missing capabilities
+ * so skip the xattr portions of the test. */
+ r = cg_set_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.oomd_test", "1", 1, 0);
+ test_xattrs = !ERRNO_IS_PRIVILEGE(r) && !ERRNO_IS_NOT_SUPPORTED(r);
+
+ if (test_xattrs) {
+ assert_se(cg_set_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.oomd_omit", "1", 1, 0) >= 0);
+ assert_se(cg_set_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.oomd_avoid", "1", 1, 0) >= 0);
+ }
+
assert_se(oomd_cgroup_context_acquire(cgroup, &ctx) == 0);
assert_se(streq(ctx->path, cgroup));
@@ -110,12 +122,28 @@ static void test_oomd_cgroup_context_acquire_and_insert(void) {
assert_se(ctx->swap_usage == 0);
assert_se(ctx->last_pgscan == 0);
assert_se(ctx->pgscan == 0);
+ /* omit takes precedence over avoid when both are set to true */
+ if (test_xattrs)
+ assert_se(ctx->preference == MANAGED_OOM_PREFERENCE_OMIT);
+ else
+ assert_se(ctx->preference == MANAGED_OOM_PREFERENCE_NONE);
+ ctx = oomd_cgroup_context_free(ctx);
+
+ /* also check when only avoid is set to true */
+ if (test_xattrs) {
+ assert_se(cg_set_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.oomd_omit", "0", 1, 0) >= 0);
+ assert_se(cg_set_xattr(SYSTEMD_CGROUP_CONTROLLER, cgroup, "user.oomd_avoid", "1", 1, 0) >= 0);
+ }
+ assert_se(oomd_cgroup_context_acquire(cgroup, &ctx) == 0);
+ if (test_xattrs)
+ assert_se(ctx->preference == MANAGED_OOM_PREFERENCE_AVOID);
ctx = oomd_cgroup_context_free(ctx);
/* Test the root cgroup */
assert_se(oomd_cgroup_context_acquire("", &ctx) == 0);
assert_se(streq(ctx->path, "/"));
assert_se(ctx->current_memory_usage > 0);
+ assert_se(ctx->preference == MANAGED_OOM_PREFERENCE_NONE);
/* Test hashmap inserts */
assert_se(h1 = hashmap_new(&oomd_cgroup_ctx_hash_ops));
@@ -137,6 +165,14 @@ static void test_oomd_cgroup_context_acquire_and_insert(void) {
assert_se(c2->last_pgscan == 5555);
assert_se(c2->mem_pressure_limit == 6789);
assert_se(c2->last_hit_mem_pressure_limit == 42);
+
+ /* Assert that avoid/omit are not set if the cgroup is not owned by root */
+ if (test_xattrs) {
+ ctx = oomd_cgroup_context_free(ctx);
+ assert_se(cg_set_access(SYSTEMD_CGROUP_CONTROLLER, cgroup, 65534, 0) >= 0);
+ assert_se(oomd_cgroup_context_acquire(cgroup, &ctx) == 0);
+ assert_se(ctx->preference == MANAGED_OOM_PREFERENCE_NONE);
+ }
}
static void test_oomd_system_context_acquire(void) {
@@ -287,21 +323,35 @@ static void test_oomd_sort_cgroups(void) {
char **paths = STRV_MAKE("/herp.slice",
"/herp.slice/derp.scope",
"/herp.slice/derp.scope/sheep.service",
- "/zupa.slice");
+ "/zupa.slice",
+ "/omitted.slice",
+ "/avoid.slice");
- OomdCGroupContext ctx[4] = {
+ OomdCGroupContext ctx[6] = {
{ .path = paths[0],
.swap_usage = 20,
- .pgscan = 60 },
+ .pgscan = 60,
+ .current_memory_usage = 10 },
{ .path = paths[1],
.swap_usage = 60,
- .pgscan = 40 },
+ .pgscan = 40,
+ .current_memory_usage = 20 },
{ .path = paths[2],
.swap_usage = 40,
- .pgscan = 20 },
+ .pgscan = 40,
+ .current_memory_usage = 40 },
{ .path = paths[3],
.swap_usage = 10,
- .pgscan = 80 },
+ .pgscan = 80,
+ .current_memory_usage = 10 },
+ { .path = paths[4],
+ .swap_usage = 90,
+ .pgscan = 100,
+ .preference = MANAGED_OOM_PREFERENCE_OMIT },
+ { .path = paths[5],
+ .swap_usage = 99,
+ .pgscan = 200,
+ .preference = MANAGED_OOM_PREFERENCE_AVOID },
};
assert_se(h = hashmap_new(&string_hash_ops));
@@ -310,26 +360,32 @@ static void test_oomd_sort_cgroups(void) {
assert_se(hashmap_put(h, "/herp.slice/derp.scope", &ctx[1]) >= 0);
assert_se(hashmap_put(h, "/herp.slice/derp.scope/sheep.service", &ctx[2]) >= 0);
assert_se(hashmap_put(h, "/zupa.slice", &ctx[3]) >= 0);
+ assert_se(hashmap_put(h, "/omitted.slice", &ctx[4]) >= 0);
+ assert_se(hashmap_put(h, "/avoid.slice", &ctx[5]) >= 0);
- assert_se(oomd_sort_cgroup_contexts(h, compare_swap_usage, NULL, &sorted_cgroups) == 4);
+ assert_se(oomd_sort_cgroup_contexts(h, compare_swap_usage, NULL, &sorted_cgroups) == 5);
assert_se(sorted_cgroups[0] == &ctx[1]);
assert_se(sorted_cgroups[1] == &ctx[2]);
assert_se(sorted_cgroups[2] == &ctx[0]);
assert_se(sorted_cgroups[3] == &ctx[3]);
+ assert_se(sorted_cgroups[4] == &ctx[5]);
sorted_cgroups = mfree(sorted_cgroups);
- assert_se(oomd_sort_cgroup_contexts(h, compare_pgscan, NULL, &sorted_cgroups) == 4);
+ assert_se(oomd_sort_cgroup_contexts(h, compare_pgscan_and_memory_usage, NULL, &sorted_cgroups) == 5);
assert_se(sorted_cgroups[0] == &ctx[3]);
assert_se(sorted_cgroups[1] == &ctx[0]);
- assert_se(sorted_cgroups[2] == &ctx[1]);
- assert_se(sorted_cgroups[3] == &ctx[2]);
+ assert_se(sorted_cgroups[2] == &ctx[2]);
+ assert_se(sorted_cgroups[3] == &ctx[1]);
+ assert_se(sorted_cgroups[4] == &ctx[5]);
sorted_cgroups = mfree(sorted_cgroups);
- assert_se(oomd_sort_cgroup_contexts(h, compare_pgscan, "/herp.slice/derp.scope", &sorted_cgroups) == 2);
- assert_se(sorted_cgroups[0] == &ctx[1]);
- assert_se(sorted_cgroups[1] == &ctx[2]);
+ assert_se(oomd_sort_cgroup_contexts(h, compare_pgscan_and_memory_usage, "/herp.slice/derp.scope", &sorted_cgroups) == 2);
+ assert_se(sorted_cgroups[0] == &ctx[2]);
+ assert_se(sorted_cgroups[1] == &ctx[1]);
assert_se(sorted_cgroups[2] == 0);
assert_se(sorted_cgroups[3] == 0);
+ assert_se(sorted_cgroups[4] == 0);
+ assert_se(sorted_cgroups[5] == 0);
sorted_cgroups = mfree(sorted_cgroups);
}
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index 84f57d94d2..5bbaa07dd1 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -435,7 +435,8 @@ static int bus_append_cgroup_property(sd_bus_message *m, const char *field, cons
if (STR_IN_SET(field, "DevicePolicy",
"Slice",
"ManagedOOMSwap",
- "ManagedOOMMemoryPressure"))
+ "ManagedOOMMemoryPressure",
+ "ManagedOOMPreference"))
return bus_append_string(m, field, eq);
if (STR_IN_SET(field, "ManagedOOMMemoryPressureLimit")) {
diff --git a/src/test/test-tables.c b/src/test/test-tables.c
index 641cadec85..cc93bbbc74 100644
--- a/src/test/test-tables.c
+++ b/src/test/test-tables.c
@@ -73,6 +73,7 @@ int main(int argc, char **argv) {
test_table(log_target, LOG_TARGET);
test_table(mac_address_policy, MAC_ADDRESS_POLICY);
test_table(managed_oom_mode, MANAGED_OOM_MODE);
+ test_table(managed_oom_preference, MANAGED_OOM_PREFERENCE);
test_table(manager_state, MANAGER_STATE);
test_table(manager_timestamp, MANAGER_TIMESTAMP);
test_table(mount_exec_command, MOUNT_EXEC_COMMAND);
diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service
index 15fa556dd6..0c7ded6786 100644
--- a/test/fuzz/fuzz-unit-file/directives.service
+++ b/test/fuzz/fuzz-unit-file/directives.service
@@ -138,6 +138,10 @@ MakeDirectory=
Mark=
MaxConnections=
MaxConnectionsPerSource=
+ManagedOOMSwap=
+ManagedOOMMemoryPressure=
+ManagedOOMMemoryPressureLimitPercent=
+ManagedOOMPreference=
MemoryAccounting=
MemoryHigh=
MemoryLimit=
diff --git a/test/test-functions b/test/test-functions
index e5f2495395..7cce7cdb1b 100644
--- a/test/test-functions
+++ b/test/test-functions
@@ -124,6 +124,7 @@ BASICTOOLS=(
rmdir
sed
seq
+ setfattr
setfont
setsid
sfdisk
diff --git a/test/units/testsuite-56-testmunch.service b/test/units/testsuite-56-testmunch.service
new file mode 100644
index 0000000000..b4b925a7af
--- /dev/null
+++ b/test/units/testsuite-56-testmunch.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Create some memory pressure
+
+[Service]
+MemoryHigh=2M
+Slice=testsuite-56-workload.slice
+ExecStart=/usr/lib/systemd/tests/testdata/units/testsuite-56-slowgrowth.sh
diff --git a/test/units/testsuite-56.sh b/test/units/testsuite-56.sh
index 8b01fe37ed..1884f81468 100755
--- a/test/units/testsuite-56.sh
+++ b/test/units/testsuite-56.sh
@@ -13,6 +13,8 @@ if [[ "$cgroup_type" != *"cgroup2"* ]] && [[ "$cgroup_type" != *"0x63677270"* ]]
fi
[[ -e /skipped ]] && exit 0 || true
+rm -rf /etc/systemd/system/testsuite-56-testbloat.service.d
+
echo "DefaultMemoryPressureDurationSec=5s" >> /etc/systemd/oomd.conf
systemctl start testsuite-56-testchill.service
@@ -23,20 +25,47 @@ oomctl | grep "/testsuite-56-workload.slice"
oomctl | grep "1.00%"
oomctl | grep "Default Memory Pressure Duration: 5s"
-# systemd-oomd watches for elevated pressure for 30 seconds before acting.
-# It can take time to build up pressure so either wait 5 minutes or for the service to fail.
-timeout=$(date -ud "5 minutes" +%s)
+# systemd-oomd watches for elevated pressure for 5 seconds before acting.
+# It can take time to build up pressure so either wait 2 minutes or for the service to fail.
+timeout=$(date -ud "2 minutes" +%s)
while [[ $(date -u +%s) -le $timeout ]]; do
if ! systemctl status testsuite-56-testbloat.service; then
break
fi
- sleep 15
+ sleep 5
done
# testbloat should be killed and testchill should be fine
if systemctl status testsuite-56-testbloat.service; then exit 42; fi
if ! systemctl status testsuite-56-testchill.service; then exit 24; fi
+# only run this portion of the test if we can set xattrs
+if setfattr -n user.xattr_test -v 1 /sys/fs/cgroup/; then
+ sleep 120 # wait for systemd-oomd kill cool down and elevated memory pressure to come down
+
+ mkdir -p /etc/systemd/system/testsuite-56-testbloat.service.d/
+ echo "[Service]" > /etc/systemd/system/testsuite-56-testbloat.service.d/override.conf
+ echo "ManagedOOMPreference=avoid" >> /etc/systemd/system/testsuite-56-testbloat.service.d/override.conf
+
+ systemctl daemon-reload
+ systemctl start testsuite-56-testchill.service
+ systemctl start testsuite-56-testmunch.service
+ systemctl start testsuite-56-testbloat.service
+
+ timeout=$(date -ud "2 minutes" +%s)
+ while [[ $(date -u +%s) -le $timeout ]]; do
+ if ! systemctl status testsuite-56-testmunch.service; then
+ break
+ fi
+ sleep 5
+ done
+
+ # testmunch should be killed since testbloat had the avoid xattr on it
+ if ! systemctl status testsuite-56-testbloat.service; then exit 25; fi
+ if systemctl status testsuite-56-testmunch.service; then exit 43; fi
+ if ! systemctl status testsuite-56-testchill.service; then exit 24; fi
+fi
+
systemd-analyze log-level info
echo OK > /testok