Skip to content

Commit 0bbbac7

Browse files
authored
Merge pull request #278 from MikeFalcon77/feature/gts_resolver
feat(modkit): add GtsPluginSelector and ThrottledLog for lazy plugin resolution
2 parents b826163 + 80f11a4 commit 0bbbac7

12 files changed

Lines changed: 426 additions & 53 deletions

File tree

examples/plugin-modules/tenant_resolver/tenant_resolver-gw/src/api/rest/error.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ impl From<DomainError> for Problem {
1212
"PluginNotFound",
1313
format!("No plugin instances found for vendor '{vendor}'"),
1414
),
15-
DomainError::PluginClientNotFound { gts_id } => Problem::new(
16-
StatusCode::INTERNAL_SERVER_ERROR,
17-
"PluginClientNotFound",
18-
format!("Plugin client not found in ClientHub for '{gts_id}'"),
15+
DomainError::PluginUnavailable { gts_id, reason } => Problem::new(
16+
StatusCode::SERVICE_UNAVAILABLE,
17+
"PluginUnavailable",
18+
format!("Plugin '{gts_id}' is not available: {reason}"),
1919
),
2020
DomainError::InvalidPluginInstance { gts_id, reason } => Problem::new(
2121
StatusCode::BAD_REQUEST,

examples/plugin-modules/tenant_resolver/tenant_resolver-gw/src/domain/error.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ pub enum DomainError {
1010
#[error("invalid plugin instance content for '{gts_id}': {reason}")]
1111
InvalidPluginInstance { gts_id: String, reason: String },
1212

13-
#[error("plugin client not registered in ClientHub for '{gts_id}'")]
14-
PluginClientNotFound { gts_id: String },
13+
#[error("plugin not available for '{gts_id}': {reason}")]
14+
PluginUnavailable { gts_id: String, reason: String },
1515

1616
#[error("tenant not found: {0}")]
1717
TenantNotFound(String),
@@ -33,6 +33,10 @@ impl From<tenant_resolver_sdk::TenantResolverError> for DomainError {
3333
TenantResolverError::PermissionDenied(msg) | TenantResolverError::Unauthorized(msg) => {
3434
Self::PermissionDenied(msg)
3535
}
36+
TenantResolverError::ServiceUnavailable(msg) => Self::PluginUnavailable {
37+
gts_id: "unknown".to_owned(),
38+
reason: msg,
39+
},
3640
TenantResolverError::Internal(msg) => Self::Internal(msg),
3741
}
3842
}
@@ -65,8 +69,8 @@ impl From<DomainError> for tenant_resolver_sdk::TenantResolverError {
6569
DomainError::InvalidPluginInstance { gts_id, reason } => {
6670
Self::Internal(format!("invalid plugin instance '{gts_id}': {reason}"))
6771
}
68-
DomainError::PluginClientNotFound { gts_id } => {
69-
Self::Internal(format!("plugin client not registered for '{gts_id}'"))
72+
DomainError::PluginUnavailable { gts_id, reason } => {
73+
Self::ServiceUnavailable(format!("plugin not available for '{gts_id}': {reason}"))
7074
}
7175
DomainError::TenantNotFound(msg) => Self::NotFound(msg),
7276
DomainError::PermissionDenied(msg) => Self::PermissionDenied(msg),

examples/plugin-modules/tenant_resolver/tenant_resolver-gw/src/domain/service.rs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
//! after `types_registry` has switched to ready mode.
55
66
use std::sync::Arc;
7+
use std::time::Duration;
78

89
use modkit::client_hub::{ClientHub, ClientScope};
910
use modkit::gts::BaseModkitPluginV1;
11+
use modkit::plugins::GtsPluginSelector;
12+
use modkit::telemetry::ThrottledLog;
1013
use modkit_odata::{ODataQuery, Page};
1114
use modkit_security::SecurityContext;
1215
use tenant_resolver_sdk::{
1316
AccessOptions, GetParentsResponse, Tenant, TenantFilter, TenantResolverPluginClient,
1417
TenantResolverPluginSpecV1,
1518
};
16-
use tokio::sync::OnceCell;
1719
use tracing::info;
1820
use types_registry_sdk::{GtsEntity, ListQuery, TypesRegistryClient};
1921

@@ -22,21 +24,20 @@ use types_registry_sdk::{GtsEntity, ListQuery, TypesRegistryClient};
2224

2325
use crate::domain::error::DomainError;
2426

25-
/// Cached result of plugin resolution.
26-
struct ResolvedPlugin {
27-
gts_id: String,
28-
scope: ClientScope,
29-
}
27+
/// Throttle interval for unavailable plugin warnings.
28+
const UNAVAILABLE_LOG_THROTTLE: Duration = Duration::from_secs(10);
3029

3130
/// Tenant Resolver Gateway service.
3231
///
3332
/// Holds a reference to `ClientHub` and the configured vendor.
34-
/// Plugin discovery is lazy and cached via `OnceCell`.
33+
/// Plugin discovery is lazy and cached via `GtsPluginSelector`.
3534
pub struct Service {
3635
hub: Arc<ClientHub>,
3736
vendor: String,
38-
/// Lazily resolved plugin (cached after first call).
39-
resolved: OnceCell<ResolvedPlugin>,
37+
/// Shared selector for plugin instance IDs.
38+
selector: GtsPluginSelector,
39+
/// Throttle for plugin unavailable warnings.
40+
unavailable_log_throttle: ThrottledLog,
4041
}
4142

4243
impl Service {
@@ -46,7 +47,8 @@ impl Service {
4647
Self {
4748
hub,
4849
vendor,
49-
resolved: OnceCell::new(),
50+
selector: GtsPluginSelector::new(),
51+
unavailable_log_throttle: ThrottledLog::new(UNAVAILABLE_LOG_THROTTLE),
5052
}
5153
}
5254

@@ -55,21 +57,32 @@ impl Service {
5557
/// On first call, queries `types_registry` to find the plugin instance
5658
/// matching the configured vendor. Result is cached for subsequent calls.
5759
async fn get_plugin(&self) -> Result<Arc<dyn TenantResolverPluginClient>, DomainError> {
58-
let resolved = self
59-
.resolved
60-
.get_or_try_init(|| self.resolve_plugin())
61-
.await?;
60+
let instance_id = self.selector.get_or_init(|| self.resolve_plugin()).await?;
61+
let scope = ClientScope::gts_id(instance_id.as_ref());
6262

63-
self.hub
64-
.get_scoped::<dyn TenantResolverPluginClient>(&resolved.scope)
65-
.map_err(|_| DomainError::PluginClientNotFound {
66-
gts_id: resolved.gts_id.clone(),
63+
if let Some(client) = self
64+
.hub
65+
.try_get_scoped::<dyn TenantResolverPluginClient>(&scope)
66+
{
67+
Ok(client)
68+
} else {
69+
if self.unavailable_log_throttle.should_log() {
70+
tracing::warn!(
71+
plugin_gts_id = %instance_id,
72+
vendor = %self.vendor,
73+
"Plugin client not registered yet"
74+
);
75+
}
76+
Err(DomainError::PluginUnavailable {
77+
gts_id: instance_id.to_string(),
78+
reason: "client not registered yet".into(),
6779
})
80+
}
6881
}
6982

7083
/// Resolves the plugin instance from `types_registry`.
7184
#[tracing::instrument(skip_all, fields(vendor = %self.vendor))]
72-
async fn resolve_plugin(&self) -> Result<ResolvedPlugin, DomainError> {
85+
async fn resolve_plugin(&self) -> Result<String, DomainError> {
7386
info!("Resolving tenant resolver plugin");
7487

7588
let registry = self
@@ -90,8 +103,7 @@ impl Service {
90103
let gts_id = choose_plugin_instance(&self.vendor, &instances)?;
91104
info!(plugin_gts_id = %gts_id, "Selected tenant resolver plugin instance");
92105

93-
let scope = ClientScope::gts_id(&gts_id);
94-
Ok(ResolvedPlugin { gts_id, scope })
106+
Ok(gts_id)
95107
}
96108

97109
/// Returns the root tenant.

examples/plugin-modules/tenant_resolver/tenant_resolver-sdk/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ pub enum TenantResolverError {
1212
#[error("not found: {0}")]
1313
NotFound(String),
1414

15+
#[error("service unavailable: {0}")]
16+
ServiceUnavailable(String),
17+
1518
#[error("internal error: {0}")]
1619
Internal(String),
1720
}

libs/modkit/src/client_hub.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,23 @@ impl ClientHub {
216216
})
217217
}
218218

219+
/// Try to fetch a scoped client by interface type `T` and scope.
220+
///
221+
/// Returns `None` if not found or if the stored type doesn't match.
222+
pub fn try_get_scoped<T>(&self, scope: &ClientScope) -> Option<Arc<T>>
223+
where
224+
T: ?Sized + Send + Sync + 'static,
225+
{
226+
let key = ScopedKey {
227+
type_key: TypeKey::of::<T>(),
228+
scope: scope.clone(),
229+
};
230+
let r = self.scoped_map.read();
231+
let boxed = r.get(&key)?;
232+
233+
boxed.downcast_ref::<Arc<T>>().cloned()
234+
}
235+
219236
/// Remove a client by interface type; returns the removed client if it was present.
220237
pub fn remove<T>(&self) -> Option<Arc<T>>
221238
where
@@ -355,4 +372,23 @@ mod tests {
355372
assert_eq!(&*hub.get::<str>().unwrap(), "global");
356373
assert_eq!(&*hub.get_scoped::<str>(&scope).unwrap(), "scoped");
357374
}
375+
376+
#[test]
377+
fn try_get_scoped_returns_some_on_hit() {
378+
let hub = ClientHub::new();
379+
let scope = ClientScope::gts_id("gts.x.core.modkit.plugins.v1~x.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0");
380+
hub.register_scoped::<str>(scope.clone(), Arc::from("scoped"));
381+
382+
let got = hub.try_get_scoped::<str>(&scope);
383+
assert_eq!(got.as_deref(), Some("scoped"));
384+
}
385+
386+
#[test]
387+
fn try_get_scoped_returns_none_on_miss() {
388+
let hub = ClientHub::new();
389+
let scope = ClientScope::gts_id("gts.x.core.modkit.plugins.v1~x.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0");
390+
391+
let got = hub.try_get_scoped::<str>(&scope);
392+
assert!(got.is_none());
393+
}
358394
}

libs/modkit/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ pub mod telemetry;
115115

116116
pub mod backends;
117117
pub mod lifecycle;
118+
pub mod plugins;
118119
pub mod runtime;
119120

120121
// Error catalog runtime support
@@ -142,6 +143,7 @@ pub use backends::{
142143
OopModuleConfig, OopSpawnConfig,
143144
};
144145
pub use lifecycle::{Lifecycle, Runnable, Status, StopReason, WithLifecycle};
146+
pub use plugins::GtsPluginSelector;
145147
pub use runtime::{
146148
DbOptions, Endpoint, ModuleInstance, ModuleManager, OopModuleSpawnConfig, OopSpawnOptions,
147149
RunOptions, ShutdownOptions, run,

0 commit comments

Comments
 (0)