Implemented request attachment: If a request with the same
user, path and input data is already being evaluated, the request is attached to that evaluation instead of starting a new one. This should fix the very common double- or multiple click problem, among others.
This commit is contained in:
parent
9fa2d5154f
commit
69d9b49ce3
4 changed files with 94 additions and 14 deletions
|
@ -17,8 +17,11 @@ public class RequestTrans implements Serializable {
|
||||||
public String path;
|
public String path;
|
||||||
public String session;
|
public String session;
|
||||||
private Hashtable values;
|
private Hashtable values;
|
||||||
|
|
||||||
// this is used to hold the EcmaScript form data object
|
// this is used to hold the EcmaScript form data object
|
||||||
public transient Object data;
|
public transient Object data;
|
||||||
|
// when was execution started on this request?
|
||||||
|
public transient long startTime;
|
||||||
|
|
||||||
public RequestTrans () {
|
public RequestTrans () {
|
||||||
super ();
|
super ();
|
||||||
|
@ -49,4 +52,24 @@ public class RequestTrans implements Serializable {
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int hashCode () {
|
||||||
|
return session == null ? super.hashCode () : session.hashCode ();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request is considered equal to another one if it has the same user, path,
|
||||||
|
* and request data. This is used to evaluate multiple simultanous requests only once
|
||||||
|
*/
|
||||||
|
public boolean equals (Object what) {
|
||||||
|
try {
|
||||||
|
RequestTrans other = (RequestTrans) what;
|
||||||
|
return (session.equals (other.session) &&
|
||||||
|
path.equalsIgnoreCase (other.path) &&
|
||||||
|
values.equals (other.getReqData ()));
|
||||||
|
} catch (Exception x) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,12 +142,28 @@ public class ResponseTrans implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called after writin to this response has finished and before it is shipped back to the
|
* This has to be called after writing to this response has finished and before it is shipped back to the
|
||||||
* web server. Transforms the string buffer into a char array to minimize size.
|
* web server. Transforms the string buffer into a char array to minimize size.
|
||||||
*/
|
*/
|
||||||
public void close () {
|
public synchronized void close () {
|
||||||
if (buffer != null)
|
if (buffer != null) {
|
||||||
response = new String (buffer).toCharArray();
|
response = new String (buffer).toCharArray();
|
||||||
|
buffer = null; // make sure this is done only once, even with more requsts attached
|
||||||
|
} else {
|
||||||
|
response = new char[0];
|
||||||
|
}
|
||||||
|
notifyAll ();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we just attached to evaluation we call this instead of close because only the primary thread
|
||||||
|
* is responsible for closing the result
|
||||||
|
*/
|
||||||
|
public synchronized void waitForClose () {
|
||||||
|
try {
|
||||||
|
if (response == null)
|
||||||
|
wait (10000l);
|
||||||
|
} catch (InterruptedException ix) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getContentString () {
|
public String getContentString () {
|
||||||
|
|
|
@ -53,6 +53,9 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
|
||||||
Thread worker;
|
Thread worker;
|
||||||
long requestTimeout = 60000; // 60 seconds for request timeout.
|
long requestTimeout = 60000; // 60 seconds for request timeout.
|
||||||
ThreadGroup threadgroup;
|
ThreadGroup threadgroup;
|
||||||
|
|
||||||
|
// Map of requesttrans -> active requestevaluators
|
||||||
|
Hashtable activeRequests;
|
||||||
|
|
||||||
protected String templateExtension, scriptExtension, actionExtension, skinExtension;
|
protected String templateExtension, scriptExtension, actionExtension, skinExtension;
|
||||||
|
|
||||||
|
@ -124,6 +127,7 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
|
||||||
freeThreads.push (ev);
|
freeThreads.push (ev);
|
||||||
allThreads.addElement (ev);
|
allThreads.addElement (ev);
|
||||||
}
|
}
|
||||||
|
activeRequests = new Hashtable ();
|
||||||
|
|
||||||
typemgr = new TypeManager (this);
|
typemgr = new TypeManager (this);
|
||||||
typemgr.check ();
|
typemgr.check ();
|
||||||
|
@ -229,9 +233,23 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
|
||||||
|
|
||||||
ResponseTrans res = null;
|
ResponseTrans res = null;
|
||||||
RequestEvaluator ev = null;
|
RequestEvaluator ev = null;
|
||||||
|
// are we responsible for releasing the evaluator and closing the result?
|
||||||
|
boolean primaryRequest = false;
|
||||||
try {
|
try {
|
||||||
ev = getEvaluator ();
|
// first look if a request with same user/path/data is already being executed.
|
||||||
res = ev.invoke (req, u);
|
// if so, attach the request to its output instead of starting a new evaluation
|
||||||
|
// this helps to cleanly solve "doubleclick" kind of users
|
||||||
|
ev = (RequestEvaluator) activeRequests.get (req);
|
||||||
|
if (ev != null) {
|
||||||
|
res = ev.attachRequest (req);
|
||||||
|
}
|
||||||
|
if (res == null) {
|
||||||
|
primaryRequest = true;
|
||||||
|
// if attachRequest returns null this means we came too late
|
||||||
|
// and the other request was finished in the meantime
|
||||||
|
ev = getEvaluator ();
|
||||||
|
res = ev.invoke (req, u);
|
||||||
|
}
|
||||||
} catch (ApplicationStoppedException stopped) {
|
} catch (ApplicationStoppedException stopped) {
|
||||||
// let the servlet know that this application has gone to heaven
|
// let the servlet know that this application has gone to heaven
|
||||||
throw stopped;
|
throw stopped;
|
||||||
|
@ -240,10 +258,14 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
|
||||||
res = new ResponseTrans ();
|
res = new ResponseTrans ();
|
||||||
res.write ("Error in application: <b>" + x.getMessage () + "</b>");
|
res.write ("Error in application: <b>" + x.getMessage () + "</b>");
|
||||||
} finally {
|
} finally {
|
||||||
releaseEvaluator (ev);
|
if (primaryRequest) {
|
||||||
|
releaseEvaluator (ev);
|
||||||
|
res.close (); // this needs to be done before sending it back
|
||||||
|
} else {
|
||||||
|
res.waitForClose ();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.close (); // this needs to be done before sending it back
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -533,18 +533,37 @@ public class RequestEvaluator implements Runnable {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.res = new ResponseTrans ();
|
this.res = new ResponseTrans ();
|
||||||
|
|
||||||
checkThread ();
|
try {
|
||||||
wait (app.requestTimeout);
|
app.activeRequests.put (req, this);
|
||||||
if (reqtype != NONE) {
|
|
||||||
IServer.getLogger().log ("Stopping Thread for Request "+app.getName()+"/"+req.path);
|
checkThread ();
|
||||||
stopThread ();
|
wait (app.requestTimeout);
|
||||||
res.reset ();
|
if (reqtype != NONE) {
|
||||||
res.write ("<b>Error in application '"+app.getName()+"':</b> <br><br><pre>Request timed out.</pre>");
|
IServer.getLogger().log ("Stopping Thread for Request "+app.getName()+"/"+req.path);
|
||||||
|
stopThread ();
|
||||||
|
res.reset ();
|
||||||
|
res.write ("<b>Error in application '"+app.getName()+"':</b> <br><br><pre>Request timed out.</pre>");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
app.activeRequests.remove (req);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This checks if the Evaluator is already executing an equal request. If so, attach to it and
|
||||||
|
* wait for it to complete. Otherwise return null, so the application knows it has to run the request.
|
||||||
|
*/
|
||||||
|
public synchronized ResponseTrans attachRequest (RequestTrans req) throws InterruptedException {
|
||||||
|
if (this.req == null || !this.req.equals (req) || reqtype == NONE)
|
||||||
|
return null;
|
||||||
|
// we already know our response object
|
||||||
|
ResponseTrans r = res;
|
||||||
|
wait (app.requestTimeout);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public synchronized Object invokeXmlRpc (String method, Vector args) throws Exception {
|
public synchronized Object invokeXmlRpc (String method, Vector args) throws Exception {
|
||||||
this.reqtype = XMLRPC;
|
this.reqtype = XMLRPC;
|
||||||
|
|
Loading…
Add table
Reference in a new issue