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:
hns 2001-02-26 19:28:55 +00:00
parent 9fa2d5154f
commit 69d9b49ce3
4 changed files with 94 additions and 14 deletions

View file

@ -17,8 +17,11 @@ public class RequestTrans implements Serializable {
public String path;
public String session;
private Hashtable values;
// this is used to hold the EcmaScript form data object
public transient Object data;
// when was execution started on this request?
public transient long startTime;
public RequestTrans () {
super ();
@ -49,4 +52,24 @@ public class RequestTrans implements Serializable {
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;
}
}
}

View file

@ -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.
*/
public void close () {
if (buffer != null)
public synchronized void close () {
if (buffer != null) {
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 () {

View file

@ -54,6 +54,9 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
long requestTimeout = 60000; // 60 seconds for request timeout.
ThreadGroup threadgroup;
// Map of requesttrans -> active requestevaluators
Hashtable activeRequests;
protected String templateExtension, scriptExtension, actionExtension, skinExtension;
// A transient node that is shared among all evaluators
@ -124,6 +127,7 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
freeThreads.push (ev);
allThreads.addElement (ev);
}
activeRequests = new Hashtable ();
typemgr = new TypeManager (this);
typemgr.check ();
@ -229,9 +233,23 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
ResponseTrans res = null;
RequestEvaluator ev = null;
// are we responsible for releasing the evaluator and closing the result?
boolean primaryRequest = false;
try {
ev = getEvaluator ();
res = ev.invoke (req, u);
// first look if a request with same user/path/data is already being executed.
// 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) {
// let the servlet know that this application has gone to heaven
throw stopped;
@ -240,10 +258,14 @@ public class Application extends UnicastRemoteObject implements IRemoteApp, Runn
res = new ResponseTrans ();
res.write ("Error in application: <b>" + x.getMessage () + "</b>");
} 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;
}

View file

@ -533,18 +533,37 @@ public class RequestEvaluator implements Runnable {
this.user = user;
this.res = new ResponseTrans ();
checkThread ();
wait (app.requestTimeout);
if (reqtype != NONE) {
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>");
try {
app.activeRequests.put (req, this);
checkThread ();
wait (app.requestTimeout);
if (reqtype != NONE) {
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;
}
/**
* 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 {
this.reqtype = XMLRPC;