Giter VIP home page Giter VIP logo

ambienttasks's Introduction

AmbientTasks NuGet badge MyGet badge Gitter badge Build status badge codecov badge

All notable changes are documented in CHANGELOG.md.

Enables scoped completion tracking and error handling of tasks as an alternative to fire-and-forget and async void. Easy to produce and consume, and test-friendly.

Benefits:

  • Avoids async void which, while being semantically correct for top-level event handlers, is very easy to misuse.

  • Avoids fire-and-forget (async Task but ignoring the task). This comes with its own pitfalls, leaking the exception to TaskScheduler.UnobservedTaskException or never discovering a defect due to suppressing exceptions.

  • Test code can use a simple API to know exactly how long to wait for asynchronous processes triggered by non-async APIs before doing a final assert.

  • Exceptions are no longer missed in test code due to the test not waiting long enough or the exception being unhandled on a thread pool thread.

  • Unhandled task exceptions are sent to a chosen global handler immediately rather than waiting until the next garbage collection (arbitrarily far in the future) finalizes an orphaned task and triggers TaskScheduler.UnobservedTaskException.

Example 1 (view model)

When the UI picker bound to SelectedFooId changes the property, the displayed label bound to SelectedFooName should update to reflect information about the selection.

(See the How to use section to see what you’d probably want to add to your Program.Main.)

public class ViewModel
{
    private int selectedFooId;

    public int SelectedFooId
    {
        get => selectedFooId;
        set
        {
            if (selectedFooId == value) return;
            selectedFooId = value;
            OnPropertyChanged();

            // Start task without waiting for it
            AmbientTasks.Add(UpdateSelectedFooNameAsync(selectedFooId));
        }
    }

    // Never use async void (or fire-and-forget which is in the same spirit)
    private async Task UpdateSelectedFooNameAsync(int fooId)
    {
        SelectedFooName = null;

        var foo = await LoadFooAsync(fooId);
        if (selectedFooId != fooId) return;

        // Update UI
        SelectedFooName = foo.Name;
    }
}

Test code

[Test]
public void Changing_selected_ID_loads_and_updates_selected_name()
{
    // Set up a delay
    var vm = new ViewModel(...);

    vm.SelectedFooId = 42;

    await AmbientTasks.WaitAllAsync();
    Assert.That(vm.SelectedFooName, Is.EqualTo("Some name"));
}

Example 2 (form)

(See the How to use section to see what you’d probably want to add to your Program.Main.)

public class MainForm
{
    private void FooComboBox_GotFocus(object sender, EventArgs e)
    {
        // Due to idiosyncrasies of the third-party control, ShowPopup doesn’t work properly when called
        // during the processing of this event. The recommendation is usually to queue ShowPopup to happen
        // right after the event is no longer being handled via Control.BeginInvoke or similar.

        // Use AmbientTasks.Post rather than:
        //  - Control.BeginInvoke
        //  - SynchronizationContext.Post
        //  - await Task.Yield() (requires async void event handler)

        // This way, your tests know how long to wait and exceptions are automatically propagated to them.
        AmbientTasks.Post(() => FooComboBox.ShowPopup());
    }
}

Test code

[Test]
public void Foo_combo_box_opens_when_it_receives_focus()
{
    var form = new MainForm(...);
    form.Show();

    WindowsFormsUtils.RunWithMessagePump(async () =>
    {
        form.FooComboBox.Focus();

        await AmbientTasks.WaitAllAsync();
        Assert.That(form.FooComboBox.IsPopupOpen, Is.True);
    });
}

How to use

If your application has a top-level exception handler which grabs diagnostics or displays a prompt to send logs or restart, you’ll want to add this to the top of Program.Main:

AmbientTasks.BeginContext(ex => GlobalExceptionHandler(ex));

Any failure in a task passed to AmbientTasks.Add will be immediately handled there rather than throwing the exception on a background thread or synchronization context.

Use AmbientTasks.Add and Post any time a non-async call starts off an asynchronous or queued procedure. (See the example section.) This includes replacing fire-and-forget by passing the task to AmbientTasks.Add and replacing async void by changing it to void and moving the awaits into an async Task method or lambda. For example:

Before
private async void SomeEventHandler(object sender, EventArgs e)
{
    // Update UI

    var info = await GetInfoAsync(...);

    // Update UI using info
}
After
private void SomeEventHandler(object sender, EventArgs e)
{
    // Update UI

    AmbientTasks.Add(async () =>
    {
        var info = await GetInfoAsync(...);

        // Update UI using info
    });
}

Finally, await AmbientTasks.WaitAllAsync() in your test code whenever AmbientTasks.Add is used. This gets the timing right and routes any background exceptions to the responsible test.

It could potentially make sense to delay the application exit until AmbientTasks.WaitAllAsync() completes, too, depending on your needs.

Debugging into AmbientTasks source

Stepping into AmbientTasks source code, pausing the debugger while execution is inside AmbientTasks code and seeing the source, and setting breakpoints in AmbientTasks all require loading symbols for AmbientTasks. To do this in Visual Studio:

  1. Go to Debug > Options, and uncheck ‘Enable Just My Code.’ (It’s a good idea to reenable this as soon as you’re finished with the task that requires debugging into a specific external library.)
    Before doing this, because Visual Studio can become unresponsive when attempting to load symbols for absolutely everything, I recommend going to Debugging > Symbols within the Options window and selecting ‘Load only specified modules.’

  2. If you are using a version that was released to nuget.org, enable the built-in ‘NuGet.org Symbol Server’ symbol location.
    If you are using a prerelease version of AmbientTasks package, go to Debugging > Symbols within the Options window and add this as a new symbol location: https://www.myget.org/F/ambienttasks/api/v2/symbolpackage/

  3. If ‘Load only specified modules’ is selected in Options > Debugging > Symbols, you will have to explicitly tell Visual Studio to load symbols for AmbientTasks. One way to do this while debugging is to go to Debug > Windows > Modules and right-click on AmbientTasks. Select ‘Load Symbols’ if you only want to do it for the current debugging session. Select ‘Always Load Automatically’ if you want to load symbols now and also add the file name to a list so that Visual Studio loads AmbientTasks symbols in all future debug sessions when Just My Code is disabled.

ambienttasks's People

Contributors

jnm2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

ambienttasks's Issues

Add convenience overload AmbientTasks.Add(Func<Task>)

This comes up just about all the time. Instead of pushing you towards a local function or a method, a lambda should just be an option so that you don't have to come up with a name:

From the current readme:

private void SomeEventHandler(object sender, EventArgs e)
{
    // Update UI

    AmbientTasks.Add(UpdateInfoAsync());

    async Task UpdateInfoAsync()
    {
        var info = await GetInfoAsync(...);

        // Update UI using info
    }
}

This would be nicer as:

private void SomeEventHandler(object sender, EventArgs e)
{
    // Update UI

    AmbientTasks.Add(() =>
    {
        var info = await GetInfoAsync(...);

        // Update UI using info
    });
}

I wasn't sure at first, but my own usage and examples like in #2 have convinced me.

InvalidOperationException: More calls to EndTask than StartTask.

InvalidOperationException: More calls to EndTask than StartTask.

at AmbientTasks.AmbientTaskContext.EndTask()
at AmbientTasks.OnTaskCompleted(System.Threading.Tasks.Task completedTask, object state)
at Se.Util.Wpf.General.AsyncRelayCommand.Execute.__ExecuteInternal|0() Line 115

The code thats calling it looks like this:

		protected override void Execute(T parameter)
		{
			AmbientTasks.Add(ExecuteInternal());

			async Task ExecuteInternal()
			{
				var t = _execute?.Invoke(parameter);
				if (t != null)
				{
					IsRunning = true;
					try
					{
						await t.ConfigureAwait(false);
					}
					catch (Exception ex)
					{
						_log.Error("Async command has faulted", ex);
						throw;
					}
					finally
					{
						IsRunning = false;
					}
				}
			}

		}

Shared global fallback context

I really want to understand this and get the scenario right. It seems bad to rush something in without understanding a use case for it, and it's just as bad to prevent real-world apps from using AmbientTasks out of some kind of idealism. Instead of the strategy of dragging my feet and hoping things magically solve themselves, I'm proposing a compromise. See the final question in the 'Outstanding questions' section.

Goals

The best outcome from my point of view would be to play around with an example app until we come to a full understanding of the problem space.

The next best outcome—with the same disclaimer—is to add a back door with the guidance that we don't fully understand why it exists (until this changes) and that it should only be used as a last resort

The worst outcome is to delay this many more weeks. Nearly the worst outcome is to block the folks that need this from being able to use the NuGet package.

Problem description

/cc @YairHalberstadt who found it necessary in their project to add the following code (https://gitter.im/dotnet/csharplang?at=5d039b76e527d95addd12695):

private static AmbientTaskContext _globalContext = new AmbientTaskContext(null);
private static readonly AsyncLocal<AmbientTaskContext> _localContext = new AsyncLocal<AmbientTaskContext>();

private static AmbientTaskContext CurrentContext => _localContext.Value ?? (_localContext.Value = _globalContext);

/// <summary>
/// <para>
/// Replaces the current async-local scope with a new scope which has its own exception handler and isolated set
/// of tracked tasks.
/// </para>
/// <para>If <paramref name="exceptionHandler"/> is <see langword="null"/>, exceptions will be left uncaught. In
/// the case of tracked <see cref="Task"/> objects, the exception will be rethrown on the synchronization
/// context which began tracking it.
/// </para>
/// </summary>
public static void BeginContext(Action<Exception> exceptionHandler = null)
{
	_localContext.Value = new AmbientTaskContext(exceptionHandler);
}

/// <summary>
/// <para>
/// Sets a global context which will be used by any tasks which do not have a scope.
/// </para>
/// <para>If <paramref name="exceptionHandler"/> is <see langword="null"/>, exceptions will be left uncaught. In
/// the case of tracked <see cref="Task"/> objects, the exception will be rethrown on the synchronization
/// context which began tracking it.
/// </para>
/// </summary>
public static void SetGlobalContext(Action<Exception> exceptionHandler = null)
{
	_globalContext = new AmbientTaskContext(exceptionHandler);
}

Reasons to avoid this

Subverting the purpose of BeginContext

Using a single shared global context takes less thought than thinking about what contexts mean. I'm afraid people will use this when AmbientTasks.BeginContext is actually possible, so I'll make the XML documentation obnoxious.

Shared global locks

There is a possibility of throughput issues due to context-wide locks in AmbientTask methods. If everything is using the same global context, there are now global locks between all AmbientTask calls.
Could these locks be removed? Probably yes, if we have the time and expertise to do something lock-free.

Outstanding questions

  1. Has the need for this disappeared along with the five significant fixes and changes that I implemented before publishing the MyGet package? Maybe the whole thing was/is due to a bug.

  2. If not, can the use case be clarified? Why is AmbientTasks.Add/Post being called from a place where there was no execution context flow from the AmbientTasks.BeginContext call?

    • Was the AmbientTasks.BeginContext call too late in the program?
    • Is the program using Unsafe methods to opt out of flowing ExecutionContext? Why? (Using AmbientTasks across this boundary seems very bad.)
    • Is the use case only for AmbientTasks.Add/Post, or does AmbientTasks.WaitAllAsync have this same need? I'd like to make WaitAllAsync throw.
    • Can I see an example that I can run? ❤️
  3. Is it acceptable to add this API as AmbientTasks.Experimental.SetGlobalFallbackHandler with warnings in the XML docs?

Enable tests to report the stack traces of AmbientsTasks entry points for incomplete tasks

Reporting entry point stack traces would make the test assertion message significantly more useful in the case where you aren't sure where or why you should be adding AmbientTasks.WaitAllAsync(); to your test:

The test started ambient tasks but did not wait for them.

using System;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using Techsola;

[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public sealed class WaitForAmbientTasksAttribute : Attribute, ITestAction
{
    public ActionTargets Targets => ActionTargets.Test;

    public void BeforeTest(ITest test)
    {
        AmbientTasks.BeginContext();
    }

    public void AfterTest(ITest test)
    {
        switch (TestContext.CurrentContext.Result.Outcome.Status)
        {
            case TestStatus.Failed:
            case TestStatus.Inconclusive:
            case TestStatus.Skipped:
                return;
        }

        var task = AmbientTasks.WaitAllAsync();
        if (!task.IsCompleted) Assert.Fail("The test started ambient tasks but did not wait for them.");
        task.GetAwaiter().GetResult();
    }
}

Convenience overload AmbientTasks.Post(Func<Task>)

One place so far, refactoring from:

private async void EventHandler(object sender, EventArgs e)
{
    await Task.Yield();

    // ...

    await BarAsync();
}

This would be a nice result (proposed):

private void EventHandler(object sender, EventArgs e)
{
    AmbientTasks.Post(async () =>
    {
        // ...

        await BarAsync();
    });
}

This workaround happened to work out, though it would not be as simple if there had been multiple awaits:

private void EventHandler(object sender, EventArgs e)
{
    AmbientTasks.Post(() =>
    {
        // ...

        AmbientTasks.Add(BarAsync());
    });
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.