15

I'm having problems canceling my XHR requests when navigating between pages. I have a page that has 8 requests that get fired off. I cancel them on click of a link outside of the current page. The page stalls as it waits on the next document to load. They XHR requests appear as cancelled in developer tools, but the new document stalls as if it is waiting for them to come back.

Here you can see the page is stalled even though all the other requests are cancelled. The new page is the only pending request...

enter image description here

And here you can see once the page finally did make the jump the TTFB is 52.52s. If I wait for the calls to come back before clicking away the jump is instant.

enter image description here

Here are the headers for the new pages once it finally loads if that helps... enter image description here

I use the following frankenstein code to manage XHR requests. I have a cancelAll function towards the bottom that aborts the requests...

 XHRManager = {
Requests: [],
pendingRequests: [],
addNextRequest: function (r) {
    var timeout = 0;
    if (trace.isDevelopment()) {
        timeout = 350;
    }
    setTimeout(function () {
        if (r.url ==  XHRManager.pendingRequests[0].url && r.start ==  XHRManager.pendingRequests[0].start) {
             XHRManager.pendingRequests.splice(0, 1);
        }
        else {
            $( XHRManager.pendingRequests).each(function (ii, dd) {
                if (dd.url == r.url && dd.start == r.start) {
                     XHRManager.pendingRequests.splice(ii, 1);
                }
            });
        }
         XHRManager.startNextRequest();
        if (trace.findLocalStorage()) {
             XHRManager.showTrace = true;
            trace.show();
        }
    }, timeout);
},
requests: [],
intervals: [],
requestsInt: 0,
firstRun: true,
delay: 500,
globalTimeout: 5000,
showTrace: false,
startNextRequest: function () {
    $( XHRManager.pendingRequests).each(function (i, d) {
        if (d.start) {

        }
        if (i == 0) {
            if (trace.domWatcher.constructor == Function) {
                trace.domWatcher(d.requestNumber);
            }
            trace.log("Request #" + d.requestNumber + " started");
            d.requestType(d);
        }
    });
    if ( XHRManager.pendingRequests.length == 0) {
        if (trace.isDevelopment()) {
            trace.show();
        }
    }
},
AddToPendingRequests: function (url, params, cb, type, errCB) {
    var rI =  XHRManager.requestsInt;
     XHRManager.requestsInt++;
    var req = {url: url, params: params, cb: cb, requestNumber: rI, requestType: type};
    if (errCB) {
        req.errCB = errCB;
    }
     XHRManager.pendingRequests.push(req);
    // if(trace.findLocalStorage()){
    //    trace.show();
    //  }
    if (rI == 0 ||  XHRManager.pendingRequests.length == 1) {
         XHRManager.startNextRequest();
    }
},
writeVals: function (url, params, data, start, cb, requestNumber) {
    if ($("meta[content='development']").length > 0) {
        try {
            var response = {};
            response.requestNumber = requestNumber;
            if (data.sql != "" && data.sql != undefined) {
                response.sql = data.sql;
            }
            if (data.debug) {
                if (data.debug.sql != "" && data.debug.sql != undefined) {
                    response.sql = data.debug.sql;
                }
            }
            if (data.data != "" && data.data != undefined) {
                response.data = data.data;
            }
            else {
                if (data != "" || data != undefined) {
                    response.data = data;
                }
            }
            if (url != "" && url != undefined) {
                response.url = url;
            }
            if (params != "" && params != undefined) {
                response.params = params;
            }
            if (cb) {
                response.cb = cb.toString();
            }
            else {
                response.cb = "";
            }
            response.requestStats = {};
            response.requestStats.start = start;
            response.requestStats.end = Date();
            response.requestStats.totalTime = ((new Date(response.requestStats.end)).getTime() - (new Date(start)).getTime()) / 1000 + " sec(s)";
             XHRManager.Requests.push(response);

        }
        catch (e) {
            trace.log(e);
        }
    }
},
_create: function (r) {
    var xm =  XHRManager;
    var start = Date();
    var req = $.get(r.url, r.params, r.cb)
        .done(function (data) {
             XHRManager.writeVals(r.url, r.params, data, start, r.cb, r.requestNumber);
            if (trace.isDevelopment() && trace.isOn()) {
                 XHRManager.addNextRequest(r);
            }
        });
    xm.requests.push(req);
},
_createAjax: function (r) {
    var xm =  XHRManager;
    var start = Date();
    if (r.type == "PUT" || r.type == "DELETE") {
        var req = $.ajax({
            type: r.type,
            xhrFields: {
                withCredentials: true
            },
            url: r.url,
            data: r.params,
            success: function (data) {
                 XHRManager.writeVals(r.url, r.params, r.data, r.start, r.cb, r.requestNumber);
                r.cb(data);
                if (trace.isDevelopment() && trace.isOn()) {
                     XHRManager.addNextRequest(r);
                }
            },
            error: r.errCB
        });
        xm.requests.push(req);
    }
    else {
        var req = $.ajax({
            type: r.type,
            xhrFields: {
                withCredentials: true
            },
            dataType: 'json',
            json: 'json',
            url: r.url,
            data: r.params,
            success: function (data) {
                 XHRManager.writeVals(r.url, r.params, data, start, r.cb, r.requestNumber);
                r.cb(data);
                if (trace.isDevelopment() && trace.isOn()) {
                     XHRManager.addNextRequest(r);
                }
            },
            error: r.errCB
        });
        xm.requests.push(req);
    }
},
_createJSON: function (r) {
    var start = Date();
    var xm =  XHRManager;
    var req = $.getJSON(r.url, r.params, r.cb)
        .done(function (data) {
             XHRManager.writeVals(r.url, r.params, data, start, r.cb, r.requestNumber);
            if (trace.isDevelopment() && trace.isOn()) {
                 XHRManager.addNextRequest(r);
            }
        });
    xm.requests.push(req);
},
create: function (url, params, cb) {
    if (trace.isDevelopment() && trace.isOn()) {
         XHRManager.AddToPendingRequests(url, params, cb,  XHRManager._create);
    }
    else {
        var r = {};
        r.url = url;
        r.params = params;
        r.cb = cb;
         XHRManager._create(r);
    }
},
createAjax: function (url, params, type, cb, errCB) {
    if (trace.isDevelopment() && trace.isOn()) {
         XHRManager.AddToPendingRequests(url, params, cb,  XHRManager._createAjax, errCB);
    }
    else {
        var r = {};
        r.url = url;
        r.params = params;
        r.cb = cb;
        r.type = type;
        r.errCB = errCB;
         XHRManager._createAjax(r);
    }

},
createJSON: function (url, params, cb) {
    if (trace.isDevelopment() && trace.isOn()) {
         XHRManager.AddToPendingRequests(url, params, cb,  XHRManager._createJSON);
    }
    else {
        var r = {};
        r.url = url;
        r.params = params;
        r.cb = cb;
         XHRManager._createJSON(r);
    }
},
remove: function (xhr) {
    var xm =  XHRManager;
    var index = xm.requests.indexOf(xhr);
    if (index > -1) {
        xm.requests.splice(index, 1);
    }
    index = xm.intervals.indexOf(xhr.interval);
    if (index > -1) {
        xm.intervals.splice(index, 1);
    }
},
cancelAll: function () {
    var xm =  XHRManager;
    $(xm.requests).each(function () {
        var t = this;
        t.abort();
    });
    $(xm.intervals).each(function () {
        var t = this;
        clearInterval(t);
    });
    xm.requests = [];
    xm.intervals = [];
}
};

The site uses jQuery, PHP, Zend Framework 2, and SQL, Apache. What am I missing?

Machavity
  • 30,841
  • 27
  • 92
  • 100
Adam Youngers
  • 6,421
  • 7
  • 38
  • 50
  • After more review, this does appear to be a server side PHP issue? Still not sure how to tackle it though. – Adam Youngers Jul 12 '15 at 20:01
  • Apache imight be queueing the requests coming from the same client and waiting to finish old ones before processing new requests. At least you can try to locate the problem by posting to a php script which includes ` – Ugur Jul 13 '15 at 20:44
  • How is php running? php-fpm, mod_php, suphp or [...]? What is the output of `ini_get('ignore_user_abort');`? What is the server side script doing (ie. one long heavy task, a single but very expensive database query, a bunch of smaller tasks or [...])? Maybe [this](http://www.php.net/manual/en/features.connection-handling.php) could help. – Mikk3lRo Jul 15 '15 at 19:26
  • Are you using sessions in PHP? – Brendan Smith Jul 17 '15 at 07:00

3 Answers3

7

Probable causal chain

  1. the server does not realise the XHR requests are cancelled, and so the corresponding PHP processes keep running
  2. these PHP processes use sessions, and prevent concurrent access to this session until they terminate

Possible solutions

Adressing either of the above two points breaks the chain and may fix the problem:

  1. (a) ignore_user_abort is FALSE by default, but you could be using a non-standard setting. Change this setting back to FALSE in you php.ini or call ignore_user_abort(false) in the scripts that handle these interruptible requests.

Drawback: the script just terminates. Any work in progress is dropped, possibly leaving the system in a dirty state.

  1. (b) By default, PHP will not detect that the user has aborted the connection until an attempt is made to send information to the client. Do echo something periodically during the course of your long-running script.

Drawback: this dummy data might corrupt the normal output of your script. And here too, the script may leave the system in a dirty state.

  1. A PHP sessions is stored as a file on the server. On session_start(), the script opens the session file in write mode, effectively acquiring an exclusive lock on it. Subsequent requests that use the same session are put on hold until the lock is released. This happens when the script terminates, unless you close the session explicitely. Call session_write_close() or session_abort() as early as possible.

Drawback: when closed, the session cannot be written anymore (unless you reopen the session, but this is somewhat inelegant a hack). Also the script does keep running, possibly wasting resources.

I definitely recommend the last option.

Community
  • 1
  • 1
RandomSeed
  • 29,301
  • 6
  • 52
  • 87
  • I suggest move sessions into DB and in this case this issue will be solved. I had a similar problem and I solved it this way (your point # 2). – stepozer Jul 18 '15 at 11:27
  • Well this does not quite address the issue IMHO. Either you allow more than one script to write session data and you introduce a race condition, or you set proper locks on the database rows and you are back to square one. – RandomSeed Jul 19 '15 at 10:44
  • Nevertheless, the database approach does make it easier to lock/unlock the session at various stages of the scripts. – RandomSeed Jul 19 '15 at 10:48
1

Are you storing your Ajax Request on a variable?. If not, that's what you need to do to completely cancel a request

var xhr = $.ajax({
    type: "POST",
    url: "anyScript.php",
    data: "data1=0&data2=1",
    success: function(msg){
       //Success Function
    }
});

//here you abort the request
xhr.abort()
jecarfor
  • 498
  • 8
  • 22
  • I am assigning them to a variable and running abort() on them. The lines in red above are cancelled requests so the abort() seems to be working. – Adam Youngers Jul 09 '15 at 21:42
1

I assume that you have done that, but check all log files (php and apache).

Also try this:

php.ini

upload_max_filesize = 256M
post_max_size = 256M

.htaccess

php_value upload_max_filesize 256M
php_value post_max_size 256M

Another thing that bugs me is this part.

$(xm.requests).each(function () {
    var t = this;
    t.abort();
});
$(xm.intervals).each(function () {
    var t = this;
    clearInterval(t);
});

Try passing arguments to the callback and abort through them. I have seen cases, where assigning this to a variable withing $.each loop actually points to a different object or the global window.

$(xm.requests).each(function (index, value) {
        value.abort();
    });
    $(xm.intervals).each(function (index, value) {
        clearInterval(value);
    });
Stanimir Dimitrov
  • 1,872
  • 2
  • 20
  • 25