(I apologize for changing the question)
The following snippet is from a MVC.NET controller (.NET: v4.5; AspNet.MVC: v5.2.3) .
After LongOperation is called, it:
Spawn a process
Waits for its completion
Monitors a few LOG files
Uses SignalR to notify browser of the progress from the LOG files
(I have omitted the code for simplicity)
All this works, only while LongOperation is running, no other HTTP requests are handled by the controllers.
They get handled after the LongOperation completes and the action method returns result to the AJAX call.
What am I messing up?
Thank you in advance.
Update (for #angelsix comment):
Here is a simplified setup:
I have removed async/await as advised
Added breakpoints as advised
Verified they are hit as explained in the above
Basically: same result, see the console.log-ed text and timestamps
Will appreciate any help from the community.
Thank you in advance!
GUI and log
Action methods in the Controller
[AjaxOnly]
public ActionResult _RunLongOperation(string hubId)
{
try
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
ProgressNotifierHub.Notify(hubId, string.Format("Notification from _RunLongOperation {0}", i));
}
return new HttpStatusCodeResult(HttpStatusCode.OK, "_RunLongOperation : OK");
}
catch (Exception)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "_RunLongOperation : NOK");
}
}
[AjaxOnly]
public ActionResult _RunAnotherOperation(string hubId)
{
return new HttpStatusCodeResult(HttpStatusCode.OK, "_RunAnotherOperation : OK");
}
Razor View (partial) and javascript with SignalR hub setup Ajax calls
<script src="~/signalr/hubs"></script>
#{
Layout = null;
}
<button id="longOperationBtn" type="button" class="t-button" style='width: 155px'>Long Operation</button>
<button id="anotherOperationBtn" type="button" class="t-button" style='width: 155px'>Another Operation</button>
<script type="text/javascript">
$(function () {
setupEventHandlers();
setupProgressNorificator();
});
function setupEventHandlers() {
$('#longOperationBtn').click(function (event) {
requestOperation('_RunLongOperation')
});
$('#anotherOperationBtn').click(function (event) {
requestOperation('_RunAnotherOperation')
});
}
function requestOperation(method) {
trace(method + ' requested');
$.ajax({
url: '/Profiles/Validate/' + method,
type: 'GET',
data: { hubId: $.connection.hub.id },
contentType: 'application/json; charset=utf-8',
success: function () {
trace(method + ' completed');
},
error: function () {
trace(method + ' failed');
}
});
}
function setupProgressNorificator(profileId) {
var hub = $.connection.progressNotifierHub;
hub.client.notify = function (notification) {
console.log(notification);
};
$.connection.hub.start();
}
function trace(s) {
console.log('[' + new Date().toUTCString() + '] ' + s);
}
</script>
It looks like you are running the client "test" on Chrome. I'm not sure what the 2018 limitation is (it seems to change from release to release), but browsers do limit the number of concurrent connections allowed to the same host. I believe Chrome's limit is 6 or 8.
The log you posted appears to be from the client side. If it is, the behavior you are seeing could actually be the client waiting for a free connection to connect to the server - the problem may not be with ASP.NET at all, but rather with how you are testing.
It would be easy to validate - write another test that you can run concurrently with your current test in a separate browser window that just calls the "short" operation. If it has latency, I'm wrong. If it doesn't, well hopefully I've helped out!
The issue
Your response does not mention anything about caching. So I suspect the browser is caching the response and using that.
Verify
To verify press F12 in the browser to open developer tools and then see if the second response shows a status of 304 or states Cached
Solve
To prevent caching on the action, inside the action you want to do this
Prevent Caching in ASP.NET MVC for specific actions using an attribute
According to this:
Some ... protocols like HTTP Polling (XHR) use up to two simultaneous connections per ... client. It is important to understand that the maximum number of connections is per browser and not per browser tab. Attempting to run multiple clients within the same browser might cause this limit to be reached.
In my case, SignalR client was using long polling. A fried reported that when Web sockets are used, there was no blocking issue. Closing the subject.
Thanks for the help Joe.
You need to set your controller to have read-only session state behavior. Then you can do ajax requests to the controller during running a long controller method. In other case (as the one that you are complaining about) the requests will be queued and call all at once after finishing the controller action call. Just put this before controller class definition
[SessionState(SessionStateBehavior.ReadOnly)]
Related
I have a asp.net MVC application which uses J query and Ajax. And one of cshtml page which includes a process running for a long time, so i made a async method and using ajax i was able to trigger it so hoping that the user can do some other work in a different page while the long process running in the background.
Its working fine when I'm working in the same page. But when i redirect to another page while the code is running in back end I'm not getting any output. Is it not possible to redirect while the another method is running? if so how can i get this done.
My c# contoller
public async System.Threading.Tasks.Task<JsonResult> TestAsyncPro(string ComponentId)
{
try
{
await System.Threading.Tasks.Task.Delay(4000);
return Json(new { Success = true, Error = "" }, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(new { Success = false, Error = ex.Message }, JsonRequestBehavior.AllowGet);
}
}
Ajax Call
$('#testasnc').click(function ()
{
$.ajax({
url: '../AccountOpening/TestAsyncPro',
type: 'POST',
dataType: 'json',
cache: false,
async: true,
data: { ComponentId: '21' },
success: function (data) {
if (data.Success == true)
{
$(window).scrollTop(0);
$("#alrtTestAsnc").show();
setTimeout(function () { $("#alrtTestAsnc").hide(); }, 5000);
}
}
});
var url = $("#RedirectTo").val();
location.href = url;
});
so i made a async method and using ajax i was able to trigger it so hoping that the user can do some other work in a different page while the long process running in the background.
That's not what async is for.
What you really want is an independent background worker. ASP.NET works by handling HTTP requests, and you want some work done outside an HTTP request (i.e., after the HTTP request completes). Thus, the work you want to do is not a natural fit for ASP.NET.
I usually recommend a proper distributed system design for this kind of situation. This means adding a reliable queue (Azure Queue / Amazon SQS / etc), and a separate backend processor (Azure Function / Amazon Lambda / ASP.NET Core background service / Win32 service / etc). Then the ASP.NET handler will enqueue the background work to do to the queue, and return a redirect; the backend processor will pick up the work from the queue and actually execute it.
Hangfire is a low-entry solution; it uses a database as a reliable queue and has an in-process backend processor (similar to ASP.NET Core background service hosted in an ASP.NET Core HTTP application). There are some tradeoffs with this approach (e.g., you can't scale your backend independently), but it's an OK solution if you're not on ASP.NET Core yet.
I'm developing a video website using ASP.NET MVC.
One functionality I want to have in my application is transocding video. But as the transcoding process could be very time-consuming, I want to show the progress of that process to the client user.
So, my schema is to use one controller action to handle the whole transcoding process and write its progress into a file stored on the server. Meanwhile I use Ajax to call another controller action to read the specified file, retrieve the progress information and send it back to the client for display every 2 seconds during the transcoding process.
To fulfill my plan, I have written the following code:
Server Side:
public class VideoController : Controller
{
//Other action methods
....
//Action method for transcoding a video given by its id
[HttpPost]
public async Task<ActionResult> Transcode(int vid=0)
{
VideoModel VideoModel = new VideoModel();
Video video = VideoModel.GetVideo(vid);
string src = Server.MapPath("~/videos/")+video.Path;
string trg = Server.MapPath("~/videos/") + +video.Id+".mp4";
//The file that stores the progress information
string logPath = Server.MapPath("~/videos/") + "transcode.txt";
string pathHeader=Server.MapPath("../");
if (await VideoModel.ConvertVideo(src.Trim(), trg.Trim(), logPath))
{
return Json(new { result = "" });
}
else
{
return Json(new { result = "Transcoding failed, please try again." });
}
}
//Action method for retrieving the progress value from the specified log file
public ActionResult GetProgress()
{
string logPath = Server.MapPath("~/videos/") + "transcode.txt";
//Retrive the progress from the specified log file.
...
return Json(new { progress = progress });
}
}
Client Side:
var progressTimer = null;
var TranscodeProgress = null;
// The function that requests server for handling the transcoding process
function Transcode(vid) {
// Calls the Transcode action in VideoController
var htmlobj = $.ajax({
url: "/Video/Transcode",
type: "POST",
//dataType: 'JSON',
data: { 'vid': vid },
success: function(data)
{
if(data.result!="")
alert(data.result);
}
else
{
//finalization works
....
}
}
});
//Wait for 1 seconds to start retrieving transcoding progress
progressTimer=setTimeout(function ()
{
//Display progress bar
...
//Set up the procedure of retrieving progress every 2 seconds
TranscodeProgress = setInterval(Transcoding, 2000);
}, 1000);
}
//The function that requests the server for retrieving the progress information every 2 seconds.
function Transcoding()
{
//Calls the GetProgress action in VideoController
$.ajax({
url: "/Video/GetProgress",
type: "POST",
//dataType: 'JSON',
success: function (data)
{
if (data.progress == undefined || data.progress == null)
return;
progressPerc = parseFloat(data.progress);
//Update progress bar
...
}
});
}
Now the Client-side code and the Transcode action method all work fine. The problem is that the GetProgress method will never get called until the Transcode action finishes its whole procedure. So what's wrong with my code? How can I modify it to make those two actions work spontaneously so as to achieve my goal?
Update
Based on Alex's answer, I found that my problem is caused by the session lock mechanism of Asp.Net framework. So disabling the SessionState of my VideoController or setting it as read-only does make the controller responses to the request for retrieving transcoding progress when the action method of transcoding video is being executed. But because I use Session in my VideoController to store some variables for use across multiple requests, this way couldn't be a suitable solution for my problem. Is there a better way to solve it?
You misunderstood the whole point about async/await. It doesn't change the fact that for each single request, there is a single response that is returned. When you call await in your action, nothing is returned to client yet. The only thing it does (in a very high level abstraction) is to release the current thread that handles this request to a thread pool so it could be used for processing other requests. So basically it allows you to use your server resources more efficiently since there are no threads that are wasted waiting for long I/O operations to complete. Once the I/O operation is completed the the execution of the action (that called for await) continued. Only at the end of an action the response is sent to the client.
As for you scenario, if it is a long running task, I would use some kind of background processing solution such as Hangfire and use SignalR to push updates from server.Here is an example
You can also implement something similar on your own (example).
UPDATE:
As #Menahem stated in his comment I maybe misinterpreted part of your question.
Request queuing issue may be caused by incorrect configuration of SessionStateBehavior. Since MvcHandler handler which is used by ASP.NET MVC is marked with IRequiresSessionState interface, only one request at time could be processed per session. In order to change that, make you controller sessionless (or ar least make sure that you are not writing into session in this controller) and mark it with
[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)] attribute.
File creation is blocking call. In other words, until first thread will not close file, second one which makes report will not be able to read contents of that file. As workaround you can create files with percentage of progress. For example movie1-5-percent.txt, movie1-10-percent.txt, movie1-15-percent.txt etc, in order to avoid blocking calls to file system. Then you can check, if for movie1 there is file movie1-15-percent.txt, then you can report to ajax call, that 15 percent of movie was converted. Or choose another non blocking storage. For example you can report result to db in first thread, and read results from db in another.
I have an AJAX action that can take a couple of minutes to complete depending upon the amount of data involved. If a user gets frustrated and navigates away while this action is still running, what happens to the controller? Does it complete anyway? Does it know the request should be abandoned and dispose of the controller object?
It will not cancel the request to the server as the act of navigating away does not send any information back to the server regarding that request. The client (browser), however, will stop listening for it. After the request finishes, regardless if the client was listening for it or not, the controller will dispose as it typically would.
With that said, you could get fancy and use a combination of listening for a page change on the client side and calling abort on the AJAX request to the server.
This SO question discusses how to abort a request. I could imagine setting a variable when you first start the AJAX request and then unsetting it when it finished.
Warning - Pseudo code below
var isProcessing = false;
var xhr = $.ajax({
type: "POST",
url: "myUrl",
beforeSend: function(){
isProcessing = true;
}
complete: function(){
isProcessing = false;
}
});
window.onbeforeunload = function(){
if(isProcessing){
xhr.abort();
}
}
The above is very basic idea of the concept, but there should probably be some checks around if the xhr object exists, perhaps also bind/unbind the window.onbeforeunload in the beforeSend and complete object handlers for the .ajax() item.
Here is my simple ajax function:
var callback = function () {
$.ajax({
url: "/Home/Timer",
success: function (response) {
console.log(response); // Fails, but only in IE10
$("#target").html(response);
}
});
}
setInterval(callback, 1000);
and the Controller/Action:
public String Timer()
{
Debug.WriteLine(DateTime.Now.ToString()); // Shows correctly in all browsers
return DateTime.Now.ToString();
}
which works fine in Opera, Chrome, Firefox, but not IE10 for the weirdest reason. In every other browser, the console logs the current time but in IE10, it keeps logging the same time over and over again. I put a breakpoint on my Timer method, and it hits the method correctly, but somehow when it gets back to the success callback it reports the wrong time. Why would that happen?
cache maybe?
try setting cache:false in the ajax options
In safari and some older browsers, (from what I've experienced till now, cache:false doesnt seem to work sometimes). for a more cross browser compatible solution, you could add a data option to the ajax() call and add a random number generator as a parameter. It would be something like this.
var callback = function () {
$.ajax({
url: "/Home/Timer",
//start random number generator
data : { r: Math.random() }
//end random number generator
success: function (response) {
console.log(response); // Fails, but only in IE10
$("#target").html(response);
}
});
}
This way, everytime a server call happens, a new random number will be generated, and since the data is different from the previous requests, ajax requests wont be cached and will ensure a fresh server call every single time.
edit under package and set cache:false in your CSS and call each intermediate function under main class
Im trying to call a server method in my aspx.cs to delete all the files in a directory when a user closes the browser.
[WebMethod]
public static void fileDelete()
{
string[] uploadedFiles = Directory.GetFiles(#"C:\Users\Lambo\Documents\Visual Studio 2010\Projects\test\test\testPdfIn");
foreach (string uploaded in uploadedFiles)
{
File.Delete(uploaded);
}
}
======================================================================
EDIT
I've tried the POST method but it still doesn't seem to work. I've changed the URL too.
Over at the client side im using this:
$(function () {
$(window).unload(function () {
alert("Files have been deleted")
jQuery.ajax({ type: 'POST', url: "http://localhost:19642/success.aspx/fileDelete", async: true });
});
});
However it doesnt seem to be working. Are the codes wrong in someway?
To investigate failures of AJAX calls use HTTP debugger (like Fiddler ) to see what requests are made and what responses are received.
My guess is that your Url is wrong and request is made to wrong file. Consider making absolute (or at lest server relative) url.