diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/LoggingTests.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/LoggingTests.cs index 82abd5b4de3..c42ab33783a 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/LoggingTests.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/LoggingTests.cs @@ -52,7 +52,7 @@ public async Task PlaybackLogsSanitizedRequest() HttpResponse response = new DefaultHttpContext().Response; await testRecordingHandler.HandlePlaybackRequest(recordingId, request, response); - AssertLogs(logger, 4, 8, 12); + AssertLogs(logger, 5, 9, 12); } finally { diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/RecordSessionTests.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/RecordSessionTests.cs index 4e5a8ee765b..c3562c89370 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/RecordSessionTests.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/RecordSessionTests.cs @@ -402,6 +402,7 @@ public void RecordMatcherThrowsExceptionsWithDetails() TestRecordingMismatchException exception = Assert.Throws(() => matcher.FindMatch(requestEntry, entries)); Assert.Equal( "Unable to find a record for the request HEAD http://localhost/" + Environment.NewLine + + "Remaining entry: http://remote-host" + Environment.NewLine + "Method doesn't match, request record " + Environment.NewLine + "Uri doesn't match:" + Environment.NewLine + " request " + Environment.NewLine + diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs index b320e79bf73..aa8bc59dbe0 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using Azure.Core; @@ -127,7 +127,7 @@ public virtual RecordEntry FindMatch(RecordEntry request, IList ent } } - throw new TestRecordingMismatchException(GenerateException(request, bestScoreEntry)); + throw new TestRecordingMismatchException(GenerateException(request, bestScoreEntry, entries)); } public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, StringBuilder descriptionBuilder = null) @@ -213,11 +213,19 @@ private string NormalizeUri(string uriToNormalize) return req.ToUri().ToString(); } - private string GenerateException(RecordEntry request, RecordEntry bestScoreEntry) + private string GenerateException(RecordEntry request, RecordEntry bestScoreEntry, IList entries = null) { StringBuilder builder = new StringBuilder(); builder.AppendLine($"Unable to find a record for the request {request.RequestMethod} {request.RequestUri}"); + if (entries != null) + { + foreach (var entry in entries) + { + builder.AppendLine($"Remaining entry: {entry.RequestUri}"); + } + } + if (bestScoreEntry == null) { builder.AppendLine("No records to match."); diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordSession.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordSession.cs index 8cd39ca831b..d8ad2a76a4d 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordSession.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordSession.cs @@ -82,7 +82,7 @@ public void Record(RecordEntry entry) } } - public RecordEntry Lookup(RecordEntry requestEntry, RecordMatcher matcher, IEnumerable sanitizers, bool remove = true) + public RecordEntry Lookup(RecordEntry requestEntry, RecordMatcher matcher, IEnumerable sanitizers, bool remove = true, string sessionId = null) { foreach (RecordedTestSanitizer sanitizer in sanitizers) { @@ -91,24 +91,19 @@ public RecordEntry Lookup(RecordEntry requestEntry, RecordMatcher matcher, IEnum // normalize request body with STJ using relaxed escaping to match behavior when Deserializing from session files RecordEntry.NormalizeJsonBody(requestEntry.Request); - lock (Entries) + RecordEntry entry = matcher.FindMatch(requestEntry, Entries); + if (remove) { - RecordEntry entry = matcher.FindMatch(requestEntry, Entries); - if (remove) - { - Entries.Remove(entry); - } - - return entry; + Entries.Remove(entry); + DebugLogger.LogInformation($"We successfully matched and popped request URI {entry.RequestUri} for recordingId {sessionId}"); } + + return entry; } public void Remove(RecordEntry entry) { - lock (Entries) - { - Entries.Remove(entry); - } + Entries.Remove(entry); } public void Sanitize(RecordedTestSanitizer sanitizer) diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/RecordingHandler.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/RecordingHandler.cs index c0d892ef6f1..962b623fd8a 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/RecordingHandler.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/RecordingHandler.cs @@ -394,6 +394,7 @@ public async Task StartPlaybackAsync(string sessionId, HttpResponse outgoingResp { Path = path }; + DebugLogger.LogInformation($"Recording file {assetsPath} is associated with recording id {id}"); } if (!PlaybackSessions.TryAdd(id, session)) @@ -410,6 +411,7 @@ public async Task StartPlaybackAsync(string sessionId, HttpResponse outgoingResp // Write to the response await outgoingResponse.WriteAsync(json); + DebugLogger.LogTrace($"PLAYBACK START END {id}."); } @@ -470,50 +472,53 @@ public async Task HandlePlaybackRequest(string recordingId, HttpRequest incoming var entry = (await CreateEntryAsync(incomingRequest).ConfigureAwait(false)).Item1; - // Session may be removed later, but only after response has been fully written - var match = session.Session.Lookup(entry, session.CustomMatcher ?? Matcher, sanitizers, remove: false); - - foreach (ResponseTransform transform in Transforms.Concat(session.AdditionalTransforms)) + lock (session.Session.Entries) { - transform.Transform(incomingRequest, match); - } + // Session may be removed later, but only after response has been fully written + var match = session.Session.Lookup(entry, session.CustomMatcher ?? Matcher, sanitizers, remove: false, sessionId: recordingId); - outgoingResponse.StatusCode = match.StatusCode; + foreach (ResponseTransform transform in Transforms.Concat(session.AdditionalTransforms)) + { + transform.Transform(incomingRequest, match); + } - foreach (var header in match.Response.Headers) - { - outgoingResponse.Headers.Add(header.Key, header.Value.ToArray()); - } + outgoingResponse.StatusCode = match.StatusCode; - outgoingResponse.Headers.Remove("Transfer-Encoding"); + foreach (var header in match.Response.Headers) + { + outgoingResponse.Headers.Add(header.Key, header.Value.ToArray()); + } - if (match.Response.Body?.Length > 0) - { - var bodyData = CompressionUtilities.CompressBody(match.Response.Body, match.Response.Headers); + outgoingResponse.Headers.Remove("Transfer-Encoding"); - if (match.Response.Headers.ContainsKey("Content-Length")) + if (match.Response.Body?.Length > 0) { - outgoingResponse.ContentLength = bodyData.Length; - } + var bodyData = CompressionUtilities.CompressBody(match.Response.Body, match.Response.Headers); - await WriteBodyBytes(bodyData, session.PlaybackResponseTime, outgoingResponse); - } + if (match.Response.Headers.ContainsKey("Content-Length")) + { + outgoingResponse.ContentLength = bodyData.Length; + } - Interlocked.Increment(ref Startup.RequestsPlayedBack); + WriteBodyBytes(bodyData, session.PlaybackResponseTime, outgoingResponse); + } - // Only remove session once body has been written, to minimize probability client retries but test-proxy has already removed the session - var remove = true; + Interlocked.Increment(ref Startup.RequestsPlayedBack); - // If request contains "x-recording-remove: false", then request is not removed from session after playback. - // Used by perf tests to play back the same request multiple times. - if (incomingRequest.Headers.TryGetValue("x-recording-remove", out var removeHeader)) - { - remove = bool.Parse(removeHeader); - } + // Only remove session once body has been written, to minimize probability client retries but test-proxy has already removed the session + var remove = true; - if (remove) - { - session.Session.Remove(match); + // If request contains "x-recording-remove: false", then request is not removed from session after playback. + // Used by perf tests to play back the same request multiple times. + if (incomingRequest.Headers.TryGetValue("x-recording-remove", out var removeHeader)) + { + remove = bool.Parse(removeHeader); + } + + if (remove) + { + session.Session.Remove(match); + } } } @@ -543,7 +548,35 @@ public byte[][] GetBatches(byte[] bodyData, int batchCount) return batches; } - public async Task WriteBodyBytes(byte[] bodyData, int playbackResponseTime, HttpResponse outgoingResponse) + public void WriteBodyBytes(byte[] bodyData, int playbackResponseTime, HttpResponse outgoingResponse) + { + if (playbackResponseTime > 0) + { + int batchCount = 10; + int sleepLength = playbackResponseTime / batchCount; + + byte[][] chunks = GetBatches(bodyData, batchCount); + + for (int i = 0; i < chunks.Length; i++) + { + var chunk = chunks[i]; + + outgoingResponse.Body.Write(chunk, 0, chunk.Length); + + if (i != chunks.Length - 1) + { + Thread.Sleep(sleepLength); + } + } + + } + else + { + outgoingResponse.Body.Write(bodyData, 0, bodyData.Length); + } + } + + public async Task WriteBodyBytesAsync(byte[] bodyData, int playbackResponseTime, HttpResponse outgoingResponse) { if (playbackResponseTime > 0) { diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Startup.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Startup.cs index 6e021b99187..e4f1a073273 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Startup.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Startup.cs @@ -207,6 +207,11 @@ public void ConfigureServices(IServiceCollection services) }); }); + services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + services.AddControllers(options => { options.InputFormatters.Add(new EmptyBodyFormatter());