Skip to content

Commit da603b3

Browse files
chore: fix clippy warnings, refactor core into submodules, and update test configuration
Co-authored-by: Iris Seravelle <iris.seravelle@gmail.com>
1 parent 165381b commit da603b3

52 files changed

Lines changed: 2825 additions & 2845 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,28 @@ jobs:
1616
- name: Set up Python
1717
uses: actions/setup-python@v5
1818
with:
19-
python-version: '3.10'
19+
python-version: '3.12'
2020

2121
- name: Install Rust toolchain
2222
uses: dtolnay/rust-toolchain@stable
2323
with:
24-
components: rustfmt, clippy
24+
components: rustfmt
2525

2626
- name: Rust Cache
2727
uses: Swatinem/rust-cache@v2
2828

29-
- name: Build
29+
- name: Check Formatting
30+
run: cargo fmt -- --check
31+
32+
- name: Build (Base)
3033
run: cargo build --verbose --no-default-features
3134

32-
- name: Run Rust Tests
35+
- name: Run Tests (Base)
3336
env:
3437
RUST_BACKTRACE: 1
3538
run: cargo test --workspace --verbose --no-default-features
39+
40+
- name: Run Tests (PyO3)
41+
env:
42+
RUST_BACKTRACE: 1
43+
run: cargo test --workspace --verbose --no-default-features --features=pyo3

Cargo.toml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ crossbeam-channel = "0.5"
2424
crossbeam-queue = "0.3.11"
2525

2626
# Python (Default)
27-
pyo3 = { version = "0.20", features = ["extension-module", "auto-initialize"], optional = true}
27+
pyo3 = { version = "0.20", features = ["auto-initialize"], optional = true}
2828
pyo3-asyncio = { version = "0.20", features = ["tokio-runtime"], optional = true }
2929

3030
# JIT (Optional and experimental)
@@ -42,8 +42,9 @@ napi-derive = { version = "2.14", optional = true }
4242
napi-build = { version = "2.0", optional = true }
4343

4444
[features]
45-
default = ["pyo3"]
45+
default = ["pyo3", "jit"]
4646
pyo3 = ["dep:pyo3", "dep:pyo3-asyncio"]
47+
extension-module = ["pyo3/extension-module"]
4748
jit = ["dep:cranelift", "dep:cranelift-module", "dep:cranelift-jit", "dep:cranelift-native", "dep:wide"]
4849
node = ["dep:napi", "dep:napi-derive", "dep:napi-build"]
4950
vortex = ["pyo3"]
@@ -64,10 +65,7 @@ strip = true
6465
opt-level = 0
6566
debug = 0
6667
strip = true
67-
panic = "abort"
68+
panic = "unwind"
6869
incremental = true
6970
lto = false
7071
codegen-units = 256
71-
72-
[profile.test]
73-
panic = "unwind"

src/core/behavior.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// src/core/behavior.rs
2+
use super::types::MAX_BEHAVIOR_HISTORY;
3+
use super::Runtime;
4+
use crate::mailbox;
5+
use crate::pid::Pid;
6+
7+
impl Runtime {
8+
/// Send a Hot Swap signal to the actor.
9+
pub fn hot_swap(&self, pid: Pid, handler_ptr: usize) {
10+
if let Some(sender) = self.mailboxes.get(&pid) {
11+
if sender
12+
.send_system(mailbox::SystemMessage::HotSwap(handler_ptr))
13+
.is_ok()
14+
{
15+
if let Some(mut ver) = self.behavior_versions.get_mut(&pid) {
16+
*ver += 1;
17+
} else {
18+
// initial behavior is version 1; first successful swap -> v2
19+
self.behavior_versions.insert(pid, 2);
20+
}
21+
22+
let mut history = self.behavior_history.entry(pid).or_default();
23+
history.push(handler_ptr);
24+
if history.len() > MAX_BEHAVIOR_HISTORY {
25+
let overflow = history.len() - MAX_BEHAVIOR_HISTORY;
26+
history.drain(0..overflow);
27+
}
28+
}
29+
}
30+
}
31+
32+
/// Return current behavior version for an actor.
33+
pub fn behavior_version(&self, pid: Pid) -> u64 {
34+
self.behavior_versions
35+
.get(&pid)
36+
.map(|entry| *entry.value())
37+
.unwrap_or(1)
38+
}
39+
40+
/// Roll back behavior by `steps` previously hot-swapped versions.
41+
///
42+
/// Returns the new behavior version on success.
43+
pub fn rollback_behavior(&self, pid: Pid, steps: usize) -> Result<u64, String> {
44+
if steps == 0 {
45+
return Ok(self.behavior_version(pid));
46+
}
47+
48+
let target_ptr = {
49+
let mut history = self
50+
.behavior_history
51+
.get_mut(&pid)
52+
.ok_or_else(|| "rollback failed: no behavior history".to_string())?;
53+
54+
if history.len() <= steps {
55+
return Err("rollback failed: insufficient behavior history".to_string());
56+
}
57+
58+
let target_idx = history.len() - 1 - steps;
59+
let target_ptr = history[target_idx];
60+
history.truncate(target_idx + 1);
61+
target_ptr
62+
};
63+
64+
let sender = self
65+
.mailboxes
66+
.get(&pid)
67+
.ok_or_else(|| "rollback failed: pid not found".to_string())?;
68+
69+
sender
70+
.send_system(mailbox::SystemMessage::HotSwap(target_ptr))
71+
.map_err(|_| "rollback failed: could not send hot swap".to_string())?;
72+
73+
let next = self
74+
.behavior_version(pid)
75+
.saturating_sub(steps as u64)
76+
.max(1);
77+
self.behavior_versions.insert(pid, next);
78+
Ok(next)
79+
}
80+
}

src/buffer.rs renamed to src/core/buffer.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ impl BufferRegistry {
4242
}
4343
}
4444

45+
impl Default for BufferRegistry {
46+
fn default() -> Self {
47+
Self::new()
48+
}
49+
}
50+
4551
pub fn global_registry() -> &'static BufferRegistry {
4652
use once_cell::sync::Lazy;
47-
static REG: Lazy<BufferRegistry> = Lazy::new(|| BufferRegistry::new());
53+
static REG: Lazy<BufferRegistry> = Lazy::new(BufferRegistry::new);
4854
&REG
4955
}
File renamed without changes.

src/mailbox.rs renamed to src/core/mailbox.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ impl MailboxSender {
222222
self.counter.load(Ordering::Relaxed)
223223
}
224224

225+
/// Return true if the mailbox has no user messages.
226+
pub fn is_empty(&self) -> bool {
227+
self.len() == 0
228+
}
229+
225230
/// Compute backpressure level given an optional configured capacity.
226231
pub fn backpressure_level(&self, capacity: Option<usize>) -> BackpressureLevel {
227232
if let Some(cap) = capacity {
@@ -454,7 +459,7 @@ impl MailboxReceiver {
454459
}
455460

456461
// First, search stash for a matching message (preserve ordering).
457-
if let Some(idx) = self.stash.iter().position(|m| matcher(m)) {
462+
if let Some(idx) = self.stash.iter().position(&mut matcher) {
458463
let m = self.stash.remove(idx);
459464
if let Some(Message::User(_)) = m.as_ref() {
460465
self.counter.fetch_sub(1, Ordering::SeqCst);

src/core/mod.rs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
use dashmap::DashMap;
2+
use once_cell::sync::Lazy;
3+
use std::collections::HashMap;
4+
use std::sync::atomic::AtomicU64;
5+
use std::sync::{Arc, Mutex};
6+
use tokio::runtime::Runtime as TokioRuntime;
7+
use tokio::time::Duration;
8+
9+
pub mod buffer;
10+
pub mod logging;
11+
pub mod mailbox;
12+
pub mod network;
13+
pub mod pid;
14+
pub mod registry;
15+
pub mod supervisor;
16+
17+
pub mod behavior;
18+
pub mod send;
19+
pub mod spawn;
20+
pub mod types;
21+
22+
#[cfg(feature = "vortex")]
23+
pub mod vortex;
24+
#[cfg(feature = "vortex")]
25+
pub mod vortex_rt;
26+
27+
use pid::Pid;
28+
use types::VirtualActorSpec;
29+
30+
#[cfg(feature = "vortex")]
31+
use vortex::{VortexEngine, VortexGhostPolicy};
32+
33+
/// A global, multi-threaded Tokio runtime shared by all Iris instances.
34+
pub(crate) static RUNTIME: Lazy<TokioRuntime> = Lazy::new(|| {
35+
tokio::runtime::Builder::new_multi_thread()
36+
.enable_all()
37+
.build()
38+
.expect("Failed to create Iris Tokio Runtime")
39+
});
40+
41+
/// Lightweight runtime for spawning actors and managing distributed nodes.
42+
#[derive(Clone)]
43+
pub struct Runtime {
44+
pub(crate) slab: Arc<Mutex<pid::SlabAllocator>>,
45+
pub(crate) mailboxes: Arc<DashMap<Pid, mailbox::MailboxSender>>,
46+
pub(crate) supervisor: Arc<supervisor::Supervisor>,
47+
pub(crate) observers: Arc<DashMap<Pid, Arc<Mutex<Vec<mailbox::Message>>>>>,
48+
pub(crate) network: Arc<Mutex<Option<network::NetworkManager>>>,
49+
// network configuration (timeouts/limits/backoff)
50+
pub(crate) network_io_timeout: Arc<Mutex<Duration>>,
51+
pub(crate) network_max_payload: Arc<Mutex<usize>>,
52+
pub(crate) network_max_name_len: Arc<Mutex<usize>>,
53+
pub(crate) monitor_backoff_factor: Arc<Mutex<f64>>,
54+
pub(crate) monitor_backoff_max: Arc<Mutex<Duration>>,
55+
pub(crate) monitor_failure_threshold: Arc<Mutex<usize>>,
56+
#[cfg(feature = "vortex")]
57+
pub(crate) vortex_engine: Option<Arc<Mutex<VortexEngine>>>,
58+
#[cfg(feature = "vortex")]
59+
pub(crate) vortex_watcher: Option<Arc<vortex::VortexWatcher>>,
60+
pub(crate) registry: Arc<registry::NameRegistry>,
61+
/// Mapping for locally‑spawned proxies that forward to remote actors.
62+
pub(crate) remote_proxies: Arc<DashMap<Pid, (String, Pid)>>,
63+
/// Reverse lookup from (address, remote_pid) -> local proxy PID.
64+
pub(crate) proxy_by_remote: Arc<DashMap<(String, Pid), Pid>>,
65+
/// Behavior version per actor PID (starts at 1).
66+
pub(crate) behavior_versions: Arc<DashMap<Pid, u64>>,
67+
/// Recent hot-swapped pointers used for rollback (capped).
68+
pub(crate) behavior_history: Arc<DashMap<Pid, Vec<usize>>>,
69+
/// Optional per-path supervisors (shallow supervisors keyed by path).
70+
pub(crate) path_supervisors: Arc<DashMap<String, Arc<supervisor::Supervisor>>>,
71+
/// Maps a child PID to its parent PID for structured concurrency.
72+
pub(crate) parent_of: Arc<DashMap<Pid, Pid>>,
73+
/// Tracks direct children of each parent PID.
74+
pub(crate) children_by_parent: Arc<DashMap<Pid, Vec<Pid>>>,
75+
/// Capacity for bounded mailboxes.
76+
pub(crate) bounded_capacity: Arc<DashMap<Pid, usize>>,
77+
/// Overflow policies for bounded mailboxes; default is DropNew if absent.
78+
pub(crate) overflow_policy: Arc<DashMap<Pid, mailbox::OverflowPolicy>>,
79+
/// Lazy/virtual actor specs reserved by PID and activated on first send.
80+
pub(crate) virtual_specs: Arc<DashMap<Pid, VirtualActorSpec>>,
81+
/// Per-virtual-actor activation lock to prevent duplicate activation races.
82+
pub(crate) virtual_activate_locks: Arc<DashMap<Pid, Arc<Mutex<()>>>>,
83+
/// Track last known backpressure level for each pid, to emit signals on change.
84+
pub(crate) backpressure_state: Arc<DashMap<Pid, mailbox::BackpressureLevel>>,
85+
// Runtime-configurable limits for Python GIL-release behavior
86+
pub(crate) release_gil_max_threads: Arc<Mutex<usize>>,
87+
pub(crate) gil_pool_size: Arc<Mutex<usize>>,
88+
pub(crate) release_gil_strict: Arc<Mutex<bool>>,
89+
// Timers: map from timer id -> cancellation sender
90+
pub(crate) timers: Arc<Mutex<HashMap<u64, tokio::sync::oneshot::Sender<()>>>>,
91+
pub(crate) timer_counter: Arc<AtomicU64>,
92+
#[cfg(feature = "vortex")]
93+
pub(crate) vortex_ghost_counter: Arc<AtomicU64>,
94+
#[cfg(feature = "vortex")]
95+
pub(crate) vortex_auto_replay_count: Arc<AtomicU64>,
96+
#[cfg(feature = "vortex")]
97+
pub(crate) vortex_auto_primary_wins: Arc<AtomicU64>,
98+
#[cfg(feature = "vortex")]
99+
pub(crate) vortex_auto_ghost_wins: Arc<AtomicU64>,
100+
#[cfg(feature = "vortex")]
101+
pub(crate) vortex_auto_policy: Arc<Mutex<VortexGhostPolicy>>,
102+
#[cfg(feature = "vortex")]
103+
pub(crate) vortex_genetic_budgeting_enabled: Arc<Mutex<bool>>,
104+
#[cfg(feature = "vortex")]
105+
pub(crate) vortex_genetic_thresholds: Arc<Mutex<(f64, f64)>>,
106+
#[cfg(feature = "vortex")]
107+
pub(crate) vortex_isolation_disallowed_ops: Arc<Mutex<std::collections::HashSet<u8>>>,
108+
#[cfg(feature = "vortex")]
109+
pub(crate) vortex_genetic_history: Arc<DashMap<Pid, (usize, usize)>>,
110+
}
111+
112+
impl Default for Runtime {
113+
fn default() -> Self {
114+
Self::new()
115+
}
116+
}
117+
118+
impl Runtime {
119+
/// Create a new runtime instance and initialize the networking and registry sub-systems.
120+
pub fn new() -> Self {
121+
logging::init_logger();
122+
123+
#[cfg(feature = "pyo3")]
124+
{
125+
pyo3::prepare_freethreaded_python();
126+
}
127+
128+
let rt = Runtime {
129+
slab: Arc::new(Mutex::new(pid::SlabAllocator::new())),
130+
mailboxes: Arc::new(DashMap::new()),
131+
supervisor: Arc::new(supervisor::Supervisor::new()),
132+
observers: Arc::new(DashMap::new()),
133+
network: Arc::new(Mutex::new(None)),
134+
registry: Arc::new(registry::NameRegistry::new()),
135+
path_supervisors: Arc::new(DashMap::new()),
136+
parent_of: Arc::new(DashMap::new()),
137+
children_by_parent: Arc::new(DashMap::new()),
138+
bounded_capacity: Arc::new(DashMap::new()),
139+
overflow_policy: Arc::new(DashMap::new()),
140+
backpressure_state: Arc::new(DashMap::new()),
141+
virtual_specs: Arc::new(DashMap::new()),
142+
virtual_activate_locks: Arc::new(DashMap::new()),
143+
release_gil_max_threads: Arc::new(Mutex::new(0)),
144+
gil_pool_size: Arc::new(Mutex::new(8)),
145+
release_gil_strict: Arc::new(Mutex::new(false)),
146+
timers: Arc::new(Mutex::new(HashMap::new())),
147+
timer_counter: Arc::new(AtomicU64::new(0)),
148+
#[cfg(feature = "vortex")]
149+
vortex_ghost_counter: Arc::new(AtomicU64::new(1)),
150+
#[cfg(feature = "vortex")]
151+
vortex_auto_replay_count: Arc::new(AtomicU64::new(0)),
152+
#[cfg(feature = "vortex")]
153+
vortex_auto_primary_wins: Arc::new(AtomicU64::new(0)),
154+
#[cfg(feature = "vortex")]
155+
vortex_auto_ghost_wins: Arc::new(AtomicU64::new(0)),
156+
#[cfg(feature = "vortex")]
157+
vortex_auto_policy: Arc::new(Mutex::new(VortexGhostPolicy::FirstSafePointWins)),
158+
#[cfg(feature = "vortex")]
159+
vortex_genetic_budgeting_enabled: Arc::new(Mutex::new(false)),
160+
#[cfg(feature = "vortex")]
161+
vortex_genetic_thresholds: Arc::new(Mutex::new((0.4, 0.7))),
162+
#[cfg(feature = "vortex")]
163+
vortex_isolation_disallowed_ops: Arc::new(Mutex::new(std::collections::HashSet::new())),
164+
#[cfg(feature = "vortex")]
165+
vortex_genetic_history: Arc::new(DashMap::new()),
166+
network_io_timeout: Arc::new(Mutex::new(Duration::from_secs(5))),
167+
network_max_payload: Arc::new(Mutex::new(1024 * 1024)),
168+
network_max_name_len: Arc::new(Mutex::new(1024)),
169+
monitor_backoff_factor: Arc::new(Mutex::new(2.0)),
170+
monitor_backoff_max: Arc::new(Mutex::new(Duration::from_secs(60))),
171+
monitor_failure_threshold: Arc::new(Mutex::new(1)),
172+
remote_proxies: Arc::new(DashMap::new()),
173+
proxy_by_remote: Arc::new(DashMap::new()),
174+
behavior_versions: Arc::new(DashMap::new()),
175+
behavior_history: Arc::new(DashMap::new()),
176+
#[cfg(feature = "vortex")]
177+
vortex_engine: Some(Arc::new(Mutex::new(VortexEngine::new()))),
178+
#[cfg(feature = "vortex")]
179+
vortex_watcher: Some(Arc::new(vortex::VortexWatcher::new())),
180+
};
181+
182+
let net_manager = network::NetworkManager::new(Arc::new(rt.clone()));
183+
*rt.network.lock().unwrap() = Some(net_manager);
184+
185+
rt
186+
}
187+
188+
/// Set runtime limits for GIL release handling.
189+
///
190+
/// `max_threads = 0` forces pooled mode (no dedicated thread per actor).
191+
pub fn set_release_gil_limits(&self, max_threads: usize, pool_size: usize) {
192+
*self.release_gil_max_threads.lock().unwrap() = max_threads;
193+
*self.gil_pool_size.lock().unwrap() = pool_size;
194+
}
195+
196+
/// Enable or disable strict failure mode: when true, spawning an actor with
197+
/// `release_gil=true` will return an error if the dedicated-thread limit is exceeded.
198+
pub fn set_release_gil_strict(&self, strict: bool) {
199+
*self.release_gil_strict.lock().unwrap() = strict;
200+
}
201+
202+
/// Get the current release_gil limits (max_threads, pool_size).
203+
pub fn get_release_gil_limits(&self) -> (usize, usize) {
204+
(
205+
*self.release_gil_max_threads.lock().unwrap(),
206+
*self.gil_pool_size.lock().unwrap(),
207+
)
208+
}
209+
210+
/// Returns whether strict failure mode is enabled.
211+
pub fn is_release_gil_strict(&self) -> bool {
212+
*self.release_gil_strict.lock().unwrap()
213+
}
214+
}

0 commit comments

Comments
 (0)