Hi,
I'm trying to save a business object which as async rules, which is not bound to a form/windows.
I'm creating the object, filling it, and immediatly trying to save it, in the UI thread.
As a consequence, when saving, IsBusy is still true (my async business rules never completed), and save fails.
I assumed that I need to release the UI thread, to let my async rules return and update my object.
Therefire, I tried solving my provlem by overriding SaveAsync, as below.
Protected Overrides Async Function SaveAsync(forceUpdate As Boolean, userState As Object, isSync As Boolean) As Task(Of T)
For i As Integer = 0 To 40 'loop to simulate delays, up to 4s (40 x 100ms)
If Not Me.IsBusy Then Exit For
Await TaskEx.Delay(TimeSpan.FromMilliseconds(100)) 'this call blocks the UI thread and never return
Next
Return Await MyBase.SaveAsync(forceUpdate, userState, isSync)
End Function
I do not understand why the Await TaskEx.Delay is blocking my UI thread.
Has anyone face such issue? Has better alternatives?
I'm using 4.5.11 with local dataportal on .NET 4 and AsyncTargetingPack (on VisualStudio 2012).
Regards,
Gilles
Note there is a similar thread: http://forums.lhotka.net/forums/p/10156/47658.aspx on older version of CSLA / without AsyncTargettingPack
I don't know how they implemented Delay in TaskEx, perhaps it blocks the UI message pump and prevents dispatching back to the UI thread.
It is generally better to avoid that type of a fake-sync block though. You might be happier using a TaskCompletionSource to wait for the validation completed event.
Delay should be getting back on the captured context if one exists which is in this case probably DispatcherSynchronizationContext if it's WPF client.
Therefore, if UI thread is not blocked the completition should be invoked i.e. the rest of code executed.
You can try configuring awaiter not to capture context:
Await TaskEx.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
So the completition is going to be invoked on worker thread.
However, I remember reporting issue with ConfigureAwait in earlier versions of Bcl.Async, so you can try clearing current context prior to awaiting:
SynchronizationContext.Current = null;
That being said, it's really not good to sync BO persistence with this timing sensitive loop.
- ngm
Hi ngm,
I'm still using the old version of the AsyncTargettingPack (and not the latest Bcl.Async). I'm in a Winform context.. in case this has side effects.
I will try your proposal.
Gilles
Gilles,
Get far away from those old versions, they are very buggy and I can confirm there are a number of them related to the context capturing.
- ngm
Hi ngm,
Upgrading to the latest Bcl.Async prerelease (1.0.14) fixed the issue: the call is not blocking anymore.
I'm aware this is not the good solution, but I was pessimistic implemeting something clean if this was not working.
I'll be in vacation for the next 2 weeks (in between as a workaround, we moved our async rules to be sync). I will update the thread when I get back with a 'go live' solution.
Thanks for your help.
Gilles
Rocky,
Do people complain a lot about this?
What about async persistence method variant that awaits all async broken rules prior to invoking Data Portal? Or even dedicated broken rules method that when awaited will make sure all async rules are complete prior to completing itself. That Save might use this one internally.
I know that async rules are not awaiting friendly but it wouldn't be too complicated wrapping it since there's already central point where BusinessRules handle completitions from async rules.
Along that thinking, any plans to redesign rules engine to be fully async/await?
- ngm
Do people complain a lot about this?
The use of async/await is limited, as it is so new. As a result, this is (I think) the first thread where this has come up - at least since 2008 when Silverlight started rolling out - and of course the answer then is different from now.
To make this easier (originally for unit testing, and then for a Magenic project last year) there's an event raised to indicate when rule processing is complete. That makes it relatively easy to wait for all rules to finish before moving on by using a TaskCompletionSource.
If this becomes enough of a common issue I'm sure BusinessBase could have a method that returned a Task on which you could wait - probably just implementing the use of a TCS in that method :)
You probably think on AllRulesComplete method that raises ValidtionComplete event?
If so, that's probably okay to be used for helper or extension method that implements syncing Save which waits for async rules to complete. But I'm not quite sure it's good idea for CSLA internal implementation.
Having as critical method as Save synced by subscribing to external event where there's no guarantee of the invocation order can potentially lead to trouble.
Anyway, rules engine is really impressive piece of CSLA. It would be nice seeing its async mechanism implemented as awaitables at some point.
- ngm
Hi Rocky,
I hope you are recovering well.
I'm reopening the discussion, as my attempts to solve the problem were not successful yet.
Following your advice, I'm trying to override the SaveAsync using a TaskCompletionSource to block until being notified that all async rules completed.
It is my first attempt to use TaskCompletionSource, so I might miss the obvious... I'm still using CSLA 4.5.10 / .NET 4 client profile, with the latest Async targetting packs.
I'm sharing my code below, in case there is something obvious that I'm missing.
Regads,
Gilles
Protected Overrides Async Function SaveAsync(forceUpdate As Boolean, userState As Object, isSync As Boolean) As Task(Of T)
If Me.IsBusy Then
Dim busyTcs As New TaskCompletionSource(Of Boolean) 'Creates a blocking task until all async rules complete
Dim saveTcs As New TaskCompletionSource(Of T)
Dim bw As New Csla.Threading.BackgroundWorker() 'Ensure that I do not wait for the busyTcs on the UI thread.
AddHandler bw.DoWork, Sub(s, o)
busyTcs.Task.Wait(TimeSpan.FromSeconds(10)) 'This always block for 10s!
o.Result = MyBase.SaveAsync(forceUpdate, userState, isSync).Result
End Sub
AddHandler bw.RunWorkerCompleted, Sub(s, o)
If o.Error Is Nothing Then
saveTcs.TrySetResult(CType(o.Result, T))
Else
saveTcs.TrySetException(o.Error)
End If
End Sub
AddHandler ValidationComplete, Sub(sender, e)
busyTcs.TrySetResult(True)
End Sub
If Not Me.IsBusy Then busyTcs.TrySetResult(True)
bw.RunWorkerAsync()
Return Await saveTcs.Task
Else
Return Await MyBase.SaveAsync(forceUpdate, userState, isSync)
End If
Copyright (c) Marimer LLC