提交 4f7b5f3b 编写于 作者: J Jesse Glick

ProgressiveRendering

上级 991072bf
/*
* The MIT License
*
* Copyright 2012 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.util;
import edu.umd.cs.findbugs.annotations.SuppressWarnings;
import hudson.model.Computer;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import net.sf.json.JSON;
import net.sf.json.JSONObject;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.bind.JavaScriptMethod;
/**
* A helper thread which does some computation in the background and displays incremental results using JavaScript.
* This is appropriate when the computation may be slow—too slow to do synchronously within the initial HTTP request—and has no side effects
* (since it may be canceled if the user simply browses to another page while it is running).
* <ol>
* <li>Write a {@code <script>} section defining {@code function display(data)}.
* <li>Use {@code <l:progressiveRendering handler="${it.something()}" callback="display"/>} from your Jelly page to display a progress bar and initialize JavaScript infrastructure.
* <li>Implement {@code something()} to create an instance of your subclass of {@code ProgressiveRendering}.
* <li>Perform your work in {@link #compute}.
* <li>Periodically check {@link #canceled}.
* <li>As results become available, call {@link #progress}.
* <li>Make {@link #data} produce whatever JSON you want to send to the page to be displayed.
* </ol>
* {@code ui-samples-plugin} demonstrates all this.
* @since XXX
*/
public abstract class ProgressiveRendering {
private static final Logger LOG = Logger.getLogger(ProgressiveRendering.class.getName());
private static final int CANCELED = -1;
private static final int ERROR = -2;
private double status = 0;
private long lastNewsTime;
/** just for logging */
private final String uri;
private long start;
/** Constructor for subclasses. */
protected ProgressiveRendering() {
StaplerRequest currentRequest = Stapler.getCurrentRequest();
uri = currentRequest != null ? currentRequest.getRequestURI() : "?";
}
/**
* For internal use.
*/
@SuppressWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
public final void start() {
final SecurityContext securityContext = SecurityContextHolder.getContext();
executorService().submit(new Runnable() {
public void run() {
lastNewsTime = start = System.currentTimeMillis();
SecurityContext orig = SecurityContextHolder.getContext();
try {
SecurityContextHolder.setContext(securityContext);
compute();
if (status != CANCELED && status != ERROR) {
status = 1;
}
} catch (Exception x) {
LOG.log(Level.WARNING, "failed to compute " + uri, x);
status = ERROR;
} finally {
SecurityContextHolder.setContext(orig);
LOG.log(Level.FINE, "{0} finished in {1}msec with status {2}", new Object[] {uri, System.currentTimeMillis() - start, status});
}
}
});
}
/**
* Actually do the work.
* <p>The security context will be that in effect when the web request was made.
* @throws Exception whenever you like; the progress bar will indicate that an error occurred but details go to the log only
*/
protected abstract void compute() throws Exception;
/**
* Provide current data to the web page for display.
* <p>While this could be an aggregate of everything that has been computed so far,
* more likely you want to supply only that data that is new since the last call
* (maybe just {@code {}} or {@code []}),
* so that the page can incrementally update bits of HTML rather than refreshing everything.
* <p>You may want to make your implementation {@code synchronized}, so that it
* can track what was sent on a previous call, in which case any code running in
* {@link #compute} which modifies these fields should also <em>temporarily</em> be synchronized
* on the same monitor such as {@code this}.
* @return any JSON data you like
*/
protected abstract @Nonnull JSON data();
/**
* Indicate what portion of the work has been done.
* (Once {@link #compute} returns, the work is assumed to be complete regardless of this method.)
* @param completedFraction estimated portion of work now done, from 0 (~ 0%) to 1 (~ 100%)
*/
protected final void progress(double completedFraction) {
if (completedFraction < 0 || completedFraction > 1) {
throw new IllegalArgumentException(completedFraction + " should be in [0,1]");
}
status = completedFraction;
}
/**
* Checks whether the task has been canceled.
* If the rendering page fails to send a heartbeat within a certain amount of time,
* the user is assumed to have moved on.
* Therefore {@link #compute} should periodically say:
* {@code if (canceled()) return;}
* @return true if user seems to have abandoned us, false if we should still run
*/
protected final boolean canceled() {
if (status == ERROR) {
return true; // recent call to data() failed
}
long now = System.currentTimeMillis();
long elapsed = now - lastNewsTime;
if (elapsed > timeout()) {
status = CANCELED;
LOG.log(Level.FINE, "{0} canceled due to {1}msec inactivity after {2}msec", new Object[] {uri, elapsed, now - start});
return true;
} else {
return false;
}
}
/**
* For internal use.
*/
@JavaScriptMethod public final JSONObject news() {
lastNewsTime = System.currentTimeMillis();
JSONObject r = new JSONObject();
try {
r.put("data", data());
} catch (RuntimeException x) {
LOG.log(Level.WARNING, "failed to update " + uri, x);
status = ERROR;
}
r.put("status", status == 1 ? "done" : status == CANCELED ? "canceled" : status == ERROR ? "error" : status);
lastNewsTime = System.currentTimeMillis();
LOG.log(Level.FINE, "news from {0}: {1}", new Object[] {uri, r});
return r;
}
/**
* May be overridden to provide an alternate executor service.
* @return by default, {@link Computer#threadPoolForRemoting}
*/
protected ExecutorService executorService() {
return Computer.threadPoolForRemoting;
}
/**
* May be overridden to control the inactivity timeout.
* If no request from the browser is received within this time,
* the next call to {@link #canceled} will be true.
* @return timeout in milliseconds; by default, 15000 (~ 15 seconds)
*/
protected long timeout() {
return 15000;
}
}
<!--
The MIT License
Copyright 2012 Jesse Glick.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:x="jelly:xml">
<st:documentation>
<st:attribute name="handler" use="required">
Instance of jenkins.util.ProgressiveRendering.
</st:attribute>
<st:attribute name="callback" use="required">
Name of JavaScript function taking one JSON-valued parameter which renders results as they come.
</st:attribute>
<st:attribute name="tooltip">
Optional tooltip for progress bar.
</st:attribute>
</st:documentation>
<st:adjunct includes="lib.layout.progressiveRendering.progressiveRendering"/>
<!-- XXX generate an ID so we can use this more than once per page -->
<t:progressBar id="status" pos="0" tooltip="${tooltip ?: 'Computation in progress.'}"/><!-- XXX I18N -->
<j:invoke method="start" on="${handler}"/>
<script>progressivelyRender(<st:bind value="${handler}"/>, <st:out value="${callback}"/>);</script>
</j:jelly>
/*
* The MIT License
*
* Copyright 2012 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
function progressivelyRender(handler, callback) {
function checkNews(response) {
var r = response.responseObject();
if (r.status == 'done') {
// XXX why does window[callback] not work?
eval(callback)(r.data);
$('status').style.display = 'none';
} else if (r.status == 'canceled') {
// XXX ugly; replace with single tr of class=unknown?
$$('#status .progress-bar-done')[0].innerHTML = 'Aborted.';
} else if (r.status == 'error') {
$$('#status .progress-bar-done')[0].style.width = '100%';
$$('#status .progress-bar-left')[0].style.width = '0%';
$('status').class = 'progress-bar red'; // XXX does not seem to work
} else {
eval(callback)(r.data);
$$('#status .progress-bar-done')[0].style.width = (100 * r.status) + '%';
$$('#status .progress-bar-left')[0].style.width = (100 - 100 * r.status) + '%';
setTimeout(function() {
handler.news(checkNews);
}, 500);
}
}
handler.news(checkNews);
}
/*
* The MIT License
*
* Copyright 2012 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.plugins.ui_samples;
import hudson.Extension;
import java.util.LinkedList;
import java.util.List;
import jenkins.util.ProgressiveRendering;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
@Extension
public class ProgressivelyRendered extends UISample {
@Override public String getDescription() {
return "Shows how to use progressively rendered content to avoid overloading the server with a slow HTTP request.";
}
public ProgressiveRendering factor(final String numberS) {
return new ProgressiveRendering() {
final List<Integer> newFactors = new LinkedList<Integer>();
@Override protected void compute() throws Exception {
int number = Integer.parseInt(numberS); // try entering a nonnumeric value!
// Deliberately inefficient:
for (int i = 1; i <= number; i++) {
if (canceled()) {
return;
}
if (i % 1000000 == 0) {
Thread.sleep(10); // take a breather
}
if (number % i == 0) {
synchronized (this) {
newFactors.add(i);
}
}
progress(((double) i) / number);
}
}
@Override protected synchronized JSON data() {
JSONArray r = new JSONArray();
for (int i : newFactors) {
r.add(i);
}
newFactors.clear();
return new JSONObject().accumulate("newfactors", r);
}
};
}
@Extension
public static final class DescriptorImpl extends UISampleDescriptor {}
}
<!--
The MIT License
Copyright 2012 Jesse Glick.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:s="/lib/samples">
<s:sample title="Progressively Rendered">
<p>
Shows how to do something slow on the server while displaying progress.
</p>
<j:set var="number" value="${request.getParameter('number')}"/>
<form method="POST" action=".">
Enter a big number: <input name="number" type="text" value="${number}"/> <input name="Find Factors" type="submit"/>
</form>
<j:if test="${number != null}">
<script>
function display(r) {
for (var i = 0; r.newfactors.length > i; i++) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(r.newfactors[i]));
$(factors).appendChild(li);
}
}
</script>
<p>Factors of ${number}:</p>
<l:progressiveRendering handler="${it.factor(number)}" callback="display" tooltip="Factoring…"/>
<ul id="factors"/>
</j:if>
</s:sample>
</j:jelly>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册