// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Agents.AI.Purview.Models.Common; using Microsoft.Agents.AI.Purview.Models.Jobs; using Microsoft.Agents.AI.Purview.Models.Requests; using Microsoft.Agents.AI.Purview.Models.Responses; using Microsoft.Extensions.Logging; namespace Microsoft.Agents.AI.Purview; /// /// Service that runs jobs in background threads. /// internal sealed class BackgroundJobRunner : IBackgroundJobRunner { private readonly IChannelHandler _channelHandler; private readonly IPurviewClient _purviewClient; private readonly ICacheProvider _cacheProvider; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The channel handler used to manage job channels. /// The Purview client used to send requests to Purview. /// The cache provider used to store protection scopes results. /// The logger used to log information about background jobs. /// The settings used to configure Purview client behavior. public BackgroundJobRunner(IChannelHandler channelHandler, IPurviewClient purviewClient, ICacheProvider cacheProvider, ILogger logger, PurviewSettings purviewSettings) { this._channelHandler = channelHandler; this._purviewClient = purviewClient; this._cacheProvider = cacheProvider; this._logger = logger; for (int i = 0; i < purviewSettings.MaxConcurrentJobConsumers; i++) { this._channelHandler.AddRunner(async (Channel channel) => { await foreach (BackgroundJobBase job in channel.Reader.ReadAllAsync().ConfigureAwait(false)) { try { await this.RunJobAsync(job).ConfigureAwait(false); } catch (Exception e) when (e is not OperationCanceledException and not SystemException) { if (this._logger.IsEnabled(LogLevel.Error)) { this._logger.LogError(e, "Error running background job {BackgroundJobError}.", e.Message); } } } }); } } /// /// Runs a job. /// /// The job to run. /// A task representing the job. private async Task RunJobAsync(BackgroundJobBase job) { switch (job) { case ProcessContentJob processContentJob: _ = await this._purviewClient.ProcessContentAsync(processContentJob.Request, CancellationToken.None).ConfigureAwait(false); break; case ContentActivityJob contentActivityJob: _ = await this._purviewClient.SendContentActivitiesAsync(contentActivityJob.Request, CancellationToken.None).ConfigureAwait(false); break; case ScopeRetrievalJob scopeRetrievalJob: try { ProtectionScopesResponse response = await this._purviewClient.GetProtectionScopesAsync(scopeRetrievalJob.Request, CancellationToken.None).ConfigureAwait(false); await this._cacheProvider.SetAsync(scopeRetrievalJob.CacheKey, response, CancellationToken.None).ConfigureAwait(false); (bool shouldProcess, List _, ExecutionMode _) = ScopedContentProcessor.CheckApplicableScopes(scopeRetrievalJob.ProcessContentRequest, response); if (!shouldProcess) { ProcessContentRequest pcRequest = scopeRetrievalJob.ProcessContentRequest; ContentActivitiesRequest caRequest = new(pcRequest.UserId, pcRequest.TenantId, pcRequest.ContentToProcess, pcRequest.CorrelationId); this._channelHandler.QueueJob(new ContentActivityJob(caRequest)); } } catch (PurviewPaymentRequiredException ex) { await this._cacheProvider.SetAsync( new PaymentRequiredCacheKey(scopeRetrievalJob.Request.TenantId), new PaymentRequiredCacheEntry(ex.Message), CancellationToken.None).ConfigureAwait(false); } break; } } /// /// Shutdown the job runners. /// public async Task ShutdownAsync() { await this._channelHandler.StopAndWaitForCompletionAsync().ConfigureAwait(false); } }