From 9a24ad651ee67c3f6a3598d5161cfe76b619bcfd Mon Sep 17 00:00:00 2001 From: Heng Sin Low Date: Fri, 30 Nov 2012 01:17:44 +0800 Subject: [PATCH] IDEMPIERE-522 Zk: Random Freeze of screen update --- .../src/org/compiere/Adempiere.java | 2 +- org.adempiere.ui.zk/.classpath | 24 +- org.adempiere.ui.zk/META-INF/MANIFEST.MF | 13 +- .../zk/atmosphere/AtmosphereServerPush.java | 98 +- .../js/jawwa/atmosphere/jquery.atmosphere.js | 1360 ++++++++++++----- .../src/web/js/jawwa/atmosphere/serverpush.js | 74 +- org.adempiere.ui.zk/build.properties | 10 +- 7 files changed, 1085 insertions(+), 496 deletions(-) diff --git a/org.adempiere.base/src/org/compiere/Adempiere.java b/org.adempiere.base/src/org/compiere/Adempiere.java index 73ad45a85a..dc483443c0 100644 --- a/org.adempiere.base/src/org/compiere/Adempiere.java +++ b/org.adempiere.base/src/org/compiere/Adempiere.java @@ -553,7 +553,7 @@ public final class Adempiere } // startup private static void createThreadPool() { - int max = Runtime.getRuntime().availableProcessors() * 3; + int max = Runtime.getRuntime().availableProcessors() * 20; int min = max / 2; int defaultMax = max; int defaultMin = min; diff --git a/org.adempiere.ui.zk/.classpath b/org.adempiere.ui.zk/.classpath index 291b7a5ac1..86305e2f48 100644 --- a/org.adempiere.ui.zk/.classpath +++ b/org.adempiere.ui.zk/.classpath @@ -1,12 +1,12 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/org.adempiere.ui.zk/META-INF/MANIFEST.MF b/org.adempiere.ui.zk/META-INF/MANIFEST.MF index 45c5b862bb..2ece757b45 100644 --- a/org.adempiere.ui.zk/META-INF/MANIFEST.MF +++ b/org.adempiere.ui.zk/META-INF/MANIFEST.MF @@ -16,12 +16,13 @@ Import-Package: javax.servlet, org.slf4j.helpers;version="1.6.1", org.slf4j.spi;version="1.6.1" DynamicImport-Package: action.images -Bundle-ClassPath: WEB-INF/classes/, - WEB-INF/lib/atmosphere-runtime-0.9.jar, - WEB-INF/lib/atmosphere-compat-jbossweb-0.9.jar, - WEB-INF/lib/atmosphere-compat-tomcat-0.9.jar, - WEB-INF/lib/atmosphere-compat-tomcat7-0.9.jar, - WEB-INF/lib/calendar.jar +Bundle-ClassPath: ., + WEB-INF/classes/, + WEB-INF/lib/calendar.jar, + WEB-INF/lib/atmosphere-compat-jbossweb-1.0.4.jar, + WEB-INF/lib/atmosphere-compat-tomcat-1.0.4.jar, + WEB-INF/lib/atmosphere-compat-tomcat7-1.0.4.jar, + WEB-INF/lib/atmosphere-runtime-1.0.4.jar Export-Package: metainfo.zk, org.adempiere.webui, org.adempiere.webui.acct, diff --git a/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java index 8b03c20341..a9c8ef6dda 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java +++ b/org.adempiere.ui.zk/WEB-INF/src/fi/jawsy/jawwa/zk/atmosphere/AtmosphereServerPush.java @@ -20,9 +20,11 @@ the License. package fi.jawsy.jawwa.zk.atmosphere; import java.io.IOException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.atmosphere.cpr.AtmosphereResource; +import org.compiere.Adempiere; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zkoss.lang.Library; @@ -34,6 +36,7 @@ import org.zkoss.zk.ui.UiException; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.impl.ExecutionCarryOver; +import org.zkoss.zk.ui.sys.DesktopCtrl; import org.zkoss.zk.ui.sys.Scheduler; import org.zkoss.zk.ui.sys.ServerPush; import org.zkoss.zk.ui.util.Clients; @@ -112,35 +115,19 @@ public class AtmosphereServerPush implements ServerPush { return true; } - public synchronized void clearResource(AtmosphereResource resource) { + public void clearResource(AtmosphereResource resource) { this.resource.compareAndSet(resource, null); } - private synchronized void commitResponse() throws IOException { + private boolean commitResponse() throws IOException { AtmosphereResource resource = this.resource.getAndSet(null); - if (resource != null) { - resource.resume(); - } + if (resource != null && resource.isSuspended()) { + resource.resume(); + return true; + } + return false; } - private synchronized void onPush() throws IOException { - AtmosphereResource resource = this.resource.get(); - if (resource != null) { - switch (resource.transport()) { - case POLLING: - case LONG_POLLING: - if (resource.isSuspended()) - commitResponse(); - break; - case WEBSOCKET : - case STREAMING: - resource.getResponse().getWriter().write(""); - resource.getResponse().getWriter().flush(); - break; - } - } - } - @Override public boolean deactivate(boolean stop) { boolean stopped = false; @@ -175,11 +162,11 @@ public class AtmosphereServerPush implements ServerPush { } @Override - public synchronized void schedule(EventListener task, T event, + public void schedule(EventListener task, T event, Scheduler scheduler) { scheduler.schedule(task, event); try { - onPush(); + commitResponse(); } catch (IOException e) { log.error(e.getLocalizedMessage(), e); } @@ -215,26 +202,53 @@ public class AtmosphereServerPush implements ServerPush { } } - public synchronized void onRequest(AtmosphereResource resource) { - if (this.resource.get() != null) { - AtmosphereResource aResource = this.resource.get(); - if (aResource != resource) { - try { - onPush(); - } catch (IOException e) { - log.error(e.getLocalizedMessage(), e); - } - } - } - - this.resource.set(resource); - if (log.isTraceEnabled()) { + public void onRequest(AtmosphereResource resource) { + if (log.isTraceEnabled()) { log.trace(resource.transport().name()); } - if (!resource.isSuspended()) { - resource.suspend(-1, true); - } + + try { + commitResponse(); + } catch (IOException e) { + log.error(e.getLocalizedMessage(), e); + } + + DesktopCtrl desktopCtrl = (DesktopCtrl) this.desktop.get(); + if (desktopCtrl == null) { + log.error("No desktop available"); + return; + } + + boolean suspend = !desktopCtrl.scheduledServerPush(); + if (suspend) { + if (!resource.isSuspended()) { + resource.suspend(-1, true); + } + this.resource.set(resource); + + //check again, just in case task is schedule between the resource.suspend and resource.set call + if (desktopCtrl.scheduledServerPush()) { + scheduleCommit(); + } + } } + + private void scheduleCommit() { + Adempiere.getThreadPoolExecutor().schedule(new Runnable() { + @Override + public void run() { + DesktopCtrl desktopCtrl = (DesktopCtrl) AtmosphereServerPush.this.desktop.get(); + if (desktopCtrl == null) return; + if (desktopCtrl.scheduledServerPush()) { + try { + commitResponse(); + } catch (IOException e) { + log.error(e.getLocalizedMessage(), e); + } + } + } + }, 100, TimeUnit.MILLISECONDS); + } private static class ThreadInfo { private final Thread thread; diff --git a/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js index 1b90e0df6d..286c728330 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js +++ b/org.adempiere.ui.zk/WEB-INF/src/web/js/jawwa/atmosphere/jquery.atmosphere.js @@ -1,4 +1,6 @@ /** + * Copyright 2012 Jeanfrancois Arcand + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -12,23 +14,32 @@ * limitations under the License. */ /* - * Part of this code has been taked from - * - * jQuery Stream @VERSION - * Comet Streaming JavaScript Library - * http://code.google.com/p/jquery-stream/ + * IE streaming/XDR supports is copied/highly inspired by http://code.google.com/p/jquery-stream/ * * Copyright 2011, Donghwan Kim * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 * - * Compatible with jQuery 1.5+ + * LocalStorage supports is copied/highly inspired by https://github.com/flowersinthesand/jquery-socket + * Copyright 2011, Donghwan Kim + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * */ +/** + * Official documentation of this library: https://github.com/Atmosphere/atmosphere/wiki/jQuery.atmosphere.js-API */ jQuery.atmosphere = function() { - jQuery(window).unload(function() { + jQuery(window).bind("unload.atmosphere", function() { jQuery.atmosphere.unsubscribe(); }); + // Prevent ESC to kill the connection from Firefox. + jQuery(window).keypress(function(e){ + if(e.keyCode == 27){ + e.preventDefault(); + } + }); + var parseHeaders = function(headerString) { var match, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, headers = {}; while (match = rheaders.exec(headerString)) { @@ -38,7 +49,7 @@ jQuery.atmosphere = function() { }; return { - version : 0.9, + version : "1.0.3", requests : [], callbacks : [], @@ -54,6 +65,10 @@ jQuery.atmosphere = function() { }, onMessagePublished : function(response) { }, + onTransportFailure : function(response) { + }, + onLocalMessage : function (response) { + }, AtmosphereRequest : function(options) { @@ -66,15 +81,12 @@ jQuery.atmosphere = function() { method: 'GET', headers: {}, contentType : '', - cache: true, - async: true, - ifModified: false, callback: null, - dataType: '', url : '', data : '', suspend : true, maxRequest : 60, + reconnect : true, maxStreamingLength : 10000000, lastIndex : 0, logLevel : 'info', @@ -95,6 +107,11 @@ jQuery.atmosphere = function() { trackMessageLength : false , messageDelimiter : '|', connectTimeout : -1, + reconnectInterval : 0, + dropAtmosphereHeaders : true, + uuid : 0, + shared : false, + readResponsesHeaders : true, onError : function(response) { }, onClose : function(response) { @@ -106,6 +123,10 @@ jQuery.atmosphere = function() { onReconnect : function(request, response) { }, onMessagePublished : function(response) { + }, + onTransportFailure : function (reason, request) { + }, + onLocalMessage : function (request) { } }; @@ -116,21 +137,15 @@ jQuery.atmosphere = function() { var _response = { status: 200, responseBody : '', - expectedBodySize : -1, headers : [], state : "messageReceived", transport : "polling", error: null, + request : null, + partialMessage : "", id : 0 }; - /** - * {number} Request id. - * - * @private - */ - var _uuid = 0; - /** * {websocket} Opened web socket. * @@ -188,6 +203,33 @@ jQuery.atmosphere = function() { */ var _abordingConnection = false; + /** + * A local "channel' of communication. + * @private + */ + var _localSocketF = null; + + /** + * The storage used. + * @private + */ + var _storageService; + + /** + * Local communication + * @private + */ + var _localStorageService = null; + + /** + * A Unique ID + * @private + */ + var guid = jQuery.now(); + + /** Trace time */ + var _traceTimer; + // Automatic call to subscribe _subscribe(options); @@ -197,7 +239,6 @@ jQuery.atmosphere = function() { * @private */ function _init() { - _uuid = 0; _subscribed = true; _abordingConnection = false; _requestCount = 0; @@ -213,7 +254,7 @@ jQuery.atmosphere = function() { * @private */ function _reinit() { - _close(); + _clearState(); _init(); } @@ -229,9 +270,6 @@ jQuery.atmosphere = function() { _reinit(); _request = jQuery.extend(_request, options); - _uuid = jQuery.atmosphere.guid(); - - _execute(); } /** @@ -267,40 +305,393 @@ jQuery.atmosphere = function() { * @private */ function _execute() { + // Shared across multiple tabs/windows. + if (_request.shared) { + + var version = 0; + if (navigator.appVersion.indexOf("MSIE") != -1) { + version = parseFloat(navigator.appVersion.split("MSIE")[1]); + } + + // Multi Tab aren't working on IE 8. Tested with atmosphere.js and jquery-socket.js + // both pops up a blank page. + if (version != 8) { + _localStorageService = _local(_request); + if (_localStorageService != null) { + if (_request.logLevel == 'debug') { + jQuery.atmosphere.debug("Storage service available. All communication will be local"); + } + + if (_localStorageService.open(_request)) { + // Local connection. + return; + } + } + + if (_request.logLevel == 'debug') { + jQuery.atmosphere.debug("No Storage service available."); + } + } else { + jQuery.atmosphere.info("Multi tab not supported on IE 8."); + } + // Wasn't local or an error occurred + _localStorageService = null; + } + if (_request.transport != 'websocket' && _request.transport != 'sse') { - _open('opening', _request.transport); + _open('opening', _request.transport, _request); _executeRequest(); } else if (_request.transport == 'websocket') { if (!_supportWebsocket()) { - jQuery.atmosphere.log(_request.logLevel, ["Websocket is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"]); - _open('opening', _request.fallbackTransport); - _reconnectWithFallbackTransport(); + _reconnectWithFallbackTransport("Websocket is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"); } else { _executeWebSocket(false); } } else if (_request.transport == 'sse') { if (!_supportSSE()) { - jQuery.atmosphere.log(_request.logLevel, ["Server Side Events(SSE) is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"]); - _open('opening', _request.fallbackTransport); - _reconnectWithFallbackTransport(); + _reconnectWithFallbackTransport("Server Side Events(SSE) is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"); } else { _executeSSE(false); } } } + function _local(request) { + var trace, connector, orphan, name = "atmosphere-" + request.url, connectors = { + storage: function() { + if (!jQuery.atmosphere.supportStorage()) { + return; + } + + var storage = window.localStorage, + get = function(key) { + return jQuery.parseJSON(storage.getItem(name + "-" + key)); + }, + set = function(key, value) { + storage.setItem(name + "-" + key, jQuery.stringifyJSON(value)); + }; + + return { + init: function() { + set("children", get("children").concat([guid])); + jQuery(window).on("storage.socket", function(event) { + event = event.originalEvent; + if (event.key === name && event.newValue) { + listener(event.newValue); + } + }); + return get("opened"); + }, + signal: function(type, data) { + storage.setItem(name, jQuery.stringifyJSON({target: "p", type: type, data: data})); + }, + close: function() { + var index, children = get("children"); + + jQuery(window).off("storage.socket"); + if (children) { + index = jQuery.inArray(request.id, children); + if (index > -1) { + children.splice(index, 1); + set("children", children); + } + } + } + }; + }, + windowref: function() { + var win = window.open("", name.replace(/\W/g, "")); + + if (!win || win.closed || !win.callbacks) { + return; + } + + return { + init: function() { + win.callbacks.push(listener); + win.children.push(guid); + return win.opened; + }, + signal: function(type, data) { + if (!win.closed && win.fire) { + win.fire(jQuery.stringifyJSON({target: "p", type: type, data: data})); + } + }, + close : function() { + function remove(array, e) { + var index = jQuery.inArray(e, array); + if (index > -1) { + array.splice(index, 1); + } + } + + // Removes traces only if the parent is alive + if (!orphan) { + remove(win.callbacks, listener); + remove(win.children, guid); + } + } + + }; + } + }; + + // Receives open, close and message command from the parent + function listener(string) { + var command = jQuery.parseJSON(string), data = command.data; + + if (command.target === "c") { + switch (command.type) { + case "open": + _open("opening", 'local', _request) + break; + case "close": + if (!orphan) { + orphan = true; + if (data.reason === "aborted") { + _close(); + } else { + // Gives the heir some time to reconnect + if (data.heir === guid) { + _execute(); + } else { + setTimeout(function() { + _execute(); + }, 100); + } + } + } + break; + case "message": + _prepareCallback(data, "messageReceived", 200, request.transport); + break; + case "localMessage": + _localMessage(data); + break; + } + } + } + + function findTrace() { + var matcher = new RegExp("(?:^|; )(" + encodeURIComponent(name) + ")=([^;]*)").exec(document.cookie); + if (matcher) { + return jQuery.parseJSON(decodeURIComponent(matcher[2])); + } + } + + // Finds and validates the parent socket's trace from the cookie + trace = findTrace(); + if (!trace || jQuery.now() - trace.ts > 1000) { + return; + } + + // Chooses a connector + connector = connectors.storage() || connectors.windowref(); + if (!connector) { + return; + } + + return { + open: function() { + var parentOpened; + + // Checks the shared one is alive + _traceTimer = setInterval(function() { + var oldTrace = trace; + trace = findTrace(); + if (!trace || oldTrace.ts === trace.ts) { + // Simulates a close signal + listener(jQuery.stringifyJSON({target: "c", type: "close", data: {reason: "error", heir: oldTrace.heir}})); + } + }, 1000); + + parentOpened = connector.init(); + if (parentOpened) { + // Firing the open event without delay robs the user of the opportunity to bind connecting event handlers + setTimeout(function() { + _open("opening", 'local', request) + }, 50); + } + return parentOpened; + }, + send: function(event) { + connector.signal("send", event); + }, + localSend: function(event) { + connector.signal("localSend", jQuery.stringifyJSON({id: guid , event: event})); + }, + close: function() { + // Do not signal the parent if this method is executed by the unload event handler + if (!_abordingConnection) { + clearInterval(_traceTimer); + connector.signal("close"); + connector.close(); + } + } + }; + }; + + function share() { + var storageService, name = "atmosphere-" + _request.url, servers = { + // Powered by the storage event and the localStorage + // http://www.w3.org/TR/webstorage/#event-storage + storage: function() { + if (!jQuery.atmosphere.supportStorage()) { + return; + } + + var storage = window.localStorage; + + return { + init: function() { + // Handles the storage event + jQuery(window).on("storage.socket", function(event) { + event = event.originalEvent; + // When a deletion, newValue initialized to null + if (event.key === name && event.newValue) { + listener(event.newValue); + } + }); + }, + signal: function(type, data) { + storage.setItem(name, jQuery.stringifyJSON({target: "c", type: type, data: data})); + }, + get: function(key) { + return jQuery.parseJSON(storage.getItem(name + "-" + key)); + }, + set: function(key, value) { + storage.setItem(name + "-" + key, jQuery.stringifyJSON(value)); + }, + close : function() { + jQuery(window).off("storage.socket"); + storage.removeItem(name); + storage.removeItem(name + "-opened"); + storage.removeItem(name + "-children"); + } + + }; + }, + // Powered by the window.open method + // https://developer.mozilla.org/en/DOM/window.open + windowref: function() { + // Internet Explorer raises an invalid argument error + // when calling the window.open method with the name containing non-word characters + var neim = name.replace(/\W/g, ""), win = (jQuery('iframe[name="' + neim + '"]')[0] + || jQuery('