refactor(proto): remove duplicate config_service.proto

config_service.proto (package foxhunt.config, 4 RPCs) was a subset of
config.proto (package config, 12 RPCs). Migrated api_gateway to implement
ConfigService from config.proto directly. Removed dead config_service()
method from fxt client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-04 14:17:44 +01:00
parent 19a3c66959
commit edfe4da2e8
9 changed files with 223 additions and 212 deletions

View File

@@ -14,7 +14,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"monitoring",
"risk",
"data_acquisition",
"config_service",
]
.iter()
.map(|p| format!("{proto_root}/{p}.proto"))
@@ -40,7 +39,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"monitoring",
"risk",
"data_acquisition",
"config_service",
] {
println!("cargo:rerun-if-changed={proto_root}/{proto}.proto");
}

View File

@@ -176,17 +176,4 @@ impl FoxhuntClient {
)
}
/// API Gateway configuration service client (package `foxhunt.config`).
///
/// Distinct from [`Self::config`] which targets the trading service's
/// configuration proto (package `config`).
pub fn config_service(
&self,
) -> crate::proto::config_service::configuration_service_client::ConfigurationServiceClient<AuthChannel>
{
crate::proto::config_service::configuration_service_client::ConfigurationServiceClient::with_interceptor(
self.channel.clone(),
self.interceptor.clone(),
)
}
}

View File

@@ -122,12 +122,4 @@ pub mod proto {
tonic::include_proto!("data_acquisition");
}
/// Config service protobuf definitions (package `foxhunt.config`)
///
/// Note: This is from `config_service.proto` (API Gateway config service),
/// distinct from `config.proto` (trading service config, package `config`).
#[allow(clippy::shadow_reuse, clippy::impl_trait_in_params, clippy::empty_structs_with_brackets)]
pub mod config_service {
tonic::include_proto!("foxhunt.config");
}
}

View File

@@ -1,81 +0,0 @@
syntax = "proto3";
package foxhunt.config;
// Configuration Management Service
service ConfigurationService {
// Get a single configuration value
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse);
// Update a configuration value
rpc UpdateConfig(UpdateConfigRequest) returns (UpdateConfigResponse);
// List all configurations for a service scope
rpc ListConfigs(ListConfigsRequest) returns (ListConfigsResponse);
// Trigger configuration reload
rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse);
}
// Get configuration request
message GetConfigRequest {
string service_scope = 1;
string config_key = 2;
}
// Get configuration response
message GetConfigResponse {
string config_value = 1; // JSON-serialized value
string data_type = 2;
string description = 3;
int64 updated_at = 4; // Unix timestamp
string updated_by = 5;
}
// Update configuration request
message UpdateConfigRequest {
string service_scope = 1;
string config_key = 2;
string new_value = 3; // JSON-serialized value
string updated_by = 4;
}
// Update configuration response
message UpdateConfigResponse {
bool success = 1;
string message = 2;
}
// List configurations request
message ListConfigsRequest {
optional string service_scope = 1; // If not provided, lists all scopes
}
// Configuration item
message ConfigItem {
string service_scope = 1;
string config_key = 2;
string config_value = 3; // JSON-serialized value
string data_type = 4;
string description = 5;
int64 created_at = 6;
int64 updated_at = 7;
string updated_by = 8;
}
// List configurations response
message ListConfigsResponse {
repeated ConfigItem configs = 1;
}
// Reload configuration request
message ReloadConfigRequest {
optional string service_scope = 1;
optional string config_key = 2;
}
// Reload configuration response
message ReloadConfigResponse {
bool success = 1;
string message = 2;
}

View File

@@ -4,10 +4,9 @@
//! Proto packages:
//! - `foxhunt.tli` (fxt_trading.proto) - Fat-client unified interface (server+client)
//! - `trading` (trading.proto) - Backend trading service (client-only)
//! - `foxhunt.config` (config_service.proto) - Config service (server+client)
//! - `risk` (risk.proto) - Risk service (client-only)
//! - `monitoring` (monitoring.proto) - Monitoring service (server+client)
//! - `config` (config.proto) - Config backend (client-only)
//! - `config` (config.proto) - Config service (server+client)
//! - `ml_training` (ml_training.proto) - ML Training service (server+client)
//! - `trading_agent` (trading_agent.proto) - Trading Agent service (server+client)
@@ -17,19 +16,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?);
// Compile Config Service proto (foxhunt.config package)
config
.clone()
.build_server(true)
.build_client(true)
.file_descriptor_set_path(out_dir.join("config_service_descriptor.bin"))
.compile_well_known_types(true)
.extern_path(".google.protobuf", "::prost_types")
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
.server_mod_attribute(".", "#[allow(unused_qualifications)]")
.client_mod_attribute(".", "#[allow(unused_qualifications)]")
.compile_protos(&["../../proto/config_service.proto"], &["../../proto"])?;
// Compile fat-client TLI proto (package: foxhunt.tli) — server+client
// Contains TradingService, BacktestingService, and MLService
// API Gateway acts as server for incoming client requests
@@ -193,7 +179,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
&["../../proto"],
)?;
println!("cargo:rerun-if-changed=../../proto/config_service.proto");
println!("cargo:rerun-if-changed=../../proto/fxt_trading.proto");
println!("cargo:rerun-if-changed=../../proto/trading.proto");
println!("cargo:rerun-if-changed=../../proto/risk.proto");

View File

@@ -2,24 +2,35 @@
use crate::config::ConfigurationManager;
use crate::error::GatewayConfigError;
use futures::Stream;
use std::pin::Pin;
use tonic::{Request, Response, Status};
use tracing::{debug, info, instrument};
// Import generated proto code from crate root
use crate::foxhunt::config_proto::{
configuration_service_server::{ConfigurationService, ConfigurationServiceServer},
ConfigItem as ProtoConfigItem, GetConfigRequest, GetConfigResponse, ListConfigsRequest,
ListConfigsResponse, ReloadConfigRequest, ReloadConfigResponse, UpdateConfigRequest,
UpdateConfigResponse,
use crate::config_backend::{
config_service_server::{ConfigService, ConfigServiceServer},
ConfigChangeEvent, ConfigDataType, ConfigurationCategory, ConfigurationSetting,
DeleteConfigurationRequest, DeleteConfigurationResponse, ExportConfigurationRequest,
ExportConfigurationResponse, GetConfigSchemaRequest, GetConfigSchemaResponse,
GetConfigurationHistoryRequest, GetConfigurationHistoryResponse, GetConfigurationRequest,
GetConfigurationResponse, ImportConfigurationRequest, ImportConfigurationResponse,
ListCategoriesRequest, ListCategoriesResponse, RollbackConfigurationRequest,
RollbackConfigurationResponse, StreamConfigChangesRequest, UpdateConfigSchemaRequest,
UpdateConfigSchemaResponse, UpdateConfigurationRequest, UpdateConfigurationResponse,
ValidateConfigurationRequest, ValidateConfigurationResponse,
};
/// gRPC service implementation for configuration management
pub struct ConfigurationServiceImpl {
/// gRPC service implementation for gateway-local configuration management.
///
/// Implements the `config.ConfigService` trait from config.proto.
/// Handles GetConfiguration, UpdateConfiguration, and ListCategories locally;
/// all other RPCs return `Status::unimplemented`.
pub struct GatewayConfigService {
manager: std::sync::Arc<tokio::sync::RwLock<ConfigurationManager>>,
}
impl ConfigurationServiceImpl {
/// Creates a new ConfigurationServiceImpl
impl GatewayConfigService {
/// Creates a new GatewayConfigService
pub fn new(manager: ConfigurationManager) -> Self {
Self {
manager: std::sync::Arc::new(tokio::sync::RwLock::new(manager)),
@@ -27,63 +38,94 @@ impl ConfigurationServiceImpl {
}
/// Returns the gRPC server instance
pub fn into_server(self) -> ConfigurationServiceServer<Self> {
ConfigurationServiceServer::new(self)
pub fn into_server(self) -> ConfigServiceServer<Self> {
ConfigServiceServer::new(self)
}
}
#[tonic::async_trait]
impl ConfigurationService for ConfigurationServiceImpl {
impl ConfigService for GatewayConfigService {
type StreamConfigChangesStream =
Pin<Box<dyn Stream<Item = Result<ConfigChangeEvent, Status>> + Send>>;
#[instrument(skip_all)]
async fn get_config(
async fn get_configuration(
&self,
request: Request<GetConfigRequest>,
) -> Result<Response<GetConfigResponse>, Status> {
request: Request<GetConfigurationRequest>,
) -> Result<Response<GetConfigurationResponse>, Status> {
let req = request.into_inner();
debug!("GetConfig: {}/{}", req.service_scope, req.config_key);
let category = req.category.unwrap_or_default();
let key = req.key.unwrap_or_default();
debug!("GetConfiguration: {}/{}", category, key);
if category.is_empty() && key.is_empty() {
// List all configs
let manager = self.manager.read().await;
let configs = manager
.list_configs(None)
.await
.map_err(|e| Status::internal(format!("{}", e)))?;
let settings: Result<Vec<ConfigurationSetting>, Status> = configs
.into_iter()
.map(|c| config_to_setting(&c))
.collect();
return Ok(Response::new(GetConfigurationResponse {
settings: settings?,
}));
}
let manager = self.manager.read().await;
let config = manager
.get_config(&req.service_scope, &req.config_key)
.await
.map_err(|e| match e {
GatewayConfigError::NotFound { .. } => Status::not_found(format!("{}", e)),
_ => Status::internal(format!("{}", e)),
})?;
Ok(Response::new(GetConfigResponse {
config_value: serde_json::to_string(&config.config_value)
.map_err(|e| Status::internal(format!("Serialization error: {}", e)))?,
data_type: config.data_type,
description: config.description.unwrap_or_default(),
updated_at: config.updated_at.timestamp(),
updated_by: config.updated_by.unwrap_or_default(),
}))
if !key.is_empty() {
// Get specific config
let config = manager
.get_config(&category, &key)
.await
.map_err(|e| match e {
GatewayConfigError::NotFound { .. } => Status::not_found(format!("{}", e)),
_ => Status::internal(format!("{}", e)),
})?;
Ok(Response::new(GetConfigurationResponse {
settings: vec![config_to_setting(&config)?],
}))
} else {
// List by category
let configs = manager
.list_configs(Some(&category))
.await
.map_err(|e| Status::internal(format!("{}", e)))?;
let settings: Result<Vec<ConfigurationSetting>, Status> = configs
.into_iter()
.map(|c| config_to_setting(&c))
.collect();
Ok(Response::new(GetConfigurationResponse {
settings: settings?,
}))
}
}
#[instrument(skip_all)]
async fn update_config(
async fn update_configuration(
&self,
request: Request<UpdateConfigRequest>,
) -> Result<Response<UpdateConfigResponse>, Status> {
request: Request<UpdateConfigurationRequest>,
) -> Result<Response<UpdateConfigurationResponse>, Status> {
let req = request.into_inner();
info!(
"UpdateConfig: {}/{} by {}",
req.service_scope, req.config_key, req.updated_by
"UpdateConfiguration: {}/{} by {}",
req.category, req.key, req.changed_by
);
// Parse new value as JSON
let new_value: serde_json::Value = serde_json::from_str(&req.new_value)
let new_value: serde_json::Value = serde_json::from_str(&req.value)
.map_err(|e| Status::invalid_argument(format!("Invalid JSON: {}", e)))?;
let manager = self.manager.read().await;
manager
.update_config(
&req.service_scope,
&req.config_key,
new_value,
&req.updated_by,
)
.update_config(&req.category, &req.key, new_value, &req.changed_by)
.await
.map_err(|e| match e {
GatewayConfigError::Validation(msg) => Status::invalid_argument(msg),
@@ -91,65 +133,159 @@ impl ConfigurationService for ConfigurationServiceImpl {
_ => Status::internal(format!("{}", e)),
})?;
Ok(Response::new(UpdateConfigResponse {
Ok(Response::new(UpdateConfigurationResponse {
success: true,
message: format!(
"Successfully updated {}/{}",
req.service_scope, req.config_key
),
message: format!("Successfully updated {}/{}", req.category, req.key),
validation_result: None,
timestamp: chrono::Utc::now().timestamp(),
}))
}
#[instrument(skip_all)]
async fn list_configs(
async fn delete_configuration(
&self,
request: Request<ListConfigsRequest>,
) -> Result<Response<ListConfigsResponse>, Status> {
_request: Request<DeleteConfigurationRequest>,
) -> Result<Response<DeleteConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Delete not supported by gateway config",
))
}
#[instrument(skip_all)]
async fn list_categories(
&self,
request: Request<ListCategoriesRequest>,
) -> Result<Response<ListCategoriesResponse>, Status> {
let req = request.into_inner();
debug!("ListConfigs: {:?}", req.service_scope);
debug!("ListCategories: {:?}", req.parent_category);
let manager = self.manager.read().await;
let configs = manager
.list_configs(req.service_scope.as_deref())
.list_configs(req.parent_category.as_deref())
.await
.map_err(|e| Status::internal(format!("{}", e)))?;
let proto_configs: Result<Vec<ProtoConfigItem>, Status> = configs
.into_iter()
.map(|c| {
Ok(ProtoConfigItem {
service_scope: c.service_scope,
config_key: c.config_key,
config_value: serde_json::to_string(&c.config_value)
.map_err(|e| Status::internal(format!("{}", e)))?,
data_type: c.data_type,
description: c.description.unwrap_or_default(),
created_at: c.created_at.timestamp(),
updated_at: c.updated_at.timestamp(),
updated_by: c.updated_by.unwrap_or_default(),
})
// Extract unique categories from configs
let mut seen = std::collections::HashSet::new();
let categories = configs
.iter()
.filter_map(|c| {
if seen.insert(c.service_scope.clone()) {
Some(ConfigurationCategory {
id: 0,
name: c.service_scope.clone(),
description: String::new(),
parent_id: None,
display_order: 0,
icon: None,
created_at: c.created_at.timestamp(),
children: vec![],
})
} else {
None
}
})
.collect();
Ok(Response::new(ListConfigsResponse {
configs: proto_configs?,
}))
Ok(Response::new(ListCategoriesResponse { categories }))
}
#[instrument(skip_all)]
async fn reload_config(
async fn stream_config_changes(
&self,
request: Request<ReloadConfigRequest>,
) -> Result<Response<ReloadConfigResponse>, Status> {
let req = request.into_inner();
info!("ReloadConfig: {:?}/{:?}", req.service_scope, req.config_key);
_request: Request<StreamConfigChangesRequest>,
) -> Result<Response<Self::StreamConfigChangesStream>, Status> {
Err(Status::unimplemented(
"Streaming not supported by gateway config",
))
}
// For now, this is a no-op since hot-reload is automatic via NOTIFY/LISTEN
// In the future, this could force a cache invalidation
async fn validate_configuration(
&self,
_request: Request<ValidateConfigurationRequest>,
) -> Result<Response<ValidateConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Validation not supported by gateway config",
))
}
Ok(Response::new(ReloadConfigResponse {
success: true,
message: "Configuration reload triggered (hot-reload active)".to_string(),
}))
async fn get_configuration_history(
&self,
_request: Request<GetConfigurationHistoryRequest>,
) -> Result<Response<GetConfigurationHistoryResponse>, Status> {
Err(Status::unimplemented(
"History not supported by gateway config",
))
}
async fn rollback_configuration(
&self,
_request: Request<RollbackConfigurationRequest>,
) -> Result<Response<RollbackConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Rollback not supported by gateway config",
))
}
async fn export_configuration(
&self,
_request: Request<ExportConfigurationRequest>,
) -> Result<Response<ExportConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Export not supported by gateway config",
))
}
async fn import_configuration(
&self,
_request: Request<ImportConfigurationRequest>,
) -> Result<Response<ImportConfigurationResponse>, Status> {
Err(Status::unimplemented(
"Import not supported by gateway config",
))
}
async fn get_config_schema(
&self,
_request: Request<GetConfigSchemaRequest>,
) -> Result<Response<GetConfigSchemaResponse>, Status> {
Err(Status::unimplemented(
"Schema not supported by gateway config",
))
}
async fn update_config_schema(
&self,
_request: Request<UpdateConfigSchemaRequest>,
) -> Result<Response<UpdateConfigSchemaResponse>, Status> {
Err(Status::unimplemented(
"Schema update not supported by gateway config",
))
}
}
/// Convert a ConfigItem (from ConfigurationManager) to a ConfigurationSetting proto message
fn config_to_setting(c: &crate::config::ConfigItem) -> Result<ConfigurationSetting, Status> {
Ok(ConfigurationSetting {
id: 0,
category: c.service_scope.clone(),
key: c.config_key.clone(),
value: serde_json::to_string(&c.config_value)
.map_err(|e| Status::internal(format!("Serialization error: {}", e)))?,
data_type: ConfigDataType::String as i32,
hot_reload: false,
description: c.description.clone().unwrap_or_default(),
default_value: None,
required: false,
sensitive: false,
validation_rule: None,
environment_override: None,
min_value: None,
max_value: None,
enum_values: None,
depends_on: vec![],
tags: vec![],
display_order: 0,
created_at: c.created_at.timestamp(),
modified_at: c.updated_at.timestamp(),
})
}

View File

@@ -5,7 +5,7 @@ pub mod endpoints;
pub mod manager;
pub mod validator;
pub use endpoints::ConfigurationServiceImpl;
pub use endpoints::GatewayConfigService;
pub use manager::{ConfigItem, ConfigurationManager};
pub use validator::{ConfigValidator, ValidationRules};

View File

@@ -26,8 +26,6 @@ pub const ML_TRAINING_FILE_DESCRIPTOR_SET: &[u8] =
tonic::include_file_descriptor_set!("ml_training_descriptor");
pub const TRADING_AGENT_FILE_DESCRIPTOR_SET: &[u8] =
tonic::include_file_descriptor_set!("trading_agent_descriptor");
pub const CONFIG_SERVICE_FILE_DESCRIPTOR_SET: &[u8] =
tonic::include_file_descriptor_set!("config_service_descriptor");
pub const TRADING_FILE_DESCRIPTOR_SET: &[u8] =
tonic::include_file_descriptor_set!("trading_descriptor");
pub const RISK_FILE_DESCRIPTOR_SET: &[u8] =
@@ -46,9 +44,6 @@ pub mod foxhunt {
pub mod tli {
tonic::include_proto!("foxhunt.tli");
}
pub mod config_proto {
tonic::include_proto!("foxhunt.config");
}
}
pub mod ml_training {
@@ -113,7 +108,7 @@ pub use error::{GatewayConfigError, GatewayConfigResult};
// Re-export configuration management types
pub use config::{
AuthzMetrics, AuthzService, ConfigItem, ConfigValidator, ConfigurationManager,
ConfigurationServiceImpl, PermissionResult,
GatewayConfigService, PermissionResult,
};
// Re-export routing and rate limiting types

View File

@@ -682,7 +682,6 @@ async fn main() -> Result<()> {
.register_encoded_file_descriptor_set(api_gateway::MONITORING_FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(api_gateway::ML_TRAINING_FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(api_gateway::TRADING_AGENT_FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(api_gateway::CONFIG_SERVICE_FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(api_gateway::TRADING_FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(api_gateway::RISK_FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(api_gateway::CONFIG_FILE_DESCRIPTOR_SET)