ChannelReader Completion Task is never completed after OperationCanceledException - c#

If I call Stop(), OperationCanceledException is happened and _writer.TryComplete(exp) is true. But _reader.Completion Task is still not completed.
Is it desired behavior of Channels? If yes can someone tell me how to stop a Channel without waiting till it's empty and have its Completion Task in Completed state?
public interface IItem
{
Uri SourceUri { get; }
string TargetPath { get; }
}
public class Item : IItem
{
public Item(Uri sourceUri, string targetPath)
{
SourceUri = sourceUri;
TargetPath = targetPath;
}
public Uri SourceUri { get; }
public string TargetPath { get; }
}
public class TestService
{
private readonly ChannelWriter<IItem> _writer;
private readonly ChannelReader<IItem> _reader;
private readonly CancellationTokenSource _cts;
public TestService()
{
_cts = new CancellationTokenSource();
Channel<IItem> channel = Channel.CreateUnbounded<IItem>();
_reader = channel.Reader;
_writer = channel.Writer;
}
public async Task QueueDownload(IItem information)
{
await _writer.WriteAsync(information);
}
public void StartDownload()
{
Task.Factory.StartNew(async () =>
{
await ProcessDownloadAsync();
}, TaskCreationOptions.LongRunning);
}
public void Stop()
{
_cts.Cancel();
//_writer.Complete();
//_writer = null;
Console.WriteLine("Stop");
}
public async Task Wait()
{
await _reader.Completion;
}
private async Task ProcessDownloadAsync()
{
try
{
while (await _reader.WaitToReadAsync(_cts.Token))
{
IItem information = await _reader.ReadAsync(_cts.Token);
using (WebClient webClient = new WebClient())
{
Console.WriteLine(information.TargetPath);
await webClient.DownloadFileTaskAsync(information.SourceUri,
information.TargetPath);
}
}
}
catch (OperationCanceledException exp)
{
bool res = _writer.TryComplete(exp);
}
}
}
static class Program
{
static async Task Main(string[] args)
{
TestService tSvc = new TestService();
await tSvc.QueueDownload(new Item(new Uri(#"https://images.pexels.com/" +
#"photos/753626/pexels-photo-753626.jpeg"), #"D:\\Temp\1.png"));
await tSvc.QueueDownload(new Item(new Uri(#"https://images.pexels.com/" +
#"photos/753626/pexels-photo-753626.jpeg"), #"D:\\Temp\1.png"));
await tSvc.QueueDownload(new Item(new Uri(#"https://images.pexels.com/" +
#"photos/753626/pexels-photo-753626.jpeg"), #"D:\\Temp\1.png"));
await tSvc.QueueDownload(new Item(new Uri(#"https://images.pexels.com/" +
#"photos/753626/pexels-photo-753626.jpeg"), #"D:\\Temp\1.png"));
tSvc.StartDownload();
Task t = tSvc.Wait();
tSvc.Stop();
await t;
Console.WriteLine("Finished");
}
}

The ChannelWriter.Complete method behaves a bit differently than one would expect. It is not invalidating instantly the contents of the channel. Instead, it just prevents adding more items in the channel. The existing items are still valid for consumption, and the ChannelReader.Completion property will not complete before all stored items are consumed.
The example below demonstrates this behavior:
var channel = Channel.CreateUnbounded<int>();
channel.Writer.TryWrite(1);
channel.Writer.Complete(new FileNotFoundException());
//channel.Reader.TryRead(out var data);
var completed = channel.Reader.Completion.Wait(500);
Console.WriteLine($"Completion: {(completed ? "OK" : "Timed-out")}");
Output:
Completion: Timed-out
You can uncomment the channel.Reader.TryRead line, to see the FileNotFoundException to emerge.

Related

Code stops when using HttpContent.ReadAsAsync

I am trying to read async from HttpContent but my code exists on this method. It used to work but I recently had to switch to a console app because I wanted to make a discord bot. Does anyone know why the code "stops" when I use this in a console app?
I've tried using an async Main as suggested but it does not work
I am using this code:
public class BazaarInfo
{
[JsonProperty("products")]
public List<BazaarProduct> Products { get; set; }
public static async Task<BazaarInfo> BuildAsync()
{
string url = "https://api.hypixel.net/skyblock/bazaar";
using (HttpResponseMessage response = await ApiHelper.GetApiClient("application/json").GetAsync(url))
{
if (response.IsSuccessStatusCode)
{
BazaarInfo output = await response.Content.ReadAsAsync<BazaarInfo>(); //Stops
return output;
}
else
{
return null;
}
}
}
}
I call it from here:
public class Bazaar
{
public Dictionary<string, BazaarProduct> Products { get; set; }
public static async Task<Bazaar> BuildAsync()
{
var output = new Bazaar();
var bazaarInfo = await BazaarInfo.BuildAsync();
output.Products = bazaarInfo.Products.ToDictionary(product => product.Name);
return output;
}
}
And this:
[Command("bazaar")]
public async Task BuildBzItem ([Remainder]string id)
{
var bazaar = await Bazaar.BuildAsync();
string sellSummary = "";
foreach (var summary in bazaar.Products[id].SellSummary)
sellSummary += summary.Amount + summary.Price;
var builder = new Discord.EmbedBuilder()
{
Description = sellSummary
};
await ReplyAsync("", false, builder.Build());
}
And then here with a discord chat event:
private async Task HandleCommandAsync(SocketMessage arg)
{
var message = arg as SocketUserMessage;
var context = new SocketCommandContext(Client, message);
if (message.Author.IsBot) return;
int argPos = 0;
if(message.HasStringPrefix("!", ref argPos))
{
var result = await Commands.ExecuteAsync(context, argPos, Services);
if (!result.IsSuccess) Console.Write(result.ErrorReason);
}
}
And this event is assigned here:
public static async Task Main(string[] args) => await new Program().RunBotAsync();
private DiscordSocketClient Client { get; set; }
private CommandService Commands { get; set; }
private IServiceProvider Services { get; set; }
public async Task RunBotAsync()
{
Client = new DiscordSocketClient();
Commands = new CommandService();
Services = new ServiceCollection().AddSingleton(Client).AddSingleton(Commands).BuildServiceProvider();
string token = "ODQ1MzE1OTY2OTcxODA1NzI3.YKfL1w.SPXi_0xXbbrMziZ9JWiqHFX4dto";
Client.Log += ClientLog;
await RegisterCommandsAsync();
await Client.LoginAsync(TokenType.Bot, token);
await Client.StartAsync();
await Task.Delay(-1);
}
private Task ClientLog(LogMessage arg)
{
Console.WriteLine(arg);
return Task.CompletedTask;
}
public async Task RegisterCommandsAsync()
{
Client.MessageReceived += HandleCommandAsync;
await Commands.AddModulesAsync(Assembly.GetEntryAssembly(), Services);
}
I've tried using an async Main as suggested but it does not work
If you can't use async Main, then block in the Main method:
public static void Main(string[] args) => new Program().RunBotAsync().GetAwaiter().GetResult();

Canceling an independent task

I need to spawn multiple tasks having in mind that I could add more tasks through a SignalR hub later on. Those tasks should be named, so I can search for a specific task. Each of these tasks should be able to be canceled independently (separate CancellationTokenSource).
Questions:
Is there a different collection that I can use?
Can I improve my code in any way?
public interface IMyClass
{
void Start(string name);
void Stop(string name);
}
public class MyClass : IMyClass
{
private List<Tuple<string, Task, CancellationTokenSource>> _tasks = new List<Tuple<string, Task, CancellationTokenSource>>();
public void Start(string name)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Factory.StartNew(() => DoWork(name, cts.Token));
_tasks.Add(new Tuple<string, Task, CancellationTokenSource>(name, task, cts));
}
public void Stop(string name)
{
foreach (var tuple in _tasks)
{
if (tuple.Item1.Contains(name))
{
CancellationTokenSource cts = tuple.Item3;
cts.Cancel();
}
}
}
public void DoWork(string name, CancellationToken token)
{
try
{
while (true)
{
Console.WriteLine($"{name} is working");
// long operation...
Thread.Sleep(1000);
if (token.IsCancellationRequested)
{
Console.WriteLine($"{name} canceled");
token.ThrowIfCancellationRequested();
}
}
}
catch (OperationCanceledException ex)
{
Console.WriteLine(ex.Message);
}
}
}
I think it looks fine, but you may consider if it is allowed to use the same task name twice.
And instead of
private List<Tuple<string, Task, CancellationTokenSource>> _tasks = new List<Tuple<string, Task, CancellationTokenSource>>();
you could use a dictionary
private Dictionary<string,Tuple<Task, CancellationTokenSource>> _tasks = new Dictionary<string,Tuple<Task, CancellationTokenSource>>();
I would personally wrap the information about the task in a new type, that makes it easier to maintain and add new features later. Something like this:
public class TaskInfo
{
public string Name { get; set; }
public Task Task { get; set; }
public CancellationTokenSource Token { get; set; }
}
// Define other methods and classes here
public interface IMyClass
{
void Start(string name);
void Stop(string name);
}
public class MyClass : IMyClass
{
private Dictionary<string,TaskInfo> _tasks = new Dictionary<string,TaskInfo>();
public void Start(string name)
{
if(_tasks.ContainsKey(name))
throw new Exception($"Task with name {name} already exists");
CancellationTokenSource cts = new CancellationTokenSource();
TaskInfo taskInfo = new TaskInfo() {
Token = cts,
Name = name,
Task = Task.Factory.StartNew(() => DoWork(name, cts.Token))
};
_tasks.Add(name,taskInfo);
}
public void Stop(string name)
{
if (_tasks.ContainsKey(name)) {
_tasks[name].Token.Cancel();
}
}
public void DoWork(string name, CancellationToken token)
{
try
{
while (true)
{
Console.WriteLine($"{name} is working");
// long operation...
Thread.Sleep(1000);
if (token.IsCancellationRequested)
{
Console.WriteLine($"{name} canceled");
token.ThrowIfCancellationRequested();
}
}
}
catch (OperationCanceledException ex)
{
Console.WriteLine(ex.Message);
}
}
}
this may not be a better solution, but i prefer an approach like this

Fill ObservableCollection with results of multiple async calls without waiting

In my Prism module in ViewModel class in OnNavigatedTo method
I would like to fill an ObservableCollection with results of multiple async calls without waiting for all calls to complete.
I am using answer from this question:
How to hydrate a Dictionary with the results of async calls?
The following code is a cleaned-up version of my real code:
My status class:
public class Status
{
public string ipAddress;
public string status;
}
My view model:
using Prism.Mvvm;
using Prism.Regions;
public class StatusViewModel : BindableBase, INavigationAware
{
ObservableCollection<Status> statusCollection = new ObservableCollection<Status>();
HttpClient httpClient = new HttpClient();
Ping ping = new Ping();
List<string> ipAddressList = new List<string>();
public void OnNavigatedTo(NavigationContext navigationContext)
{
GetEveryStatus();
}
public void GetEveryIp() // this is not important, works ok
{
var addressBytes = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork).GetAddressBytes();
for (byte i = 1; i < 255; ++i)
{
addressBytes[3] = i;
string ipAddress = new IPAddress(addressBytes).ToString();
if (ping.Send(ipAddress, 10).Status == IPStatus.Success)
{
ipAddressList.Add(ipAddress);
}
}
}
public void GetEveryStatus() // this is important, here is the problem
{
GetEveryIp();
var task = GetStatusArray(ipAddressList);
statusCollection.AddRange(task.Result);
}
// solution from stackoverflow, but it throws exception
public async Task<Status[]> GetStatusArray(List<string> ipAddressList)
{
Status[] statusArray = await Task.WhenAll(
ipAddressList.Select(
async ipAddress => new Status(
ipAddress,
await httpClient.GetStringAsync("http://" + ipAddress + ":8080" + "/status")
)
)
);
return statusArray;
}
}
but that didn't work because GetStringAsync can throw an exception, so I changed it to this:
public void GetEveryStatus() // this is important, here is the problem
{
GetEveryIp();
foreach (string ipAddress in ipAddressList)
{
try
{
var task = httpClient.GetStringAsync("http://" + ipAddress + ":8080" + "/status");
statusCollection.Add(new Status(ipAddress, task.Result));
}
catch (Exception)
{
}
}
}
but it still doesn't work.
What is the right way to do this? Thank you!
Thanks to #AccessDenied for explaining the role of async in interface implementation.
Thanks to #Selvin for explaining Task.Result and Task.Wait.
If anyone is interesed in the final solution, here it is:
PositioningModule is a hardware device, this class has nothing to do with Prism.Modularity.IModule
public class PositioningModule
{
public string IpAddress { get; set; }
public PositioningModuleStatus PositioningModuleStatus { get; set; }
public PositioningModule(string ipAddress, PositioningModuleStatus positioningModuleStatus)
{
IpAddress = ipAddress;
PositioningModuleStatus = positioningModuleStatus;
}
}
The ViewModel:
I had to use BindingOperations.EnableCollectionSynchronization and lock on the ObservableCollection. This is the main reason why it didn't work async before!
Changing OnNavigatedTo to async blocked the UI, so I used Task.Run().
using Prism.Mvvm;
using Prism.Regions;
public class DomePositioningViewModel : BindableBase, INavigationAware
{
ObservableCollection<PositioningModule> _positioningModuleCollection = new ObservableCollection<PositioningModule>();
readonly object _lock = new object();
DomePositioningModel _domePositioningModel = new DomePositioningModel();
public DomePositioningViewModel()
{
BindingOperations.EnableCollectionSynchronization(_positioningModuleCollection, _lock);
}
public /* async */ void OnNavigatedTo(NavigationContext navigationContext)
{
//await _domePositioningModel.ScanForModulesAsync(AddModule); - this blocks the UI
Task.Run(() => _domePositioningModel.ScanForModulesAsync(AddModule));
}
private void AddModule(PositioningModule module)
{
lock (_lock)
{
_positioningModuleCollection.Add(module);
}
}
}
The Model:
I changed Send to SendPingAsync and I had to use new Ping() instead of ping.
Using Select instead of foreach to make the calls parallel made everything much faster!
#define PARALLEL
public class DomePositioningModel
{
private readonly HttpClient _httpClient = new HttpClient();
public DomePositioningModel()
{
_httpClient.Timeout = TimeSpan.FromMilliseconds(50);
}
public async Task ScanForModulesAsync(Action<PositioningModule> AddModule)
{
List<string> ipAddressList = new List<string>();
var addressBytes = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork).GetAddressBytes();
for (addressBytes[3] = 1; addressBytes[3] < 255; ++addressBytes[3])
{
ipAddressList.Add(new IPAddress(addressBytes).ToString());
}
//Ping ping = new Ping(); - this behaves strangely, use "new Ping()" instead of "ping"
#if PARALLEL
var tasks = ipAddressList.Select(async ipAddress => // much faster
#else
foreach (string ipAddress in ipAddressList) // much slower
#endif
{
PingReply pingReply = await new Ping().SendPingAsync(ipAddress, 10); // use "new Ping()" instead of "ping"
if (pingReply.Status == IPStatus.Success)
{
try
{
string status = await _httpClient.GetStringAsync("http://" + ipAddress + ":8080" + "/status");
if (Enum.TryParse(status, true, out PositioningModuleStatus positioningModuleStatus))
{
AddModule?.Invoke(new PositioningModule(ipAddress, positioningModuleStatus));
}
}
catch (TaskCanceledException) // timeout
{
}
catch (HttpRequestException) // could not reach IP
{
}
catch (Exception ex)
{
System.Windows.MessageBox.Show(ex.Message);
}
}
}
#if PARALLEL
);
await Task.WhenAll(tasks);
#endif
}
}
It didn't benchmark it because the difference is so obvious - about 0.5 sec instead of 14 sec!

C# async Deserializion method does not cancel

I'm running into the issue where the task being executed below runs asynchronously but is unable to be cancelled when the serializer reads the memory stream. When the user makes the cancellation request (by pressing a cancel button), a cancellation (the method cancel() is called fromt he token) is made but the task continues.
Service Class:
Async method called from LoadHelper() in Main class
public async void StartTask(Action callback, CancellationToken token)
{
await Task.Run(callback, token);
}
Main Class:
private void LoadHelper()
{
_services.GetInstance<IThreadService>().StartTask(
() => LoadHelperAsync(), _cancelService.Token);
}
Method being run async
private void LoadHelperAsync()
{
var serializer = new DataContractSerializer(typeof(UserDatabase));
string selectedDir;
ComparisonResult = null;
_services.GetInstance<IOpenFileService>().DisplayOpenFileDialog(
"Select a saved comparison",
"Comparison File(*.xml; *.cmp)|*.xml;*.cmp",
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
out selectedDir);
if (!string.IsNullOrEmpty(selectedDir))
{
_dispatchService.BeginInvoke(() => IsExecuting = true);
using (FileStream fileStream = new FileStream(selectedDir, FileMode.Open, FileAccess.Read))
using (DeflateStream compressedStream = new DeflateStream(fileStream, CompressionMode.Decompress))
using (BufferedStream regularStream = new BufferedStream(fileStream))
{
Stream memoryStream;
//Use filename to determine compression
if (selectedDir.EndsWith(".cmp", true, null))
{
memoryStream = compressedStream;
}
else
{
memoryStream = regularStream;
}
Report("Loading comparison");
Report(0);
IsExecuting = true;
ComparisonResult = (UserDatabase)serializer.ReadObject(memoryStream);
memoryStream.Close();
fileStream.Close();
IsExecuting = false;
Report("Comparison loaded");
}
_dispatchService.BeginInvoke(() => IsExecuting = false);
_dispatchService.BeginInvoke(() => ViewResults.ExecuteIfAble());
}
else
{
Report("No comparison loaded");
}
Cancellation code:
This command is binded to a "cancel" button in the view.
CancelCompare = new Command(o => _cancelService.Cancel(), o => IsExecuting);
From the CancellationService class
public class CancellationService : ICancellationService, IDisposable
{
private CancellationTokenSource _tokenSource;
public CancellationService()
{
Reset();
}
public CancellationToken Token { get; private set; }
public void Cancel()
{
_tokenSource.Cancel();
}
public void Reset()
{
_tokenSource = new CancellationTokenSource();
Token = _tokenSource.Token;
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_tokenSource.Cancel();
_tokenSource.Dispose();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Calling _tokenSource.Cancel(); won't do anything to a running task. A cancellation token interrupts task execution only if it was canceled before Task.Run
Take a look:
static void Main(string[] args)
{
using (var tokenSource = new CancellationTokenSource())
{
var aTask = StartTask(() =>
{
while (true)
{
Console.WriteLine("Nothing is going to happen.");
// Some long operation
Thread.Sleep(1000);
}
}, tokenSource.Token);
tokenSource.Cancel();
aTask.Wait();
}
Console.ReadKey();
}
static async Task StartTask(Action callback, CancellationToken cancellationToken)
{
await Task.Run(callback, cancellationToken);
}
If you want to cancel the execution, you should check the cancellation token by yourself, there is no any magic which cancels the task for you:
static void Main(string[] args)
{
using (var tokenSource = new CancellationTokenSource())
{
var aTask = StartTask(cancellationToken =>
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine("Will stop before that if canceled.");
// Some long operation
// Also pass the token all way down
Task.Delay(1000, cancellationToken).Wait();
}
}, tokenSource.Token);
// Try 0, 500, 1500 to see the difference
Thread.Sleep(1500);
tokenSource.Cancel();
try
{
aTask.Wait();
}
catch (Exception ex)
{
// AggregateException with OperationCanceledException
Console.WriteLine("Task was canceled.");
Console.WriteLine(ex.ToString());
}
}
Console.ReadKey();
}
static async Task StartTask(Action<CancellationToken> callback, CancellationToken cancellationToken)
{
await Task.Run(() => callback(cancellationToken), cancellationToken);
}
Note that this code only illustrates how the task works, never write anything like this in production.

Volatile IEnlistmentNotification, TransactionScope.AsyncFlowEnabled = true and complex async/wait

This is a followup question to the following question:
Volatile IEnlistmentNotification and TransactionScope.AsyncFlowEnabled = true
The approach accepted in the question above works as long as you don't await multiple statements. Let me show an example:
public class SendResourceManager : IEnlistmentNotification
{
private readonly Action onCommit;
public SendResourceManager(Action onCommit)
{
this.onCommit = onCommit;
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
preparingEnlistment.Prepared();
}
public void Commit(Enlistment enlistment)
{
Debug.WriteLine("Committing");
this.onCommit();
Debug.WriteLine("Committed");
enlistment.Done();
}
public void Rollback(Enlistment enlistment)
{
enlistment.Done();
}
public void InDoubt(Enlistment enlistment)
{
enlistment.Done();
}
}
public class AsyncTransactionalMessageSender : ISendMessagesAsync
{
private readonly List<Message> sentMessages = new List<Message>();
public IReadOnlyCollection<Message> SentMessages
{
get { return new ReadOnlyCollection<Message>(this.sentMessages); }
}
public async Task SendAsync(Message message)
{
if (Transaction.Current != null)
{
await Transaction.Current.EnlistVolatileAsync(
new SendResourceManager(async () => await this.SendInternal(message)),
EnlistmentOptions.None);
}
else
{
await this.SendInternal(message);
}
}
private async Task SendInternal(Message message)
{
Debug.WriteLine("Sending");
await Task.Delay(1000);
this.sentMessages.Add(message);
Debug.WriteLine("Sent");
}
}
[Test]
public async Task ScopeRollbackAsync_DoesntSend()
{
var sender = new AsyncTransactionalMessageSender();
using (var tx = new System.Transactions.TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
await sender.SendAsync(new Message("First"));
await sender.SendAsync(new Message("Second"));
await sender.SendAsync(new Message("Last"));
// We do not commit the scope
}
sender.SentMessages.Should().BeEmpty();
}
[Test]
public async Task ScopeCompleteAsync_Sends()
{
var sender = new AsyncTransactionalMessageSender();
using (var tx = new System.Transactions.TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
await sender.SendAsync(new Message("First"));
await sender.SendAsync(new Message("Second"));
await sender.SendAsync(new Message("Last"));
tx.Complete();
}
sender.SentMessages.Should().HaveCount(3)
.And.Contain(m => m.Value == "First")
.And.Contain(m => m.Value == "Second")
.And.Contain(m => m.Value == "Last");
}
As soon as you introduce a Task.Delay like shown in the example above the generated asynchronous statemachine will never come back and invoke the this.sentMessages.Add(message) and Debug.WriteLine("Sent")
The problem is I currently see now way to properly enlist asynchronous code inside the enlistment notification. Any ideas how to tackle this challenge?

Categories