Skip to content

Commit 5113681

Browse files
authored
refactor: version plugin component effective names (#889)
#### Overview Relay creates effective names for dynamically registered plugin components so runtime discovery and middleware gates can unambiguously identify their owner. This change replaces the ambiguous dunder naming with a versioned, parsed format that carries the plugin kind, an always-present one-based component ordinal, and the local registration name. It makes singleton and multi-component registrations follow the same contract while retaining structured discovery as the stable client-facing identity. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Replace the ambiguous dunder key with the versioned v1 grammar: `nemo-relay-plugin.v1.{percent-encoded-kind}:{one-based-ordinal}:{percent-encoded-local-name}`. - Percent-encode UTF-8 component kinds and local names using RFC 3986 unreserved-character rules, and strictly decode only canonical v1 names. Invalid, legacy, and ordinary global names retain the global-API fallback. - Preserve the existing discovery, worker, FFI, Python, Node, and Go owner transport. Relay-created plugin-component discovery now consistently reports `component_ordinal: Some(1)` for a singleton. - Preserve encoding for PII child contexts and native runtime-created conditional-middleware gate names. - Update exact cross-language expectations and the conditional-middleware guide. Breaking behavior: effective names for Relay-created plugin components change from the unmerged dunder format to the v1 format. The structured discovery identity remains the public contract; clients must discover effective names and must not construct or persist them. #### Where should the reviewer start? Start with `crates/core/src/plugin.rs` for the namespace encoder/decoder, then `crates/core/src/api/registry.rs` for strict ownership discovery and `crates/core/tests/unit/plugin_tests.rs` for encoded-field and global-fallback coverage. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Relates to: #886 ## Summary by CodeRabbit * **New Features** * Introduced versioned, encoded registration names for plugin components. * Plugin registrations now provide clearer ownership metadata, including component kind and one-based ordinal. * Improved namespace handling for nested plugin components and conditional middleware guardrails. * Preserved reliable registration behavior for profile-scoped integrations and singleton plugins. * **Documentation** * Documented plugin component ownership, ordinals, and registration naming rules. * **Tests** * Expanded coverage for encoded namespaces, component discovery, ownership metadata, disabled components, and singleton plugins. Authors: - Bryan Bednarski (https://github.com/bbednarski9) Approvers: - Will Killian (https://github.com/willkill07) URL: #889
1 parent 4b3e66e commit 5113681

12 files changed

Lines changed: 387 additions & 68 deletions

File tree

crates/core/src/api/registry.rs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ fn registration_identity(
139139
kind: RuntimeRegistrationKind,
140140
effective_name: &str,
141141
) -> RuntimeRegistrationIdentity {
142-
const PREFIX: &str = "__nemo_relay_plugin__";
143-
let Some(rest) = effective_name.strip_prefix(PREFIX) else {
142+
let Some((plugin_kind, component_ordinal, local_name)) =
143+
crate::plugin::decode_plugin_component_effective_name(effective_name)
144+
else {
144145
return RuntimeRegistrationIdentity {
145146
kind,
146147
local_name: effective_name.to_string(),
@@ -152,25 +153,14 @@ fn registration_identity(
152153
},
153154
};
154155
};
155-
let mut pieces = rest.split("__");
156-
let plugin_kind = pieces.next().unwrap_or_default().to_string();
157-
let second = pieces.next().unwrap_or_default();
158-
let (component_ordinal, local_name) = match second.parse::<u32>() {
159-
Ok(ordinal) => (Some(ordinal), pieces.collect::<Vec<_>>().join("__")),
160-
Err(_) => {
161-
let mut local = vec![second];
162-
local.extend(pieces);
163-
(None, local.join("__"))
164-
}
165-
};
166156
RuntimeRegistrationIdentity {
167157
kind,
168158
local_name,
169159
effective_name: effective_name.to_string(),
170160
owner: RuntimeRegistrationOwner {
171161
kind: RuntimeRegistrationOwnerKind::Plugin,
172162
plugin_kind: Some(plugin_kind),
173-
component_ordinal,
163+
component_ordinal: Some(component_ordinal),
174164
},
175165
}
176166
}

crates/core/src/plugin.rs

Lines changed: 130 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,12 @@ impl PluginRegistration {
407407
#[derive(Default)]
408408
pub struct PluginRegistrationContext {
409409
registrations: Vec<PluginRegistration>,
410-
namespace: Option<String>,
410+
namespace: Option<PluginRegistrationNamespace>,
411+
}
412+
413+
enum PluginRegistrationNamespace {
414+
Plain(String),
415+
PluginComponent(String),
411416
}
412417

413418
impl PluginRegistrationContext {
@@ -420,7 +425,38 @@ impl PluginRegistrationContext {
420425
pub fn with_namespace(namespace: impl Into<String>) -> Self {
421426
Self {
422427
registrations: vec![],
423-
namespace: Some(namespace.into()),
428+
namespace: Some(PluginRegistrationNamespace::Plain(namespace.into())),
429+
}
430+
}
431+
432+
fn with_plugin_component_namespace(namespace: String) -> Self {
433+
Self {
434+
registrations: vec![],
435+
namespace: Some(PluginRegistrationNamespace::PluginComponent(namespace)),
436+
}
437+
}
438+
439+
/// Creates a child context that extends this context's namespace.
440+
///
441+
/// Child contexts preserve Relay-created plugin component qualification so
442+
/// their local namespace segments and registration names remain encoded as
443+
/// one effective component name.
444+
pub fn with_child_namespace(&self, local_namespace: &str) -> Self {
445+
let namespace = match &self.namespace {
446+
Some(PluginRegistrationNamespace::Plain(namespace)) => {
447+
PluginRegistrationNamespace::Plain(format!("{namespace}{local_namespace}"))
448+
}
449+
Some(PluginRegistrationNamespace::PluginComponent(namespace)) => {
450+
PluginRegistrationNamespace::PluginComponent(format!(
451+
"{namespace}{}",
452+
encode_plugin_component_field(local_namespace)
453+
))
454+
}
455+
None => PluginRegistrationNamespace::Plain(local_namespace.to_string()),
456+
};
457+
Self {
458+
registrations: vec![],
459+
namespace: Some(namespace),
424460
}
425461
}
426462

@@ -431,11 +467,21 @@ impl PluginRegistrationContext {
431467
/// not have to provide component instance ids.
432468
pub fn qualify_name(&self, name: &str) -> String {
433469
match &self.namespace {
434-
Some(namespace) => format!("{namespace}{name}"),
470+
Some(PluginRegistrationNamespace::Plain(namespace)) => format!("{namespace}{name}"),
471+
Some(PluginRegistrationNamespace::PluginComponent(namespace)) => {
472+
format!("{namespace}{}", encode_plugin_component_field(name))
473+
}
435474
None => name.to_string(),
436475
}
437476
}
438477

478+
pub(crate) fn uses_plugin_component_namespace(&self) -> bool {
479+
matches!(
480+
&self.namespace,
481+
Some(PluginRegistrationNamespace::PluginComponent(_))
482+
)
483+
}
484+
439485
/// Registers an event subscriber and records its rollback closure.
440486
pub fn register_subscriber(&mut self, name: &str, callback: EventSubscriberFn) -> Result<()> {
441487
let qualified_name = self.qualify_name(name);
@@ -2653,7 +2699,6 @@ async fn initialize_plugin_components(
26532699
rollback_failures: Option<Arc<Mutex<Vec<String>>>>,
26542700
) -> Result<Vec<PluginRegistration>> {
26552701
ensure_builtin_plugins_registered()?;
2656-
let totals = plugin_component_totals(config);
26572702
let mut ordinals: HashMap<&str, usize> = HashMap::new();
26582703
let mut registrations = PendingPluginRegistrations::new(rollback_failures.clone());
26592704

@@ -2673,11 +2718,7 @@ async fn initialize_plugin_components(
26732718
.entry(component.kind.as_str())
26742719
.and_modify(|value| *value += 1)
26752720
.or_insert(1);
2676-
let namespace = component_namespace(
2677-
&component.kind,
2678-
*ordinal,
2679-
totals.get(component.kind.as_str()).copied().unwrap_or(1),
2680-
);
2721+
let namespace = component_namespace(&component.kind, *ordinal);
26812722

26822723
let mut pending =
26832724
PendingPluginRegistrationContext::new(namespace, rollback_failures.clone());
@@ -2729,7 +2770,7 @@ struct PendingPluginRegistrationContext {
27292770
impl PendingPluginRegistrationContext {
27302771
fn new(namespace: String, rollback_failures: Option<Arc<Mutex<Vec<String>>>>) -> Self {
27312772
Self {
2732-
context: PluginRegistrationContext::with_namespace(namespace),
2773+
context: PluginRegistrationContext::with_plugin_component_namespace(namespace),
27332774
rollback_failures,
27342775
}
27352776
}
@@ -2804,11 +2845,85 @@ fn plugin_component_totals(config: &PluginConfig) -> HashMap<&str, usize> {
28042845
totals
28052846
}
28062847

2807-
fn component_namespace(kind: &str, ordinal: usize, total: usize) -> String {
2808-
if total > 1 {
2809-
format!("__nemo_relay_plugin__{kind}__{ordinal}__")
2810-
} else {
2811-
format!("__nemo_relay_plugin__{kind}__")
2848+
fn component_namespace(kind: &str, ordinal: usize) -> String {
2849+
assert!(ordinal > 0, "plugin component ordinals are one-based");
2850+
format!(
2851+
"nemo-relay-plugin.v1.{}:{ordinal}:",
2852+
encode_plugin_component_field(kind)
2853+
)
2854+
}
2855+
2856+
pub(crate) fn encode_plugin_component_field(value: &str) -> String {
2857+
const HEX: &[u8; 16] = b"0123456789ABCDEF";
2858+
2859+
let mut encoded = String::with_capacity(value.len());
2860+
for byte in value.bytes() {
2861+
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
2862+
encoded.push(char::from(byte));
2863+
} else {
2864+
encoded.push('%');
2865+
encoded.push(char::from(HEX[(byte >> 4) as usize]));
2866+
encoded.push(char::from(HEX[(byte & 0x0f) as usize]));
2867+
}
2868+
}
2869+
encoded
2870+
}
2871+
2872+
pub(crate) fn decode_plugin_component_effective_name(
2873+
effective_name: &str,
2874+
) -> Option<(String, u32, String)> {
2875+
const PREFIX: &str = "nemo-relay-plugin.v1.";
2876+
2877+
let rest = effective_name.strip_prefix(PREFIX)?;
2878+
let (encoded_kind, rest) = rest.split_once(':')?;
2879+
let (ordinal_text, encoded_local_name) = rest.split_once(':')?;
2880+
let ordinal = ordinal_text
2881+
.parse::<u32>()
2882+
.ok()
2883+
.filter(|ordinal| *ordinal > 0)?;
2884+
if ordinal.to_string() != ordinal_text {
2885+
return None;
2886+
}
2887+
let plugin_kind = decode_plugin_component_field(encoded_kind)?;
2888+
let local_name = decode_plugin_component_field(encoded_local_name)?;
2889+
(!plugin_kind.is_empty() && !local_name.is_empty()).then_some((
2890+
plugin_kind,
2891+
ordinal,
2892+
local_name,
2893+
))
2894+
}
2895+
2896+
fn decode_plugin_component_field(encoded: &str) -> Option<String> {
2897+
let mut bytes = Vec::with_capacity(encoded.len());
2898+
let encoded = encoded.as_bytes();
2899+
let mut index = 0;
2900+
2901+
while index < encoded.len() {
2902+
let byte = encoded[index];
2903+
if byte == b'%' {
2904+
let high = *encoded.get(index + 1)?;
2905+
let low = *encoded.get(index + 2)?;
2906+
bytes.push((hex_value(high)? << 4) | hex_value(low)?);
2907+
index += 3;
2908+
} else if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
2909+
bytes.push(byte);
2910+
index += 1;
2911+
} else {
2912+
return None;
2913+
}
2914+
}
2915+
2916+
let decoded = String::from_utf8(bytes).ok()?;
2917+
(encode_plugin_component_field(&decoded) == std::str::from_utf8(encoded).ok()?)
2918+
.then_some(decoded)
2919+
}
2920+
2921+
fn hex_value(byte: u8) -> Option<u8> {
2922+
match byte {
2923+
b'0'..=b'9' => Some(byte - b'0'),
2924+
b'A'..=b'F' => Some(byte - b'A' + 10),
2925+
b'a'..=b'f' => Some(byte - b'a' + 10),
2926+
_ => None,
28122927
}
28132928
}
28142929

crates/core/src/plugin/dynamic/native.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ struct NativeHostPluginContext {
529529

530530
struct NativeHostPluginRuntime {
531531
namespace: String,
532+
encode_local_names: bool,
532533
active: AtomicBool,
533534
gates: Mutex<HashMap<String, NativeOwnedGate>>,
534535
}
@@ -3935,9 +3936,11 @@ unsafe extern "C" fn native_plugin_context_runtime(
39353936
Ok(ctx) => ctx,
39363937
Err(status) => return status,
39373938
};
3938-
let namespace = unsafe { &*host_ctx.ctx }.qualify_name("");
3939+
let context = unsafe { &*host_ctx.ctx };
3940+
let namespace = context.qualify_name("");
39393941
let runtime = Arc::new(NativeHostPluginRuntime {
39403942
namespace,
3943+
encode_local_names: context.uses_plugin_component_namespace(),
39413944
active: AtomicBool::new(true),
39423945
gates: Mutex::new(HashMap::new()),
39433946
});
@@ -4060,7 +4063,15 @@ unsafe extern "C" fn native_plugin_runtime_register_conditional_middleware_guard
40604063
return NemoRelayStatus::AlreadyExists;
40614064
}
40624065
let handle = format!("gate-{}", Uuid::now_v7());
4063-
let qualified_name = format!("{}{}", runtime.namespace, local_name);
4066+
let qualified_name = if runtime.encode_local_names {
4067+
format!(
4068+
"{}{}",
4069+
runtime.namespace,
4070+
crate::plugin::encode_plugin_component_field(&local_name)
4071+
)
4072+
} else {
4073+
format!("{}{}", runtime.namespace, local_name)
4074+
};
40644075
if let Err(error) = register_conditional_middleware_guardrail(
40654076
&qualified_name,
40664077
kinds,

crates/core/tests/integration/middleware_tests.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6736,7 +6736,7 @@ async fn test_runtime_registration_discovery_and_cross_owner_subscriber_gate() {
67366736
setup_isolated_thread();
67376737

67386738
let mut context =
6739-
PluginRegistrationContext::with_namespace("__nemo_relay_plugin__observability__");
6739+
PluginRegistrationContext::with_namespace("nemo-relay-plugin.v1.observability:1:");
67406740
let deliveries = Arc::new(AtomicU32::new(0));
67416741
let captured_deliveries = Arc::clone(&deliveries);
67426742
context
@@ -6757,7 +6757,7 @@ async fn test_runtime_registration_discovery_and_cross_owner_subscriber_gate() {
67576757
assert_eq!(registration.kind, RuntimeRegistrationKind::Subscriber);
67586758
assert_eq!(
67596759
registration.effective_name,
6760-
"__nemo_relay_plugin__observability__opentelemetry"
6760+
"nemo-relay-plugin.v1.observability:1:opentelemetry"
67616761
);
67626762
assert_eq!(
67636763
registration.owner.kind,
@@ -6767,6 +6767,7 @@ async fn test_runtime_registration_discovery_and_cross_owner_subscriber_gate() {
67676767
registration.owner.plugin_kind.as_deref(),
67686768
Some("observability")
67696769
);
6770+
assert_eq!(registration.owner.component_ordinal, Some(1));
67706771

67716772
register_conditional_middleware_guardrail(
67726773
"external_observability_gate",

crates/core/tests/unit/observability/plugin_component_tests.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,7 +1511,7 @@ fn opentelemetry_endpoint_header_env_rejects_missing_and_duplicate_headers() {
15111511
.read()
15121512
.unwrap()
15131513
.event_subscribers
1514-
.contains_key("__nemo_relay_plugin__observability__opentelemetry")
1514+
.contains_key("nemo-relay-plugin.v1.observability:1:opentelemetry")
15151515
);
15161516
clear_plugin_configuration().unwrap();
15171517
}
@@ -1547,7 +1547,7 @@ fn invalid_log_endpoint_keeps_valid_signal_subscriber() {
15471547
.read()
15481548
.unwrap()
15491549
.event_subscribers
1550-
.contains_key("__nemo_relay_plugin__observability__opentelemetry")
1550+
.contains_key("nemo-relay-plugin.v1.observability:1:opentelemetry")
15511551
);
15521552
clear_plugin_configuration().unwrap();
15531553
}
@@ -1583,7 +1583,7 @@ fn invalid_metric_endpoint_keeps_valid_signal_subscriber() {
15831583
.read()
15841584
.unwrap()
15851585
.event_subscribers
1586-
.contains_key("__nemo_relay_plugin__observability__opentelemetry")
1586+
.contains_key("nemo-relay-plugin.v1.observability:1:opentelemetry")
15871587
);
15881588
clear_plugin_configuration().unwrap();
15891589
}
@@ -1635,7 +1635,7 @@ fn malformed_derived_signal_endpoint_keeps_valid_peers() {
16351635
.read()
16361636
.unwrap()
16371637
.event_subscribers
1638-
.contains_key("__nemo_relay_plugin__observability__opentelemetry")
1638+
.contains_key("nemo-relay-plugin.v1.observability:1:opentelemetry")
16391639
);
16401640
clear_plugin_configuration().unwrap();
16411641
}
@@ -2576,7 +2576,7 @@ fn atof_enabled_writes_jsonl_and_teardown_flushes() {
25762576
.keys()
25772577
.cloned()
25782578
.collect::<Vec<_>>();
2579-
assert_eq!(names, vec!["__nemo_relay_plugin__observability__atof"]);
2579+
assert_eq!(names, vec!["nemo-relay-plugin.v1.observability:1:atof"]);
25802580
}
25812581

25822582
let agent = push_agent("atof-agent");
@@ -3703,11 +3703,11 @@ fn otlp_sections_register_inferred_subscribers_with_full_config() {
37033703
.keys()
37043704
.cloned()
37053705
.collect::<Vec<_>>();
3706-
assert!(names.contains(&"__nemo_relay_plugin__observability__opentelemetry".to_string()));
3706+
assert!(names.contains(&"nemo-relay-plugin.v1.observability:1:opentelemetry".to_string()));
37073707
assert_eq!(
37083708
names
37093709
.iter()
3710-
.filter(|name| *name == "__nemo_relay_plugin__observability__opentelemetry")
3710+
.filter(|name| *name == "nemo-relay-plugin.v1.observability:1:opentelemetry")
37113711
.count(),
37123712
1
37133713
);
@@ -3918,7 +3918,7 @@ fn opentelemetry_rejects_canonical_collision_during_validation_and_activation()
39183918
.read()
39193919
.unwrap()
39203920
.event_subscribers
3921-
.contains_key("__nemo_relay_plugin__observability__opentelemetry")
3921+
.contains_key("nemo-relay-plugin.v1.observability:1:opentelemetry")
39223922
);
39233923
}
39243924

@@ -3994,7 +3994,7 @@ fn invalid_later_opentelemetry_endpoint_keeps_fanout_registration() {
39943994
.read()
39953995
.unwrap()
39963996
.event_subscribers
3997-
.contains_key("__nemo_relay_plugin__observability__opentelemetry")
3997+
.contains_key("nemo-relay-plugin.v1.observability:1:opentelemetry")
39983998
);
39993999
clear_plugin_configuration().unwrap();
40004000
}

0 commit comments

Comments
 (0)