| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| /// compio's `AsyncifyPool::dispatch` panics with this exact message when a |
| /// blocking fallback is required but the pool has zero worker threads. Iggy |
| /// shard executors set `thread_pool_limit(0)` in `create_shard_executor`, so |
| /// this panic means the kernel lacks an io_uring opcode a shard operation |
| /// needed and compio could not run it natively. Shard-panic handlers match it |
| /// to surface [`print_incomplete_io_uring_ops_info`] instead of the bare |
| /// compio text. |
| /// |
| /// Best-effort: if a future compio release changes the wording, matching |
| /// degrades to logging the raw panic, which is still surfaced to the operator. |
| pub const ASYNCIFY_POOL_DISABLED_PANIC_MSG: &str = |
| "the thread pool is needed but no worker thread is running"; |
| |
| #[cfg(target_os = "linux")] |
| const DISCORD_SUPPORT_URL: &str = "https://discord.gg/apache-iggy"; |
| |
| /// Classify a failed shard-executor creation and fold the matching |
| /// remediation into the error itself. |
| /// |
| /// The verbose remediation block (current limits, per-environment fix |
| /// steps) is printed to stderr once per process; the returned error |
| /// carries a one-line summary of the cause and the primary fix, so every |
| /// propagated copy - the shard-join failure list, a panic message, a log |
| /// line hours later in a collector - documents the remediation instead |
| /// of only the stderr captured at the moment of failure. |
| /// |
| /// Errors of a kind this module has no diagnosis for pass through |
| /// unchanged. |
| #[cfg(target_os = "linux")] |
| pub fn enrich_runtime_create_error(error: std::io::Error) -> std::io::Error { |
| static RUNTIME_CREATE_DIAGNOSTIC: std::sync::Once = std::sync::Once::new(); |
| |
| let kind = error.kind(); |
| let hint = match kind { |
| std::io::ErrorKind::OutOfMemory => { |
| RUNTIME_CREATE_DIAGNOSTIC.call_once(print_locked_memory_limit_info); |
| locked_memory_limit_hint() |
| } |
| std::io::ErrorKind::PermissionDenied => { |
| RUNTIME_CREATE_DIAGNOSTIC.call_once(print_io_uring_permission_info); |
| "io_uring syscalls are blocked, typically by a container seccomp \ |
| profile: allow io_uring_setup/io_uring_enter/io_uring_register, \ |
| or run with `--security-opt seccomp=unconfined` (Docker) / \ |
| `seccompProfile: {type: Unconfined}` (Kubernetes)" |
| .to_owned() |
| } |
| std::io::ErrorKind::InvalidInput => { |
| RUNTIME_CREATE_DIAGNOSTIC.call_once(print_invalid_io_uring_args_info); |
| format!( |
| "the kernel rejected io_uring setup flags shard executors require \ |
| (IORING_SETUP_COOP_TASKRUN + IORING_SETUP_TASKRUN_FLAG need Linux \ |
| >= {MIN_KERNEL_MAJOR}.{MIN_KERNEL_MINOR} with full io_uring support; \ |
| WSL2 kernels are often incomplete)" |
| ) |
| } |
| _ => return error, |
| }; |
| std::io::Error::new(kind, format!("{error}: {hint}")) |
| } |
| |
| #[cfg(not(target_os = "linux"))] |
| pub fn enrich_runtime_create_error(error: std::io::Error) -> std::io::Error { |
| error |
| } |
| |
| /// One-line remediation for an io_uring ring allocation denied by |
| /// `RLIMIT_MEMLOCK`, with the live limits baked in so a log line is |
| /// self-sufficient evidence of the misconfiguration. |
| #[cfg(target_os = "linux")] |
| fn locked_memory_limit_hint() -> String { |
| use nix::sys::resource::{Resource, getrlimit}; |
| |
| let limits = getrlimit(Resource::RLIMIT_MEMLOCK).map_or_else( |
| |_| "RLIMIT_MEMLOCK could not be read".to_owned(), |
| |(soft, hard)| { |
| format!( |
| "RLIMIT_MEMLOCK soft={}, hard={}", |
| format_limit(soft), |
| format_limit(hard) |
| ) |
| }, |
| ); |
| format!( |
| "io_uring was denied locked memory for its rings ({limits}): raise the \ |
| limit with `ulimit -l unlimited` (shell), `LimitMEMLOCK=infinity` \ |
| (systemd), or `--ulimit memlock=-1:-1` (Docker)" |
| ) |
| } |
| |
| #[cfg(target_os = "linux")] |
| fn print_discord_link() { |
| eprintln!(" Need help? Join our Discord: {DISCORD_SUPPORT_URL}"); |
| eprintln!(); |
| } |
| |
| /// Formats an `RLIMIT_MEMLOCK` value for diagnostic output. |
| /// |
| /// `u64::MAX` is rendered as `unlimited` (the value getrlimit returns |
| /// for an uncapped limit); other values are rendered as raw bytes plus |
| /// a coarse MB-or-KB suffix so operators can eyeball whether the limit |
| /// is in the ballpark of the 4096-entry io_uring ring footprint. |
| #[cfg(target_os = "linux")] |
| fn format_limit(limit: u64) -> String { |
| if limit == u64::MAX { |
| "unlimited".to_string() |
| } else { |
| let kb = limit / 1024; |
| let mb = kb / 1024; |
| if mb > 0 { |
| format!("{limit} bytes ({mb} MB)") |
| } else { |
| format!("{limit} bytes ({kb} KB)") |
| } |
| } |
| } |
| |
| /// Prints information about locked memory limits when runtime creation fails. |
| /// This is typically needed when io_uring cannot allocate memory due to RLIMIT_MEMLOCK. |
| #[cfg(target_os = "linux")] |
| pub fn print_locked_memory_limit_info() { |
| use nix::sys::resource::{Resource, getrlimit}; |
| |
| let (soft, hard) = match getrlimit(Resource::RLIMIT_MEMLOCK) { |
| Ok(limits) => limits, |
| Err(_) => { |
| eprintln!("Failed to retrieve locked memory limits"); |
| return; |
| } |
| }; |
| |
| eprintln!(); |
| eprintln!("=== Locked Memory Limit Information ==="); |
| eprintln!("Current soft limit: {}", format_limit(soft)); |
| eprintln!("Current hard limit: {}", format_limit(hard)); |
| eprintln!(); |
| eprintln!("The io_uring runtime requires sufficient locked memory to operate."); |
| eprintln!("To increase the limit, you can:"); |
| eprintln!(); |
| eprintln!(" 1. Temporarily (current session only):"); |
| eprintln!(" ulimit -l unlimited"); |
| eprintln!(); |
| eprintln!(" 2. Docker run:"); |
| eprintln!(" docker run --ulimit memlock=-1:-1 ..."); |
| eprintln!(); |
| eprintln!(" 3. Docker Compose (add to service):"); |
| eprintln!(" ulimits:"); |
| eprintln!(" memlock:"); |
| eprintln!(" soft: -1"); |
| eprintln!(" hard: -1"); |
| eprintln!(); |
| eprintln!(" 4. Persistently (add to /etc/security/limits.conf):"); |
| eprintln!(" * soft memlock unlimited"); |
| eprintln!(" * hard memlock unlimited"); |
| eprintln!(); |
| eprintln!(" 5. For systemd services (add to service file):"); |
| eprintln!(" LimitMEMLOCK=infinity"); |
| eprintln!(); |
| print_discord_link(); |
| } |
| |
| /// Prints information about io_uring permission issues in containerized environments. |
| /// This occurs when seccomp blocks io_uring syscalls. |
| #[cfg(target_os = "linux")] |
| pub fn print_io_uring_permission_info() { |
| eprintln!(); |
| eprintln!("=== io_uring Permission Denied ==="); |
| eprintln!(); |
| eprintln!("The io_uring runtime requires specific syscalls that are blocked by default"); |
| eprintln!("in containerized environments (Docker, Podman, etc.)."); |
| eprintln!(); |
| eprintln!("To resolve this issue:"); |
| eprintln!(); |
| eprintln!(" 1. Docker Compose (add to service):"); |
| eprintln!(" security_opt:"); |
| eprintln!(" - seccomp:unconfined"); |
| eprintln!(); |
| eprintln!(" 2. Docker run:"); |
| eprintln!(" docker run --security-opt seccomp=unconfined ..."); |
| eprintln!(); |
| eprintln!(" 3. Custom seccomp profile (more secure):"); |
| eprintln!(" Create a profile allowing io_uring_setup, io_uring_enter,"); |
| eprintln!(" and io_uring_register syscalls."); |
| eprintln!(); |
| eprintln!(" 4. Kubernetes (add to pod spec):"); |
| eprintln!(" securityContext:"); |
| eprintln!(" seccompProfile:"); |
| eprintln!(" type: Unconfined"); |
| eprintln!(); |
| print_discord_link(); |
| } |
| |
| /// Minimum kernel version for IORING_SETUP_COOP_TASKRUN and IORING_SETUP_TASKRUN_FLAG. |
| #[cfg(target_os = "linux")] |
| const MIN_KERNEL_MAJOR: u32 = 5; |
| #[cfg(target_os = "linux")] |
| const MIN_KERNEL_MINOR: u32 = 19; |
| |
| /// Minimum kernel version for kernel.io_uring_disabled sysctl. |
| #[cfg(target_os = "linux")] |
| const SYSCTL_IO_URING_DISABLED_KERNEL_MAJOR: u32 = 6; |
| #[cfg(target_os = "linux")] |
| const SYSCTL_IO_URING_DISABLED_KERNEL_MINOR: u32 = 1; |
| |
| /// Prints diagnostic information when io_uring setup fails with EINVAL. |
| /// |
| /// This typically occurs when the kernel does not support the io_uring flags |
| /// required by shard executors (IORING_SETUP_COOP_TASKRUN, IORING_SETUP_TASKRUN_FLAG). |
| /// The caller is responsible for deduplication (e.g., via `std::sync::Once`). |
| #[cfg(target_os = "linux")] |
| pub fn print_invalid_io_uring_args_info() { |
| eprintln!(); |
| eprintln!("=== io_uring Invalid Argument (EINVAL) ==="); |
| eprintln!(); |
| eprintln!("The shard executor failed to initialize because the kernel rejected"); |
| eprintln!("io_uring setup flags required for shard operation."); |
| eprintln!(); |
| eprintln!(" The main thread's io_uring runtime uses default settings and initialized"); |
| eprintln!(" successfully. Shard executors require additional flags:"); |
| eprintln!(" - IORING_SETUP_COOP_TASKRUN (cooperative task running)"); |
| eprintln!(" - IORING_SETUP_TASKRUN_FLAG (task runner flag notification)"); |
| eprintln!( |
| " These flags require Linux kernel >= {MIN_KERNEL_MAJOR}.{MIN_KERNEL_MINOR} with full io_uring support." |
| ); |
| eprintln!(); |
| |
| report_io_uring_environment(); |
| } |
| |
| /// Prints diagnostic information when a shard thread panicked because compio |
| /// had to run an io_uring operation on its blocking fallback pool, which Iggy |
| /// disables (`thread_pool_limit(0)`). |
| /// |
| /// Unlike [`print_invalid_io_uring_args_info`], io_uring setup succeeded here: |
| /// the kernel accepted the ring flags and shards started, then at runtime |
| /// compio's opcode probe (`IORING_REGISTER_PROBE`) reported an opcode a shard |
| /// operation needed as unsupported. The caller is responsible for |
| /// deduplication (e.g., via `std::sync::Once`). |
| #[cfg(target_os = "linux")] |
| pub fn print_incomplete_io_uring_ops_info() { |
| eprintln!(); |
| eprintln!("=== io_uring Incomplete Opcode Support ==="); |
| eprintln!(); |
| eprintln!("A shard thread aborted because an io_uring operation it issued is not"); |
| eprintln!("supported by this kernel. Iggy shards run thread-per-core with the"); |
| eprintln!("blocking fallback pool disabled, so an unsupported opcode is fatal"); |
| eprintln!("instead of being silently offloaded to a worker thread."); |
| eprintln!(); |
| eprintln!(" io_uring setup succeeded (shards started), but at runtime compio"); |
| eprintln!(" probed the kernel and a required opcode was absent. This is common"); |
| eprintln!(" on WSL2 and on older or cut-down kernels whose io_uring support is"); |
| eprintln!(" incomplete. Some opcodes need a kernel newer than the shard-setup"); |
| eprintln!(" floor below."); |
| eprintln!(); |
| |
| report_io_uring_environment(); |
| } |
| |
| /// Probes the io_uring environment (kernel version, WSL2 detection, |
| /// `kernel.io_uring_disabled` sysctl, AppArmor confinement), prints the |
| /// findings plus any concrete issues, then prints the shared remediation |
| /// steps. Reused by both io_uring diagnostics above. |
| #[cfg(target_os = "linux")] |
| fn report_io_uring_environment() { |
| use nix::sys::utsname::uname; |
| use std::fs; |
| |
| let mut detected_issues: Vec<String> = Vec::new(); |
| |
| // 1. Kernel version check |
| let uname_info = match uname() { |
| Ok(info) => Some(info), |
| Err(_) => { |
| eprintln!(" [!] Could not retrieve kernel information via uname(2)."); |
| None |
| } |
| }; |
| |
| let mut kernel_version: Option<(u32, u32)> = None; |
| |
| if let Some(ref info) = uname_info { |
| let release = info.release().to_string_lossy(); |
| eprintln!(" Kernel release: {release}"); |
| |
| if let Some((major, minor)) = parse_kernel_version(&release) { |
| kernel_version = Some((major, minor)); |
| if (major, minor) < (MIN_KERNEL_MAJOR, MIN_KERNEL_MINOR) { |
| detected_issues.push(format!( |
| "Kernel {major}.{minor} is too old (need >= {MIN_KERNEL_MAJOR}.{MIN_KERNEL_MINOR})" |
| )); |
| } |
| } else { |
| eprintln!(" [!] Could not parse kernel version from release string."); |
| } |
| |
| // 2. WSL2 detection |
| let release_is_wsl = release.contains("microsoft") || release.contains("Microsoft"); |
| let proc_version_is_wsl = fs::read_to_string("/proc/version") |
| .map(|v| v.contains("Microsoft") || v.contains("microsoft")) |
| .unwrap_or(false); |
| |
| if release_is_wsl || proc_version_is_wsl { |
| eprintln!(" Environment: WSL2 (Microsoft kernel fork detected)"); |
| detected_issues.push( |
| "WSL2 kernels often ship incomplete io_uring: missing setup flags or \ |
| opcodes even at version >= 5.19" |
| .to_string(), |
| ); |
| } |
| } |
| |
| // 3. kernel.io_uring_disabled sysctl (available since kernel 6.1) |
| match fs::read_to_string("/proc/sys/kernel/io_uring_disabled") { |
| Ok(value) => { |
| let value = value.trim(); |
| eprintln!(" kernel.io_uring_disabled = {value}"); |
| match value { |
| "1" => detected_issues |
| .push("io_uring is disabled for unprivileged users (sysctl = 1)".to_string()), |
| "2" => detected_issues |
| .push("io_uring is fully disabled by sysctl (sysctl = 2)".to_string()), |
| _ => {} |
| } |
| } |
| Err(_) => { |
| // The sysctl was introduced in kernel 6.1. If the file is absent on a kernel >= 6.1, |
| // io_uring is likely not compiled in (CONFIG_IO_URING=n). |
| if let Some((major, minor)) = kernel_version |
| && (major, minor) |
| >= ( |
| SYSCTL_IO_URING_DISABLED_KERNEL_MAJOR, |
| SYSCTL_IO_URING_DISABLED_KERNEL_MINOR, |
| ) |
| { |
| detected_issues.push(format!( |
| "kernel.io_uring_disabled sysctl not found on kernel >= \ |
| {SYSCTL_IO_URING_DISABLED_KERNEL_MAJOR}.{SYSCTL_IO_URING_DISABLED_KERNEL_MINOR} \ |
| - io_uring may not be compiled in (CONFIG_IO_URING=n)" |
| )); |
| } |
| } |
| } |
| |
| // 4. AppArmor - informational only, not added to detected_issues |
| let apparmor_profile = fs::read_to_string("/proc/self/attr/apparmor/current") |
| .ok() |
| .map(|s| s.trim().to_string()); |
| |
| if let Some(ref profile) = apparmor_profile |
| && profile != "unconfined" |
| && !profile.is_empty() |
| { |
| eprintln!(" AppArmor profile: {profile}"); |
| } |
| |
| // Print detected issues |
| if detected_issues.is_empty() { |
| eprintln!(); |
| eprintln!(" No specific issue was detected. This kernel's io_uring support may be"); |
| eprintln!(" incomplete for the setup flags or opcodes Iggy's shard executors require."); |
| } else { |
| eprintln!(); |
| eprintln!(" Detected issues:"); |
| for (i, issue) in detected_issues.iter().enumerate() { |
| eprintln!(" {}. {issue}", i + 1); |
| } |
| } |
| |
| eprintln!(); |
| eprintln!(" To resolve this:"); |
| eprintln!(); |
| eprintln!( |
| " 1. Upgrade to Linux kernel >= {MIN_KERNEL_MAJOR}.{MIN_KERNEL_MINOR} (>= {SYSCTL_IO_URING_DISABLED_KERNEL_MAJOR}.{SYSCTL_IO_URING_DISABLED_KERNEL_MINOR} recommended)" |
| ); |
| eprintln!(); |
| eprintln!(" 2. If running under WSL2:"); |
| eprintln!(" - Update WSL: wsl --update (from PowerShell)"); |
| eprintln!(" - Or build a custom kernel with full io_uring support:"); |
| eprintln!(" https://learn.microsoft.com/en-us/windows/wsl/wsl-config#wsl-2-settings"); |
| eprintln!(" - Or use Docker Desktop / a native Linux VM instead of WSL2"); |
| eprintln!(); |
| eprintln!(" 3. If io_uring is disabled via sysctl:"); |
| eprintln!(" sudo sysctl -w kernel.io_uring_disabled=0"); |
| eprintln!(); |
| eprintln!(" 4. If AppArmor is restricting io_uring:"); |
| eprintln!(" sudo aa-complain <profile-name>"); |
| eprintln!(); |
| eprintln!(" 5. Check kernel logs for more details:"); |
| eprintln!(" dmesg | grep -i io_uring"); |
| eprintln!(); |
| print_discord_link(); |
| } |
| |
| /// Parses "major.minor[.patch...][-suffix]" from a kernel release string. |
| #[cfg(target_os = "linux")] |
| fn parse_kernel_version(release: &str) -> Option<(u32, u32)> { |
| let mut parts = release |
| .split(|c: char| !c.is_ascii_digit()) |
| .filter(|s| !s.is_empty()); |
| let major = parts.next()?.parse::<u32>().ok()?; |
| let minor = parts.next()?.parse::<u32>().ok()?; |
| Some((major, minor)) |
| } |
| |
| #[cfg(not(target_os = "linux"))] |
| pub const fn print_locked_memory_limit_info() {} |
| |
| #[cfg(not(target_os = "linux"))] |
| pub const fn print_io_uring_permission_info() {} |
| |
| #[cfg(not(target_os = "linux"))] |
| pub const fn print_invalid_io_uring_args_info() {} |
| |
| #[cfg(not(target_os = "linux"))] |
| pub const fn print_incomplete_io_uring_ops_info() {} |
| |
| #[cfg(all(test, target_os = "linux"))] |
| mod tests { |
| use super::{enrich_runtime_create_error, format_limit, parse_kernel_version}; |
| |
| #[test] |
| fn enrich_folds_memlock_remediation_into_the_error() { |
| let raw = std::io::Error::new(std::io::ErrorKind::OutOfMemory, "io_uring setup: ENOMEM"); |
| let enriched = enrich_runtime_create_error(raw); |
| let message = enriched.to_string(); |
| assert_eq!(enriched.kind(), std::io::ErrorKind::OutOfMemory); |
| assert!(message.contains("io_uring setup: ENOMEM"), "{message}"); |
| assert!(message.contains("ulimit -l unlimited"), "{message}"); |
| assert!(message.contains("RLIMIT_MEMLOCK"), "{message}"); |
| } |
| |
| #[test] |
| fn enrich_folds_seccomp_remediation_into_the_error() { |
| let raw = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "EPERM"); |
| let message = enrich_runtime_create_error(raw).to_string(); |
| assert!(message.contains("seccomp"), "{message}"); |
| } |
| |
| #[test] |
| fn enrich_folds_kernel_flag_remediation_into_the_error() { |
| let raw = std::io::Error::new(std::io::ErrorKind::InvalidInput, "EINVAL"); |
| let message = enrich_runtime_create_error(raw).to_string(); |
| assert!(message.contains("IORING_SETUP_COOP_TASKRUN"), "{message}"); |
| } |
| |
| #[test] |
| fn enrich_passes_undiagnosed_kinds_through_unchanged() { |
| let raw = std::io::Error::new(std::io::ErrorKind::Interrupted, "EINTR"); |
| let enriched = enrich_runtime_create_error(raw); |
| assert_eq!(enriched.kind(), std::io::ErrorKind::Interrupted); |
| assert_eq!(enriched.to_string(), "EINTR"); |
| } |
| |
| #[test] |
| fn parse_kernel_version_standard() { |
| assert_eq!(parse_kernel_version("6.8.0-45-generic"), Some((6, 8))); |
| } |
| |
| #[test] |
| fn parse_kernel_version_wsl2() { |
| assert_eq!( |
| parse_kernel_version("5.15.153.1-microsoft-standard-WSL2"), |
| Some((5, 15)) |
| ); |
| } |
| |
| #[test] |
| fn parse_kernel_version_minimal() { |
| assert_eq!(parse_kernel_version("5.19"), Some((5, 19))); |
| } |
| |
| #[test] |
| fn parse_kernel_version_garbage_returns_none() { |
| assert_eq!(parse_kernel_version("not-a-version"), None); |
| } |
| |
| #[test] |
| fn parse_kernel_version_empty_returns_none() { |
| assert_eq!(parse_kernel_version(""), None); |
| } |
| |
| #[test] |
| fn parse_kernel_version_overflow_returns_none() { |
| // u32::MAX + 1 in the major slot must not silently wrap. |
| assert_eq!(parse_kernel_version("4294967296.0"), None); |
| } |
| |
| #[test] |
| fn format_limit_unlimited() { |
| assert_eq!(format_limit(u64::MAX), "unlimited"); |
| } |
| |
| #[test] |
| fn format_limit_sub_mb_uses_kb_suffix() { |
| assert_eq!(format_limit(64 * 1024), "65536 bytes (64 KB)"); |
| } |
| |
| #[test] |
| fn format_limit_mb_range_uses_mb_suffix() { |
| assert_eq!(format_limit(8 * 1024 * 1024), "8388608 bytes (8 MB)"); |
| } |
| } |