0

I worked on a ASP.NET MVC 4 project in which users can apply filter on UI & extract an excel report. The data was stored in MS SQL server. To improve performances, i decided to adopt the async in my application to:

  1. Reduce the time of extraction
  2. Allow users to have parallel extraction

To do this, my approach are:

  1. When user click extraction button on UI -> an ajax call will be made from client to async function on server -> This async function in turn will create an Command object & do ExecuteReaderAsync(). Using this DbDatareader to generated an Excel file using NPOI and save the file content to TempData. The handler to retrieve file will be return to client for later download using window.location. I adopted these techniques from this post Download Excel file via AJAX MVC

  2. After the first extraction, if users want to extract another datasets in parallel, they can click extraction button again and application will repeat step 1.

The results are 2 or more data extractions can happened on the same time.

My problem is, take example, 4 extractions currently running in parallels, if any of these extractions finished & 1 file is downloaded (using window.location). The next time user click on extraction button (which repeat step 1), it doesn't async anymore & later extractions will wait for previous extraction finish before execute.

On debugging, if i restart the ISS server, the problem gone for a while until 1 file is downloaded, so I doubted that window.location do something that blocked the threads on server when any of file is downloaded.

UPDATE 1

Class:

public class QUERYREADER
{
    public DbConnection   CONNECTION { get; set; }
    public DbDataReader   READER { get; set; }
}

Model:

public async Task<QUERYREADER> GET_DATA(CancellationToken ct)
{
    //Create the query reader
    QUERYREADER qr  = new QUERYREADER();

    //Set up the database instances
    DbProviderFactory dbFactory = DbProviderFactories.GetFactory(db.Database.Connection);

    //Defined the query
    var query = "SELECT * FROM Table";

    //Set up the sql command object
    using (var cmd = dbFactory.CreateCommand())
    {
        //Try to open the database connection 
        try
        {
            //Check if SQL connection is set up
            if (cmd.Connection == null)
            {
                cmd.CommandType = CommandType.Text;
                cmd.Connection  = db.Database.Connection;
            } 
            //Open connection to SQL if current state is closed
            if (cmd.Connection.State == ConnectionState.Closed)
            {
                //Change the connection string to set the packet size to max. value = 32768 to improve efficiency for I/O transmit to SQL server 
                cmd.Connection.ConnectionString = cmd.Connection.ConnectionString + ";Packet Size=20000";
                //Open connection
                await cmd.Connection.OpenAsync(ct);
            }
            //Save the connection 
            qr.CONNECTION = cmd.Connection;

        } catch (Exception ex) {
            //If errors throw, close the connection
            cmd.Connection.Close();
        };


        //Retrieve the database reader of provided sql query
        cmd.CommandText = query;       
        DbDataReader dr = await cmd.ExecuteReaderAsync(ct);
        qr.READER.Add(dr);       
    }  

    //Return the queryreader
    return qr;
}

Controller:

  public async Task<JsonResult> SQL_TO_EXCEL()
  {        
       //Set up the subscription to client for "cancellation request, browser closing"
        CancellationToken   disToken = Response.ClientDisconnectedToken;             

        //Get the datareader
        try 
        {
            qr  = await GET_DATA(disToken);        
        } 
        catch(Exception ex) { }

        //Open the connection to SQL server
        using (qr.CONNECTION)
        {
            using (var dr = qr.READER)
            {                                            
                while (await dr.ReadAsync(disToken))
                {
                    for (int k = 0; k < dr.FieldCount; k++)
                    {
                        //.... using NPOI to write Excel file to MemoryStream
                    }
                }
                dr.Close();
            }
        }

        //Generate XL file if controller action is still running (no "cancellation request, browser closing")
        if (!disToken.IsCancellationRequested)
        {

            string file_id = Guid.NewGuid().ToString();
            //... Write the NPOI excel file to TempData and then create a handler for later download at client
            //This line caused trouble
            TempData["file_id"] = XLMemoryStream.ToArray();

            HANDLER["file_id"]      = file_id;
            HANDLER["file_name"]    = FILE["FILE_NAME"].ToString().NonUnicode() + FILE["FILE_TYPE"].ToString() ;
        }

        //Return JSON to caller
        var JSONRESULT                  = Json(JsonConvert.SerializeObject(HANDLER), JsonRequestBehavior.AllowGet);
            JSONRESULT.MaxJsonLength    = int.MaxValue;

        return JSONRESULT;
  }

    public async Task<ActionResult> DOWNLOAD_EXCEL(string file_id, string file_name)
    {
        if (TempData[file_id] != null)
        {
            byte[] data = await Task.Run(() => TempData[file_id] as byte[]);
            return File(data, "application/vnd.ms-excel", file_name);
        }
        else
        {
            return new EmptyResult();
        }
    }   

Javascript

    $.ajax({
        type: 'POST',
        async: true,
        cache: false,
        url:  'SQL_TO_EXCEL',
        success: function (data)
        {
            var response = JSON.parse(data);
            window.location =
            (
                "DOWNLOAD_EXCEL"    +
                '?file_id='         + response.file_id +
                '&file_name='       + response.file_name
            );
        },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            console.log(errorThrown);
        }
    });    

UPDATE 2:

After a lot of tests, i figured out window.location has nothing to do with threads on server, the line TempData[file_id] = XLMemoryStream.ToArray() caused the issues. It look likes the problem is similar as described in this post Two parallel ajax requests to Action methods are queued, why?

  • can you clarify; are you talking about concurrent *requests* here? i.e. are we talking about the browsers http connection? or are you talking about activity at the server? – Marc Gravell May 06 '20 at 09:54
  • @MarcGravell I updated my code on the question. I think we talking on both concurrent ajax call at client & concurrent call on server controller. When user clicked on the extraction button, a new `ajax` request will be sent to server to trigger async `SQL_TO_EXCEL()` on controller and return an handler to client with `file_id` and `file_name`, then browser will point to `DOWNLOAD_EXCEL()` with these parameters to download the file. As i mentioned, users can click extraction button multiple times to have multiple concurrent extraction. – Quang Nguyen May 06 '20 at 10:39
  • To simplify, i assumed multiple clicks on extraction button will query the same data from `"SELECT * FROM Table"` – Quang Nguyen May 06 '20 at 10:47

0 Answers0