gRPC C#: how to cancel upload correctly - c#

For uploading images from a client to the server I use chunking.
Here is the client code:
private async Task UploadPersonImage(int personId, string fileName, CancellationToken cancellationToken)
{
var stream = Client.UploadPersonImage();
PersonImageMessage personImageMessage = new PersonImageMessage();
personImageMessage.PersonId = personId;
personImageMessage.ImageType = ImageType.Jpg;
byte[] image = File.ReadAllBytes(fileName);
int imageOffset = 0;
byte[] imageChunk = new byte[imageChunkSize];
while (imageOffset < image.Length && !cancellationToken.IsCancellationRequested)
{
int length = Math.Min(imageChunkSize, image.Length - imageOffset);
Buffer.BlockCopy(image, imageOffset, imageChunk, 0, length);
imageOffset += length;
ByteString byteString = ByteString.CopyFrom(imageChunk);
personImageMessage.ImageChunk = byteString;
await stream.RequestStream.WriteAsync(personImageMessage).ConfigureAwait(false);
}
await stream.RequestStream.CompleteAsync().ConfigureAwait(false);
if (!cancellationToken.IsCancellationRequested)
{
var uploadPersonImageResult = await stream.ResponseAsync.ConfigureAwait(false);
// Process answer...
}
}
And this is the server code:
public override async Task<TransferStatusMessage> UploadPersonImage(
IAsyncStreamReader<PersonImageMessage> requestStream, ServerCallContext context)
{
TransferStatusMessage transferStatusMessage = new TransferStatusMessage();
transferStatusMessage.Status = TransferStatus.Success;
try
{
await Task.Run(
async () =>
{
CancellationToken cancellationToken = context.CancellationToken;
await using (Stream fs = File.OpenWrite(ImageFileName))
{
await foreach (PersonImageMessage personImageMessage in
requestStream.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
fs.Write(personImageMessage.ImageChunk.ToByteArray());
}
}
}).ConfigureAwait(false);
}
// Is thrown on cancellation -> ignore...
catch (OperationCanceledException)
{
transferStatusMessage.Status = TransferStatus.Cancelled;
}
catch (RpcException rpcEx)
{
if (rpcEx.StatusCode == StatusCode.Cancelled)
{
transferStatusMessage.Status = TransferStatus.Cancelled;
}
else
{
_logger.LogError($"Exception while processing image file '{ImageFileName}'. Exception: '{requestStream}'");
transferStatusMessage.Status = TransferStatus.Failure;
}
}
// Delete incomplete file
if (transferStatusMessage.Status != TransferStatus.Success)
{
File.Delete(ImageFileName);
}
return transferStatusMessage;
}
Everything works fine. I want to cancel the upload in between sending the chunks. Now, CompleteAsync() is called and the server thinks the data transfer ended successfully. I'm looking for a way to trigger the cancellation in the server (i.e. the CancellationToken in the ServerCallContext) via the client.
As a workaround I could add a flag to PersonImageMessage, something like 'upload_cancelled', to tell the server that the transfer is aborted. But there must be a built-in mechanism.
Does somebody know the trick?

You can pass cancellationToken to your Client stream RPC method. Then the server can receive it as context.cancellationToken.
var stream = Client.UploadPersonImage(cancellationToken: cancellationToken);
If the client cancels, stream.RequestStream.CompleteAsync() should not be called.

Related

Return response to caller when one of the async call is done

I need to save file in two different server using .net api.Its asp.net Core web api. I want to return the response to the caller when saving the file in any of the server is succeeded. Could this be achieved using async programming? Can we return the response from api and let it save at other location?
For now I am using Parallel option:
var imageName = $"{Guid.NewGuid().ToString()}.{extension}";
var locations = new ConcurrentDictionary<string, string>();
Parallel.ForEach(destinationFolders, folder =>
{
try
{
var fullName = $#"{folder.Value}\{imageName}";
if (!Directory.Exists(folder.Value))
Directory.CreateDirectory(folder.Value);
var bytes = Convert.FromBase64String(content);
using (var imageFile = new FileStream(fullName, FileMode.Create))
{
imageFile.Write(bytes, 0, bytes.Length);
imageFile.Flush();
}
locations.TryAdd(folder.Key, Regex.Replace(folder.Value, #"\\", #"\"));
}
catch (Exception ex)
{
Logging.Log.Error($"{Constants.ExceptionOccurred} : {ex.Message} against {folder.Key} Value : {Regex.Replace(folder.Value, #"\\", #"\")}");
}
});
However the problem here is I am making it to wait while it finishes saving in both the location and return the response.
You do not appear to need Task Parallel for this. Consider the following truly asynchronous approach. Also note, you only need to do the Base64 decode once.
public Task WriteFiles(IEnumerable<string> destinationFolders, string content)
{
byte[] bytes = Convert.FromBase64String(content);
IList<Task> tasks = new List<Task>();
foreach (var folder in destinationFolders)
{
// <snipped>
Func<Task> taskFactory = async () =>
{
using (var imageFile = new FileStream(fullName, FileMode.Create))
{
await imageFile.WriteAsync(bytes, 0, bytes.Length);
await imageFile.FlushAsync();
}
};
tasks.Add(taskFactory());
// <snipped>
}
return Task.WhenAny(tasks);
}

ASP.NET Core with WebSockets - WebSocket handshake never occurs

I am very new to C# programming, having previously only worked with Java. This project I am building should be very straightforward - we have a web page with a selection of foreign currency pairs. The element chosen is sent to the server, which responds with a hardcoded value of their exchange rate. The requirement is that both actions are implemented through the use of WebSockets. Here is the JS code on my page:
var protocol;
var wsUri;
var socket;
window.onload = function(e) {
e.preventDefault();
protocol = location.protocol === "https:" ? "wss:" : "ws:";
wsUri = protocol + "//" + window.location.host;
socket = new WebSocket(wsUri);
socket.onopen = e => {
console.log("socket opened", e);
};
document.getElementById("currencypair").onchange = function()
{
var selector = document.getElementById("currencypair");
var text = selector.options[selector.selectedIndex].text;
socket.send(text);
};
socket.onmessage = function (evt) {
var receivedMessage = evt.data;
document.getElementById("output").html(receivedMessage);
};
};
Here is a snippet of the Startup.cs class Configure method:
app.UseWebSockets();
app.UseMiddleware<WebSocketMiddleware>();
And here is the middleware class to process requests.
public class WebSocketMiddleware
{
private readonly RequestDelegate _next;
public WebSocketMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await _next.Invoke(context);
return;
}
var ct = context.RequestAborted;
using (var socket = await context.WebSockets.AcceptWebSocketAsync())
{
while (true)
{
var stringReceived = await ReceiveStringAsync(socket, ct);
if (CurrencyPairCollection.CurrencyPairs.TryGetValue(stringReceived, out var value))
{
await SendStringAsync(socket, value.ToString(), ct);
}
else
{
throw new Exception("Unexpected value");
}
await Task.Delay(1000, ct);
}
}
}
private static async Task<string> ReceiveStringAsync(WebSocket socket, CancellationToken ct = default(CancellationToken))
{
var buffer = new ArraySegment<byte>();
using (var ms = new MemoryStream())
{
WebSocketReceiveResult result;
do
{
ct.ThrowIfCancellationRequested();
result = await socket.ReceiveAsync(buffer, ct);
ms.Write(buffer.Array, buffer.Offset, result.Count);
}
while (!result.EndOfMessage);
ms.Seek(0, SeekOrigin.Begin);
if (result.MessageType != WebSocketMessageType.Text || result.Count.Equals(0))
{
throw new Exception("Unexpected message");
}
using (var reader = new StreamReader(ms, Encoding.UTF8))
{
return await reader.ReadToEndAsync();
}
}
}
private static Task SendStringAsync(WebSocket socket, string data, CancellationToken ct = default(CancellationToken))
{
var segment = new ArraySegment<byte>(Encoding.UTF8.GetBytes(data));
return socket.SendAsync(segment, WebSocketMessageType.Text, true, ct);
}
}
Please mind I was working with the following example which contains mistakes listed by people in the comment section. I did my best to resolve them, however due to my limited experience, that may be where the fault lies.
https://www.softfluent.com/blog/dev/Using-Web-Sockets-with-ASP-NET-Core
Basically, upon running the app the browser console immediately reports this:
WebSocket connection to 'ws://localhost:51017/' failed: Error during WebSocket handshake: Unexpected response code: 200
I have been able to answer my own question. So in Startup.cs of which I provided only a snippet, a call to app.UseMvc() is made right before the lines I have already shared. This is generated by the default template. The trick was to move this call to below the following:
app.UseWebSockets();
app.UseMiddleware<WebSocketMiddleware>();
as otherwise the request pipeline is disrupted.
This will allow our socket to open, however without changing the following line in async Task ReceiveStringAsync(...)
var buffer = new ArraySegment<byte>();
to
var buffer = new ArraySegment<byte>(new byte[8192]);
it will still close prematurely. Next, just needed to correct JS syntax error. Changed
document.getElementById("output").html(receivedMessage);
to
document.getElementById("output").value = receivedMessage;
That's it, it works.

BsonBinaryReader with NetworkStream

Is there any possibility for making BsonBinaryReader accept a nonseekable stream e.g. NetworkStream?
So I don't have to save all the data persistently and afterward start parsing it via BsonBinaryReader but can instead happen on the fly?
Example:
var response = new NetworkStream(MAGIC);
var reader = new BsonBinaryReader(response)
while (!reader.EndOfStream)
{
if (reader.GotEnoughData())
{
var bson = BsonSerializer.Deserialize<BsonDocument>(reader);
}
}
I solved the issue using the answer from https://stackoverflow.com/a/28036366/1435802 with a 16MB (Max BSON document size) buffer as input to the BsonBinaryReader.
For be able to read until end of stream i used the following:
public static Task WhileNotEndOfStreamAsync(this Stream stream, Action action, CancellationToken token = default(CancellationToken))
{
return Task.Run(() => {
try
{
while (!token.IsCancellationRequested)
{
action();
}
}
catch (EndOfStreamException)
{
// Swallow the 'EndOfStream' Exception
}
}, token);
}
To read the data until EOS:
using (var response = new ReadSeekableStream(networkStream))
using (var reader = new BsonBinaryReader(response))
{
await response.WhileNotEndOfStreamAsync(() =>
{
var bson = BsonSerializer.Deserialize<BsonDocument>(reader);
}, token);
}

HttpClient.GetAsync(...) "deadlock" when there is no internet connection

Recenly I found some sort of bug while using System.Net.http.HttpClient().GetAsync(...)
When in the middle process of GetAsync(..), if I disconnect my internet connection, the app just act like deadlock, and no exception catch, even I wait for few minutes.
Here is the example of code:
private async Task<WriteableBitmap> loadImageAsync(string url)
{
using (var httpClient = new HttpClient())
{
//When this line of code start execute, immediately disable the internet connection
var response = await httpClient.GetAsync(url);
//Once it get stuck at GetAsync(..), it won't execute the codes below
byte[] imageByte = await response.Content.ReadAsByteArrayAsync();
WriteableBitmap bitmap = new WriteableBitmap(1920, 1080);
using (InMemoryRandomAccessStream randomStream = new InMemoryRandomAccessStream())
{
using (DataWriter writer = new DataWriter(randomStream))
{
writer.WriteBytes(imageByte);
await writer.StoreAsync();
await writer.FlushAsync();
writer.DetachStream();
}
randomStream.Seek(0);
await bitmap.SetSourceAsync(randomStream);
}
return bitmap;
}
}
I had try to set timeout to the httpClient, but it won't care what I set if there is no internet connection.
Is there any way to catch if the internet connection is disconnected, or did I miss something?
Edit: another async task that await loadImageAsync(..)
public async Task GetImagesAsync()
{
//skip if image is currently loading
if (IsImageLoading) { return; }
IsImageLoading = true;
ImageObjs.Clear();
try
{
int numberOfLoad = (int)((double)_LocalSettings.Values[NUMBER_OF_IMAGE_LOAD_SETTINGS]);
string region = _LocalSettings.Values[REGION].ToString();
#region Get Bing Image info
string jsonString = await new HttpClient().GetStringAsync($"http://www.bing.com/HPImageArchive.aspx?format=js&n={numberOfLoad}&mkt={region}");
ImageCollection imageCollection = JsonConvert.DeserializeObject<ImageCollection>(jsonString);
#endregion
#region Download images
foreach (var i in imageCollection.images)
{
string url;
if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.UI.ViewManagement.StatusBar"))
{
url = i.url.Replace("1920x1080", "1080x1920");
}
else
{
url = i.url;
}
ImageObjs.Add(new ImageObj(await loadImageAsync(bingLink + url), i.copyright, url, i.copyrightlink, region));
}
showToastNotification(_MainViewModel.ImageObjs.Count.ToString() + " images loaded.");
#endregion
}
catch
{
//if connection failed
}
finally
{
IsImageLoading = false;
}
}
have you tried setting timeout with CancelationToken?
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
try
{
var response = await httpClient.GetAsync(url, tokenSource.Token);
}
catch (TaskCanceledException ex)
{
// handle timeout here
}

async task cancellation c# xamarin

I have a functionality of search users. I have provided a textview and on that textview changed method I'm firing a method to get data from web server. But I'm facing problem when user types letter, because all the api hits done in async task. Service should be hit after 100 milli-sec of wait, means if user types a letter "a" then doesn't type for 100 milli-sec then We have to hit the service. But if user types "a" then "b" then "c", so one service should be hit for "abc", not for all.
I followed the official link, but it doesn't help me
https://msdn.microsoft.com/en-us/library/jj155759.aspx
So basically here is my code
textview.TextChange+= (sender,e) =>{
CancellationTokenSource cts = new CancellationTokenSource();
await Task.Delay(500);
// here some where I have to pass cancel token
var lst = await APIClient.Instance.GetUserSearch("/user/get?searchTerm=" + newText, "application/json",cts);
if (lst != null && lst.Count > 0){
lstSearch.AddRange(lst);
}
}
Here is my method to GetUser
public async Task<JResponse> GetUserSearch<JResponse>(string uri, string contentType,CancellationToken cts)
{
try
{
Console.Error.WriteLine("{0}", RestServiceBaseAddress + uri);
string url = string.Format("{0}{1}", RestServiceBaseAddress, uri);
var request = (HttpWebRequest)WebRequest.Create(url);
request.ContentType = contentType;
if (Utility.CurrentUser != null && !string.IsNullOrWhiteSpace(Utility.CurrentUser.AuthToken))
{
request.Headers.Add("api_key", Utility.CurrentUser.AuthToken);
}
request.Method = "POST";
var payload = body.ToString();
request.ContentLength = payload.Length;
byte[] byteArray = Encoding.UTF8.GetBytes(body.ToString());
request.ContentLength = byteArray.Length;
using (var stream = await request.GetRequestStreamAsync())
{
stream.Write(byteArray, 0, byteArray.Length);
stream.Close();
}
using (var webResponse = await request.GetResponseAsync())
{
var response = (HttpWebResponse)webResponse;
using (var reader1 = new StreamReader(response.GetResponseStream()))
{
Console.WriteLine("Finished : {0}", uri);
var responseStr = reader1.ReadToEnd();
var responseObj = JsonConvert.DeserializeObject<JResponse>(
responseStr,
new JsonSerializerSettings()
{
MissingMemberHandling = MissingMemberHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
});
return responseObj;
}
}
}
catch (System.Exception ex)
{
Utility.ExceptionHandler("APIClient", "ProcessRequestAsync", ex);
}
return default(JResponse);
}
In your example, you are creating a CancellationTokenSource - you need to hold a reference to it, so that the next time the handler is invoked, the previous search can be cancelled. Here is an example console app that you should be able to run, but the important piece is in the handler.
private CancellationTokenSource _cts;
private async void TextChangedHandler(string text) // async void only for event handlers
{
try
{
_cts?.Cancel(); // cancel previous search
}
catch (ObjectDisposedException) // in case previous search completed
{
}
using (_cts = new CancellationTokenSource())
{
try
{
await Task.Delay(TimeSpan.FromSeconds(1), _cts.Token); // buffer
var users = await _userService.SearchUsersAsync(text, _cts.Token);
Console.WriteLine($"Got users with IDs: {string.Join(", ", users)}");
}
catch (TaskCanceledException) // if the operation is cancelled, do nothing
{
}
}
}
Be sure to pass the CancellationToken into all of the async methods, including those that perform the web request, this way you signal the cancellation right down to the lowest level.
Try to use timer. First time then you change text - you create it. Then you change text after that - you restart timer. If you don't change text for 700 milliseconds - timer will fire PerformeSearch method. Use Timeout.Infinite for timer period parameter to prevent it from restarting.
textview.TextChange += (sender,e) =>
{
if (_fieldChangeTimer == null)
_fieldChangeTimer = new Timer(delegate
{
PerformeSearch();
}, null, 700, Timeout.Infinite);
else
{
_fieldChangeTimer.Change(700, Timeout.Infinite);
}
};
Instantiate the CancellationTokenSource.
cts = new CancellationTokenSource(); Example method
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
if (cts != null)
{
cts.Cancel();
}
}

Categories

Resources