Reputation: 6363
Given the following method signature in a WCF service:
public string Query(string request)
{
using (Log.BeginTimedOperation("Persist request"))
{
var messageCorrelationId = Guid.NewGuid().ToString();
var payloadURI = PayloadHelper.GetFullPath(messageCorrelationId);
PayloadHelper.PersistPayloadWithPath(request, payloadURI);
Log.Information("My service request {MessageCorrelationId} {RequestPayloadPath}", messageCorrelationId, payloadURI);
}
// DoWork here, code removed for brevity
return result;
}
and the corresponding extension methods:
public static string GetFullPath(string messageCorrelationId)
{
var folderDate = DateTime.Now.ToString("yyyyMMdd");
var folderHour = DateTime.Now.ToString("HH");
var logFolder = Path.Combine(ConfigurationManager.AppSettings["NetworkFiler"], "Payloads", folderDate, folderHour);
Directory.CreateDirectory(logFolder);
var fileName = $"{messageCorrelationId}-{"MyWCFService"}-{$"{DateTime.Now:yyyy-MM-dd-HH-mm-ss-fff}-{Guid.NewGuid():N}"}.{"xml"}";
return Path.Combine(logFolder, fileName);
}
public static void PersistPayloadWithPath(string text, string path)
{
var task = WriteFileAsync(path, text);
task.GetAwaiter().GetResult();
}
private static async Task WriteFileAsync(string path, string text)
{
try
{
byte[] buffer = Encoding.Unicode.GetBytes(text);
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
{
await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "WriteFileAsync");
}
}
This code will block, if for instance the file is being interrogated by anti-virus (guess) or IO slowness to the filer.
So here is the great debate, calling a asynchronous method from a synchronous method in ASP.NET. To this day I still don't know if there is a reliable way to create a fire and forget mechanism. It's not that I don't care about the failure, I do, that 'should be' handled by the catch statement and the static Serilog instance.
While writing this version of the post, it dawned on me that maybe one of the problems might be in fact the logger and it's async wrapper around the File Sink.. will test that in a few.
Any help is appreciated with this bugger.
Thank you, Stephen
UPDATE-ASYNC
public async Task<string> QueryAsync(string request)
{
using (Log.BeginTimedOperation("PersistAsync-Request"))
{
var messageCorrelationId = Guid.NewGuid().ToString();
var payloadURI = PayloadHelper.GetFullPath(messageCorrelationId);
await PayloadHelper.PersistPayloadWithPathAsync(request, payloadURI).ConfigureAwait(false);
Log.Information("My service request {MessageCorrelationId} {RequestPayloadPath}", messageCorrelationId, payloadURI);
}
// DoWork here, code removed for brevity
return result;
}
public static string GetFullPath(string messageCorrelationId)
{
var folderDate = DateTime.Now.ToString("yyyyMMdd");
var folderHour = DateTime.Now.ToString("HH");
var logFolder = Path.Combine(ConfigurationManager.AppSettings["NetworkFiler"], "Payloads", folderDate, folderHour);
Directory.CreateDirectory(logFolder);
var fileName = $"{messageCorrelationId}-MyWCFService-{DateTime.Now:yyyy-MM-dd-HH-mm-ss-fff}-{Guid.NewGuid():N}.xml";
return Path.Combine(logFolder, fileName);
}
public static async Task PersistPayloadWithPathAsync(string text, string path)
{
await WriteFileAsync(path, text).ConfigureAwait(false);
}
private static async Task WriteFileAsync(string path, string text)
{
try
{
byte[] buffer = Encoding.Unicode.GetBytes(text);
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
{
await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
}
}
catch
{
// ignored
}
}
Still blocking randomly, lick every 20-30 requests
Upvotes: 0
Views: 922
Reputation: 6363
The root caused was determined to be an incorrectly configured StealthAudit, a tool that we use to audit our filers. Once the configuration was adjusted everything works as expected.
Upvotes: 0
Reputation: 40898
So it's blocking on the new FileStream()
. Looking at the source code, the constructor calls a method called Init()
, which actually ends up opening the file. So it is doing I/O in the constructor, which really it shouldn't be since you can't await
it.
Setting useAsync
should make it run async, if it can. But maybe it can't open the file from a network drive asynchronously.
So your best bet is to just run it inside Task.Run()
to guarantee it doesn't block.
The following only applies to .NET Core (unfortunately):
You would be better off using File.WriteAllTextAsync
, which would actually make your life easier:
await File.WriteAllTextAsync(path, text);
The documentation of that doesn't really explain anything for some reason, but it works the same as File.WriteAllText
(it's just async):
Creates a new file, write the contents to the file, and then closes the file. If the target file already exists, it is overwritten.
So, exactly what you're doing in your code, but this way you can await the whole operation (including opening the file).
Upvotes: 2
Reputation: 14306
The problem is your WriteFileAsync
method. It actually runs synchronously until it hits the first await
(that's how async/await works). I believe it hangs at new FileStream(...)
.
If you want to just fire-end-forget form synchronous code, this should be enough:
public static void PersistPayloadWithPath(string text, string path)
{
Task.Run(async () => await WriteFileAsync(path, text));
}
The code above should help you when you don't have async alternatives. However as Gabriel Luci suggested in his answer you should go with await File.WriteAllTextAsync(path, text);
as it's probably optimized for async work.
You can still use Task.Run(...);
with await File.WriteAllTextAsync(path, text);
in fire-and-forget scenario.
Just beware that exceptions from inside the task (WriteFileAsync
) won't propagate to the calling thread. This won't be the problem in your case since the whole WriteFileAsync
method's body is inside try-catch block, where you log the exception.
EDIT
To illustrate how the threads behave in async/await methods, play around with the following example (try all 3 ways the Bar
function is ran):
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Main thread: {Thread.CurrentThread.ManagedThreadId}");
Task.Run(async () => await Bar());
// Task.Run(() => Bar());
// Bar();
Console.ReadLine();
}
static async Task Bar()
{
Console.WriteLine($"Bar thread before await: {Thread.CurrentThread.ManagedThreadId}");
await Foo();
Console.WriteLine($"Bar thread after await: {Thread.CurrentThread.ManagedThreadId}");
}
static async Task Foo()
{
Console.WriteLine($"Foo thread before await: {Thread.CurrentThread.ManagedThreadId}");
var c = new WebClient();
var source = await c.DownloadStringTaskAsync("http://google.com");
Console.WriteLine($"Foo thread after await: {Thread.CurrentThread.ManagedThreadId}");
}
}
}
Upvotes: 1
Reputation: 130
I think it waits becuse you use
task.GetAwaiter().GetResult();
If you don't need the result, just remove this line. It should work fine.
If you need the result you should make PersistPayloadWithPath
function also async.
Upvotes: 1