diff --git a/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs b/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs index f1ff1c79ec..f0cd3908ce 100644 --- a/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs +++ b/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs @@ -3,6 +3,7 @@ namespace Agent.Plugins { + // Use PipelineArtifactContants.cs from ADO, once the latest libs are available. public class PipelineArtifactConstants { public const string AzurePipelinesAgent = "AzurePipelinesAgent"; @@ -18,5 +19,6 @@ public class PipelineArtifactConstants public const string FileShareArtifact = "filepath"; public const string CustomPropertiesPrefix = "user-"; public const string HashType = "HashType"; + public const string DomainId = "DomainId"; } } \ No newline at end of file diff --git a/src/Agent.Plugins/Artifact/PipelineArtifactProvider.cs b/src/Agent.Plugins/Artifact/PipelineArtifactProvider.cs index cfe0bc5741..e2fd7667b8 100644 --- a/src/Agent.Plugins/Artifact/PipelineArtifactProvider.cs +++ b/src/Agent.Plugins/Artifact/PipelineArtifactProvider.cs @@ -15,6 +15,7 @@ using Microsoft.VisualStudio.Services.WebApi; using Microsoft.VisualStudio.Services.Content.Common; using Microsoft.VisualStudio.Services.BlobStore.Common; +using Microsoft.VisualStudio.Services.BlobStore.Common.Telemetry; namespace Agent.Plugins { @@ -37,12 +38,19 @@ public async Task DownloadSingleArtifactAsync( CancellationToken cancellationToken, AgentTaskPluginExecutionContext context) { + // if properties doesn't have it, use the default domain for backward compatibility + IDomainId domainId = WellKnownDomainIds.DefaultDomainId; + if(buildArtifact.Resource.Properties.TryGetValue(PipelineArtifactConstants.DomainId, out string domainIdString)) + { + domainId = DomainIdFactory.Create(domainIdString); + } + var (dedupManifestClient, clientTelemetry) = await DedupManifestArtifactClientFactory.Instance.CreateDedupManifestClientAsync( this.context.IsSystemDebugTrue(), (str) => this.context.Output(str), this.connection, DedupManifestArtifactClientFactory.Instance.GetDedupStoreClientMaxParallelism(context), - WellKnownDomainIds.DefaultDomainId, + domainId, Microsoft.VisualStudio.Services.BlobStore.WebApi.Contracts.Client.PipelineArtifact, context, cancellationToken); @@ -85,49 +93,81 @@ public async Task DownloadMultipleArtifactsAsync( CancellationToken cancellationToken, AgentTaskPluginExecutionContext context) { - var (dedupManifestClient, clientTelemetry) = await DedupManifestArtifactClientFactory.Instance.CreateDedupManifestClientAsync( - this.context.IsSystemDebugTrue(), - (str) => this.context.Output(str), - this.connection, - DedupManifestArtifactClientFactory.Instance.GetDedupStoreClientMaxParallelism(context), - WellKnownDomainIds.DefaultDomainId, - Microsoft.VisualStudio.Services.BlobStore.WebApi.Contracts.Client.PipelineArtifact, - context, - cancellationToken); + // create clients and group artifacts for each domain: + Dictionary ArtifactDictionary)> dedupManifestClients = + new(); - using (clientTelemetry) - { - var artifactNameAndManifestIds = buildArtifacts.ToDictionary( - keySelector: (a) => a.Name, // keys should be unique, if not something is really wrong - elementSelector: (a) => DedupIdentifier.Create(a.Resource.Data)); - // 2) download to the target path - var options = DownloadDedupManifestArtifactOptions.CreateWithMultiManifestIds( - artifactNameAndManifestIds, - downloadParameters.TargetDirectory, - proxyUri: null, - minimatchPatterns: downloadParameters.MinimatchFilters, - minimatchFilterWithArtifactName: downloadParameters.MinimatchFilterWithArtifactName); + foreach(var buildArtifact in buildArtifacts) + { + // if properties doesn't have it, use the default domain for backward compatibility + IDomainId domainId = WellKnownDomainIds.DefaultDomainId; + if(buildArtifact.Resource.Properties.TryGetValue(PipelineArtifactConstants.DomainId, out string domainIdString)) + { + domainId = DomainIdFactory.Create(domainIdString); + } - PipelineArtifactActionRecord downloadRecord = clientTelemetry.CreateRecord((level, uri, type) => - new PipelineArtifactActionRecord(level, uri, type, nameof(DownloadMultipleArtifactsAsync), this.context)); - await clientTelemetry.MeasureActionAsync( - record: downloadRecord, - actionAsync: async () => + // Have we already created the clients for this domain? + if(dedupManifestClients.ContainsKey(domainId)) { + // Clients already created for this domain, Just add the artifact to the list: + dedupManifestClients[domainId].ArtifactDictionary.Add(buildArtifact.Name, DedupIdentifier.Create(buildArtifact.Resource.Data)); + } + else + { + // create the clients: + var (dedupManifestClient, clientTelemetry) = await DedupManifestArtifactClientFactory.Instance.CreateDedupManifestClientAsync( + this.context.IsSystemDebugTrue(), + (str) => this.context.Output(str), + this.connection, + DedupManifestArtifactClientFactory.Instance.GetDedupStoreClientMaxParallelism(context), + domainId, + Microsoft.VisualStudio.Services.BlobStore.WebApi.Contracts.Client.PipelineArtifact, + context, + cancellationToken); + + // and create the artifact dictionary with the current artifact + var artifactDictionary = new Dictionary { - await AsyncHttpRetryHelper.InvokeVoidAsync( - async () => - { - await dedupManifestClient.DownloadAsync(options, cancellationToken); - }, - maxRetries: 3, - tracer: tracer, - canRetryDelegate: e => true, - context: nameof(DownloadMultipleArtifactsAsync), - cancellationToken: cancellationToken, - continueOnCapturedContext: false); - }); - // Send results to CustomerIntelligence - this.context.PublishTelemetry(area: PipelineArtifactConstants.AzurePipelinesAgent, feature: PipelineArtifactConstants.PipelineArtifact, record: downloadRecord); + { buildArtifact.Name, DedupIdentifier.Create(buildArtifact.Resource.Data) } + }; + + dedupManifestClients.Add(domainId, (dedupManifestClient, clientTelemetry, artifactDictionary)); + } + } + + foreach(var clientInfo in dedupManifestClients.Values) + { + using (clientInfo.Telemetry) + { + // 2) download to the target path + var options = DownloadDedupManifestArtifactOptions.CreateWithMultiManifestIds( + clientInfo.ArtifactDictionary, + downloadParameters.TargetDirectory, + proxyUri: null, + minimatchPatterns: downloadParameters.MinimatchFilters, + minimatchFilterWithArtifactName: downloadParameters.MinimatchFilterWithArtifactName); + + PipelineArtifactActionRecord downloadRecord = clientInfo.Telemetry.CreateRecord((level, uri, type) => + new PipelineArtifactActionRecord(level, uri, type, nameof(DownloadMultipleArtifactsAsync), this.context)); + + await clientInfo.Telemetry.MeasureActionAsync( + record: downloadRecord, + actionAsync: async () => + { + await AsyncHttpRetryHelper.InvokeVoidAsync( + async () => + { + await clientInfo.Client.DownloadAsync(options, cancellationToken); + }, + maxRetries: 3, + tracer: tracer, + canRetryDelegate: e => true, + context: nameof(DownloadMultipleArtifactsAsync), + cancellationToken: cancellationToken, + continueOnCapturedContext: false); + }); + // Send results to CustomerIntelligence + this.context.PublishTelemetry(area: PipelineArtifactConstants.AzurePipelinesAgent, feature: PipelineArtifactConstants.PipelineArtifact, record: downloadRecord); + } } } } diff --git a/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs b/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs index 89c0e2e0ea..9183bec208 100644 --- a/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs +++ b/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs @@ -41,15 +41,26 @@ internal async Task UploadAsync( IDictionary properties, CancellationToken cancellationToken) { + // Get the client settings, if any. + var tracer = DedupManifestArtifactClientFactory.CreateArtifactsTracer(verbose: false, (str) => context.Output(str)); VssConnection connection = context.VssConnection; - var (dedupManifestClient, clientTelemetry) = await DedupManifestArtifactClientFactory.Instance - .CreateDedupManifestClientAsync( + var clientSettings = await DedupManifestArtifactClientFactory.GetClientSettingsAsync( + connection, + Microsoft.VisualStudio.Services.BlobStore.WebApi.Contracts.Client.PipelineArtifact, + tracer, + cancellationToken); + + // Get the default domain to use: + IDomainId domainId = DedupManifestArtifactClientFactory.GetDefaultDomainId(clientSettings, tracer); + + var (dedupManifestClient, clientTelemetry) = DedupManifestArtifactClientFactory.Instance + .CreateDedupManifestClient( context.IsSystemDebugTrue(), (str) => context.Output(str), connection, DedupManifestArtifactClientFactory.Instance.GetDedupStoreClientMaxParallelism(context), - WellKnownDomainIds.DefaultDomainId, - Microsoft.VisualStudio.Services.BlobStore.WebApi.Contracts.Client.PipelineArtifact, + domainId, + clientSettings, context, cancellationToken); @@ -84,7 +95,8 @@ internal async Task UploadAsync( { PipelineArtifactConstants.RootId, result.RootId.ValueString }, { PipelineArtifactConstants.ProofNodes, StringUtil.ConvertToJson(result.ProofNodes.ToArray()) }, { PipelineArtifactConstants.ArtifactSize, result.ContentSize.ToString() }, - { PipelineArtifactConstants.HashType, dedupManifestClient.HashType.Serialize() } + { PipelineArtifactConstants.HashType, dedupManifestClient.HashType.Serialize() }, + { PipelineArtifactConstants.DomainId, domainId.Serialize() } }; BuildArtifact buildArtifact = await AsyncHttpRetryHelper.InvokeAsync( @@ -140,22 +152,11 @@ internal async Task DownloadAsync( CancellationToken cancellationToken) { VssConnection connection = context.VssConnection; - var (dedupManifestClient, clientTelemetry) = await DedupManifestArtifactClientFactory.Instance - .CreateDedupManifestClientAsync( - context.IsSystemDebugTrue(), - (str) => context.Output(str), - connection, - DedupManifestArtifactClientFactory.Instance.GetDedupStoreClientMaxParallelism(context), - WellKnownDomainIds.DefaultDomainId, - Microsoft.VisualStudio.Services.BlobStore.WebApi.Contracts.Client.PipelineArtifact, - context, - cancellationToken); + PipelineArtifactProvider provider = new PipelineArtifactProvider(context, connection, tracer); BuildServer buildServer = new(connection); - using (clientTelemetry) // download all pipeline artifacts if artifact name is missing - { if (downloadOptions == DownloadOptions.MultiDownload) { List artifacts; @@ -187,40 +188,7 @@ internal async Task DownloadAsync( else { context.Output(StringUtil.Loc("DownloadingMultiplePipelineArtifacts", pipelineArtifacts.Count())); - - var artifactNameAndManifestIds = pipelineArtifacts.ToDictionary( - keySelector: (a) => a.Name, // keys should be unique, if not something is really wrong - elementSelector: (a) => DedupIdentifier.Create(a.Resource.Data)); - // 2) download to the target path - var options = DownloadDedupManifestArtifactOptions.CreateWithMultiManifestIds( - artifactNameAndManifestIds, - downloadParameters.TargetDirectory, - proxyUri: null, - minimatchPatterns: downloadParameters.MinimatchFilters, - minimatchFilterWithArtifactName: downloadParameters.MinimatchFilterWithArtifactName, - customMinimatchOptions: downloadParameters.CustomMinimatchOptions); - - PipelineArtifactActionRecord downloadRecord = clientTelemetry.CreateRecord((level, uri, type) => - new PipelineArtifactActionRecord(level, uri, type, nameof(DownloadAsync), context)); - await clientTelemetry.MeasureActionAsync( - record: downloadRecord, - actionAsync: async () => - { - await AsyncHttpRetryHelper.InvokeVoidAsync( - async () => - { - await dedupManifestClient.DownloadAsync(options, cancellationToken); - }, - maxRetries: 3, - tracer: tracer, - canRetryDelegate: e => true, - context: nameof(DownloadAsync), - cancellationToken: cancellationToken, - continueOnCapturedContext: false); - }); - - // Send results to CustomerIntelligence - context.PublishTelemetry(area: PipelineArtifactConstants.AzurePipelinesAgent, feature: PipelineArtifactConstants.PipelineArtifact, record: downloadRecord); + await provider.DownloadMultipleArtifactsAsync(downloadParameters,artifacts, cancellationToken, context); } } else if (downloadOptions == DownloadOptions.SingleDownload) @@ -246,42 +214,12 @@ await AsyncHttpRetryHelper.InvokeVoidAsync( { throw new InvalidOperationException($"Invalid {nameof(downloadParameters.ProjectRetrievalOptions)}!"); } - - var manifestId = DedupIdentifier.Create(buildArtifact.Resource.Data); - var options = DownloadDedupManifestArtifactOptions.CreateWithManifestId( - manifestId, - downloadParameters.TargetDirectory, - proxyUri: null, - minimatchPatterns: downloadParameters.MinimatchFilters, - customMinimatchOptions: downloadParameters.CustomMinimatchOptions); - - PipelineArtifactActionRecord downloadRecord = clientTelemetry.CreateRecord((level, uri, type) => - new PipelineArtifactActionRecord(level, uri, type, nameof(DownloadAsync), context)); - await clientTelemetry.MeasureActionAsync( - record: downloadRecord, - actionAsync: async () => - { - await AsyncHttpRetryHelper.InvokeVoidAsync( - async () => - { - await dedupManifestClient.DownloadAsync(options, cancellationToken); - }, - maxRetries: 3, - tracer: tracer, - canRetryDelegate: e => true, - context: nameof(DownloadAsync), - cancellationToken: cancellationToken, - continueOnCapturedContext: false); - }); - - // Send results to CustomerIntelligence - context.PublishTelemetry(area: PipelineArtifactConstants.AzurePipelinesAgent, feature: PipelineArtifactConstants.PipelineArtifact, record: downloadRecord); + await provider.DownloadSingleArtifactAsync(downloadParameters, buildArtifact, cancellationToken, context); } else { throw new InvalidOperationException($"Invalid {nameof(downloadOptions)}!"); } - } } // Download for version 2. This decision was made because version 1 is sealed and we didn't want to break any existing customers. diff --git a/src/Microsoft.VisualStudio.Services.Agent/Blob/DedupManifestArtifactClientFactory.cs b/src/Microsoft.VisualStudio.Services.Agent/Blob/DedupManifestArtifactClientFactory.cs index a5acd170f5..cf7cfde0c4 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/Blob/DedupManifestArtifactClientFactory.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/Blob/DedupManifestArtifactClientFactory.cs @@ -31,6 +31,19 @@ public interface IDedupManifestArtifactClientFactory /// use the system default. /// Cancellation token used for both creating clients and verifying client conneciton. /// Tuple of the client and the telemtery client + (DedupManifestArtifactClient client, BlobStoreClientTelemetry telemetry) CreateDedupManifestClient( + bool verbose, + Action traceOutput, + VssConnection connection, + int maxParallelism, + IDomainId domainId, + ClientSettingsInfo clientSettings, + AgentTaskPluginExecutionContext context, + CancellationToken cancellationToken); + + /// + /// Creates a DedupManifestArtifactClient client and retrieves any client settings from the server + /// Task<(DedupManifestArtifactClient client, BlobStoreClientTelemetry telemetry)> CreateDedupManifestClientAsync( bool verbose, Action traceOutput, @@ -68,6 +81,9 @@ public interface IDedupManifestArtifactClientFactory public class DedupManifestArtifactClientFactory : IDedupManifestArtifactClientFactory { + // NOTE: this should be set to ClientSettingsConstants.DefaultDomainId when the latest update from Azure Devops is added. + private static string DefaultDomainIdKey = "DefaultDomainId"; + // Old default for hosted agents was 16*2 cores = 32. // In my tests of a node_modules folder, this 32x parallelism was consistently around 47 seconds. // At 192x it was around 16 seconds and 256x was no faster. @@ -81,6 +97,9 @@ private DedupManifestArtifactClientFactory() { } + /// + /// Creates a DedupManifestArtifactClient client and retrieves any client settings from the server + /// public async Task<(DedupManifestArtifactClient client, BlobStoreClientTelemetry telemetry)> CreateDedupManifestClientAsync( bool verbose, Action traceOutput, @@ -90,6 +109,33 @@ private DedupManifestArtifactClientFactory() BlobStore.WebApi.Contracts.Client client, AgentTaskPluginExecutionContext context, CancellationToken cancellationToken) + { + var clientSettings = await GetClientSettingsAsync( + connection, + client, + CreateArtifactsTracer(verbose, traceOutput), + cancellationToken); + + return CreateDedupManifestClient( + context.IsSystemDebugTrue(), + (str) => context.Output(str), + connection, + DedupManifestArtifactClientFactory.Instance.GetDedupStoreClientMaxParallelism(context), + domainId, + clientSettings, + context, + cancellationToken); + } + + public (DedupManifestArtifactClient client, BlobStoreClientTelemetry telemetry) CreateDedupManifestClient( + bool verbose, + Action traceOutput, + VssConnection connection, + int maxParallelism, + IDomainId domainId, + ClientSettingsInfo clientSettings, + AgentTaskPluginExecutionContext context, + CancellationToken cancellationToken) { const int maxRetries = 5; var tracer = CreateArtifactsTracer(verbose, traceOutput); @@ -97,16 +143,23 @@ private DedupManifestArtifactClientFactory() { maxParallelism = DefaultDedupStoreClientMaxParallelism; } + traceOutput($"Max dedup parallelism: {maxParallelism}"); + traceOutput($"DomainId: {domainId}"); - IDedupStoreHttpClient dedupStoreHttpClient = await AsyncHttpRetryHelper.InvokeAsync( - async () => + ArtifactHttpClientFactory factory = new ArtifactHttpClientFactory( + connection.Credentials, + connection.Settings.SendTimeout, + tracer, + cancellationToken); + + var helper = new HttpRetryHelper(maxRetries,e => true); + + IDedupStoreHttpClient dedupStoreHttpClient = helper.Invoke( + () => { - ArtifactHttpClientFactory factory = new ArtifactHttpClientFactory( - connection.Credentials, - connection.Settings.SendTimeout, - tracer, - cancellationToken); + // since our call below is hidden, check if we are cancelled and throw if we are... + cancellationToken.ThrowIfCancellationRequested(); IDedupStoreHttpClient dedupHttpclient; // this is actually a hidden network call to the location service: @@ -120,24 +173,17 @@ private DedupManifestArtifactClientFactory() dedupHttpclient = new DomainHttpClientWrapper(domainId, domainClient); } - this.HashType ??= await GetClientHashTypeAsync(factory, connection, client, tracer, context, cancellationToken); - return dedupHttpclient; - }, - maxRetries: maxRetries, - tracer: tracer, - canRetryDelegate: e => true, - context: nameof(CreateDedupManifestClientAsync), - cancellationToken: cancellationToken, - continueOnCapturedContext: false); + }); var telemetry = new BlobStoreClientTelemetry(tracer, dedupStoreHttpClient.BaseAddress); - traceOutput($"Hashtype: {this.HashType.Value}"); + this.HashType = GetClientHashType(clientSettings, context, tracer); if (this.HashType == BuildXL.Cache.ContentStore.Hashing.HashType.Dedup1024K) { dedupStoreHttpClient.RecommendedChunkCountPerCall = 10; // This is to workaround IIS limit - https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/requestlimits/ } + traceOutput($"Hashtype: {this.HashType.Value}"); var dedupClient = new DedupStoreClientWithDataport(dedupStoreHttpClient, new DedupStoreClientContext(maxParallelism), this.HashType.Value); return (new DedupManifestArtifactClient(telemetry, dedupClient, tracer), telemetry); @@ -239,35 +285,75 @@ public static IAppTraceSource CreateArtifactsTracer(bool verbose, Action includeSeverityLevel: verbose); } - private static async Task GetClientHashTypeAsync( - ArtifactHttpClientFactory factory, + /// + /// Get the client settings for the given client. + /// + /// This should only be called once per client type. This is intended to fail fast so it has no retries. + public static async Task GetClientSettingsAsync( VssConnection connection, BlobStore.WebApi.Contracts.Client client, IAppTraceSource tracer, - AgentTaskPluginExecutionContext context, CancellationToken cancellationToken) + { + try + { + ArtifactHttpClientFactory factory = new( + connection.Credentials, + connection.Settings.SendTimeout, + tracer, + cancellationToken); + + var blobUri = connection.GetClient().BaseAddress; + var clientSettingsHttpClient = factory.CreateVssHttpClient(blobUri); + return await clientSettingsHttpClient.GetSettingsAsync(client, userState: null, cancellationToken); + } + catch (Exception exception) + { + // Use info cause we don't want to fail builds with warnings as errors... + tracer.Info($"Error while retrieving client Settings for {client}. Exception: {exception}. Falling back to defaults."); + } + return null; + } + + public static IDomainId GetDefaultDomainId(ClientSettingsInfo clientSettings, IAppTraceSource tracer) + { + IDomainId domainId = WellKnownDomainIds.DefaultDomainId; + if (clientSettings != null && clientSettings.Properties.ContainsKey(DefaultDomainIdKey)) + { + try + { + domainId = DomainIdFactory.Create(clientSettings.Properties[DefaultDomainIdKey]); + } + catch (Exception exception) + { + tracer.Info($"Error converting the domain id '{clientSettings.Properties[DefaultDomainIdKey]}': {exception.Message}. Falling back to default."); + } + } + + return domainId; + } + + private static HashType GetClientHashType(ClientSettingsInfo clientSettings, AgentTaskPluginExecutionContext context, IAppTraceSource tracer) { HashType hashType = ChunkerHelper.DefaultChunkHashType; + // Note: 9/6/2023 Remove the below check in couple of months. if (AgentKnobs.AgentEnablePipelineArtifactLargeChunkSize.GetValue(context).AsBoolean()) { - try + if (clientSettings != null && clientSettings.Properties.ContainsKey(ClientSettingsConstants.ChunkSize)) { - var blobUri = connection.GetClient().BaseAddress; - var clientSettingsHttpClient = factory.CreateVssHttpClient(blobUri); - ClientSettingsInfo clientSettings = await clientSettingsHttpClient.GetSettingsAsync(client, cancellationToken); - if (clientSettings != null && clientSettings.Properties.ContainsKey(ClientSettingsConstants.ChunkSize)) + try { HashTypeExtensions.Deserialize(clientSettings.Properties[ClientSettingsConstants.ChunkSize], out hashType); } - } - catch (Exception exception) - { - tracer.Warn($"Error while retrieving hash type for {client}. Exception: {exception}"); + catch (Exception exception) + { + tracer.Info($"Error converting the chunk size '{clientSettings.Properties[ClientSettingsConstants.ChunkSize]}': {exception.Message}. Falling back to default."); + } } } return ChunkerHelper.IsHashTypeChunk(hashType) ? hashType : ChunkerHelper.DefaultChunkHashType; } - } + } } \ No newline at end of file diff --git a/src/Test/L0/Plugin/TestFileShareProvider/MockDedupManifestArtifactClientFactory.cs b/src/Test/L0/Plugin/TestFileShareProvider/MockDedupManifestArtifactClientFactory.cs index 5f3eed4f70..e00124ed09 100644 --- a/src/Test/L0/Plugin/TestFileShareProvider/MockDedupManifestArtifactClientFactory.cs +++ b/src/Test/L0/Plugin/TestFileShareProvider/MockDedupManifestArtifactClientFactory.cs @@ -7,6 +7,7 @@ using Agent.Sdk; using Microsoft.VisualStudio.Services.Agent.Blob; using Microsoft.VisualStudio.Services.BlobStore.WebApi; +using Microsoft.VisualStudio.Services.BlobStore.WebApi.Contracts; using Microsoft.VisualStudio.Services.Content.Common.Tracing; using Microsoft.VisualStudio.Services.WebApi; using Microsoft.VisualStudio.Services.BlobStore.Common.Telemetry; @@ -36,6 +37,22 @@ public class MockDedupManifestArtifactClientFactory : IDedupManifestArtifactClie telemetrySender))); } + public (DedupManifestArtifactClient client, BlobStoreClientTelemetry telemetry) CreateDedupManifestClient( + bool verbose, + Action traceOutput, + VssConnection connection, + int maxParallelism, + IDomainId domainId, + ClientSettingsInfo clientSettings, + AgentTaskPluginExecutionContext context, + CancellationToken cancellationToken) + { + telemetrySender = new TestTelemetrySender(); + return (client: (DedupManifestArtifactClient)null, telemetry: new BlobStoreClientTelemetry( + NoopAppTraceSource.Instance, + baseAddress, + telemetrySender)); + } public Task<(DedupStoreClient client, BlobStoreClientTelemetryTfs telemetry)> CreateDedupClientAsync(bool verbose, Action traceOutput, VssConnection connection, int maxParallelism, CancellationToken cancellationToken) {