1

I have a synchronous method that calls a method which collates a bunch of data on a custom object and stores it on a table entry on a Firebird database, located on a server.

On the server, a monitoring process keeps watching the first table for new entries using a database event (a table trigger raises an event which is captured by the monitor). When this event is raised, that data is sent to a third-party black-box service to be processed with the use of a proprietary library, that takes between near-naught and 1 minute to reply.

The third-party service replies with some data which is entered on a second table on the database. This second table has another trigger that the client's program monitors. The client's program must either wait until the third-party replies some data, or it times out (the same 1 minute).

I'm currently delving into the world of database events and I've reached an impasse:

Currently I have a key press that runs a synchronous method, which according to an application setting either runs another synchronous method, which runs flawlessly, or another method that inserts an entry on a Firebird database. This database is monitored by another process, which reads that entry, do some stuff, and inserts the new data on another table.

Back on the main program, what I currently have is the method has an event handler which is triggered when the new data is inserted. However, as it is an event, the rest of the method runs its course, ending prematurely, before the event handler has the chance to read the new data.

In pseudo code:

MainWindow_KeyDown(object sender, EventArgs e)
{
    if (e.Key == X)
    {
        MakeADecision()
    }
}

MakeADecision()
{
    if (Properties.Settings.Default.MySetting) Console.Write(DoLocalStuff());
    else Console.Write(DoRemoteStuff());
}

string DoRemoteStuff()
{
    using (OldDataTableAdapter)
    using (NewDataTableAdapter)
    {
        OldDataTableAdapter.Insert(OldData);
        var revent = new FBRemoteEvent(MyConnectionString);
        revent.RemoteEventCounts += (sender, e) =>
        {
            NewDataTableAdapter.Fill(NewDataDataTable);
            NewData = NewDataDataTable[0].MYCOLUMN;
        };
        revent.QueueEvents("MY_FB_EVENT");
    }
    return NewData;
}

As you can see, the issue here is that DoRemoteStuff reaches its return before the event can be triggered. I tried turning DoRemoteStuff() into an async method, but I don't know how to use events with async methods. Can anyone please help me with this? Any tips or hints on how to work with async methods?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Artur S.
  • 185
  • 3
  • 15
  • This seems to be an abuse of events. If you want to act on the data you just inserted yourself, why use an even at all? And if you want to use an event, you must be prepared to handle it asynchronously (what you're currently not doing), because events are only posted after commit, and can have an indeterminate delay depending on system load. Events are more useful to react to actions by **other** sessions than your own. – Mark Rotteveel Nov 30 '18 at 13:40
  • So you need to create some out-of-method existing events owner ( probably application-global) and pass the event ownership to it. Maybe you have to implement some variation to a so called "Actor model" – Arioch 'The Nov 30 '18 at 13:45
  • @MarkRotteveel My bad, I didn't explain myself correctly. I don't know when the data inserted by the other process is going to be inserted. It's a process ran on the server, like a monitoring process, that watches the database for data to be processed, while my program is clientside. All I can think of, other than using a remote event is a `while` loop with a boolean set as false, and a query that checks whether the expected data can be found, but I'm afraid that might overtax the database. – Artur S. Nov 30 '18 at 18:19
  • It sounds like you may have simplified your question too much. Consider improving it by better describing your problem. In any case, if you need to wait for an external proces, you will need to read up on asynchronous handling. In the example as shown it would be as simple as just moving the `Console.writeLine` into the event handler. In more complex scenarios, the event handler should **signal** to your application that the data is available for processing, and your application should act on that in some way (that could be a loop, but that is rather inefficient). – Mark Rotteveel Nov 30 '18 at 18:24
  • 1
    [Asynchronous programming with async and await (C#)](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) "Tips and tricks" aplenty! – Super Jade Dec 01 '18 at 07:17

1 Answers1

0

A possible solution would be to use a TaskCompletionSource so you can convert your method to an async method. This is based on Is it possible to await an event instead of another async method?.

MakeADecision()
{
    if (Properties.Settings.Default.MySetting)
    { 
        Console.Write(DoLocalStuff());
    }
    else
    {
        // Consider making MakeADecision async as well
        NewData = DoRemoteStuff().Result;
        Console.Write(NewData);
    }
}

async Task<string> DoRemoteStuff()
{
    Task<string> task;
    using (OldDataTableAdapter)
    {
        OldDataTableAdapter.Insert(OldData);
        task = WaitForEvent(MyConnectionString);
    }
    return await task;
}

private async Task<string> WaitForEvent(string connectionString)
{
    var taskCompletionSource = new TaskCompletionSource<string>();
    var revent = new FbRemoteEvent(connectionString);
    revent.RemoteEventCounts += (sender, e) =>
    {
        using (NewDataTableAdapter)
        {
            NewDataTableAdapter.Fill(NewDataDataTable);
            string newData = NewDataDataTable[0].MYCOLUMN;
            taskCompletionSource.SetResult(newData);
        }
        sender.Dispose();
    };
    revent.QueueEvents("MY_FB_EVENT");

    return await taskCompletionSource.Task;
}

Some things to point out:

  • You need to explicitly dispose the event to avoid a memory leak
  • The using for NewDataTableAdapter belongs within the event handler
  • The MakeADecision method seems like a candidate to be made async as well

A word of warning, my C# is a bit rusty (and I have never done much with async), so I'm not sure if this is the idiomatic way of doing it. I also did not test the code as written above (I wrote and tested a simpler version, but I may have introduced bugs while transforming your code to a similar solution).

This solution may also have the possibility of a race condition between inserting the new data triggering the event and registering for the event (unless the Dispose at the end of the using block is what commits the data), consider moving the WaitForEvent before inserting. Also consider the possibility of receiving the event from an update done for/by another change.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • I've tried your suggestion (I did switch `WaitForEvent` to before the datatable `Fill`), but, for some reason `MakeADecision` never receives `DoRemoteStuff.Result`. I've tried with a `FileWatcher` event to check whether an event inside `WaitForEvent` was being triggered, and it was not. `MakeADecision` infinitely waits for `DoRemoteStuff.Result`. – Artur S. Dec 05 '18 at 13:52
  • @ASousa I think I made a typo, and you need to use `DoRemoteStuff().Result` instead. I'll check when I have access to my Windows machine. Alternatively, `MyConnectionString` points to the wrong database. – Mark Rotteveel Dec 05 '18 at 13:56
  • Interestingly, my `DoRemoteStuff` already needs a parameter, so it's being called as a method already. I'll check the connectionstring as soon as I get the chance, too. – Artur S. Dec 07 '18 at 12:05
  • 1
    @ASousa It was a typo in my example. Testing my own code works (it waits until I post the event). Alternatively, try to register the event before inserting the data (maybe the event is sent before the event registration is complete, in that case no notification will happen and the code will wait until something else triggers the same event). – Mark Rotteveel Dec 07 '18 at 15:50
  • I will try your code, since it seems the most professional approach to the situation, but for now, I have found an acceptable compromise: Rather than call the asynch methods on my main thread, I will use a customized dialogbox that warns the user it is waiting for a reply from the server and is closed (via `DialogResult`) when the server inserts the new data for the terminal to see. I believe this also allows the user to cancel the process, should they desire so. – Artur S. Dec 08 '18 at 14:31
  • I've tried your code, and it does work. It must be something about how my main thread calls the method, so I'll have to work on that before I get this working on my code. Thanks for the help! – Artur S. Dec 12 '18 at 11:59
  • @ASousa You're welcome. If you need more help, consider asking on the firebird-net-provider list on Googe Groups. – Mark Rotteveel Dec 12 '18 at 20:03