Skip to content
Draft
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
170 changes: 170 additions & 0 deletions docs/configuration-architecture-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Configuration Architecture Analysis

## Overview

This document analyzes the configuration architecture in the Valkey.Glide C# client, focusing on the relationship between `ConnectionConfiguration` and `ConfigurationOptions`, and how configuration changes can be made through the `ConnectionMultiplexer`.

## Configuration Classes Relationship

### ConfigurationOptions
- **Purpose**: External API configuration class that follows StackExchange.Redis compatibility patterns
- **Location**: `sources/Valkey.Glide/Abstract/ConfigurationOptions.cs`
- **Role**: User-facing configuration interface

### ConnectionConfiguration
- **Purpose**: Internal configuration classes that map to the underlying FFI layer
- **Location**: `sources/Valkey.Glide/ConnectionConfiguration.cs`
- **Role**: Internal configuration representation and builder pattern implementation

## Configuration Flow

```
ConfigurationOptions → ClientConfigurationBuilder → ConnectionConfig → FFI.ConnectionConfig
```

1. **User Input**: `ConfigurationOptions` (external API)
2. **Translation**: `ConnectionMultiplexer.CreateClientConfigBuilder<T>()` method
3. **Building**: `ClientConfigurationBuilder<T>` (internal)
4. **Internal Config**: `ConnectionConfig` record
5. **FFI Layer**: `FFI.ConnectionConfig`

## Key Components Analysis

### ConnectionMultiplexer Configuration Mapping

The `ConnectionMultiplexer.CreateClientConfigBuilder<T>()` method at line 174 performs the critical translation:

```csharp
internal static T CreateClientConfigBuilder<T>(ConfigurationOptions configuration)
where T : ClientConfigurationBuilder<T>, new()
{
T config = new();
foreach (EndPoint ep in configuration.EndPoints)
{
config.Addresses += Utils.SplitEndpoint(ep);
}
config.UseTls = configuration.Ssl;
// ... other mappings
_ = configuration.ReadFrom.HasValue ? config.ReadFrom = configuration.ReadFrom.Value : new();
return config;
}
```

### Configuration Builders

The builder pattern is implemented through:
- `StandaloneClientConfigurationBuilder` (line 525)
- `ClusterClientConfigurationBuilder` (line 550)

Both inherit from `ClientConfigurationBuilder<T>` which provides:
- Fluent API methods (`WithXxx()`)
- Property setters
- Internal `ConnectionConfig Build()` method

## Configuration Mutability Analysis

### Current State: Immutable After Connection

**Connection Creation**: Configuration is set once during `ConnectionMultiplexer.ConnectAsync()`:

```csharp
public static async Task<ConnectionMultiplexer> ConnectAsync(ConfigurationOptions configuration, TextWriter? log = null)
{
// Configuration is translated and used to create the client
StandaloneClientConfiguration standaloneConfig = CreateClientConfigBuilder<StandaloneClientConfigurationBuilder>(configuration).Build();
// ... connection establishment
return new(configuration, await Database.Create(config));
}
```

**Storage**: The original `ConfigurationOptions` is stored in `RawConfig` property (line 156):

```csharp
internal ConfigurationOptions RawConfig { private set; get; }
```

### Limitations for Runtime Configuration Changes

1. **No Reconfiguration API**: `ConnectionMultiplexer` doesn't expose methods to change configuration after connection
2. **Immutable Builder Chain**: Once built, the configuration flows to FFI layer and cannot be modified
3. **Connection Recreation Required**: Any configuration change requires creating a new `ConnectionMultiplexer` instance

## Potential Configuration Change Approaches

### 1. Connection Recreation (Current Pattern)
```csharp
// Current approach - requires new connection
var newConfig = oldConfig.Clone();
newConfig.ReadFrom = new ReadFrom(ReadFromStrategy.AzAffinity, "us-west-2");
var newMultiplexer = await ConnectionMultiplexer.ConnectAsync(newConfig);
```

### 2. Potential Runtime Reconfiguration (Not Currently Implemented)

To enable runtime configuration changes, the following would need to be implemented:

```csharp
// Hypothetical API
public async Task ReconfigureAsync(Action<ConfigurationOptions> configure)
{
var newConfig = RawConfig.Clone();
configure(newConfig);

// Would need to:
// 1. Validate configuration changes
// 2. Update underlying client configuration
// 3. Potentially recreate connections
// 4. Update RawConfig
}
```

### 3. Builder Pattern Extension

A potential approach could extend the builder pattern to support updates:

```csharp
// Hypothetical API
public async Task<bool> TryUpdateConfigurationAsync<T>(Action<T> configure)
where T : ClientConfigurationBuilder<T>, new()
{
// Create new builder from current configuration
// Apply changes
// Validate and apply if possible
}
```

## ReadFrom Configuration Specifics

### Current Implementation
- `ReadFrom` is a struct (line 74) with `ReadFromStrategy` enum and optional AZ string
- Mapped in `CreateClientConfigBuilder()` at line 199
- Flows through to FFI layer via `ConnectionConfig.ToFfi()` method

### ReadFrom Change Requirements
To change `ReadFrom` configuration at runtime would require:
1. **API Design**: Method to accept new `ReadFrom` configuration
2. **Validation**: Ensure new configuration is compatible with current connection type
3. **FFI Updates**: Update the underlying client configuration
4. **Connection Management**: Handle any required connection reestablishment

## Recommendations

### Short Term
1. **Document Current Limitations**: Clearly document that configuration changes require connection recreation
2. **Helper Methods**: Provide utility methods for common reconfiguration scenarios:
```csharp
public static async Task<ConnectionMultiplexer> RecreateWithReadFromAsync(
ConnectionMultiplexer current,
ReadFrom newReadFrom)
```

### Long Term
1. **Runtime Reconfiguration API**: Implement selective runtime configuration updates for non-disruptive changes
2. **Configuration Validation**: Add validation to determine which changes require reconnection vs. runtime updates
3. **Connection Pool Management**: Consider connection pooling to minimize disruption during reconfiguration

## Conclusion

Currently, the `ConnectionMultiplexer` does not support runtime configuration changes. The architecture is designed around immutable configuration set at connection time. Any configuration changes, including `ReadFrom` strategy modifications, require creating a new `ConnectionMultiplexer` instance.

The relationship between `ConfigurationOptions` and `ConnectionConfiguration` is a translation layer where the external API (`ConfigurationOptions`) is converted to internal configuration structures (`ConnectionConfiguration`) that interface with the FFI layer.
184 changes: 184 additions & 0 deletions rust/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,187 @@ pub(crate) unsafe fn get_pipeline_options(
PipelineRetryStrategy::new(info.retry_server_error, info.retry_connection_error),
)
}

/// FFI structure for PubSub message information.
#[repr(C)]
#[derive(Debug, Clone)]
pub struct PubSubMessageInfo {
/// The message content as a null-terminated C string.
pub message: *const c_char,
/// The channel name as a null-terminated C string.
pub channel: *const c_char,
/// The pattern that matched (null if exact channel subscription).
pub pattern: *const c_char,
}

/// FFI callback function type for PubSub messages.
///
/// # Parameters
/// * `client_id` - The ID of the client that received the message
/// * `message_ptr` - Pointer to PubSubMessageInfo structure
pub type PubSubCallback = extern "C" fn(client_id: u64, message_ptr: *const PubSubMessageInfo);

/// Register a PubSub callback for the specified client.
///
/// # Safety
/// * `client` must be a valid client pointer obtained from `create_client`
/// * `callback` must be a valid function pointer that remains valid for the client's lifetime
///
/// # Parameters
/// * `client` - Pointer to the client instance
/// * `callback` - Function pointer to the PubSub message callback
#[unsafe(no_mangle)]
pub extern "C" fn register_pubsub_callback(
client_ptr: *mut std::ffi::c_void,
callback: PubSubCallback,
) {
if client_ptr.is_null() {
eprintln!("Warning: register_pubsub_callback called with null client pointer");
return;
}

// Cast the client pointer back to our Client type
// Safety: This is safe because we know the pointer came from create_client
// and points to a valid Client instance wrapped in Arc
unsafe {
// Increment the reference count to get a temporary Arc
std::sync::Arc::increment_strong_count(client_ptr);
let client_arc = std::sync::Arc::from_raw(client_ptr as *const crate::Client);

// Store the callback in the client
if let Ok(mut callback_guard) = client_arc.pubsub_callback.lock() {
*callback_guard = Some(callback);
println!("PubSub callback registered for client {:p}", client_ptr);
} else {
eprintln!("Failed to acquire lock for PubSub callback registration");
}

// Don't drop the Arc, just forget it to maintain the reference count
std::mem::forget(client_arc);

// TODO: When glide-core PubSub support is available, this is where we would:
// 1. Set up the message routing from glide-core to invoke our callback
// 2. Configure the client to handle PubSub messages
// 3. Establish the subscription channels
}
}

/// Invoke the PubSub callback for a client with a message.
/// This function is intended to be called by glide-core when a PubSub message is received.
///
/// # Safety
/// * `client_ptr` must be a valid client pointer obtained from `create_client`
/// * `message_ptr` must be a valid pointer to a PubSubMessageInfo structure
///
/// # Parameters
/// * `client_ptr` - Pointer to the client instance
/// * `message_ptr` - Pointer to the PubSub message information
pub(crate) unsafe fn invoke_pubsub_callback(
client_ptr: *const std::ffi::c_void,
message_ptr: *const PubSubMessageInfo,
) {
if client_ptr.is_null() || message_ptr.is_null() {
eprintln!("Warning: invoke_pubsub_callback called with null pointer(s)");
return;
}

unsafe {
// Increment the reference count to get a temporary Arc
std::sync::Arc::increment_strong_count(client_ptr);
let client_arc = std::sync::Arc::from_raw(client_ptr as *const crate::Client);

// Get the callback and invoke it
if let Ok(callback_guard) = client_arc.pubsub_callback.lock() {
if let Some(callback) = *callback_guard {
// Extract client ID from the pointer (simplified approach)
let client_id = client_ptr as u64;

// Invoke the callback
callback(client_id, message_ptr);
}
}

// Don't drop the Arc, just forget it to maintain the reference count
std::mem::forget(client_arc);
}
}

/// Create a PubSubMessageInfo structure from Rust strings.
/// The returned pointer must be freed using `free_pubsub_message`.
///
/// # Parameters
/// * `message` - The message content
/// * `channel` - The channel name
/// * `pattern` - The pattern that matched (None for exact channel subscriptions)
///
/// # Returns
/// * Pointer to allocated PubSubMessageInfo structure, or null on allocation failure
pub(crate) fn create_pubsub_message(
message: &str,
channel: &str,
pattern: Option<&str>,
) -> *mut PubSubMessageInfo {
use std::ffi::CString;

// Convert strings to C strings
let message_cstr = match CString::new(message) {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};

let channel_cstr = match CString::new(channel) {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};

let pattern_cstr = match pattern {
Some(p) => match CString::new(p) {
Ok(s) => Some(s),
Err(_) => return std::ptr::null_mut(),
},
None => None,
};

// Create the message info structure
let message_info = PubSubMessageInfo {
message: message_cstr.into_raw(),
channel: channel_cstr.into_raw(),
pattern: pattern_cstr.map_or(std::ptr::null(), |s| s.into_raw()),
};

// Allocate and return
Box::into_raw(Box::new(message_info))
}

/// Free memory allocated for a PubSub message.
///
/// # Safety
/// * `message_ptr` must be a valid pointer to a PubSubMessageInfo structure
/// * The structure and its string fields must have been allocated by this library
/// * This function should only be called once per message
///
/// # Parameters
/// * `message_ptr` - Pointer to the PubSubMessageInfo structure to free
#[unsafe(no_mangle)]
pub extern "C" fn free_pubsub_message(message_ptr: *mut PubSubMessageInfo) {
if message_ptr.is_null() {
return;
}

unsafe {
let message_info = Box::from_raw(message_ptr);

// Free the individual string fields if they were allocated
if !message_info.message.is_null() {
let _ = std::ffi::CString::from_raw(message_info.message as *mut c_char);
}
if !message_info.channel.is_null() {
let _ = std::ffi::CString::from_raw(message_info.channel as *mut c_char);
}
if !message_info.pattern.is_null() {
let _ = std::ffi::CString::from_raw(message_info.pattern as *mut c_char);
}

// message_info is automatically dropped here, freeing the struct itself
}
}
Loading