This is an automated email from the ASF dual-hosted git repository.

hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new 6ae792a37 refactor(configs): fork ShardingConfig for legacy and 
server-ng (#3633)
6ae792a37 is described below

commit 6ae792a37d2d470fadba7f18b780442f582bd258
Author: Hubert Gruszecki <[email protected]>
AuthorDate: Wed Jul 8 15:09:06 2026 +0200

    refactor(configs): fork ShardingConfig for legacy and server-ng (#3633)
---
 core/configs/src/lib.rs                        |   3 +-
 core/configs/src/server_config/defaults.rs     |  10 +-
 core/configs/src/server_config/displays.rs     |   3 +-
 core/configs/src/server_config/sharding.rs     | 155 +----------
 core/configs/src/server_config/system.rs       |  15 +-
 core/configs/src/server_config/validators.rs   | 288 ++++++--------------
 core/configs/src/server_ng_config/defaults.rs  |   4 +-
 core/configs/src/server_ng_config/mod.rs       |   1 +
 core/configs/src/server_ng_config/server_ng.rs |   7 +-
 core/configs/src/server_ng_config/sharding.rs  | 357 +++++++++++++++++++++++++
 core/configs_derive/src/config_env.rs          |  11 +-
 core/server-ng/config.toml                     |  14 +-
 core/server-ng/src/bootstrap.rs                |   8 +-
 core/server-ng/src/dispatch.rs                 |  16 +-
 core/server-ng/src/http.rs                     |   4 +-
 core/server-ng/src/http/state.rs               |   4 +-
 core/server-ng/src/partition_reconciler.rs     |   8 +-
 core/server-ng/src/snapshot.rs                 |  14 +-
 core/server/config.toml                        |   2 +-
 19 files changed, 520 insertions(+), 404 deletions(-)

diff --git a/core/configs/src/lib.rs b/core/configs/src/lib.rs
index f3bd7685b..4d454f56f 100644
--- a/core/configs/src/lib.rs
+++ b/core/configs/src/lib.rs
@@ -30,5 +30,6 @@ pub use server_config::{
     tcp, validators, websocket,
 };
 pub use server_ng_config::{
-    COMPONENT_NG, message_bus, quic as ng_quic, server_ng, tcp as ng_tcp, 
websocket as ng_websocket,
+    COMPONENT_NG, message_bus, quic as ng_quic, server_ng, sharding as 
ng_sharding, tcp as ng_tcp,
+    websocket as ng_websocket,
 };
diff --git a/core/configs/src/server_config/defaults.rs 
b/core/configs/src/server_config/defaults.rs
index c29baeac4..155239cd3 100644
--- a/core/configs/src/server_config/defaults.rs
+++ b/core/configs/src/server_config/defaults.rs
@@ -26,7 +26,6 @@ use super::server::{
     PersonalAccessTokenConfig, ServerConfig, TelemetryConfig, 
TelemetryLogsConfig,
     TelemetryTracesConfig,
 };
-use super::sharding::ShardingConfig;
 use super::system::{
     BackupConfig, CompatibilityConfig, CompressionConfig, EncryptionConfig, 
LoggingConfig,
     MessageDeduplicationConfig, PartitionConfig, RecoveryConfig, 
RuntimeConfig, SegmentConfig,
@@ -35,6 +34,7 @@ use super::system::{
 use super::tcp::TcpSocketConfig;
 use super::tcp::{TcpConfig, TcpTlsConfig};
 use super::websocket::{WebSocketConfig, WebSocketTlsConfig};
+use configs::ConfigEnvMappings;
 use iggy_common::IggyByteSize;
 use iggy_common::IggyDuration;
 use std::sync::Arc;
@@ -321,9 +321,9 @@ impl Default for PersonalAccessTokenCleanerConfig {
     }
 }
 
-impl Default for SystemConfig {
-    fn default() -> SystemConfig {
-        SystemConfig {
+impl<S: ConfigEnvMappings + Default> Default for SystemConfig<S> {
+    fn default() -> Self {
+        Self {
             path: SERVER_CONFIG.system.path.parse().unwrap(),
             backup: BackupConfig::default(),
             runtime: RuntimeConfig::default(),
@@ -338,7 +338,7 @@ impl Default for SystemConfig {
             message_deduplication: MessageDeduplicationConfig::default(),
             recovery: RecoveryConfig::default(),
             memory_pool: MemoryPoolConfig::default(),
-            sharding: ShardingConfig::default(),
+            sharding: S::default(),
         }
     }
 }
diff --git a/core/configs/src/server_config/displays.rs 
b/core/configs/src/server_config/displays.rs
index 24b34bf81..f7693965d 100644
--- a/core/configs/src/server_config/displays.rs
+++ b/core/configs/src/server_config/displays.rs
@@ -30,6 +30,7 @@ use super::{
     },
     tcp::{TcpConfig, TcpSocketConfig, TcpTlsConfig},
 };
+use configs::ConfigEnvMappings;
 use std::fmt::{Display, Formatter};
 
 impl Display for HttpConfig {
@@ -345,7 +346,7 @@ impl Display for TelemetryTracesConfig {
     }
 }
 
-impl Display for SystemConfig {
+impl<S: ConfigEnvMappings> Display for SystemConfig<S> {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         write!(
             f,
diff --git a/core/configs/src/server_config/sharding.rs 
b/core/configs/src/server_config/sharding.rs
index 2f23cfdb7..87a411e1f 100644
--- a/core/configs/src/server_config/sharding.rs
+++ b/core/configs/src/server_config/sharding.rs
@@ -15,11 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use iggy_common::IggyDuration;
 use serde::{Deserialize, Serialize};
-use serde_with::{DisplayFromStr, serde_as};
-use std::time::Duration;
 
+use super::defaults::SERVER_CONFIG;
 use configs::ConfigEnv;
 
 // `CpuAllocation`/`NumaConfig` are pure config types and live in their own
@@ -28,96 +26,9 @@ use configs::ConfigEnv;
 // `configs::sharding::*` path stable for existing callers.
 pub use cpu_allocation::{CpuAllocation, NumaConfig};
 
-/// Default capacity of the per-shard inter-shard inbox channel. Sized
-/// comfortably above the consensus working set, which is roughly
-/// `PIPELINE_PREPARE_QUEUE_MAX (= 32) * replica_count * directions`
-/// frames in flight per shard, without allowing a runaway producer to
-/// eat unbounded memory. Tunable via `[system.sharding] inbox_capacity`
-/// in TOML.
-///
-/// The capacity must also absorb the worst-case cross-shard client
-/// Reply burst. Unlike consensus frames, client Replies have no VSR
-/// retransmit path: a Reply lost on full inbox is gone and the client
-/// times out. A reasonable lower bound is
-/// `max_inflight_client_requests / num_shards` (assuming requests are
-/// distributed evenly across owning shards) plus the consensus
-/// headroom above.
-///
-/// Consensus frames and client-reply forwards share this one channel,
-/// so the two headrooms are not independent: a consensus burst or
-/// retransmit storm can fill the inbox with consensus frames exactly
-/// when a client Reply needs the space. A single `inbox_capacity` knob
-/// cannot isolate the two frame classes - size it for the sum of both
-/// worst cases occurring together. Watch the drop-site `tracing` logs
-/// (and, once a per-shard exporter lands, the `frame_drops_total`
-/// `{variant="forward_client_send"}` counter) to detect when the bound
-/// is too low in production.
-pub const DEFAULT_INBOX_CAPACITY: usize = 1024;
-
-/// Maximum permitted per-shard inbox depth. The channel is allocated
-/// up-front per shard, so a runaway value here OOMs the process at boot.
-/// `1 << 20` (~1M frames) is several orders of magnitude above any
-/// realistic backpressure target and still fits comfortably in process
-/// address space.
-pub const INBOX_CAPACITY_MAX: usize = 1 << 20;
-
-/// Default bus shutdown drain timeout. Sized larger than typical TCP RTT
-/// times in-flight write-batch so writers receive their full last
-/// `write_vectored_all` budget before the connection registry kicks in.
-pub const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT: Duration = Duration::from_secs(10);
-
-/// Default watchdog poll cadence for the cross-thread shutdown flag.
-/// 50ms keeps Ctrl-C latency operator-visible without measurable wakeup
-/// overhead.
-pub const DEFAULT_SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(50);
-
-/// Hard upper bound on `shutdown_drain_timeout`. A drain that never
-/// completes wedges process exit; capping at 10 minutes guarantees the
-/// watchdog eventually force-tears the bus even with a pathological
-/// config typo.
-pub const SHUTDOWN_DRAIN_TIMEOUT_MAX: Duration = Duration::from_secs(600);
-
-/// Hard upper bound on `shutdown_poll_interval`. A pollerinterval longer
-/// than the drain timeout makes the flag effectively unobservable; cap
-/// at 5s so Ctrl-C latency stays bounded regardless of config.
-pub const SHUTDOWN_POLL_INTERVAL_MAX: Duration = Duration::from_secs(5);
-
-/// Default safety-tick cadence for the partition reconciliation loop.
-/// The reconciler also wakes on every `LifecycleFrame::MetadataCommitTick`
-/// broadcast by shard 0; this fallback covers dropped wake-ups (the wake
-/// channel is intentionally capacity-1) and the initial post-bootstrap
-/// convergence window before shard 0's first tick. One second is
-/// invisible to operators yet keeps idle clusters from burning CPU
-/// re-reading the same target snapshot.
-pub const DEFAULT_RECONCILE_PERIODIC_INTERVAL: Duration = 
Duration::from_secs(1);
-
-/// Hard upper bound on `reconcile_periodic_interval`. A tick longer
-/// than ~30s makes post-failure recovery latency operator-visible; the
-/// cap reins in pathological typos without disturbing reasonable
-/// production values.
-pub const RECONCILE_PERIODIC_INTERVAL_MAX: Duration = Duration::from_secs(30);
-
-const fn default_inbox_capacity() -> usize {
-    DEFAULT_INBOX_CAPACITY
-}
-
-const fn default_pin_cores() -> bool {
-    true
-}
-
-fn default_shutdown_drain_timeout() -> IggyDuration {
-    IggyDuration::new(DEFAULT_SHUTDOWN_DRAIN_TIMEOUT)
-}
-
-fn default_shutdown_poll_interval() -> IggyDuration {
-    IggyDuration::new(DEFAULT_SHUTDOWN_POLL_INTERVAL)
-}
-
-fn default_reconcile_periodic_interval() -> IggyDuration {
-    IggyDuration::new(DEFAULT_RECONCILE_PERIODIC_INTERVAL)
-}
-
-#[serde_as]
+/// Sharding config for the legacy `core/server`. That server consumes only
+/// `cpu_allocation` and `pin_cores`; the bus / shutdown / reconcile knobs are
+/// server-ng concepts and live in [`crate::server_ng_config::sharding`].
 #[derive(Debug, Deserialize, Serialize, ConfigEnv)]
 pub struct ShardingConfig {
     #[serde(default)]
@@ -133,70 +44,14 @@ pub struct ShardingConfig {
     /// threads freely within the allowed set. With a NUMA-aware allocation,
     /// `false` drops both the CPU and memory-node bindings (and logs a
     /// warning, since NUMA placement without pinning is meaningless).
-    #[serde(default = "default_pin_cores")]
     pub pin_cores: bool,
-    /// Per-shard inter-shard inbox channel capacity. Bounded by design.
-    /// Drops on full inbox of consensus frames are recovered by VSR
-    /// retransmit. Drops of cross-shard client Reply frames are terminal:
-    /// the client never receives the reply (no in-protocol retransmit).
-    /// Both frame classes share this one channel, so a consensus burst
-    /// can starve client-reply forwards: size against the worst-case sum
-    /// of consensus working set + peak client-reply fan-out per shard
-    /// occurring together; see `DEFAULT_INBOX_CAPACITY` for the
-    /// rationale. Used by `core/server-ng`; the legacy server uses its
-    /// own hard-coded inbox sizing.
-    ///
-    // TODO(hubcio): split into two priority lanes - one bounded queue for
-    // consensus frames (drops recovered by VSR retransmit) and one for
-    // client `Reply` frames (drops terminal, must be sized for worst-case
-    // fan-out). Current single-channel design is the minimum-viable
-    // wiring so `frame_drops_total{variant,reason}` surfaces under load
-    // and yields real numbers to size the split against.
-    #[serde(default = "default_inbox_capacity")]
-    pub inbox_capacity: usize,
-    /// Wall-clock budget for a single shard's bus drain on shutdown.
-    /// Drives `IggyMessageBus::shutdown(..)` from the per-shard watchdog
-    /// and the parallel-join survivor path. Sized larger than typical
-    /// TCP RTT times in-flight write-batch so writers receive their full
-    /// last `write_vectored_all` budget before the connection registry
-    /// force-tears the bus. Slow-fsync hosts may need to extend this past
-    /// the default; the cap is `SHUTDOWN_DRAIN_TIMEOUT_MAX` so a config
-    /// typo cannot wedge process exit.
-    #[serde(default = "default_shutdown_drain_timeout")]
-    #[serde_as(as = "DisplayFromStr")]
-    #[config_env(leaf)]
-    pub shutdown_drain_timeout: IggyDuration,
-    /// Poll cadence for the cross-thread shutdown flag and for the
-    /// `await_metadata_bundle` / `broadcast_metadata_bundle` poll loops.
-    /// Trades off Ctrl-C latency against idle wakeup cost; the default
-    /// keeps shutdown observably prompt without measurable scheduler
-    /// overhead. Capped at `SHUTDOWN_POLL_INTERVAL_MAX` so the flag
-    /// remains effectively observable regardless of config.
-    #[serde(default = "default_shutdown_poll_interval")]
-    #[serde_as(as = "DisplayFromStr")]
-    #[config_env(leaf)]
-    pub shutdown_poll_interval: IggyDuration,
-    /// Safety-tick cadence for the partition reconciliation loop; the
-    /// reconciler also wakes immediately on every
-    /// `LifecycleFrame::MetadataCommitTick` from shard 0. See
-    /// [`DEFAULT_RECONCILE_PERIODIC_INTERVAL`] for the rationale; values
-    /// above [`RECONCILE_PERIODIC_INTERVAL_MAX`] are rejected by the
-    /// validator.
-    #[serde(default = "default_reconcile_periodic_interval")]
-    #[serde_as(as = "DisplayFromStr")]
-    #[config_env(leaf)]
-    pub reconcile_periodic_interval: IggyDuration,
 }
 
 impl Default for ShardingConfig {
     fn default() -> Self {
         Self {
             cpu_allocation: CpuAllocation::default(),
-            pin_cores: default_pin_cores(),
-            inbox_capacity: DEFAULT_INBOX_CAPACITY,
-            shutdown_drain_timeout: default_shutdown_drain_timeout(),
-            shutdown_poll_interval: default_shutdown_poll_interval(),
-            reconcile_periodic_interval: default_reconcile_periodic_interval(),
+            pin_cores: SERVER_CONFIG.system.sharding.pin_cores,
         }
     }
 }
diff --git a/core/configs/src/server_config/system.rs 
b/core/configs/src/server_config/system.rs
index b1d124c20..60437a94f 100644
--- a/core/configs/src/server_config/system.rs
+++ b/core/configs/src/server_config/system.rs
@@ -18,7 +18,7 @@
 use super::cache_indexes::CacheIndexesConfig;
 use super::server::MemoryPoolConfig;
 use super::sharding::ShardingConfig;
-use configs::ConfigEnv;
+use configs::{ConfigEnv, ConfigEnvMappings};
 use iggy_common::IggyByteSize;
 use iggy_common::IggyError;
 use iggy_common::IggyExpiry;
@@ -33,8 +33,13 @@ use server_common::log::LoggingSettings;
 pub const INDEX_EXTENSION: &str = "index";
 pub const LOG_EXTENSION: &str = "log";
 
+// Generic over the sharding config so the legacy server and `server-ng` each
+// bind their own `ShardingConfig` (different knob sets, different default
+// source) while sharing this whole struct and its path helpers. The default
+// type param keeps bare `SystemConfig` meaning the legacy variant, so existing
+// callers compile unchanged.
 #[derive(Debug, Deserialize, Serialize, ConfigEnv)]
-pub struct SystemConfig {
+pub struct SystemConfig<S: ConfigEnvMappings = ShardingConfig> {
     pub path: String,
     pub backup: BackupConfig,
     pub state: StateConfig,
@@ -49,7 +54,7 @@ pub struct SystemConfig {
     pub message_deduplication: MessageDeduplicationConfig,
     pub recovery: RecoveryConfig,
     pub memory_pool: MemoryPoolConfig,
-    pub sharding: ShardingConfig,
+    pub sharding: S,
 }
 
 #[derive(Debug, Deserialize, Serialize, ConfigEnv)]
@@ -188,7 +193,7 @@ pub struct StateConfig {
     pub retry_delay: IggyDuration,
 }
 
-impl SystemConfig {
+impl<S: ConfigEnvMappings> SystemConfig<S> {
     pub fn get_system_path(&self) -> String {
         self.path.to_string()
     }
@@ -352,7 +357,7 @@ impl SystemConfig {
     }
 }
 
-impl SystemPaths for SystemConfig {
+impl<S: ConfigEnvMappings> SystemPaths for SystemConfig<S> {
     fn get_system_path(&self) -> String {
         SystemConfig::get_system_path(self)
     }
diff --git a/core/configs/src/server_config/validators.rs 
b/core/configs/src/server_config/validators.rs
index ca8410e28..d35763bc6 100644
--- a/core/configs/src/server_config/validators.rs
+++ b/core/configs/src/server_config/validators.rs
@@ -21,10 +21,7 @@ use super::server::{
     DataMaintenanceConfig, MessageSaverConfig, MessagesMaintenanceConfig, 
TelemetryConfig,
 };
 use super::server::{MemoryPoolConfig, PersonalAccessTokenConfig, ServerConfig};
-use super::sharding::{
-    CpuAllocation, INBOX_CAPACITY_MAX, RECONCILE_PERIODIC_INTERVAL_MAX, 
SHUTDOWN_DRAIN_TIMEOUT_MAX,
-    SHUTDOWN_POLL_INTERVAL_MAX, ShardingConfig,
-};
+use super::sharding::{CpuAllocation, ShardingConfig};
 use super::system::SegmentConfig;
 use super::system::{CompressionConfig, LoggingConfig, PartitionConfig};
 use crate::ConfigurationError;
@@ -379,139 +376,70 @@ impl Validatable<ConfigurationError> for 
MemoryPoolConfig {
     }
 }
 
-impl Validatable<ConfigurationError> for ShardingConfig {
-    fn validate(&self) -> Result<(), ConfigurationError> {
-        if self.inbox_capacity == 0 {
-            eprintln!(
-                "Invalid sharding configuration: inbox_capacity must be > 0 
(crossfire silently \
-                 rounds 0 to 1, masking config errors)"
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-        if self.inbox_capacity > INBOX_CAPACITY_MAX {
-            eprintln!(
-                "Invalid sharding configuration: inbox_capacity {} exceeds the 
{} cap (each \
-                 shard preallocates a channel of this size; oversizing here 
OOMs the process at \
-                 boot)",
-                self.inbox_capacity, INBOX_CAPACITY_MAX
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-
-        let drain = self.shutdown_drain_timeout.get_duration();
-        if drain.is_zero() {
-            eprintln!(
-                "Invalid sharding configuration: shutdown_drain_timeout must 
be > 0 (a zero \
-                 budget force-tears the bus mid-WAL-fsync on every shutdown)"
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-        if drain > SHUTDOWN_DRAIN_TIMEOUT_MAX {
-            eprintln!(
-                "Invalid sharding configuration: shutdown_drain_timeout {:?} 
exceeds the {:?} \
-                 cap (an unbounded drain wedges process exit on bus stall)",
-                drain, SHUTDOWN_DRAIN_TIMEOUT_MAX
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-
-        let poll = self.shutdown_poll_interval.get_duration();
-        if poll.is_zero() {
-            eprintln!(
-                "Invalid sharding configuration: shutdown_poll_interval must 
be > 0 (a zero \
-                 cadence busy-loops every shard's watchdog and 
metadata-handoff poller)"
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-        if poll > SHUTDOWN_POLL_INTERVAL_MAX {
-            eprintln!(
-                "Invalid sharding configuration: shutdown_poll_interval {:?} 
exceeds the {:?} \
-                 cap (a coarse cadence stalls Ctrl-C handling and metadata 
handoff abort)",
-                poll, SHUTDOWN_POLL_INTERVAL_MAX
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-        if poll > drain {
-            eprintln!(
-                "Invalid sharding configuration: shutdown_poll_interval {:?} 
must be <= \
-                 shutdown_drain_timeout {:?} (a poll cadence coarser than the 
drain budget makes \
-                 the shutdown flag effectively unobservable)",
-                poll, drain
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-
-        let reconcile = self.reconcile_periodic_interval.get_duration();
-        if reconcile.is_zero() {
-            eprintln!(
-                "Invalid sharding configuration: reconcile_periodic_interval 
resolves to zero. \
-                 Note that \"0\", \"none\", \"unlimited\", and \"disabled\" 
all parse to zero. The \
-                 periodic reconcile tick is a safety net for dropped 
commit-wakes and cannot be \
-                 turned off; set a positive duration (default \"1s\", max 
{RECONCILE_PERIODIC_INTERVAL_MAX:?})."
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
-        }
-        if reconcile > RECONCILE_PERIODIC_INTERVAL_MAX {
-            eprintln!(
-                "Invalid sharding configuration: reconcile_periodic_interval 
{:?} exceeds the \
-                 {:?} cap (a long tick makes post-failure convergence latency 
operator-visible)",
-                reconcile, RECONCILE_PERIODIC_INTERVAL_MAX
-            );
-            return Err(ConfigurationError::InvalidConfigurationValue);
+/// Validate a [`CpuAllocation`] against the machine's available parallelism
+/// and, when pinning, the process affinity mask. Shared by the legacy and
+/// server-ng sharding configs, which both carry these two knobs.
+pub(crate) fn validate_cpu_allocation(
+    cpu_allocation: &CpuAllocation,
+    pin_cores: bool,
+) -> Result<(), ConfigurationError> {
+    let available_cpus = available_parallelism()
+        .map_err(|_| {
+            eprintln!("Failed to detect available CPU cores");
+            ConfigurationError::InvalidConfigurationValue
+        })?
+        .get();
+
+    match cpu_allocation {
+        CpuAllocation::All => Ok(()),
+        CpuAllocation::Count(count) => {
+            if *count == 0 {
+                eprintln!("Invalid sharding configuration: cpu_allocation 
count cannot be 0");
+                return Err(ConfigurationError::InvalidConfigurationValue);
+            }
+            if *count > available_cpus {
+                eprintln!(
+                    "Invalid sharding configuration: cpu_allocation count 
{count} exceeds available CPU cores {available_cpus}"
+                );
+                return Err(ConfigurationError::InvalidConfigurationValue);
+            }
+            Ok(())
         }
-
-        let available_cpus = available_parallelism()
-            .map_err(|_| {
-                eprintln!("Failed to detect available CPU cores");
-                ConfigurationError::InvalidConfigurationValue
-            })?
-            .get();
-
-        match &self.cpu_allocation {
-            CpuAllocation::All => Ok(()),
-            CpuAllocation::Count(count) => {
-                if *count == 0 {
-                    eprintln!("Invalid sharding configuration: cpu_allocation 
count cannot be 0");
-                    return Err(ConfigurationError::InvalidConfigurationValue);
-                }
-                if *count > available_cpus {
-                    eprintln!(
-                        "Invalid sharding configuration: cpu_allocation count 
{count} exceeds available CPU cores {available_cpus}"
-                    );
-                    return Err(ConfigurationError::InvalidConfigurationValue);
-                }
-                Ok(())
+        CpuAllocation::Range(start, end) => {
+            if start >= end {
+                eprintln!(
+                    "Invalid sharding configuration: cpu_allocation range 
{start}..{end} is invalid (start must be less than end)"
+                );
+                return Err(ConfigurationError::InvalidConfigurationValue);
             }
-            CpuAllocation::Range(start, end) => {
-                if start >= end {
-                    eprintln!(
-                        "Invalid sharding configuration: cpu_allocation range 
{start}..{end} is invalid (start must be less than end)"
-                    );
-                    return Err(ConfigurationError::InvalidConfigurationValue);
-                }
-                if *end - *start > available_cpus {
-                    eprintln!(
-                        "Invalid sharding configuration: cpu_allocation range 
{start}..{end} yields {} shards, exceeding available CPU cores 
{available_cpus}",
-                        *end - *start
-                    );
-                    return Err(ConfigurationError::InvalidConfigurationValue);
-                }
-                if !self.pin_cores {
-                    return Ok(());
-                }
-                let allowed = allowed_cpus();
-                if let Some(cpu) = (*start..*end).find(|cpu| 
!allowed.contains(cpu)) {
-                    eprintln!(
-                        "Invalid sharding configuration: cpu_allocation range 
{start}..{end} includes CPU {cpu}, which is outside the set of cores allowed 
for this process (affinity/cpuset mask)"
-                    );
-                    return Err(ConfigurationError::InvalidConfigurationValue);
-                }
-                Ok(())
+            if *end - *start > available_cpus {
+                eprintln!(
+                    "Invalid sharding configuration: cpu_allocation range 
{start}..{end} yields {} shards, exceeding available CPU cores 
{available_cpus}",
+                    *end - *start
+                );
+                return Err(ConfigurationError::InvalidConfigurationValue);
+            }
+            if !pin_cores {
+                return Ok(());
+            }
+            let allowed = allowed_cpus();
+            if let Some(cpu) = (*start..*end).find(|cpu| 
!allowed.contains(cpu)) {
+                eprintln!(
+                    "Invalid sharding configuration: cpu_allocation range 
{start}..{end} includes CPU {cpu}, which is outside the set of cores allowed 
for this process (affinity/cpuset mask)"
+                );
+                return Err(ConfigurationError::InvalidConfigurationValue);
             }
-            // NUMA topology validation requires hwlocality (runtime dep).
-            // Full NUMA validation happens in shard_allocator at startup.
-            CpuAllocation::NumaAware(_) => Ok(()),
+            Ok(())
         }
+        // NUMA topology validation requires hwlocality (runtime dep).
+        // Full NUMA validation happens in shard_allocator at startup.
+        CpuAllocation::NumaAware(_) => Ok(()),
+    }
+}
+
+impl Validatable<ConfigurationError> for ShardingConfig {
+    fn validate(&self) -> Result<(), ConfigurationError> {
+        validate_cpu_allocation(&self.cpu_allocation, self.pin_cores)
     }
 }
 
@@ -958,69 +886,6 @@ mod cluster_shards_count_determinism_tests {
     }
 }
 
-#[cfg(test)]
-mod sharding_shutdown_knob_tests {
-    use super::*;
-    use crate::server_config::sharding::{SHUTDOWN_DRAIN_TIMEOUT_MAX, 
SHUTDOWN_POLL_INTERVAL_MAX};
-    use iggy_common::IggyDuration;
-    use std::time::Duration;
-
-    #[test]
-    fn defaults_validate() {
-        assert!(ShardingConfig::default().validate().is_ok());
-    }
-
-    #[test]
-    fn zero_drain_is_rejected() {
-        let cfg = ShardingConfig {
-            shutdown_drain_timeout: IggyDuration::new(Duration::ZERO),
-            ..ShardingConfig::default()
-        };
-        assert!(cfg.validate().is_err());
-    }
-
-    #[test]
-    fn over_cap_drain_is_rejected() {
-        let cfg = ShardingConfig {
-            shutdown_drain_timeout: IggyDuration::new(
-                SHUTDOWN_DRAIN_TIMEOUT_MAX + Duration::from_secs(1),
-            ),
-            ..ShardingConfig::default()
-        };
-        assert!(cfg.validate().is_err());
-    }
-
-    #[test]
-    fn zero_poll_is_rejected() {
-        let cfg = ShardingConfig {
-            shutdown_poll_interval: IggyDuration::new(Duration::ZERO),
-            ..ShardingConfig::default()
-        };
-        assert!(cfg.validate().is_err());
-    }
-
-    #[test]
-    fn over_cap_poll_is_rejected() {
-        let cfg = ShardingConfig {
-            shutdown_poll_interval: IggyDuration::new(
-                SHUTDOWN_POLL_INTERVAL_MAX + Duration::from_secs(1),
-            ),
-            ..ShardingConfig::default()
-        };
-        assert!(cfg.validate().is_err());
-    }
-
-    #[test]
-    fn poll_greater_than_drain_is_rejected() {
-        let cfg = ShardingConfig {
-            shutdown_drain_timeout: 
IggyDuration::new(Duration::from_millis(20)),
-            shutdown_poll_interval: 
IggyDuration::new(Duration::from_millis(50)),
-            ..ShardingConfig::default()
-        };
-        assert!(cfg.validate().is_err());
-    }
-}
-
 #[cfg(test)]
 mod sharding_cpu_range_tests {
     use super::*;
@@ -1029,7 +894,7 @@ mod sharding_cpu_range_tests {
     fn inverted_range_is_rejected() {
         let cfg = ShardingConfig {
             cpu_allocation: CpuAllocation::Range(2, 2),
-            ..ShardingConfig::default()
+            pin_cores: true,
         };
         assert!(cfg.validate().is_err());
     }
@@ -1039,7 +904,7 @@ mod sharding_cpu_range_tests {
         let first = allowed_cpus()[0];
         let cfg = ShardingConfig {
             cpu_allocation: CpuAllocation::Range(first, first + 1),
-            ..ShardingConfig::default()
+            pin_cores: true,
         };
         assert!(cfg.validate().is_ok());
     }
@@ -1049,7 +914,7 @@ mod sharding_cpu_range_tests {
         let past_last = allowed_cpus().last().copied().unwrap() + 1;
         let cfg = ShardingConfig {
             cpu_allocation: CpuAllocation::Range(past_last, past_last + 1),
-            ..ShardingConfig::default()
+            pin_cores: true,
         };
         assert!(cfg.validate().is_err());
     }
@@ -1063,7 +928,7 @@ mod sharding_cpu_range_tests {
         let available = available_parallelism().unwrap().get();
         let cfg = ShardingConfig {
             cpu_allocation: CpuAllocation::Range(first, first + available + 1),
-            ..ShardingConfig::default()
+            pin_cores: true,
         };
         assert!(cfg.validate().is_err());
     }
@@ -1075,7 +940,6 @@ mod sharding_cpu_range_tests {
         let cfg = ShardingConfig {
             cpu_allocation: CpuAllocation::Range(1 << 20, (1 << 20) + 1),
             pin_cores: false,
-            ..ShardingConfig::default()
         };
         assert!(cfg.validate().is_ok());
 
@@ -1083,8 +947,28 @@ mod sharding_cpu_range_tests {
         let cfg = ShardingConfig {
             cpu_allocation: CpuAllocation::Range(0, available + 1),
             pin_cores: false,
-            ..ShardingConfig::default()
         };
         assert!(cfg.validate().is_err());
     }
 }
+
+#[cfg(test)]
+mod sharding_embedded_default_tests {
+    use super::*;
+    use figment::Figment;
+    use figment::providers::{Format, Toml};
+
+    // Guards the single source of truth: the legacy sharding defaults resolve
+    // from the embedded legacy TOML, not hard-coded Rust values.
+    #[test]
+    fn legacy_embedded_toml_resolves_sharding_defaults() {
+        let toml_str = include_str!("../../../server/config.toml");
+        let config: ServerConfig = Figment::new()
+            .merge(Toml::string(toml_str))
+            .extract()
+            .expect("embedded legacy TOML deserializes");
+        config.validate().expect("embedded legacy config validates");
+
+        assert!(config.system.sharding.pin_cores);
+    }
+}
diff --git a/core/configs/src/server_ng_config/defaults.rs 
b/core/configs/src/server_ng_config/defaults.rs
index ff4ba2f85..c7e724934 100644
--- a/core/configs/src/server_ng_config/defaults.rs
+++ b/core/configs/src/server_ng_config/defaults.rs
@@ -29,6 +29,7 @@
 
 use super::message_bus::MessageBusConfig;
 use super::quic::{QuicCertificateConfig, QuicConfig, QuicSocketConfig};
+use super::server_ng::NgSystemConfig;
 use super::server_ng::{ExtraConfig, ServerNgConfig};
 use super::tcp::{TcpConfig, TcpSocketConfig, TcpTlsConfig};
 use super::websocket::{WebSocketConfig, WebSocketTlsConfig};
@@ -38,7 +39,6 @@ use crate::server_config::server::{
     ConsumerGroupConfig, DataMaintenanceConfig, HeartbeatConfig, 
MessageSaverConfig,
     PersonalAccessTokenConfig, TelemetryConfig,
 };
-use crate::server_config::system::SystemConfig;
 use std::sync::Arc;
 
 static_toml::static_toml! {
@@ -55,7 +55,7 @@ impl Default for ServerNgConfig {
             heartbeat: HeartbeatConfig::default(),
             message_saver: MessageSaverConfig::default(),
             personal_access_token: PersonalAccessTokenConfig::default(),
-            system: Arc::new(SystemConfig::default()),
+            system: Arc::new(NgSystemConfig::default()),
             quic: QuicConfig::default(),
             tcp: TcpConfig::default(),
             websocket: WebSocketConfig::default(),
diff --git a/core/configs/src/server_ng_config/mod.rs 
b/core/configs/src/server_ng_config/mod.rs
index a6e353682..e3196e521 100644
--- a/core/configs/src/server_ng_config/mod.rs
+++ b/core/configs/src/server_ng_config/mod.rs
@@ -32,6 +32,7 @@ pub mod displays;
 pub mod message_bus;
 pub mod quic;
 pub mod server_ng;
+pub mod sharding;
 pub mod tcp;
 pub mod validators;
 pub mod websocket;
diff --git a/core/configs/src/server_ng_config/server_ng.rs 
b/core/configs/src/server_ng_config/server_ng.rs
index 56bd0a60d..0323f6df6 100644
--- a/core/configs/src/server_ng_config/server_ng.rs
+++ b/core/configs/src/server_ng_config/server_ng.rs
@@ -41,6 +41,11 @@ use std::sync::Arc;
 
 const DEFAULT_CONFIG_PATH: &str = "core/server-ng/config.toml";
 
+/// The `server-ng` flavour of [`SystemConfig`], bound to this crate's own
+/// [`super::sharding::ShardingConfig`]. `core/server-ng` names this alias
+/// wherever it refers to the system config.
+pub type NgSystemConfig = SystemConfig<super::sharding::ShardingConfig>;
+
 /// Top-level on-disk config schema for the `server-ng` binary.
 ///
 /// Mirrors the legacy [`crate::server::ServerConfig`] section surface
@@ -64,7 +69,7 @@ pub struct ServerNgConfig {
     pub message_saver: MessageSaverConfig,
     pub personal_access_token: PersonalAccessTokenConfig,
     pub heartbeat: HeartbeatConfig,
-    pub system: Arc<SystemConfig>,
+    pub system: Arc<NgSystemConfig>,
     pub quic: QuicConfig,
     pub tcp: TcpConfig,
     pub http: HttpConfig,
diff --git a/core/configs/src/server_ng_config/sharding.rs 
b/core/configs/src/server_ng_config/sharding.rs
new file mode 100644
index 000000000..1f2e3f419
--- /dev/null
+++ b/core/configs/src/server_ng_config/sharding.rs
@@ -0,0 +1,357 @@
+// 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.
+
+//! Sharding config for `server-ng`. Forked from the legacy
+//! [`crate::server_config::sharding`] because the two servers own different
+//! knob sets and different default sources: this type carries the full
+//! thread-per-core + bus surface and reads its defaults from the server-ng
+//! TOML, while the legacy type keeps only `cpu_allocation` + `pin_cores`.
+
+use iggy_common::IggyDuration;
+use iggy_common::Validatable;
+use serde::{Deserialize, Serialize};
+use serde_with::{DisplayFromStr, serde_as};
+use std::time::Duration;
+
+use super::defaults::SERVER_NG_CONFIG;
+use crate::ConfigurationError;
+use crate::server_config::validators::validate_cpu_allocation;
+use configs::ConfigEnv;
+
+// Re-exported so the `configs::ng_sharding::*` path mirrors the legacy
+// `configs::sharding::*` surface for callers.
+pub use cpu_allocation::{CpuAllocation, NumaConfig};
+
+/// Maximum permitted per-shard inbox depth. The channel is allocated
+/// up-front per shard, so a runaway value here OOMs the process at boot.
+/// `1 << 20` (~1M frames) is several orders of magnitude above any
+/// realistic backpressure target and still fits comfortably in process
+/// address space.
+pub const INBOX_CAPACITY_MAX: usize = 1 << 20;
+
+/// Hard upper bound on `shutdown_drain_timeout`. A drain that never
+/// completes wedges process exit; capping at 10 minutes guarantees the
+/// watchdog eventually force-tears the bus even with a pathological
+/// config typo.
+pub const SHUTDOWN_DRAIN_TIMEOUT_MAX: Duration = Duration::from_secs(600);
+
+/// Hard upper bound on `shutdown_poll_interval`. A poll interval longer
+/// than the drain timeout makes the flag effectively unobservable; cap
+/// at 5s so Ctrl-C latency stays bounded regardless of config.
+pub const SHUTDOWN_POLL_INTERVAL_MAX: Duration = Duration::from_secs(5);
+
+/// Hard upper bound on `reconcile_periodic_interval`. A tick longer
+/// than ~30s makes post-failure recovery latency operator-visible; the
+/// cap reins in pathological typos without disturbing reasonable
+/// production values.
+pub const RECONCILE_PERIODIC_INTERVAL_MAX: Duration = Duration::from_secs(30);
+
+// Every omitted field falls back to the frozen `Default`, so a partial
+// `[system.sharding]` table resolves each key independently instead of
+// failing on the first missing one (parity with the legacy type).
+#[serde_as]
+#[derive(Debug, Deserialize, Serialize, ConfigEnv)]
+#[serde(default)]
+pub struct ShardingConfig {
+    #[serde(default)]
+    #[config_env(leaf)]
+    pub cpu_allocation: CpuAllocation,
+    /// Whether shard threads are pinned to dedicated CPU cores
+    /// (`sched_setaffinity`). Pinning maximizes cache locality when this
+    /// server owns its cores (dedicated host, `numa:` allocations). Set to
+    /// `false` when the server shares cores with other workloads — e.g. a
+    /// multi-tenant host slicing CPU via cgroup quotas — where every process
+    /// pinning to the same low-numbered cores would pile onto one core while
+    /// the rest sit idle; unpinned shards let the kernel scheduler place
+    /// threads freely within the allowed set. With a NUMA-aware allocation,
+    /// `false` drops both the CPU and memory-node bindings (and logs a
+    /// warning, since NUMA placement without pinning is meaningless).
+    pub pin_cores: bool,
+    /// Per-shard inter-shard inbox channel capacity. Bounded by design.
+    /// Drops on full inbox of consensus frames are recovered by VSR
+    /// retransmit. Drops of cross-shard client Reply frames are terminal:
+    /// the client never receives the reply (no in-protocol retransmit).
+    /// Both frame classes share this one channel, so a consensus burst
+    /// can starve client-reply forwards: size against the worst-case sum
+    /// of consensus working set + peak client-reply fan-out per shard
+    /// occurring together.
+    ///
+    // TODO(hubcio): split into two priority lanes - one bounded queue for
+    // consensus frames (drops recovered by VSR retransmit) and one for
+    // client `Reply` frames (drops terminal, must be sized for worst-case
+    // fan-out). Current single-channel design is the minimum-viable
+    // wiring so `frame_drops_total{variant,reason}` surfaces under load
+    // and yields real numbers to size the split against.
+    pub inbox_capacity: usize,
+    /// Wall-clock budget for a single shard's bus drain on shutdown.
+    /// Drives `IggyMessageBus::shutdown(..)` from the per-shard watchdog
+    /// and the parallel-join survivor path. Sized larger than typical
+    /// TCP RTT times in-flight write-batch so writers receive their full
+    /// last `write_vectored_all` budget before the connection registry
+    /// force-tears the bus. Slow-fsync hosts may need to extend this past
+    /// the default; the cap is `SHUTDOWN_DRAIN_TIMEOUT_MAX` so a config
+    /// typo cannot wedge process exit.
+    #[serde_as(as = "DisplayFromStr")]
+    #[config_env(leaf)]
+    pub shutdown_drain_timeout: IggyDuration,
+    /// Poll cadence for the cross-thread shutdown flag and for the
+    /// `await_metadata_bundle` / `broadcast_metadata_bundle` poll loops.
+    /// Trades off Ctrl-C latency against idle wakeup cost; the default
+    /// keeps shutdown observably prompt without measurable scheduler
+    /// overhead. Capped at `SHUTDOWN_POLL_INTERVAL_MAX` so the flag
+    /// remains effectively observable regardless of config.
+    #[serde_as(as = "DisplayFromStr")]
+    #[config_env(leaf)]
+    pub shutdown_poll_interval: IggyDuration,
+    /// Safety-tick cadence for the partition reconciliation loop; the
+    /// reconciler also wakes immediately on every
+    /// `LifecycleFrame::MetadataCommitTick` from shard 0. This periodic
+    /// fallback covers dropped wake-ups (the wake channel is capacity-1)
+    /// and the initial post-bootstrap convergence window. Values above
+    /// [`RECONCILE_PERIODIC_INTERVAL_MAX`] are rejected by the validator.
+    #[serde_as(as = "DisplayFromStr")]
+    #[config_env(leaf)]
+    pub reconcile_periodic_interval: IggyDuration,
+}
+
+impl Default for ShardingConfig {
+    fn default() -> Self {
+        Self {
+            cpu_allocation: CpuAllocation::default(),
+            pin_cores: SERVER_NG_CONFIG.system.sharding.pin_cores,
+            inbox_capacity: SERVER_NG_CONFIG.system.sharding.inbox_capacity as 
usize,
+            shutdown_drain_timeout: SERVER_NG_CONFIG
+                .system
+                .sharding
+                .shutdown_drain_timeout
+                .parse()
+                .unwrap(),
+            shutdown_poll_interval: SERVER_NG_CONFIG
+                .system
+                .sharding
+                .shutdown_poll_interval
+                .parse()
+                .unwrap(),
+            reconcile_periodic_interval: SERVER_NG_CONFIG
+                .system
+                .sharding
+                .reconcile_periodic_interval
+                .parse()
+                .unwrap(),
+        }
+    }
+}
+
+impl Validatable<ConfigurationError> for ShardingConfig {
+    fn validate(&self) -> Result<(), ConfigurationError> {
+        if self.inbox_capacity == 0 {
+            eprintln!(
+                "Invalid sharding configuration: inbox_capacity must be > 0 
(crossfire silently \
+                 rounds 0 to 1, masking config errors)"
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+        if self.inbox_capacity > INBOX_CAPACITY_MAX {
+            eprintln!(
+                "Invalid sharding configuration: inbox_capacity {} exceeds the 
{} cap (each \
+                 shard preallocates a channel of this size; oversizing here 
OOMs the process at \
+                 boot)",
+                self.inbox_capacity, INBOX_CAPACITY_MAX
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+
+        let drain = self.shutdown_drain_timeout.get_duration();
+        if drain.is_zero() {
+            eprintln!(
+                "Invalid sharding configuration: shutdown_drain_timeout must 
be > 0 (a zero \
+                 budget force-tears the bus mid-WAL-fsync on every shutdown)"
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+        if drain > SHUTDOWN_DRAIN_TIMEOUT_MAX {
+            eprintln!(
+                "Invalid sharding configuration: shutdown_drain_timeout {:?} 
exceeds the {:?} \
+                 cap (an unbounded drain wedges process exit on bus stall)",
+                drain, SHUTDOWN_DRAIN_TIMEOUT_MAX
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+
+        let poll = self.shutdown_poll_interval.get_duration();
+        if poll.is_zero() {
+            eprintln!(
+                "Invalid sharding configuration: shutdown_poll_interval must 
be > 0 (a zero \
+                 cadence busy-loops every shard's watchdog and 
metadata-handoff poller)"
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+        if poll > SHUTDOWN_POLL_INTERVAL_MAX {
+            eprintln!(
+                "Invalid sharding configuration: shutdown_poll_interval {:?} 
exceeds the {:?} \
+                 cap (a coarse cadence stalls Ctrl-C handling and metadata 
handoff abort)",
+                poll, SHUTDOWN_POLL_INTERVAL_MAX
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+        if poll > drain {
+            eprintln!(
+                "Invalid sharding configuration: shutdown_poll_interval {:?} 
must be <= \
+                 shutdown_drain_timeout {:?} (a poll cadence coarser than the 
drain budget makes \
+                 the shutdown flag effectively unobservable)",
+                poll, drain
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+
+        let reconcile = self.reconcile_periodic_interval.get_duration();
+        if reconcile.is_zero() {
+            eprintln!(
+                "Invalid sharding configuration: reconcile_periodic_interval 
resolves to zero. \
+                 Note that \"0\", \"none\", \"unlimited\", and \"disabled\" 
all parse to zero. The \
+                 periodic reconcile tick is a safety net for dropped 
commit-wakes and cannot be \
+                 turned off; set a positive duration (default \"1s\", max 
{RECONCILE_PERIODIC_INTERVAL_MAX:?})."
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+        if reconcile > RECONCILE_PERIODIC_INTERVAL_MAX {
+            eprintln!(
+                "Invalid sharding configuration: reconcile_periodic_interval 
{:?} exceeds the \
+                 {:?} cap (a long tick makes post-failure convergence latency 
operator-visible)",
+                reconcile, RECONCILE_PERIODIC_INTERVAL_MAX
+            );
+            return Err(ConfigurationError::InvalidConfigurationValue);
+        }
+
+        validate_cpu_allocation(&self.cpu_allocation, self.pin_cores)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::server_ng_config::server_ng::ServerNgConfig;
+    use figment::Figment;
+    use figment::providers::{Format, Toml};
+
+    #[test]
+    fn defaults_validate() {
+        assert!(ShardingConfig::default().validate().is_ok());
+    }
+
+    #[test]
+    fn zero_drain_is_rejected() {
+        let cfg = ShardingConfig {
+            shutdown_drain_timeout: IggyDuration::new(Duration::ZERO),
+            ..ShardingConfig::default()
+        };
+        assert!(cfg.validate().is_err());
+    }
+
+    #[test]
+    fn over_cap_drain_is_rejected() {
+        let cfg = ShardingConfig {
+            shutdown_drain_timeout: IggyDuration::new(
+                SHUTDOWN_DRAIN_TIMEOUT_MAX + Duration::from_secs(1),
+            ),
+            ..ShardingConfig::default()
+        };
+        assert!(cfg.validate().is_err());
+    }
+
+    #[test]
+    fn zero_poll_is_rejected() {
+        let cfg = ShardingConfig {
+            shutdown_poll_interval: IggyDuration::new(Duration::ZERO),
+            ..ShardingConfig::default()
+        };
+        assert!(cfg.validate().is_err());
+    }
+
+    #[test]
+    fn over_cap_poll_is_rejected() {
+        let cfg = ShardingConfig {
+            shutdown_poll_interval: IggyDuration::new(
+                SHUTDOWN_POLL_INTERVAL_MAX + Duration::from_secs(1),
+            ),
+            ..ShardingConfig::default()
+        };
+        assert!(cfg.validate().is_err());
+    }
+
+    #[test]
+    fn poll_greater_than_drain_is_rejected() {
+        let cfg = ShardingConfig {
+            shutdown_drain_timeout: 
IggyDuration::new(Duration::from_millis(20)),
+            shutdown_poll_interval: 
IggyDuration::new(Duration::from_millis(50)),
+            ..ShardingConfig::default()
+        };
+        assert!(cfg.validate().is_err());
+    }
+
+    // Guards the single source of truth: the server-ng sharding defaults
+    // resolve from the embedded server-ng TOML, not hard-coded Rust values.
+    #[test]
+    fn ng_embedded_toml_resolves_sharding_defaults() {
+        let toml_str = include_str!("../../../server-ng/config.toml");
+        let config: ServerNgConfig = Figment::new()
+            .merge(Toml::string(toml_str))
+            .extract()
+            .expect("embedded server-ng TOML deserializes");
+        config
+            .validate()
+            .expect("embedded server-ng config validates");
+
+        let sharding = &config.system.sharding;
+        assert!(sharding.pin_cores);
+        assert_eq!(sharding.inbox_capacity, 1024);
+        assert_eq!(sharding.shutdown_drain_timeout, "10 s".parse().unwrap());
+        assert_eq!(sharding.shutdown_poll_interval, "50 ms".parse().unwrap());
+        assert_eq!(sharding.reconcile_periodic_interval, "1 
s".parse().unwrap());
+    }
+
+    // Extract straight from a raw table (no embedded base layer) so the
+    // struct-level `#[serde(default)]` is what fills the gaps, not the
+    // provider's embedded-TOML fallback.
+    #[test]
+    fn partial_table_fills_missing_fields_with_frozen_defaults() {
+        let sharding: ShardingConfig = Figment::new()
+            .merge(Toml::string("pin_cores = false"))
+            .extract()
+            .expect("partial sharding table deserializes");
+
+        assert!(!sharding.pin_cores);
+        assert_eq!(sharding.inbox_capacity, 1024);
+        assert_eq!(sharding.shutdown_drain_timeout, "10 s".parse().unwrap());
+        assert_eq!(sharding.shutdown_poll_interval, "50 ms".parse().unwrap());
+        assert_eq!(sharding.reconcile_periodic_interval, "1 
s".parse().unwrap());
+    }
+
+    #[test]
+    fn empty_table_yields_all_frozen_defaults() {
+        let sharding: ShardingConfig = Figment::new()
+            .merge(Toml::string(""))
+            .extract()
+            .expect("empty sharding table deserializes");
+
+        assert!(sharding.pin_cores);
+        assert_eq!(sharding.inbox_capacity, 1024);
+        assert_eq!(sharding.shutdown_drain_timeout, "10 s".parse().unwrap());
+        assert_eq!(sharding.shutdown_poll_interval, "50 ms".parse().unwrap());
+        assert_eq!(sharding.reconcile_periodic_interval, "1 
s".parse().unwrap());
+    }
+}
diff --git a/core/configs_derive/src/config_env.rs 
b/core/configs_derive/src/config_env.rs
index 7556cbb13..33f15e9f6 100644
--- a/core/configs_derive/src/config_env.rs
+++ b/core/configs_derive/src/config_env.rs
@@ -247,7 +247,7 @@ fn generate_struct_impl(
 
     let const_definitions = generate_const_definitions(&mappings, prefix_str);
     let mapping_entries = generate_mapping_entries(&mappings);
-    let builder_methods = generate_builder_methods(&mappings, struct_name, 
prefix_str);
+    let builder_methods = generate_builder_methods(&mappings, prefix_str);
     let builder_name = format_ident!("{}EnvBuilder", struct_name);
     let mappings_count = mappings.len();
 
@@ -572,16 +572,11 @@ fn generate_mapping_entries(mappings: &[EnvMapping]) -> 
Vec<TokenStream2> {
         .collect()
 }
 
-fn generate_builder_methods(
-    mappings: &[EnvMapping],
-    struct_name: &Ident,
-    prefix: &str,
-) -> Vec<TokenStream2> {
+fn generate_builder_methods(mappings: &[EnvMapping], prefix: &str) -> 
Vec<TokenStream2> {
     mappings
         .iter()
         .map(|m| {
             let method_name = &m.builder_method_name;
-            let const_name = &m.const_name;
             let env_var_name = format!("{}{}", prefix, m.env_suffix);
             let doc_secret = if m.is_secret {
                 " (secret - will be masked in logs)"
@@ -593,7 +588,7 @@ fn generate_builder_methods(
             quote! {
                 #[doc = #doc]
                 pub fn #method_name(mut self, value: impl std::fmt::Display) 
-> Self {
-                    self.envs.insert(#struct_name::#const_name.into(), 
value.to_string());
+                    self.envs.insert(#env_var_name.into(), value.to_string());
                     self
                 }
             }
diff --git a/core/server-ng/config.toml b/core/server-ng/config.toml
index 70af981f5..92638aa18 100644
--- a/core/server-ng/config.toml
+++ b/core/server-ng/config.toml
@@ -637,7 +637,14 @@ cpu_allocation = 1
 # (e.g. a multi-tenant host slicing CPU via cgroup quotas): unpinned shards
 # let the kernel scheduler place threads freely instead of piling every
 # process onto the same low-numbered cores.
-# pin_cores = true
+pin_cores = true
+
+# Per-shard inter-shard inbox capacity. Bounded by design: consensus-frame
+# drops recover via VSR retransmit, but cross-shard client-reply drops are
+# terminal. Size for the worst-case sum of both: the consensus working set
+# (~ PIPELINE_PREPARE_QUEUE_MAX (32) * replica_count * directions) plus peak
+# client-reply fan-out per shard.
+inbox_capacity = 1024
 
 # Wall-clock budget for a single shard's bus drain on shutdown. Drives
 # the per-shard watchdog and the parallel-join survivor path; sized
@@ -653,6 +660,11 @@ shutdown_drain_timeout = "10 s"
 # scheduler overhead. Must be less than or equal to shutdown_drain_timeout.
 shutdown_poll_interval = "50 ms"
 
+# Safety-tick cadence for the partition reconciliation loop. The reconciler
+# also wakes on every metadata commit from shard 0, so this only covers
+# dropped wake-ups and the initial post-bootstrap convergence window.
+reconcile_periodic_interval = "1 s"
+
 [websocket]
 enabled = true
 address = "127.0.0.1:8092"
diff --git a/core/server-ng/src/bootstrap.rs b/core/server-ng/src/bootstrap.rs
index 1c3a6799a..8d079bcd8 100644
--- a/core/server-ng/src/bootstrap.rs
+++ b/core/server-ng/src/bootstrap.rs
@@ -30,10 +30,10 @@ use crate::partition_helpers::{
 use crate::segment_recovery::{RecoveredSegment, load_persisted_segments};
 use crate::server_error::{ServerNgError, ShardJoinFailure, 
ShardJoinFailureKind};
 use crate::session_manager::SessionManager;
-use configs::server_ng::ServerNgConfig;
-use configs::sharding::{
+use configs::ng_sharding::{
     INBOX_CAPACITY_MAX, SHUTDOWN_DRAIN_TIMEOUT_MAX, SHUTDOWN_POLL_INTERVAL_MAX,
 };
+use configs::server_ng::ServerNgConfig;
 use consensus::{LocalPipeline, MetadataHandle, PartitionsHandle, Sequencer, 
VsrConsensus};
 // `try_send` / `try_recv` resolve through these traits on `MAsyncTx` /
 // `MAsyncRx`; the metadata-handoff loops below depend on the
@@ -462,7 +462,7 @@ pub async fn load_config(logging: &mut Logging) -> 
Result<ServerNgConfig, Server
 /// invariant is held by the type system, not by hoping the operator
 /// never configures 65535 cores worth of shards.
 fn resolve_shard_assignments(
-    sharding: &configs::sharding::ShardingConfig,
+    sharding: &configs::ng_sharding::ShardingConfig,
 ) -> Result<(Vec<ShardInfo>, u16), ServerNgError> {
     let allocator = ShardAllocator::new(&sharding.cpu_allocation, 
sharding.pin_cores)
         .map_err(ServerNgError::ShardAllocator)?;
@@ -486,7 +486,7 @@ fn resolve_shard_assignments(
 /// usage) cannot OOM at boot or wedge process exit with an out-of-range
 /// value.
 fn validate_sharding_runtime_knobs(
-    sharding: &configs::sharding::ShardingConfig,
+    sharding: &configs::ng_sharding::ShardingConfig,
 ) -> Result<(), ServerNgError> {
     let inbox_capacity = sharding.inbox_capacity;
     if inbox_capacity == 0 || inbox_capacity > INBOX_CAPACITY_MAX {
diff --git a/core/server-ng/src/dispatch.rs b/core/server-ng/src/dispatch.rs
index 5d510860a..6838a742a 100644
--- a/core/server-ng/src/dispatch.rs
+++ b/core/server-ng/src/dispatch.rs
@@ -51,7 +51,7 @@ use crate::snapshot;
 use crate::users::maybe_rewrite_user_password_request;
 use crate::wire::{request_body, usize_to_u32};
 use bytes::Bytes;
-use configs::system::SystemConfig;
+use configs::server_ng::NgSystemConfig;
 use consensus::{
     Consensus, EvictionContext, MetadataHandle, PartitionsHandle, 
build_eviction_message,
     build_incompatible_protocol_eviction_message,
@@ -112,7 +112,7 @@ pub(crate) type ActiveClientRequests = 
Rc<RefCell<HashSet<u128>>>;
 pub(crate) fn make_client_request_handler(
     shard: &Rc<ServerNgShard>,
     sessions: &Rc<RefCell<SessionManager>>,
-    system_config: Arc<SystemConfig>,
+    system_config: Arc<NgSystemConfig>,
 ) -> RequestHandler {
     let shard = Rc::clone(shard);
     let sessions = Rc::clone(sessions);
@@ -404,7 +404,7 @@ pub(crate) fn make_deferred_client_request_handler(
     bus: &Rc<IggyMessageBus>,
     shard_handle: &ServerNgShardHandle,
     sessions: &Rc<RefCell<SessionManager>>,
-    system_config: Arc<SystemConfig>,
+    system_config: Arc<NgSystemConfig>,
 ) -> RequestHandler {
     let shard_handle = Rc::clone(shard_handle);
     let sessions = Rc::clone(sessions);
@@ -536,7 +536,7 @@ pub(crate) fn make_metadata_submit_handler(
 fn enqueue_client_request(
     shard: Rc<ServerNgShard>,
     sessions: Rc<RefCell<SessionManager>>,
-    system_config: Arc<SystemConfig>,
+    system_config: Arc<NgSystemConfig>,
     queues: ClientRequestQueues,
     active: ActiveClientRequests,
     client_id: u128,
@@ -561,7 +561,7 @@ fn enqueue_client_request(
 async fn drain_client_requests(
     shard: Rc<ServerNgShard>,
     sessions: Rc<RefCell<SessionManager>>,
-    system_config: Arc<SystemConfig>,
+    system_config: Arc<NgSystemConfig>,
     queues: ClientRequestQueues,
     active: ActiveClientRequests,
     client_id: u128,
@@ -598,7 +598,7 @@ fn pop_next_client_request(
 async fn handle_client_request(
     shard: &Rc<ServerNgShard>,
     sessions: &Rc<RefCell<SessionManager>>,
-    system_config: &Arc<SystemConfig>,
+    system_config: &Arc<NgSystemConfig>,
     transport_client_id: u128,
     message: Message<iggy_binary_protocol::GenericHeader>,
 ) {
@@ -1035,7 +1035,7 @@ pub(crate) async fn dispatch_partition_request(
 async fn handle_non_replicated_request(
     shard: &Rc<ServerNgShard>,
     sessions: &Rc<RefCell<SessionManager>>,
-    system_config: &Arc<SystemConfig>,
+    system_config: &Arc<NgSystemConfig>,
     transport_client_id: u128,
     request: Message<RequestHeader>,
 ) {
@@ -1232,7 +1232,7 @@ async fn handle_default_non_replicated(
 #[allow(clippy::future_not_send)]
 async fn handle_get_snapshot(
     shard: &Rc<ServerNgShard>,
-    system_config: &Arc<SystemConfig>,
+    system_config: &Arc<NgSystemConfig>,
     transport_client_id: u128,
     request: &Message<RequestHeader>,
     user_id: Option<u32>,
diff --git a/core/server-ng/src/http.rs b/core/server-ng/src/http.rs
index 38c402940..4163d83d8 100644
--- a/core/server-ng/src/http.rs
+++ b/core/server-ng/src/http.rs
@@ -46,7 +46,7 @@ use axum::middleware::{Next, from_fn};
 use axum::routing::{delete, get, post, put};
 use configs::cluster::{ClusterConfig, TransportPorts};
 use configs::http::HttpConfig;
-use configs::system::SystemConfig;
+use configs::server_ng::NgSystemConfig;
 use message_bus::client_listener;
 use send_wrapper::SendWrapper;
 use tracing::{error, info};
@@ -83,7 +83,7 @@ pub async fn start(
     addr: SocketAddr,
     http_config: &HttpConfig,
     cluster: &ClusterConfig,
-    system_config: Arc<SystemConfig>,
+    system_config: Arc<NgSystemConfig>,
     self_ports: TransportPorts,
 ) -> Result<(), ServerNgError> {
     let jwt = JwtManager::build(&http_config.jwt)?;
diff --git a/core/server-ng/src/http/state.rs b/core/server-ng/src/http/state.rs
index 9ddfb811f..55df0d8e8 100644
--- a/core/server-ng/src/http/state.rs
+++ b/core/server-ng/src/http/state.rs
@@ -26,7 +26,7 @@ use std::sync::Arc;
 
 use axum::http::{HeaderName, HeaderValue};
 use axum::response::Response;
-use configs::system::SystemConfig;
+use configs::server_ng::NgSystemConfig;
 use consensus::{MetadataHandle, VsrConsensus};
 use iggy_common::{ClusterMetadata, IggyTimestamp};
 use message_bus::InstanceToken;
@@ -67,7 +67,7 @@ pub(in crate::http) struct HttpInner {
     /// Read-only server config for the snapshot collector (log directory +
     /// runtime config paths); the shard does not expose config on the read
     /// path.
-    pub(in crate::http) system_config: Arc<SystemConfig>,
+    pub(in crate::http) system_config: Arc<NgSystemConfig>,
     /// Per-credential VSR sessions keyed by JWT `jti` / PAT hash. `RefCell` is
     /// sound here - shard 0 is single-threaded and the `SendWrapper` state
     /// bridge tolerates the `!Sync` interior - but the guard must never be 
held
diff --git a/core/server-ng/src/partition_reconciler.rs 
b/core/server-ng/src/partition_reconciler.rs
index ca0da51cd..37d3f5d3b 100644
--- a/core/server-ng/src/partition_reconciler.rs
+++ b/core/server-ng/src/partition_reconciler.rs
@@ -822,7 +822,7 @@ pub fn install_tick_handler(shard: &Rc<ServerNgShard>, 
wake_tx: WakeTx) {
 #[cfg(test)]
 mod tests {
     use super::{FailureCause, FailureRecord, ReconcilerCtx, reconcile_once};
-    use configs::server_ng::ServerNgConfig;
+    use configs::server_ng::{NgSystemConfig, ServerNgConfig};
     use consensus::{MetadataHandle, PartitionsHandle};
     use iggy_binary_protocol::codec::WireEncode;
     use iggy_binary_protocol::primitives::identifier::WireName;
@@ -1024,13 +1024,13 @@ mod tests {
 
     fn test_config(tmp: &TempDir) -> ServerNgConfig {
         let mut cfg = ServerNgConfig::default();
-        // `SystemConfig` is not `Clone`, so `Arc::make_mut` is out; build a
+        // `NgSystemConfig` is not `Clone`, so `Arc::make_mut` is out; build a
         // fresh value via struct-update syntax and swap the Arc wholesale.
         // Only `path` differs from the default; every other field uses the
         // runtime's defaults.
-        let system = configs::system::SystemConfig {
+        let system = NgSystemConfig {
             path: tmp.path().to_string_lossy().into_owned(),
-            ..configs::system::SystemConfig::default()
+            ..NgSystemConfig::default()
         };
         cfg.system = Arc::new(system);
         cfg
diff --git a/core/server-ng/src/snapshot.rs b/core/server-ng/src/snapshot.rs
index e67447da7..8761265ae 100644
--- a/core/server-ng/src/snapshot.rs
+++ b/core/server-ng/src/snapshot.rs
@@ -29,7 +29,7 @@ use std::time::Instant;
 
 use async_zip::base::write::ZipFileWriter;
 use async_zip::{Compression, ZipEntryBuilder};
-use configs::system::SystemConfig;
+use configs::server_ng::NgSystemConfig;
 use futures::channel::oneshot;
 use iggy_common::{IggyDuration, IggyError, SnapshotCompression, 
SystemSnapshotType};
 use tracing::{error, info, warn};
@@ -55,7 +55,7 @@ static SNAPSHOT_IN_PROGRESS: AtomicBool = 
AtomicBool::new(false);
 /// [`SNAPSHOT_IN_PROGRESS`]); a concurrent request busy-rejects with
 /// [`IggyError::SnapshotFileCompletionFailed`].
 pub async fn collect(
-    system_config: Arc<SystemConfig>,
+    system_config: Arc<NgSystemConfig>,
     compression: SnapshotCompression,
     snapshot_types: Vec<SystemSnapshotType>,
 ) -> Result<Vec<u8>, IggyError> {
@@ -128,7 +128,7 @@ impl Drop for SnapshotInProgressGuard {
 }
 
 fn collect_blocking(
-    system_config: &SystemConfig,
+    system_config: &NgSystemConfig,
     compression: SnapshotCompression,
     snapshot_types: &[SystemSnapshotType],
 ) -> Result<Vec<u8>, IggyError> {
@@ -156,7 +156,7 @@ fn collect_blocking(
 
 fn capture(
     snapshot_type: &SystemSnapshotType,
-    system_config: &SystemConfig,
+    system_config: &NgSystemConfig,
 ) -> io::Result<Vec<u8>> {
     match snapshot_type {
         SystemSnapshotType::FilesystemOverview => {
@@ -195,7 +195,7 @@ fn process_list() -> io::Result<Vec<u8>> {
     Ok(content)
 }
 
-fn server_logs(system_config: &SystemConfig) -> io::Result<Vec<u8>> {
+fn server_logs(system_config: &NgSystemConfig) -> io::Result<Vec<u8>> {
     // Mirror the logger's path derivation (server_common 
`Logging::late_init`):
     // it canonicalizes the configured subdirectory before joining the system
     // path, so a relative `logging.path` that already exists resolves against 
the
@@ -224,7 +224,7 @@ fn server_logs(system_config: &SystemConfig) -> 
io::Result<Vec<u8>> {
     Ok(content)
 }
 
-fn server_config(system_config: &SystemConfig) -> io::Result<Vec<u8>> {
+fn server_config(system_config: &NgSystemConfig) -> io::Result<Vec<u8>> {
     let config_path = 
PathBuf::from(system_config.get_runtime_path()).join("current_config.toml");
     std::fs::read(config_path)
 }
@@ -277,7 +277,7 @@ mod tests {
         // second collector thread) rather than piling up threads.
         let held = SnapshotInProgressGuard::acquire().expect("flag starts 
free");
         let result = futures::executor::block_on(collect(
-            Arc::new(SystemConfig::default()),
+            Arc::new(NgSystemConfig::default()),
             SnapshotCompression::Stored,
             vec![SystemSnapshotType::Test],
         ));
diff --git a/core/server/config.toml b/core/server/config.toml
index b31bf8a1c..2add7ca24 100644
--- a/core/server/config.toml
+++ b/core/server/config.toml
@@ -599,7 +599,7 @@ cpu_allocation = "numa:auto"
 # (e.g. a multi-tenant host slicing CPU via cgroup quotas): unpinned shards
 # let the kernel scheduler place threads freely instead of piling every
 # process onto the same low-numbered cores.
-# pin_cores = true
+pin_cores = true
 
 [websocket]
 enabled = true

Reply via email to