1

I believe I have a race condition below. I am manually constructing an HttpResponseMessage with JSON output to stream asynchronously. The issue seems to be with the counter (i). I want to add a comma before any element after the first write from the list.

At the beginning of the list, sometimes the the first couple of records following the first write(i've seen up to 3) will not have the preceeding comma. The number is inconsistent and sometimes works as expected. I did not see it on my local machine, but in deployed environments with beefier hardware it's present.

var LastUpdate = JsonConvert.SerializeObject(dt);
var pre = $"{{ \"LastUpdate\": {LastUpdate}, \"List\":[";
var post = "]}";

HttpResponseMessage response = Request.CreateResponse();
response.Content = new PushStreamContent(
    async (stream, http, context) =>
    {
        try
        {
            int i = 0;
            var buffer = Encoding.UTF8.GetBytes(pre);
            await stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);

            var query = getQuery(id);                               
            await query
                .ForEachAsync(async entity =>
                {
                    var student = MapRecord(entity);
                    if (student != null)
                    {
                        var json = JsonConvert.SerializeObject(student);
                        buffer = Encoding.UTF8.GetBytes(((i > 0) ? ", " : "") + json);
                        await stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
                        i++;
                    }
                }, cancellationToken).ConfigureAwait(false);

            buffer = Encoding.UTF8.GetBytes(post);
            await stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
        }
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
ajberry
  • 1,027
  • 2
  • 9
  • 8

2 Answers2

4

If you're using QueryableExtentions.ForEachAsync (thanks @juharr), then yes, you have a race condition.

The signature for the method is:

public static Task ForEachAsync<T>(
    this IQueryable<T> source,
    Action<T> action
)

Notice that the method accepts a Action<T>. In the async world, this is an equivalent for async void. This means that each time you await inside the async delegate, the ForEachAsync iterator is actually continuing to the next element, not waiting for your delegate to complete.

Instead (if the query isn't invoked on a very large dataset), use a regular foreach statement and await inside it:

foreach (var entity in query)
{
    var student = MapRecord(entity);
    if (student != null)
    {
        var json = JsonConvert.SerializeObject(student);
        buffer = Encoding.UTF8.GetBytes(((i > 0) ? ", " : "") + json);
        await stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken)
                    .ConfigureAwait(false);
        i++;
    }
}
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • using foreachasync to prevent blocking at the db level (this query returns ~ 1 million rows), any other suggestions on synchronizing the counter? – ajberry Mar 17 '16 at 13:05
  • If it's 1 million rows, `foreach` may not be the best idea. Is there any reason you're actuall manually doing the serialization? If that is unavoidable, I would recommand simply using a synchronous delegate and using `stream.Write` instead of `stream.WriteAsync`. Or even better, create a pull request for EF and add an option for invoking asynchronous delegates inside `ForEachAsync` :) – Yuval Itzchakov Mar 17 '16 at 13:07
  • bringing the entire result set in, mapping and serializing hits out of memory exceptions. The manual construction is not ideal, but does allow to stream small bits at a time. Their is very little resource footprint with this method. – ajberry Mar 17 '16 at 13:10
  • Perhaps a smarter idea would be to use `JsonTextWriter`. See [this](http://stackoverflow.com/questions/8157636/can-json-net-serialize-deserialize-to-from-a-stream) question. – Yuval Itzchakov Mar 17 '16 at 13:13
0

If your concern is primarily with the (i) counter, you can use the Interlocked.Increment to thread safely increment i. This will cause a small amount of performance contention for synchronization, but you can consistently update i this way.

Example:

Interlocked.Increment(ref i);

Interlocked.Increment MSDN

davidallyoung
  • 1,302
  • 11
  • 15