Turning an asynchronous call into a synchronous call

January 29 2009
c#

Scenario
There is a bug in our Premise Utility COM component that causes streetid's to be set to 0 whenever an existing premise record is saved.  StreetId's are essential to accurate location information when despatching ambulances, so getting a fix done was high on the agenda.  The problem is that for whatever (not technical) reason the bug cannot immediately be fixed in the Premise Utility.  I thought I'd instead create a small app that will be notified when a premise record changes and perform a reverse geo code on the supplied latitude and longitude to obtain a streetid and update the premise record.



What did I do?
The existing .NET Reverse GeoCode component inherits from System.Windows.Forms and contains an old VB OCX that performs the logic (we're still in process of converting rather our large code base from VB6/C++ to .NET).  This component is designed to be called from a Windows Forms app, where the user enters the Latitude and Longitude and waits for a response, which is returned by an Event to the caller:

private void button1_Click(object sender, EventArgs e)
{
var geo = new ReverseGeovalidator(100);
geo.ReverseGeovalidationComplete += new ReverseGeovalidator.ReverseGeovalidationEventHandler(geo_ReverseGeovalidationComplete);
var latitude = 62531667;
var longitude = 26988333;

geo.ReverseGeovalidate(latitude, longitude);
}
void geo_ReverseGeovalidationComplete(object sender, ReverseGeovalidationEventArgs eventArgs)
{
// Populate the form with the data received from eventArgs
}

My application needed to run unattented (Window Service), monitoring the IPC server for notifications that a Premise had changed.  This meant no windows form and no waiting around for a display to be populated.

I needed my application to accept a geo-coordinate and process reverse geo-code synchronously, so I could associated the resultant streetid with the premise id of the changed premise and then commit to the database.

My original thinking was to use ManualResetEvents.WaitOne() to block the caller until the event handler had been called and I could call ManualResetEvents.Set() to continue the operation.


public class GeoCoder
{
ManualResetEvent geoDone;
ReverseGeovalidationEventArgs data;
public GeoCoder() {
geoDone = new ManualResetEvent(false);
}
public int ReverseGeoCode(int latitude, int longitude)
{
geoDone.Reset();
var geo = new ReverseGeovalidator(1500);
geo.ReverseGeovalidationComplete += new ReverseGeovalidator.ReverseGeovalidationEventHandler(geo_ReverseGeovalidationComplete);

geo.ReverseGeovalidate(latitude, longitude);
geoDone.WaitOne();
if (data != null)
{
return data.StreetID;
}
return 0;
}
void geo_ReverseGeovalidationComplete(object sender, ReverseGeovalidationEventArgs eventArgs)
{
data = eventArgs;
geoDone.Set();
}
}

The problem I found was that my unit test would lock up.

[TestMethod]
public void ReverseGeoCodeTest()
{
var gc = new GeoCoder);
var latitude = 62531667
var longitude = 26988333;
var streetId = gc.ReverseGeoCode(latitude, longitude);
Assert.AreEqual(275535, streetId);
}

After a little bit of pondering I realised that geoDone.WaitOne() is blocking on the current thread, which is the same thread the event handler is on.  Result? The event handler will never get called.

I needed to put the event handler a separate thread, so it would eventually get called?  But How?

After a bit of playing around, I decided (realised?) to put the entire lot on a worker thread, a la Command Pattern.  I created a test Geo Processor that is essentially a job queue that co-ordinates off the queue every 2 seconds and processes them, returning the results when done.

public class GeoProcessor
{
public delegate void GeoCodeEventHandler(object sender, GeoCodeEventArgs e);
private Queue workQueue;
public event GeoCodeEventHandler GeoCodeEventComplete;
// Timer will pull Coordinates off the work queue and process them
System.Timers.Timer workTimer;
public GeoProcessor()
{
workQueue = new Queue();
workTimer = new System.Timers.Timer();
workTimer.Elapsed +=new ElapsedEventHandler(workTimer_Elapsed);
workTimer.Interval = 2000;
}
// Pulls items from the queue and processes.
void workTimer_Elapsed(object sender, ElapsedEventArgs e)
{
workTimer.Stop();
if (workQueue.Count > 0)
{
var coord = workQueue.Dequeue()
//
// Do some processing here
//

// Raise event to notify subscriber that work is done
OnGeoCodeComplete(new GeoCodeEventArgs(275535));
if (workQueue.Count > 0) workTimer.Start();
}
}

// Queues the coordinate up for processing in the work queue
public void QueueGeo(int latitude, int longitude)
{
workQueue.Enqueue(new GeoCoordinate { Latitude = latitude, Longitude = longitude });
workTimer.Start();
}
protected void OnGeoCodeComplete(GeoCodeEventArgs e)
{
if (GeoCodeEventCompleted != null)
{
GeoCodeEventCompleted(this, e);
}
}
}

Below is the EventArgs sub class I use to pass the "processed" information back, along with the geo coordinate class.

public class GeoCodeEventArgs : EventArgs
{
private int streetId;
public GeoCodeEventArgs(int streetId) { this.streetId = streetId;
}

public int StreetId
{
get { return this.streetId; }
}
}
public class GeoCoordinate
{
public int Latitude { get; set; }
public int Longitude { get; set; }
}

This is my command pattern implementation that will run on a separate worker thread and call the GeoProcessor.

internal class GeoWorker
{
    GeoCoder geoCoder;
    int latitude;
    int longitude;
public GeoWorker(GeoCoder geoCoder, int latitude, int longitude) { this.latitude = latitude; this.longitude = longitude; this.geoCoder = geoCoder; }
public void Process() { var geo = new GeoProcessor(); geo.GeoCodeEventCompleted += new GeoProcessor.GeoCodeEventHandler(geo_GeoCodeEventCompleted); geo.QueueGeo(latitude, longitude); }
void geo_GeoCodeEventCompleted(object sender, GeoCodeEventArgs e) { geoCoder.Data = e; geoCoder.GeoDone.Set(); } }

My GeoCoder class is now only responsible for starting the worker thread that will call the GeoProcessor and then waiting for the reset event to be set.  Once set (when the worker handles the event from the GeoProcessor and calls Set() on the shared Reset Event) GeoCoder can continue.

public class GeoCoder
{
    ManualResetEvent geoDone;
    Thread geoWorkerThread;
    int latitude;
    int longitude;
public GeoCoder() { geoDone = new ManualResetEvent(false); }
public GeoCodeEventArgs Data { get; set; } public ManualResetEvent GeoDone { get; }
public int ReverseGeoCode(int latitude, int longitude) { this.latitude = latitude; this.longitude = longitude;
GeoDone.Reset();
geoWorkerThread = new Thread(new ThreadStart(this.CallGeoWorker)); geoWorkerThread.Name = "Reverse Geo Code Worker"; geoWorkerThread.SetApartmentState(ApartmentState.STA); geoWorkerThread.Start();
GeoDone.WaitOne(); if (Data != null) { return Data.StreetId; } return 0; }
private void CallGeoWorker() { var geoWorker = new GeoWorker(this, latitude, longitude); geoWorker.Process(); } }

The asynchronous (event) call has now been turned into synchronous.

The biggest problem is the showstopper though.  COM doesn't play well using this model, thanks to it's STA threading (I believe) and the event handler never gets called.

Overall a great learning experience, but I still can't call the COM wrapper object synchronously and now, admitting defeat, I am rolling my own class to calculate the StreetId and not using the OCX.

Post a comment

comments powered by Disqus