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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/actions/setup-build-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ runs:
restore-keys: |
${{ runner.os }}-cargo-${{ inputs.rust-toolchain }}-
${{ runner.os }}-cargo-

- uses: cargo-bins/cargo-binstall@main
- name: Install cargo tools
if: inputs.install-cargo-tools == 'true'
shell: bash
run: |
test -e ~/.cargo/bin/cargo-nextest || cargo install cargo-nextest
test -e ~/.cargo/bin/cargo-nextest || cargo binstall cargo-nextest --secure
test -e ~/.cargo/bin/cargo-llvm-cov || cargo install cargo-llvm-cov

- name: Create and activate virtual environment
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

**[中文文档](README.zh.md)**

**Lightweight distributed framework designed for high-performance AI applications.**
**Pulsing is a distributed actor framework that provides a communication backbone for building distributed systems, with specialized support for AI applications.**

🚀 **Zero Dependencies** — Pure Rust + Tokio, no NATS/etcd/Redis

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

**[English](README.md)**

**轻量级分布式框架,专为高性能 AI 应用设计。**
**Pulsing 是一个分布式 actor 框架,为构建分布式系统提供通信骨干,并为 AI 应用提供专门支持。**

🚀 **零外部依赖** — 纯 Rust + Tokio,无需 NATS/etcd/Redis

Expand Down
27 changes: 0 additions & 27 deletions comparison_examples/pulsing_pingpong.py

This file was deleted.

59 changes: 0 additions & 59 deletions comparison_examples/pulsing_research.py

This file was deleted.

2 changes: 2 additions & 0 deletions crates/pulsing-actor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ integration = []
otlp = ["opentelemetry-otlp"]
# Enable TLS support with passphrase-derived certificates
tls = ["dep:rustls", "dep:tokio-rustls", "dep:rcgen", "dep:ring", "dep:rustls-pemfile", "dep:time"]
# Enable test helpers module for testing in downstream crates
test-helper = []

[dependencies]
# Async runtime
Expand Down
128 changes: 43 additions & 85 deletions crates/pulsing-actor/src/actor/address.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
//! Actor addressing - URI-based actor addressing scheme
//!
//! This module implements the actor addressing scheme as defined in the design document.
//!
//! ## Address Types
//!
//! 1. Named Actor Service Address: `actor:///namespace/path/name`
//! 2. Named Actor Instance Address: `actor:///namespace/path/name@node_id`
//! 3. Global Actor Address: `actor://node_id/actor_id`
//! 4. Local Reference: `actor://0/actor_id` (node_id=0 means local)
//! Actor addressing (URI-based).

use super::traits::NodeId;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -65,14 +56,7 @@ impl fmt::Display for AddressParseError {

impl std::error::Error for AddressParseError {}

/// Actor path for named actors (namespace + hierarchical path + name)
///
/// A path must have at least two segments: namespace and name.
/// Additional segments can be used for logical grouping.
///
/// Examples:
/// - `services/llm/router` (namespace: services, name: router)
/// - `workers/inference/pool` (namespace: workers, name: pool)
/// Actor path for named actors (namespace + path + name).
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct ActorPath {
/// Path segments, e.g., ["services", "llm", "router"]
Expand All @@ -89,34 +73,10 @@ impl ActorPath {
/// Maximum single segment length
pub const MAX_SEGMENT_LENGTH: usize = 64;

/// Create a new actor path from a string
///
/// The path must have at least two segments (namespace/name).
///
/// # Validation Rules
/// - Path cannot be empty
/// - Path cannot exceed 256 characters
/// - Each segment cannot exceed 64 characters
/// - Each segment can only contain alphanumeric characters, `_`, and `-`
/// - Path must have at least two segments (namespace/name)
/// - User code cannot use reserved system namespaces (use `new_system` for internal use)
///
/// # Examples
/// ```
/// use pulsing_actor::actor::ActorPath;
///
/// let path = ActorPath::new("services/llm/router").unwrap();
/// assert_eq!(path.namespace(), "services");
/// assert_eq!(path.name(), "router");
///
/// // These will fail:
/// // ActorPath::new("system/internal").unwrap(); // Reserved namespace
/// // ActorPath::new("a".repeat(300)).unwrap(); // Too long
/// ```
pub fn new(path: impl AsRef<str>) -> Result<Self, AddressParseError> {
let path = path.as_ref().trim_matches('/');
/// Validate and parse path components (shared by `new()` and `new_system()`).
fn validate_path_components(path: &str) -> Result<Vec<String>, AddressParseError> {
let path = path.trim_matches('/');

// Check total path length
if path.len() > Self::MAX_PATH_LENGTH {
return Err(AddressParseError::PathTooLong);
}
Expand All @@ -127,7 +87,6 @@ impl ActorPath {

let segments: Vec<String> = path.split('/').map(|s| s.trim().to_string()).collect();

// Validate segments
for segment in &segments {
if segment.is_empty() {
return Err(AddressParseError::EmptySegment);
Expand All @@ -140,11 +99,47 @@ impl ActorPath {
}
}

// Must have at least namespace/name
if segments.len() < 2 {
return Err(AddressParseError::MissingNamespace);
}

Ok(segments)
}

/// Check if a segment contains only valid characters
fn is_valid_segment(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
}

/// Create a new actor path from a string
///
/// The path must have at least two segments (namespace/name).
///
/// # Validation Rules
/// - Path cannot be empty
/// - Path cannot exceed 256 characters
/// - Each segment cannot exceed 64 characters
/// - Each segment can only contain alphanumeric characters, `_`, and `-`
/// - Path must have at least two segments (namespace/name)
/// - User code cannot use reserved system namespaces (use `new_system` for internal use)
///
/// # Examples
/// ```
/// use pulsing_actor::actor::ActorPath;
///
/// let path = ActorPath::new("services/llm/router").unwrap();
/// assert_eq!(path.namespace(), "services");
/// assert_eq!(path.name(), "router");
///
/// // These will fail:
/// // ActorPath::new("system/internal").unwrap(); // Reserved namespace
/// // ActorPath::new("a".repeat(300)).unwrap(); // Too long
/// ```
pub fn new(path: impl AsRef<str>) -> Result<Self, AddressParseError> {
let segments = Self::validate_path_components(path.as_ref())?;

// Check for reserved system namespaces (user code cannot use these)
if Self::SYSTEM_NAMESPACES.contains(&segments[0].as_str()) {
return Err(AddressParseError::ReservedNamespace);
Expand All @@ -164,47 +159,10 @@ impl ActorPath {
/// - Python bindings for `PythonActorService` at `system/python_actor_service`
#[doc(hidden)]
pub fn new_system(path: impl AsRef<str>) -> Result<Self, AddressParseError> {
let path = path.as_ref().trim_matches('/');

// Check total path length
if path.len() > Self::MAX_PATH_LENGTH {
return Err(AddressParseError::PathTooLong);
}

if path.is_empty() {
return Err(AddressParseError::MissingNamespace);
}

let segments: Vec<String> = path.split('/').map(|s| s.trim().to_string()).collect();

// Validate segments
for segment in &segments {
if segment.is_empty() {
return Err(AddressParseError::EmptySegment);
}
if segment.len() > Self::MAX_SEGMENT_LENGTH {
return Err(AddressParseError::SegmentTooLong);
}
if !Self::is_valid_segment(segment) {
return Err(AddressParseError::InvalidCharacter);
}
}

// Must have at least namespace/name
if segments.len() < 2 {
return Err(AddressParseError::MissingNamespace);
}

let segments = Self::validate_path_components(path.as_ref())?;
Ok(Self { segments })
}

/// Check if a segment contains only valid characters
fn is_valid_segment(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
}

/// Get the namespace (first segment)
pub fn namespace(&self) -> &str {
&self.segments[0]
Expand Down
Loading
Loading