Search code examples
asp.netperformanceasp.net-mvc-5httpwebrequest

Handle large number of PUT requests to a rest api


I have been trying to find a way to make this task more efficient. I am consuming a REST based web service and need to update information for over 2500 clients.

I am using fiddler to watch the requests, and I'm also updating a table with an update time when its complete. I'm getting about 1 response per second. Are my expectations to high? I'm not even sure what I would define as 'fast' in this context.

I am handling everything in my controller and have tried running multiple web requests in parallel based on examples around the place but it doesn't seem to make a difference. To be honest I don't understand it well enough and was just trying to get it to build. I suspect it is still waiting for each request to complete before firing again.

I have also increased connections in my web config file as per another suggestion with no success:

 <system.net>
    <connectionManagement>
      <add address="*" maxconnection="20" />
    </connectionManagement>
  </system.net>

My Controllers action method looks like this:

public async Task<ActionResult> UpdateMattersAsync()
        {
           //Only get  matters we haven't synced yet
            List<MatterClientRepair> repairList = Data.Get.AllUnsyncedMatterClientRepairs(true);
        //Take the next 500
        List<MatterClientRepair> subRepairList = repairList.Take(500).ToList();          

        FinalisedMatterViewModel vm = new FinalisedMatterViewModel();


        using (ApplicationDbContext db = new ApplicationDbContext())
        {
            int jobCount = 0;
            foreach (var job in subRepairList)
            {
                // If not yet synced - it shouldn't ever be!!
                if (!job.Synced)
                {
                    jobCount++;

                   // set up some Authentication fields
                    var oauth = new OAuth.Manager();
                    oauth["access_token"] = Session["AccessToken"].ToString();
                    string uri = "https://app.com/api/v2/matters/" + job.Matter;

                    // prepare the json object for the body
                    MatterClientJob jsonBody = new MatterClientJob();
                    jsonBody.matter = new MatterForUpload();
                    jsonBody.matter.client_id = job.NewClient;
                    string jsonString = jsonBody.ToJSON();                        

                   // Send it off. It returns the whole object we updated - we don't actually do anything with it
                    Matter result = await oauth.Update<Matter>(uri, oauth["access_token"], "PUT", jsonString);

                    // update our entities                       
                    var updateJob = db.MatterClientRepairs.Find(job.ID);
                    updateJob.Synced = true;
                    updateJob.Update_Time = DateTime.Now;
                    db.Entry(updateJob).State = System.Data.Entity.EntityState.Modified;
                    if (jobCount % 50 == 0)
                    {
                        // save every 50 changes
                        db.SaveChanges();
                    }
                }
            }

            // if there are remaining files to save
            if (jobCount % 50 != 0)
            {
                db.SaveChanges();
            }

            return View("FinalisedMatters", Data.Get.AllMatterClientRepairs());
        }
    }

And of course the Update method itself which handles the Web requesting:

  public async Task<T> Update<T>(string uri, string token, string method, string json)
                {  
                var authzHeader = GenerateAuthzHeader(uri, method);
                // prepare the token request

                var request = (HttpWebRequest)WebRequest.Create(uri);
                request.Headers.Add("Authorization", authzHeader);
                request.Method = method;
            request.ContentType = "application/json";
            request.Accept = "application/json, text/javascript";
            byte[] bytes = System.Text.Encoding.ASCII.GetBytes(json);
            request.ContentLength = bytes.Length;
            System.IO.Stream os = request.GetRequestStream();
            os.Write(bytes, 0, bytes.Length);
            os.Close();
            WebResponse response = await request.GetResponseAsync();

            using (var reader = new System.IO.StreamReader(response.GetResponseStream()))
            {
                return JsonConvert.DeserializeObject<T>(reader.ReadToEnd());

            }

        }

If it's not possible to do more than 1 request per second then I'm interested in looking at an Ajax solution so I can give the user some feedback while it is processing. In my current solution I cannot give the user feedback while the action method hasn't reached 'return' yet can I?


Solution

  • Okay it's taken me a few days (and a LOT of trial and error) but I've worked this out. Hopefully it can help others. I finally found my silver bullet. And it was probably the place I should have started: MSDN: Consuming the Task-based Asynchronous Pattern

    In the end this following line of code is what brought it all to light.

    string [] pages = await Task.WhenAll(from url in urls select  DownloadStringAsync(url));
    

    I substituted a few things to make it work for a Put request as follows:

    HttpResponseMessage[] results = await Task.WhenAll(from p in toUpload select client.PutAsync(p.uri, p.jsonContent));
    

    'toUpload' is a List of MyClass:

      public class MyClass
        {
            // the URI should be relative to the base pase
            // (ie: /api/v2/matters/101)
            public string uri { get; set; }
    
            // a string in JSON format, being the body of the PUT request
            public StringContent jsonContent { get; set; }
        }
    

    The key was to stop trying to put my PutAsync method inside a loop. My new line of code IS still blocking until ALL responses have come back, but that is what I wanted. Also, learning that I could use this LINQ style expression to create a Task List on the fly was immeasurably helpful. I won't post all the code (unless someone wants it) because it's not as nicely refactored as the original and I still need to check whether the response of each item was 200 OK before I record it as successfully saved in my database. So how much faster is it?

    Results

    I tested a sample of 50 web service calls from my local machine. (There is some saving of records to a SQL Database in Azure at the end).

    Original Synchronous Code: 70.73 seconds

    Asynchronous Code: 8.89 seconds

    That's gone from 1.4146 requests per second down to a mind melting 0.1778 requests per second! (if you average it out)

    Conclusion

    My journey isn't over. I've just scratched the surface of asynchronous programming and am loving it. I need to now work out how to save only the results that have returned 200 OK. I can deserialize the HttpResponse which returns a JSON object (which has a unique ID I can look up etc.) OR I could use the Task.WhenAny method, and experiment with Interleaving.