Skip to content

Commit

Permalink
[FabricRuntime] Support Package change notification callbacks (#96)
Browse files Browse the repository at this point in the history
End user applications probably want to handle package change
notifications.

This PR currently tackles only ConfigurationPackage.

If you're happy with the approach, I'll do the same exact implementation
(edit: in additional PRs) for CodePackage and DataPackage and that will
close out #97.
  • Loading branch information
cgettys-microsoft authored Dec 28, 2024
1 parent c18570a commit f8cf77f
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 18 deletions.
2 changes: 2 additions & 0 deletions crates/libs/core/src/client/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ impl PartialOrd for ServiceEndpointsVersion {

// Bridge implementation for the notification handler to turn rust code into SF com object.
#[windows_core::implement(IFabricServiceNotificationEventHandler)]
#[allow(non_camel_case_types)] // Suppress lint for _Impl struct
pub struct ServiceNotificationEventHandlerBridge<T>
where
T: ServiceNotificationEventHandler,
Expand Down Expand Up @@ -174,6 +175,7 @@ where
/// Lambda implemnentation of ServiceNotificationEventHandler trait.
/// This is used in FabricClientBuilder to build function into handler.
/// Not exposed to user.
/// This isn't strictly required by the implementation as written. But it leaves open the door to non-lambda implementations in future.
pub struct LambdaServiceNotificationHandler<T>
where
T: Fn(&ServiceNotification) -> crate::Result<()> + 'static,
Expand Down
47 changes: 45 additions & 2 deletions crates/libs/core/src/runtime/activation_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
// ------------------------------------------------------------

use mssf_com::{
FabricRuntime::IFabricCodePackageActivationContext6,
FabricRuntime::{
IFabricCodePackageActivationContext6, IFabricConfigurationPackageChangeHandler,
},
FabricTypes::{FABRIC_HEALTH_INFORMATION, FABRIC_HEALTH_REPORT_SEND_OPTIONS},
};

Expand All @@ -14,7 +16,16 @@ use crate::{
Error, WString, PCWSTR,
};

use super::config::ConfigurationPackage;
use super::{
config::ConfigurationPackage,
package_change::{
config::{
ConfigurationPackageChangeCallbackHandle, ConfigurationPackageChangeEventHandlerBridge,
LambdaConfigurationPackageEventHandler,
},
ConfigurationPackageChangeEvent,
},
};

#[derive(Debug, Clone)]
pub struct CodePackageActivationContext {
Expand Down Expand Up @@ -132,6 +143,38 @@ impl CodePackageActivationContext {
pub fn get_com(&self) -> IFabricCodePackageActivationContext6 {
self.com_impl.clone()
}

pub fn register_configuration_package_change_handler<T>(
&self,
handler: T,
) -> crate::Result<ConfigurationPackageChangeCallbackHandle>
where
T: Fn(&ConfigurationPackageChangeEvent) + 'static,
{
let lambda_handler = LambdaConfigurationPackageEventHandler::new(handler);
let bridge = ConfigurationPackageChangeEventHandlerBridge::new(lambda_handler);
let callback: IFabricConfigurationPackageChangeHandler = bridge.into();
// SAFETY: bridge implements the required COM interface
let raw_handle = unsafe {
self.com_impl
.RegisterConfigurationPackageChangeHandler(&callback)
}?;
// SAFETY: raw_handle is a configuration package change handler id, not some other id.
Ok(unsafe { ConfigurationPackageChangeCallbackHandle::from_com(raw_handle) })
}

pub fn unregister_configuration_package_change_handler(
&self,
handle: ConfigurationPackageChangeCallbackHandle,
) -> crate::Result<()> {
// SAFETY: we assume that only 1 activation context can be
unsafe {
self.com_impl
.UnregisterConfigurationPackageChangeHandler(handle.0)
}
.unwrap();
Ok(())
}
}

impl From<IFabricCodePackageActivationContext6> for CodePackageActivationContext {
Expand Down
3 changes: 3 additions & 0 deletions crates/libs/core/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub mod error;
pub mod executor;
#[cfg(feature = "tokio_async")]
pub mod node_context;

pub mod package_change;

#[cfg(feature = "tokio_async")]
pub mod runtime_wrapper;
#[cfg(feature = "tokio_async")]
Expand Down
119 changes: 119 additions & 0 deletions crates/libs/core/src/runtime/package_change/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------
//! Handle callbacks for configuration package changes
//! TODO: We probably should also provide a helpful callback to use in conjunction with the config-rs support (so that it processes configuration changes)
use mssf_com::FabricRuntime::{
IFabricConfigurationPackageChangeHandler, IFabricConfigurationPackageChangeHandler_Impl,
};

use crate::runtime::config::ConfigurationPackage;

use super::ConfigurationPackageChangeEvent;

/// Rust trait to turn rust code into IFabricConfigurationPackageChangeHandler.
/// Not exposed to user
pub trait ConfigurationPackageChangeEventHandler: 'static {
fn on_change(&self, change: &ConfigurationPackageChangeEvent);
}

// Bridge implementation for the change handler to turn rust code into SF com object.
#[windows_core::implement(IFabricConfigurationPackageChangeHandler)]
#[allow(non_camel_case_types)] // Suppress lint for _Impl struct
pub struct ConfigurationPackageChangeEventHandlerBridge<T>
where
T: ConfigurationPackageChangeEventHandler,
{
inner: T,
}

impl<T> ConfigurationPackageChangeEventHandlerBridge<T>
where
T: ConfigurationPackageChangeEventHandler,
{
pub fn new(inner: T) -> Self {
Self { inner }
}
}

impl<T> IFabricConfigurationPackageChangeHandler_Impl
for ConfigurationPackageChangeEventHandlerBridge_Impl<T>
where
T: ConfigurationPackageChangeEventHandler,
{
fn OnPackageAdded(
&self,
_source: Option<&mssf_com::FabricRuntime::IFabricCodePackageActivationContext>,
configpackage: Option<&mssf_com::FabricRuntime::IFabricConfigurationPackage>,
) {
let new_package = ConfigurationPackage::from_com(configpackage.unwrap().clone());
let event = ConfigurationPackageChangeEvent::Addition { new_package };
self.inner.on_change(&event)
}

fn OnPackageRemoved(
&self,
_source: Option<&mssf_com::FabricRuntime::IFabricCodePackageActivationContext>,
configpackage: Option<&mssf_com::FabricRuntime::IFabricConfigurationPackage>,
) {
let previous_package = ConfigurationPackage::from_com(configpackage.unwrap().clone());
let event = ConfigurationPackageChangeEvent::Removal { previous_package };
self.inner.on_change(&event)
}

fn OnPackageModified(
&self,
_source: Option<&mssf_com::FabricRuntime::IFabricCodePackageActivationContext>,
previousconfigpackage: Option<&mssf_com::FabricRuntime::IFabricConfigurationPackage>,
configpackage: Option<&mssf_com::FabricRuntime::IFabricConfigurationPackage>,
) {
let new_package = ConfigurationPackage::from_com(configpackage.unwrap().clone());
let previous_package =
ConfigurationPackage::from_com(previousconfigpackage.unwrap().clone());
let event = ConfigurationPackageChangeEvent::Modification {
previous_package,
new_package,
};
self.inner.on_change(&event)
}
}

/// Lambda implementation of ConfigurationPackageChangeEventHandler trait.
/// This is used in FabricClientBuilder to build function into handler.
/// Not exposed to user.
/// Strictly speaking we don't need this layer. But it would allow us to open the door to trait implementations someday
pub(crate) struct LambdaConfigurationPackageEventHandler<T>
where
T: Fn(&ConfigurationPackageChangeEvent),
{
f: T,
}

impl<T> LambdaConfigurationPackageEventHandler<T>
where
T: Fn(&ConfigurationPackageChangeEvent) + 'static,
{
pub fn new(f: T) -> Self {
Self { f }
}
}

impl<T> ConfigurationPackageChangeEventHandler for LambdaConfigurationPackageEventHandler<T>
where
T: Fn(&ConfigurationPackageChangeEvent) + 'static,
{
fn on_change(&self, change: &ConfigurationPackageChangeEvent) {
(self.f)(change)
}
}

pub struct ConfigurationPackageChangeCallbackHandle(pub(crate) i64);

impl ConfigurationPackageChangeCallbackHandle {
/// # Safety
/// Caller ensures this is a registered callback id
pub const unsafe fn from_com(com: i64) -> Self {
Self(com)
}
}
17 changes: 17 additions & 0 deletions crates/libs/core/src/runtime/package_change/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------
//! This module supports implementing callbacks when Service Fabric Packages are changed
//!
pub mod config;

/// The ways a given Service Fabric Package (e.g. ConfigurationPackage or DataPackage) can change
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum PackageChangeEvent<T> {
Addition { new_package: T },
Removal { previous_package: T },
Modification { previous_package: T, new_package: T },
}

pub type ConfigurationPackageChangeEvent = PackageChangeEvent<super::config::ConfigurationPackage>;
49 changes: 38 additions & 11 deletions crates/samples/echomain/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use mssf_core::conf::{Config, FabricConfigSource};
use mssf_core::debug::wait_for_debugger;
use mssf_core::error::FabricError;
use mssf_core::runtime::config::ConfigurationPackage;
use mssf_core::runtime::executor::{DefaultExecutor, Executor};
use mssf_core::runtime::node_context::NodeContext;
use mssf_core::runtime::package_change::PackageChangeEvent;
use mssf_core::runtime::CodePackageActivationContext;
use mssf_core::types::{HealthInformation, HealthReportSendOption};
use mssf_core::WString;
Expand Down Expand Up @@ -84,6 +86,37 @@ fn validate_configs(actctx: &CodePackageActivationContext) {
let config = actctx
.get_configuration_package(&WString::from("Config"))
.unwrap();
let s = build_config(config);
let val = s.get::<String>("my_config_section.my_string").unwrap();
info!("entry: {}", val);
// note that the config name lookup is case sensitive for struct fields.
let settings = s.try_deserialize::<MySettings>().unwrap();
info!("settings: {:?}", settings);
let sect = settings.my_config_section;
assert_eq!(sect.my_string, "Value1");
assert!(sect.my_bool);
assert_eq!(sect.my_int, 99);
actctx.register_configuration_package_change_handler(|change| {
let (some_package, change_type, validate_new) = match change
{
PackageChangeEvent::Addition { new_package } => (new_package, "Addition", true),
PackageChangeEvent::Removal { previous_package } => (previous_package, "Removal", false),
PackageChangeEvent::Modification { previous_package: _, new_package } => (new_package, "Modification", true),
};
let changed_package_name = some_package.get_description().name.to_string_lossy();
let changed_package_str = &changed_package_name;
info!("Received config package change of type {change_type:?} to package {changed_package_str}");
if validate_new
{
// This is a bit hacky, but if there was a removal, not much point in validating the old package
// In an application that actually uses its config settings, we'd probably put the result of this into a OnceLock<RwLock<Config>>
// or something more complicated, like a ArcSwap<Config> or similar
build_config(some_package.clone());
}
}).unwrap();
}

fn build_config(config: ConfigurationPackage) -> Config {
let settings = config.get_settings();
settings.sections.iter().for_each(|section| {
info!("Section: {}", section.name);
Expand All @@ -103,22 +136,16 @@ fn validate_configs(actctx: &CodePackageActivationContext) {
assert_eq!(v.to_string_lossy(), "Value1");
assert!(!encrypt);

// TODO: add a overrideable parameter in the manifest / settings and log it here

// Use the config framework
let source = FabricConfigSource::new(config);
let s = Config::builder()

Config::builder()
.add_source(source)
.build()
.inspect_err(|e| info!("config build failed: {}", e))
.unwrap();
let val = s.get::<String>("my_config_section.my_string").unwrap();
info!("entry: {}", val);
// note that the config name lookup is case sensitive for struct fields.
let settings = s.try_deserialize::<MySettings>().unwrap();
info!("settings: {:?}", settings);
let sect = settings.my_config_section;
assert_eq!(sect.my_string, "Value1");
assert!(sect.my_bool);
assert_eq!(sect.my_int, 99);
.unwrap()
}

/// Send health ok to SF to validate health reporting code
Expand Down
24 changes: 19 additions & 5 deletions crates/samples/no_default_features/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,25 @@
//!
//! This sample demonstrates it is possible to use the library with default-features = false and ensures that that scenario remains compiling as PRs go into the repository.
//!
use mssf_core::runtime::CodePackageActivationContext;
use mssf_core::runtime::{package_change::PackageChangeEvent, CodePackageActivationContext};
#[no_mangle]
fn test_fn() {
// Make sure we link something
//
let my_ctx = CodePackageActivationContext::create();
my_ctx.unwrap();
let my_ctx = CodePackageActivationContext::create().unwrap();

// One might wish to use such a callback to e.g. trigger custom handling of configuration changes
// This doesn't require the config feature to be enabled
let _handler = my_ctx.register_configuration_package_change_handler( |c|
{
let (some_package, change_type) = match c
{
PackageChangeEvent::Addition { new_package } => (new_package, "Addition"),
PackageChangeEvent::Removal { previous_package } => (previous_package, "Removal"),
PackageChangeEvent::Modification { previous_package: _, new_package } => (new_package, "Modification"),
};
let changed_package_name = some_package.get_description().name.to_string_lossy();
let changed_package_str = &changed_package_name;
println!("Received config package change of type {change_type:?} to package {changed_package_str}");
}
).unwrap();
}

0 comments on commit f8cf77f

Please sign in to comment.