Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ sandbox.max_cpu_usage (integer, optional)
Description: Specifies the maximum CPU usage limit for the component, expressed as a percentage (%) of total CPU capacity.
Constraint: Must be greater than 0.

sandbox.affinity_mask (string, optional)
Description: Specifies the affinity mask defining on which CPU cores the component may be scheduled, as a hexadecimal string prefixed with ``0x`` (e.g. ``"0x3"``). Bit 0 corresponds to core 0, bit 1 to core 1, and so on; up to 64 cores are supported. For example, ``"0x5"`` selects cores 0 and 2. If not configured, the component inherits the CPU affinity of the **Launch Manager** itself, i.e. no affinity system call is made on its behalf.
Constraint: Must be a hexadecimal string with the ``0x``/``0X`` prefix, at most 16 hex digits (64 bits), and at least one bit set.

.. _lm_conf_launch_manager_root_properties:

Launch manager root properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ struct Sandbox
std::int32_t scheduling_priority{};
std::optional<std::uint64_t> max_memory_usage;
std::optional<std::uint32_t> max_cpu_usage;
std::optional<std::uint64_t> affinity_mask;
};

struct DeploymentConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@
"type": "integer",
"exclusiveMinimum": 0,
"description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage (%) of total CPU capacity."
},
"affinity_mask": {
"type": "string",
"pattern": "^0[xX](?!0{1,16}$)[0-9a-fA-F]{1,16}$",
"description": "Specifies the affinity mask defining on which CPU cores the process may be scheduled, as a hexadecimal string (e.g. \"0x3\" for cores 0 and 1). Bit 0 corresponds to core 0; up to 64 cores are supported. At least one bit must be set. If not configured, the component inherits the CPU affinity of the **Launch Manager** itself, i.e. no affinity system call is made on its behalf."
}
},
"additionalProperties": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,8 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSandbox)
fb::SchedulingPolicy::FIFO,
50 /*scheduling_priority*/,
4096 /*max_memory_usage*/,
80 /*max_cpu_usage*/);
80 /*max_cpu_usage*/,
3 /*affinity_mask*/);

auto bin_dir = fbb.CreateString("/opt");
auto work_dir = fbb.CreateString("/tmp");
Expand Down Expand Up @@ -554,6 +555,41 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSandbox)
EXPECT_THAT(sb.max_memory_usage.value(), Eq(4096U));
ASSERT_THAT(sb.max_cpu_usage.has_value(), IsTrue());
EXPECT_THAT(sb.max_cpu_usage.value(), Eq(80U));
ASSERT_THAT(sb.affinity_mask.has_value(), IsTrue());
EXPECT_THAT(sb.affinity_mask.value(), Eq(3U));
}

TEST_F(FlatbufferConfigLoaderTest, LoadSandboxWithoutAffinityMaskHasNoValue)
{
RecordProperty("Description", "Sandbox without affinity_mask configured leaves it as std::nullopt.");

::flatbuffers::FlatBufferBuilder fbb;

auto sandbox = buildDefaultSandbox(fbb);

auto bin_dir = fbb.CreateString("/opt");
auto work_dir = fbb.CreateString("/tmp");
auto deploy = fb::CreateDeploymentConfig(
fbb,
500 /*ready_timeout_ms*/,
500 /*shutdown_timeout_ms*/,
0 /*environmental_variables*/,
bin_dir,
work_dir,
0 /*ready_recovery_action*/,
0 /*recovery_action*/,
sandbox);

auto comp = buildDefaultComponent(fbb, "unsandboxed_comp", buildDefaultComponentProperties(fbb), deploy);
auto comps = fbb.CreateVector(std::vector<::flatbuffers::Offset<fb::Component>>{comp});

auto result = loadBuffer(buildConfigWithComponents(fbb, comps));

ASSERT_THAT(result.has_value(), IsTrue());
ASSERT_THAT(result->components().size(), Eq(1U));

const auto& sb = result->components()[0].deployment_config.sandbox;
EXPECT_THAT(sb.affinity_mask.has_value(), IsFalse());
}

TEST_F(FlatbufferConfigLoaderTest, LoadComponentAliveSupervision)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ score::cpp::expected<Sandbox, IConfigLoader::Error> convertSandbox(const fb::San
result.scheduling_priority = *scheduling_priority;
result.max_memory_usage = optionalScalarValue(fb_sb->max_memory_usage());
result.max_cpu_usage = optionalScalarValue(fb_sb->max_cpu_usage());
result.affinity_mask = optionalScalarValue(fb_sb->affinity_mask());
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@ TEST_F(ConverterTest, ConvertSandboxValid)
fb::SchedulingPolicy::FIFO,
50 /*scheduling_priority*/,
4096 /*max_memory_usage*/,
80 /*max_cpu_usage*/);
80 /*max_cpu_usage*/,
3 /*affinity_mask*/);
fbb.Finish(sandbox);
const auto* ptr = ::flatbuffers::GetRoot<fb::Sandbox>(fbb.GetBufferPointer());

Expand All @@ -678,6 +679,21 @@ TEST_F(ConverterTest, ConvertSandboxValid)
EXPECT_THAT(result->max_memory_usage.value(), Eq(4096U));
ASSERT_THAT(result->max_cpu_usage.has_value(), IsTrue());
EXPECT_THAT(result->max_cpu_usage.value(), Eq(80U));
ASSERT_THAT(result->affinity_mask.has_value(), IsTrue());
EXPECT_THAT(result->affinity_mask.value(), Eq(3U));
}

TEST_F(ConverterTest, ConvertSandboxWithoutAffinityMaskHasNoValue)
{
RecordProperty("Description", "Sandbox without affinity_mask configured leaves it as std::nullopt.");
::flatbuffers::FlatBufferBuilder fbb;
auto sandbox = buildDefaultSandbox(fbb);
fbb.Finish(sandbox);
const auto* ptr = ::flatbuffers::GetRoot<fb::Sandbox>(fbb.GetBufferPointer());

auto result = convertSandbox(ptr);
ASSERT_THAT(result.has_value(), IsTrue());
EXPECT_THAT(result->affinity_mask.has_value(), IsFalse());
}

TEST_F(ConverterTest, ConvertSandboxMissingUidReturnsError)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ table Sandbox {
max_memory_usage:uint64 = null; // optional
// Maximum CPU usage as a percentage of total CPU capacity.
max_cpu_usage:uint32 = null; // optional
// Affinity mask defining on which CPU cores the process may be scheduled.
affinity_mask:uint64 = null; // optional
}

// Deployment configuration for a component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <climits>
#include <csignal>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
Expand Down Expand Up @@ -55,6 +56,7 @@ class SyscallMock
MOCK_METHOD(int, chdir, (const char* __path), ());
MOCK_METHOD(int, setrlimit, (int __resource, const struct rlimit* __rlimits), ());
MOCK_METHOD(int, setSecurityPolicy, (const char* policy), ());
MOCK_METHOD(std::int32_t, setaffinity, (std::uint64_t cpumask), ());
MOCK_METHOD(pid_t, getpid, (), ());
MOCK_METHOD(int, fcntl, (int __fd, int __cmd, void* arg), ());
MOCK_METHOD(int, setgroups, (size_t n, const gid_t* groups), ());
Expand Down Expand Up @@ -392,6 +394,11 @@ int setgroups(size_t n, const gid_t* groups)
return g_syscall_mock->setgroups(n, groups);
}

std::int32_t setaffinity(std::uint64_t cpumask) noexcept(true)
{
return g_syscall_mock->setaffinity(cpumask);
}

} // namespace score::mw::lifecycle::internal::osal

// NOLINTEND
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,15 @@ OsalReturnType ProcessLauncher::setSchedulingAndSecurity(const configuration::Sa
retval = OsalReturnType::kFail;
}

// Set core affinity using OS specific functionality in osal - not in new config, skip
// if (-1 == osal::setaffinity(0))
// {
// static_cast<void>(signal_safe_log_errno(errno, "setaffinity failed"));
// retval = OsalReturnType::kFail;
// }
// Set core affinity using OS specific functionality in osal
if (config.affinity_mask.has_value())
Comment thread
NicolasFussberger marked this conversation as resolved.
{
if (-1 == osal::setaffinity(config.affinity_mask.value()))
{
static_cast<void>(signal_safe_log_errno(errno, "setaffinity failed"));
retval = OsalReturnType::kFail;
}
}

// Set group ID
if (-1 == setgid(config.gid))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,38 @@ TEST_F(SetSchedulingAndSecurityTest, setgroupsFails)
EXPECT_THROW(static_cast<void>(process_launcher->startProcess(pid_, sync_, config_)), SysExitException);
}

TEST_F(SetSchedulingAndSecurityTest, setaffinityFails)
{
RecordProperty("Description", "Verify that the forked process exits if setting the CPU affinity fails");

config_.deployment_config.sandbox.affinity_mask = 0x1U;

EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setaffinity(0x1U)).WillOnce(SetErrnoAndReturn(EINVAL, -1));
EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0));

EXPECT_CALL(*g_syscall_mock, sysexit(EXIT_FAILURE)).WillOnce(Throw(SysExitException{}));

EXPECT_THROW(static_cast<void>(process_launcher->startProcess(pid_, sync_, config_)), SysExitException);
}

TEST_F(SetSchedulingAndSecurityTest, setaffinityIgnore)
{
RecordProperty("Description", "Verify that the CPU affinity is not changed if no affinity mask is configured");

config_.deployment_config.sandbox.affinity_mask = std::nullopt;

EXPECT_CALL(*g_syscall_mock, setpgid).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, sched_setscheduler).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setgid).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setuid).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setaffinity).Times(0);

static_cast<void>(process_launcher->startProcess(pid_, sync_, config_)); // No return from forked process
}

TEST_F(StartProcessTest, chdirFails)
{
RecordProperty("Description", "Verify that the forked process exits if changing the working dir fails");
Expand Down Expand Up @@ -635,6 +667,7 @@ TEST_F(StartProcessTest, startProcessChildSuccess)
const std::vector<gid_t> sgids = {1, 2, 3};
const std::uint64_t mem_limit = 4096;
const std::uint32_t cpu_limit = 500;
const std::uint64_t affinity_mask = 0x8000000000000005U;
const std::string security_policy = "security";
const std::vector<std::string> args_in = {"-c 2", "--argument yes"};
const std::vector<std::string> expected_launch_args = {
Expand All @@ -647,6 +680,7 @@ TEST_F(StartProcessTest, startProcessChildSuccess)
config_.deployment_config.sandbox.supplementary_group_ids = sgids;
config_.deployment_config.sandbox.max_memory_usage = mem_limit;
config_.deployment_config.sandbox.max_cpu_usage = cpu_limit;
config_.deployment_config.sandbox.affinity_mask = affinity_mask;
config_.deployment_config.sandbox.security_policy = security_policy;
config_.deployment_config.environmental_variables.add("environment", "yes");
config_.deployment_config.environmental_variables.add("errors", "no");
Expand All @@ -661,6 +695,7 @@ TEST_F(StartProcessTest, startProcessChildSuccess)
EXPECT_CALL(*g_syscall_mock, sem_init).Times(2).WillRepeatedly(Return(0));
EXPECT_CALL(*g_syscall_mock, setpgid(0, forked_pid));
EXPECT_CALL(*g_syscall_mock, sched_setscheduler(0, scheduler, _)).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setaffinity(affinity_mask)).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setuid(uid)).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setgid(gid)).WillOnce(Return(0));
EXPECT_CALL(*g_syscall_mock, setgroups(sgids.size(), CArrayMatches(sgids))).WillOnce(Return(0));
Expand Down
27 changes: 27 additions & 0 deletions scripts/config_mapping/lifecycle_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,29 @@ def is_supervised(application_type):
}


def parse_affinity_mask(value, component_name):
"""Convert the user-facing hex string (e.g. "0x3") into the uint64 the flatbuffer expects."""
if not isinstance(value, str) or not value.lower().startswith("0x"):
raise ValueError(
f"Component '{component_name}': affinity_mask must be a hexadecimal string "
f'such as "0x3" with the "0x" prefix, but got {value!r}.'
)
try:
mask = int(value, 16)
except ValueError:
raise ValueError(
f"Component '{component_name}': affinity_mask must be a hexadecimal string "
f'such as "0x3", but got {value!r}.'
)

if mask < 1 or mask > 0xFFFFFFFFFFFFFFFF:
raise ValueError(
f"Component '{component_name}': affinity_mask must select at least one core "
f"and at most 64 cores, but got {value!r}."
)
return mask


def output_filename(input_filename):
stem = Path(input_filename).stem
return f"{stem}_gen.json"
Expand Down Expand Up @@ -340,6 +363,10 @@ def gen_config(output_dir, config, input_filename):
sandbox_out["max_memory_usage"] = sandbox["max_memory_usage"]
if "max_cpu_usage" in sandbox:
sandbox_out["max_cpu_usage"] = sandbox["max_cpu_usage"]
if "affinity_mask" in sandbox:
sandbox_out["affinity_mask"] = parse_affinity_mask(
sandbox["affinity_mask"], component_name
)

deployment = {
"ready_timeout_ms": depl_cfg["ready_timeout_ms"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"scheduling_policy": "RR",
"scheduling_priority": 75,
"max_memory_usage": 268435456,
"max_cpu_usage": 50
"max_cpu_usage": 50,
"affinity_mask": 3
},
"environmental_variables": [
{
Expand Down Expand Up @@ -108,7 +109,8 @@
"scheduling_policy": "FIFO",
"scheduling_priority": 50,
"max_memory_usage": 536870912,
"max_cpu_usage": 80
"max_cpu_usage": 80,
"affinity_mask": 3
},
"environmental_variables": [
{
Expand Down Expand Up @@ -171,7 +173,8 @@
"scheduling_policy": "FIFO",
"scheduling_priority": 50,
"max_memory_usage": 536870912,
"max_cpu_usage": 80
"max_cpu_usage": 80,
"affinity_mask": 3
},
"environmental_variables": [
{
Expand Down Expand Up @@ -234,7 +237,8 @@
"scheduling_policy": "FIFO",
"scheduling_priority": 50,
"max_memory_usage": 536870912,
"max_cpu_usage": 80
"max_cpu_usage": 80,
"affinity_mask": 3
},
"environmental_variables": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"scheduling_policy": "SCHED_FIFO",
"scheduling_priority": 50,
"max_memory_usage": 536870912,
"max_cpu_usage": 80
"max_cpu_usage": 80,
"affinity_mask": "0x3"
}
},
"component_properties": {
Expand Down
Loading
Loading