1

I need to make multiple db calls by looping through the connection strings. There is only 1 matching record across dbs and If I find a matching record then I can return data and cancel other async calls.

using (var Contexts = instContextfactory.GetContextList())
{
    foreach(var context in Contexts.GetContextList())
    {    
        // how do I make all the calls and return data from the first call that finds data and continue with further process.(don't care about other calls if any single call finds data.           
        context.Insurance.GetInsuranceByANI(ani);
    }
}

GetInsuranceByANI

public Task<IEnumerable<Insurance>> GetInsuranceByANI(string ani)
{
    using (ITransaction transaction = Session.Value.BeginTransaction())
    {
        transaction.Rollback();
        IDbCommand command = new SqlCommand();
        command.Connection = Session.Value.Connection;

        transaction.Enlist(command);

        string storedProcName = "spGetInsurance";

        command.CommandText = storedProcName;
        command.Parameters.Add(new SqlParameter("@ANI", SqlDbType.Char, 0, ParameterDirection.Input, false, 0, 0, null, DataRowVersion.Default, ani));

        var rdr = command.ExecuteReader();
        return Task.FromResult(MapInsurance(rdr));
    }
}

For example : I'm looping through with 5(a, b, c, d, e) different db connection strings. I need make asyn calls to all the 5 dbs. If I find matching record in db : b then I can return that data and continue to next steps and can stop making calls to other dbs

Fábio Nascimento
  • 2,644
  • 1
  • 21
  • 27
ivj
  • 39
  • 6
  • Does GetInsuranceByANI return a `Task<>`? – StriplingWarrior Jun 28 '19 at 16:36
  • @StriplingWarrior Yes GetInsuranceByANI returns Task> – ivj Jun 28 '19 at 16:39
  • Do you have all databases installed in the same machine or are located in different machines? – Theodor Zoulias Jun 28 '19 at 16:44
  • They are all in same server @TheodorZoulias – ivj Jun 28 '19 at 16:45
  • Then it may be less efficient to parallelize the requests, but I may be wrong. See this question, it may help you: [Task.WhenAny with cancellation of the non completed tasks and timeout](https://stackoverflow.com/questions/56207326/task-whenany-with-cancellation-of-the-non-completed-tasks-and-timeout) – Theodor Zoulias Jun 28 '19 at 16:47
  • There will be only one record across the dbs and if I find that record from any call then I don't have to look anymore in other dbs. I can just return the data and cancel reaming calls @TheodorZoulias – ivj Jun 28 '19 at 16:49
  • @ivj yes, I got that. I am worried that making multiple parallel requests to databases located in the same machine will make the IO access to your hard disk a bottleneck, and the requests will become serialized out of necessity. – Theodor Zoulias Jun 28 '19 at 16:55
  • Seriously consider why you don't know which database the info is in and see if you can fix that. The benefit of cancelling 4 requests (which may have already completed with no results) is likely dwarfed by the cost of starting up those extra queries in the first place. – Damien_The_Unbeliever Jun 28 '19 at 17:02
  • Or to put it another way, what's the benefit to splitting stuff across databases if queries load down *all* of the databases (until you know which one has the data you want)? – Damien_The_Unbeliever Jun 28 '19 at 17:06
  • @Damien_The_Unbeliever, I have separate dbs for each client, usually I pass client Id to identify the db I need to query. Now I have a situation where I don't have client Id and need to pull records using some other details which can exist in any db. – ivj Jun 28 '19 at 17:08
  • @ivj could you please include the code of the method `GetInsuranceByANI`? You will need to make changes in this method, if you want to pass a `CancellationToken` and make it work correctly. – Theodor Zoulias Jun 28 '19 at 17:08
  • So what is ani? It presumably came from a database at some time. Make it (16/32/64) bits wider and encode the source database. (Don't use any internal IDs provided by the RDBMS though) – Damien_The_Unbeliever Jun 28 '19 at 17:13
  • @TheodorZoulias added that method in above question, thank you – ivj Jun 28 '19 at 17:13
  • @Damien_The_Unbeliever, ANI is phone number, I'm looking up records using phone number. It can be any clients phone number. – ivj Jun 28 '19 at 17:15
  • So, again, take a step back. If you need to search phone numbers "globally", store those mappings in a central DB, telling you which DB to query further. You're solving the *wrong* problem. Good websites don't scale by querying *every* database for anything. – Damien_The_Unbeliever Jun 28 '19 at 18:03
  • @Damien_The_Unbeliever Thanks for the suggestion, right now I have no choice. I need to make these async calls and maybe in future I'll update it. – ivj Jun 28 '19 at 19:10
  • What is `Session` and where does it come from? – Paulo Morgado Jun 29 '19 at 16:15

2 Answers2

2

How about returning the value as soon as you get it. Not allowing the flow to move forwards and break the loop.

using (var Contexts = instContextfactory.GetContextList())
    {
           foreach(var context in Contexts.GetContextList())
           {    
               // how do I make all the calls and return data from the first call that finds data and continue with the further process.(don't care about other calls if any single call finds data.           
                var result = await context.Insurance.GetInsuranceByANI(ani);

                if(result.Any())
                {
                    return result.First();
                }
           }
    }
Shahzad
  • 1,677
  • 1
  • 12
  • 25
  • The OP said in a comment that `GetInsuranceByANI` returns a `Task`. – Theodor Zoulias Jun 28 '19 at 16:56
  • Thank you for the reply, I want the calls to be made asynchronously and when the first call that finds data will return it and the reaming calls can be cancelled. I did little research and came to it can be done using cancellationToken to cancel calls, but not sure how to use it. – ivj Jun 28 '19 at 16:58
  • 2
    @ivj Okay. You need to first get a cancellation token and keep that with you. When you want to cancel a task, you just need to send the cancellation token along with the cancellation request – Shahzad Jun 28 '19 at 17:00
  • @Shahzad now it is better. But I am under the impression that the OP wants the requests to be parallel, not sequential. – Theodor Zoulias Jun 28 '19 at 17:03
  • @TheodorZoulias Yes I guess so, I think he wants to make possible parallel calls and cancel the call as soon as he has a value using cancellation token – Shahzad Jun 28 '19 at 17:09
0

To make things simple you should first change back your GetInsuranceByANI method to become synchronous again. We will spawn tasks to call it asynchronously later.

public IEnumerable<Insurance> GetInsuranceByANI(string ani)
{
    using (ITransaction transaction = Session.Value.BeginTransaction())
    {
        transaction.Rollback();
        IDbCommand command = new SqlCommand();
        command.Connection = Session.Value.Connection;

        transaction.Enlist(command);

        string storedProcName = "spGetInsurance";

        command.CommandText = storedProcName;
        command.Parameters.Add(new SqlParameter("@ANI", SqlDbType.Char, 0, ParameterDirection.Input, false, 0, 0, null, DataRowVersion.Default, ani));

        var rdr = command.ExecuteReader();
        return MapInsurance(rdr);
    }
}

Now to the implementation of the method that searches all databases asynchronously. We will create a task for each database, running in a thread-pool thread. This is debatable, but we are trying to keep things simple. We also instantiate a CancellationTokenSource, and pass its Token to all Task.Run methods. This will only ensure that after we have our result, no more tasks are going to start. If the available threads in the thread-pool are more than the databases to search, all tasks are going to start immediately, and the cancellation token will not actually cancel anything. In other words all queries that are started are going to be completed no matter what. This is obviously a waste of resources, but again we are trying to keep things simple.

After starting the tasks we are entering a loop awaiting the next task to complete (using the method Task.WhenAny). If the result is found we cancel the token and return the result. If the result is not found we continue the loop for the next result. If all tasks complete and we still don't have a result, we return null.

async Task<IEnumerable<Insurance>> SearchAllByANI(string ani)
{
    var tasks = new HashSet<Task<IEnumerable<Insurance>>>();
    var cts = new CancellationTokenSource();
    using (var Contexts = instContextfactory.GetContextList())
    {
        foreach (var context in Contexts.GetContextList())
        {
            tasks.Add(Task.Run(() =>
            {
                return context.Insurance.GetInsuranceByANI(ani);
            }, cts.Token));
        }
    }
    while (tasks.Count > 0)
    {
        var task = await Task.WhenAny(tasks);
        var result = await task;
        if (result != null && result.Any())
        {
            cts.Cancel();
            return result;
        }
        tasks.Remove(task);
    }
    return null;
}

Usage example:

IEnumerable<Insurance> result = await SearchAllByANI("12345");
if (result == null)
{
    // Nothing fould
}
else
{
    // Do something with result
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • There doesn't seem to be any good reason to block on the db access, just to later run it with `Task.Run`. And even if you do really want the db access on a separate thread it still has no reason to be synchronous. – JSteward Jun 28 '19 at 20:22
  • 1
    @JSteward I would use an asynchronus DB call if it was available. But the current implementation fakes the asynchronity by returning `Task.FromResult(MapInsurance(rdr))`. Feel free to provide an answer demonstrating the usage of the truly asynchronous ADO.NET API if you want. – Theodor Zoulias Jun 28 '19 at 20:44
  • 1
    Thank you @TheodorZoulias, I'll write some unit tests around it to test the flow. – ivj Jun 28 '19 at 20:59
  • @TheodorZoulias _if it was available?_ Are you implying that there isn't async ado.net methods? – JSteward Jun 28 '19 at 21:04
  • @JSteward I mean if it was available in the code provided by the OP. I am not familiar with the async ADO.NET API myself, so I am not gonna rewrite a working sync code, just because async is better for scalability. The OP doesn't have a scalability problem right now, he has a problem of not having code that works! – Theodor Zoulias Jun 29 '19 at 02:31