2025-06-01 19:14:21 +00:00
using AIStudio.Tools.PluginSystem ;
namespace AIStudio.Tools.Services ;
2025-06-02 18:08:25 +00:00
public sealed class EnterpriseEnvironmentService ( ILogger < EnterpriseEnvironmentService > logger , RustService rustService ) : BackgroundService
2025-06-01 19:14:21 +00:00
{
2026-02-15 17:11:57 +00:00
public static List < EnterpriseEnvironment > CURRENT_ENVIRONMENTS = [ ] ;
2026-02-19 19:43:47 +00:00
public static bool HasValidEnterpriseSnapshot { get ; private set ; }
2025-06-02 18:08:25 +00:00
#if DEBUG
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan . FromMinutes ( 6 ) ;
#else
2025-06-01 19:14:21 +00:00
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan . FromMinutes ( 16 ) ;
2025-06-02 18:08:25 +00:00
#endif
2025-06-01 19:14:21 +00:00
#region Overrides of BackgroundService
protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
{
logger . LogInformation ( "The enterprise environment service was initialized." ) ;
2025-06-02 18:08:25 +00:00
await this . StartUpdating ( isFirstRun : true ) ;
2025-06-01 19:14:21 +00:00
while ( ! stoppingToken . IsCancellationRequested )
{
await Task . Delay ( CHECK_INTERVAL , stoppingToken ) ;
await this . StartUpdating ( ) ;
}
}
#endregion
2025-06-02 18:08:25 +00:00
private async Task StartUpdating ( bool isFirstRun = false )
2025-06-01 19:14:21 +00:00
{
try
{
2025-06-27 18:52:33 +00:00
logger . LogInformation ( "Start updating of the enterprise environment." ) ;
2026-02-19 19:43:47 +00:00
HasValidEnterpriseSnapshot = false ;
2026-02-15 17:11:57 +00:00
//
2026-02-19 19:43:47 +00:00
// Step 1: Fetch all active configurations.
2026-02-15 17:11:57 +00:00
//
List < EnterpriseEnvironment > fetchedConfigs ;
2026-01-24 19:17:35 +00:00
try
{
2026-02-15 17:11:57 +00:00
fetchedConfigs = await rustService . EnterpriseEnvConfigs ( ) ;
2026-01-24 19:17:35 +00:00
}
catch ( Exception e )
{
2026-02-15 17:11:57 +00:00
logger . LogError ( e , "Failed to fetch the enterprise configurations from the Rust service." ) ;
await MessageBus . INSTANCE . SendMessage ( null , Event . RUST_SERVICE_UNAVAILABLE , "EnterpriseEnvConfigs failed" ) ;
2026-01-24 19:17:35 +00:00
return ;
}
2026-02-15 17:11:57 +00:00
//
2026-02-19 19:43:47 +00:00
// Step 2: Determine ETags and build the list of reachable configurations.
// IMPORTANT: when one config server fails, we continue with the others.
2026-02-15 17:11:57 +00:00
//
2026-02-19 19:43:47 +00:00
var reachableEnvironments = new List < EnterpriseEnvironment > ( ) ;
var failedConfigIds = new HashSet < Guid > ( ) ;
var currentEnvironmentsById = CURRENT_ENVIRONMENTS
. GroupBy ( env = > env . ConfigurationId )
. ToDictionary ( group = > group . Key , group = > group . Last ( ) ) ;
var activeFetchedEnvironmentsById = fetchedConfigs
. Where ( config = > config . IsActive )
. GroupBy ( config = > config . ConfigurationId )
. ToDictionary ( group = > group . Key , group = > group . Last ( ) ) ;
2026-02-15 17:11:57 +00:00
foreach ( var config in fetchedConfigs )
2026-01-24 19:17:35 +00:00
{
2026-02-15 17:11:57 +00:00
if ( ! config . IsActive )
{
logger . LogWarning ( "Skipping inactive enterprise configuration with ID '{ConfigId}'. There is either no valid server URL or config ID set." , config . ConfigurationId ) ;
continue ;
}
2026-02-19 19:43:47 +00:00
var etagResponse = await PluginFactory . DetermineConfigPluginETagAsync ( config . ConfigurationId , config . ConfigurationServerUrl ) ;
if ( ! etagResponse . Success )
2026-02-15 17:11:57 +00:00
{
2026-02-19 19:43:47 +00:00
failedConfigIds . Add ( config . ConfigurationId ) ;
logger . LogWarning ( "Failed to read enterprise config metadata for '{ConfigId}' from '{ServerUrl}': {Issue}. Keeping the current plugin state for this configuration." , config . ConfigurationId , config . ConfigurationServerUrl , etagResponse . Issue ? ? "Unknown issue" ) ;
continue ;
2026-02-15 17:11:57 +00:00
}
2026-02-19 19:43:47 +00:00
reachableEnvironments . Add ( config with { ETag = etagResponse . ETag } ) ;
2026-01-24 19:17:35 +00:00
}
2026-02-15 17:11:57 +00:00
//
2026-02-19 19:43:47 +00:00
// Step 3: Compare with current environments and process changes.
// Download per configuration. A single failure must not block others.
2026-02-15 17:11:57 +00:00
//
2026-02-19 19:43:47 +00:00
var shouldDeferStartupDownloads = isFirstRun & & ! PluginFactory . IsInitialized ;
var effectiveEnvironmentsById = new Dictionary < Guid , EnterpriseEnvironment > ( ) ;
2026-02-15 17:11:57 +00:00
// Process new or changed configs:
2026-02-19 19:43:47 +00:00
foreach ( var nextEnv in reachableEnvironments )
2025-06-01 19:14:21 +00:00
{
2026-02-19 19:43:47 +00:00
var hasCurrentEnvironment = currentEnvironmentsById . TryGetValue ( nextEnv . ConfigurationId , out var currentEnv ) ;
if ( hasCurrentEnvironment & & currentEnv = = nextEnv ) // Hint: This relies on the record equality to check if anything relevant has changed (e.g. server URL or ETag).
2025-06-01 19:14:21 +00:00
{
2026-02-15 17:11:57 +00:00
logger . LogInformation ( "Enterprise configuration '{ConfigId}' has not changed. No update required." , nextEnv . ConfigurationId ) ;
2026-02-19 19:43:47 +00:00
effectiveEnvironmentsById [ nextEnv . ConfigurationId ] = nextEnv ;
2026-02-15 17:11:57 +00:00
continue ;
2025-06-01 19:14:21 +00:00
}
2026-02-15 17:11:57 +00:00
2026-02-19 19:43:47 +00:00
if ( ! hasCurrentEnvironment )
2026-02-15 17:11:57 +00:00
logger . LogInformation ( "Detected new enterprise configuration with ID '{ConfigId}' and server URL '{ServerUrl}'." , nextEnv . ConfigurationId , nextEnv . ConfigurationServerUrl ) ;
else
logger . LogInformation ( "Detected change in enterprise configuration with ID '{ConfigId}'. Server URL or ETag has changed." , nextEnv . ConfigurationId ) ;
2026-02-19 19:43:47 +00:00
if ( shouldDeferStartupDownloads )
{
2026-02-15 17:11:57 +00:00
MessageBus . INSTANCE . DeferMessage ( null , Event . STARTUP_ENTERPRISE_ENVIRONMENT , nextEnv ) ;
2026-02-19 19:43:47 +00:00
effectiveEnvironmentsById [ nextEnv . ConfigurationId ] = nextEnv ;
}
2026-02-15 17:11:57 +00:00
else
2026-02-19 19:43:47 +00:00
{
var wasDownloadSuccessful = await PluginFactory . TryDownloadingConfigPluginAsync ( nextEnv . ConfigurationId , nextEnv . ConfigurationServerUrl ) ;
if ( ! wasDownloadSuccessful )
{
failedConfigIds . Add ( nextEnv . ConfigurationId ) ;
if ( hasCurrentEnvironment )
{
logger . LogWarning ( "Failed to update enterprise configuration '{ConfigId}'. Keeping the previously active version." , nextEnv . ConfigurationId ) ;
effectiveEnvironmentsById [ nextEnv . ConfigurationId ] = currentEnv ;
}
else
logger . LogWarning ( "Failed to download the new enterprise configuration '{ConfigId}'. Skipping activation for now." , nextEnv . ConfigurationId ) ;
continue ;
}
effectiveEnvironmentsById [ nextEnv . ConfigurationId ] = nextEnv ;
}
2025-06-01 19:14:21 +00:00
}
2026-02-15 17:11:57 +00:00
2026-02-19 19:43:47 +00:00
// Retain configurations for all failed IDs. On cold start there might be no
// previous in-memory snapshot yet, so we also keep the current fetched entry
// to protect it from cleanup while the server is unreachable.
foreach ( var failedConfigId in failedConfigIds )
{
if ( effectiveEnvironmentsById . ContainsKey ( failedConfigId ) )
continue ;
if ( ! currentEnvironmentsById . TryGetValue ( failedConfigId , out var retainedEnvironment ) )
{
if ( ! activeFetchedEnvironmentsById . TryGetValue ( failedConfigId , out retainedEnvironment ) )
continue ;
logger . LogWarning ( "Could not refresh enterprise configuration '{ConfigId}'. Protecting it from cleanup until connectivity is restored." , failedConfigId ) ;
}
else
logger . LogWarning ( "Could not refresh enterprise configuration '{ConfigId}'. Keeping the previously active version." , failedConfigId ) ;
effectiveEnvironmentsById [ failedConfigId ] = retainedEnvironment ;
}
var effectiveEnvironments = effectiveEnvironmentsById . Values . ToList ( ) ;
// Cleanup is only allowed after a successful sync cycle:
if ( PluginFactory . IsInitialized & & ! shouldDeferStartupDownloads )
PluginFactory . RemoveUnreferencedManagedConfigurationPlugins ( effectiveEnvironmentsById . Keys . ToHashSet ( ) ) ;
if ( effectiveEnvironments . Count = = 0 )
logger . LogInformation ( "AI Studio runs without any enterprise configurations." ) ;
CURRENT_ENVIRONMENTS = effectiveEnvironments ;
HasValidEnterpriseSnapshot = true ;
2025-06-01 19:14:21 +00:00
}
catch ( Exception e )
{
logger . LogError ( e , "An error occurred while updating the enterprise environment." ) ;
}
}
}