我正在开发一个在OpenShift上运行的WildFly 10上的应用程序,利用Nashorn对React应用程序进行服务器端渲染。
应用程序在我的本地机器上运行正常(我不喜欢性能水平),但是当我将它部署到OpenShift时,会发生一些神秘的事情。
在几次请求之后,在先前请求中运行查找的代码突然开始抛出
java.lang.ClassCastException:jdk.nashorn.internal.objects.NativeRegExpExecResult无法强制转换为jdk.nashorn.internal.objects.NativeArray
堆栈跟踪揭示了这一行来自code in React-Router ......我手动添加了几行日志记录到react-router,奇怪的是反应路由器正在处理的参数失败的情景。这就是改变后的代码:
if (match != null) {
if (captureRemaining) {if (typeof global != 'undefined') {global.log.warn('match.length=' + match.length);}
remainingPathname = match.pop();
var matchedPath = match[0].substr(0, match[0].length - remainingPathname.length);
if (typeof global != 'undefined') {global.log.warn('remainingPathname=' + remainingPathname + ', matchedPath=' + matchedPath);}
// If we didn't match the entire pathname, then make sure that the match
// we did get ends at a path separator (potentially the one we added
// above at the beginning of the path, if the actual match was empty).
(请注意对global.log.warn
的调用...我添加了这些内容)
如果查看full logs,您可以看到第一次请求时,事情似乎正常,但突然之间,它开始抛出此ClassCastException
并且不再停止。对于任何请求,我的所有应用都会返回503 service not available
。
我弄乱了代码,多次重写以获得正确的行为,但我有点卡住了。最后,我陷入synchronized
块以试图消除线程问题,但问题仍然存在。奇怪的是,如果我在WildFly中将max-worker-threads
设置为1
,问题似乎就会消失。
我说似乎是因为我发现很难确定问题,OpenShift的部署时间长以及问题的“随机”行为是什么。
以下是我ReactRenderFilter
的相关代码。 Full code on pastebin
public class ReactRenderFilter implements Filter {
private static final Object LOCK = new Object();
private static final ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");
private static final List<CompiledScript> scripts = new ArrayList<>();
private static final ThreadLocal<RenderEngine> renderEngine = new ThreadLocal<>();
private FilterConfig config;
private String bundlePath;
private String jspPath;
public static class RenderEngine {
private final ScriptContext context;
private final ReactRenderer renderer;
private final long lastModified;
public RenderEngine(File bundle) throws ScriptException, UnsupportedEncodingException, FileNotFoundException {
context = new SimpleScriptContext();
Bindings global = engine.createBindings();
context.setBindings(global, ScriptContext.ENGINE_SCOPE);
global.put("global", global);
for (CompiledScript script : scripts) {
script.eval(context);
}
engine.eval(new InputStreamReader(new FileInputStream(bundle), "utf-8"), context);
lastModified = bundle.lastModified();
LOG.finer("Getting renderer");
renderer = ((ScriptObjectMirror) engine.eval("global.render", context)).to(ReactRenderer.class);
}
String render(String path, String initialDataJSON) {
return renderer.render(path, initialDataJSON);
}
boolean isOutdated(File bundle) {
return lastModified != bundle.lastModified();
}
}
public ReactRenderFilter() {super();}
@Override public void destroy() {}
@Override public void init(FilterConfig filterConfig) throws ServletException {
config = filterConfig;
try {
String[] paths = ...
for (String path : paths) {
if (path.trim().isEmpty()) {continue;}
File file = new File(config.getServletContext().getRealPath(path.trim()));
scripts.add(((Compilable) engine).compile(new InputStreamReader(new FileInputStream(file), "utf-8")));
}
bundlePath = config.getServletContext().getRealPath(config.getInitParameter(PARAM_APP_BUNDLE_PATH).trim());
jspPath = config.getInitParameter(PARAM_MARKUP_JSP_PATH).trim();
} catch (UnsupportedEncodingException | FileNotFoundException | ScriptException e) {
throw new ServletException("Unable to initialize ReactRenderServlet.", e);
}
}
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
File bundle = new File(bundlePath);
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String path = req.getRequestURI().substring(req.getContextPath().length());
String initialDataJSON = "{}";
@SuppressWarnings("unchecked")
Map<String, Object> initialData = (Map<String, Object>) req.getAttribute("initialData");
if (initialData != null) {
ObjectMapper mapper = new ObjectMapper();
initialDataJSON = mapper.writeValueAsString(initialData);
req.setAttribute("initialDataJSON", initialDataJSON);
}
String renderResult = null;
try {
if (renderEngine.get() == null || renderEngine.get().isOutdated(bundle)) {
// prevent multiple render engines to be instantiated simultaneously
synchronized (LOCK) {
renderEngine.set(new RenderEngine(bundle));
}
}
// I sure hope there is a way around this... locking on central object
// during rendering can't be good for performance... But it beats having
// only one worker thread
synchronized (LOCK) {
renderResult = renderEngine.get().render(path, initialDataJSON);
}
if (renderResult.startsWith(MARKUP)) {
String markup = renderResult.substring(MARKUP.length());
req.setAttribute("markup", markup);
int maxAge = 60 * 60; // 60 minutes
res.addHeader("Cache-Control", "public, max-age=" + maxAge);
res.addDateHeader("Expires", new Date().getTime() + maxAge);
req.getRequestDispatcher(jspPath).forward(request, response);
}
else if (renderResult.startsWith(REDIRECT)) {
String url = renderResult.substring(REDIRECT.length());
res.sendRedirect(url);
}
else if (renderResult.startsWith(NOTFOUND)) {
int maxAge = 365 * 24 * 60 * 60; // 365 days
res.addHeader("Cache-Control", "public, max-age=" + maxAge);
res.addDateHeader("Expires", new Date().getTime() + maxAge);
chain.doFilter(request, response);
}
else {
String msg = renderResult.substring(ERROR.length());
throw new ServletException("Unable to generate response for route [" + path + "]: " + msg);
}
} catch (ScriptException e) {
throw new ServletException(e);
}
}
}
正如您所看到的,我为每个帖子(ScriptEngine
)都有一个静态ScriptContext
和一个单独的Bindings
+ ThreadLocal
...我认为(基于在我发现的文档上,这应该是线程安全的...在绝望中我在这个锁上添加了LOCK
和synchronized
块,但它似乎没有帮助。
我不确定它是否与线程有关,但确实如此。
上述代码是否是创建同时使用的多个脚本上下文的正确方法?
有什么提示可以摆脱这个问题,甚至调试它?