やっとtree-maps復旧しましたよ〜
なんでデプロイが失敗しだしたのか結局解らなかったよ!!
tree-mapsはとりあえず機能を使ううえでは問題無いレベルに復旧しました。
og:title等の設定が残っていますが、アプリに操作に影響を与えるものではありません。
tree-mapsに一体何が起きたのか
突如デプロイが失敗しだす
tree-mapsは以下の環境で開発していました。
環境 | Google App Engine / Java |
---|---|
JDK | v1.7(GAE/Jは1.8に未対応) |
フレームワーク | play framework v1.2.5 |
Appengine SDK | v1.9.17 |
ざっくりこんな感じでした。
ClientLoginがついに無くなる
tree-mapsは今年始めくらいに1度デプロイしたきりで、しばらくデプロイしていませんでした。先週GooglemapClusterのアイコンが404になってるよ〜、というバグ報告がきたので修正してデプロイしようとしたら、謎の404エラーが発生しました。
今まで appengine-java-sdk-1.9.17 だったのですが、v1.9.17は内部でClientLoginという機能を使ってデプロイ時の認証をしていたようなのですが、実は結構前からDeprecated(非推奨、いずれ削除される)だったようで、今回ついに削除に至ったため、謎の404が発生したわけでした。
ClientLogin for Installed Applications | Google Identity Platform | Google Developers
これは単純に最新版のSDK、今回は appengine-java-sdk-1.9.37 にバージョンアップする事で解決しました。
今度は403が発生しだす
さてSDKのバージョンアップをして認証処理ができるようになったですぞ〜、等と呑気にコンソールを眺めていると、今度は403(認証失敗)が発生しました。
Beginning interaction for module default... 5 17, 2016 11:15:50 午後 com.google.appengine.tools.admin.AbstractServerConnection send1 警告: Error posting to URL: https://appengine.google.com/api/appversion/getresourcelimits?app_id=tree-maps&version=v1& 403 Forbidden You do not have permission to modify this app (app_id=u's~tree-maps'). This is try #0 5 17, 2016 11:15:51 午後 com.google.appengine.tools.admin.AbstractServerConnection send1 警告: Error posting to URL: https://appengine.google.com/api/appversion/getresourcelimits?app_id=tree-maps&version=v1& 403 Forbidden ・・・・・・・・・略・・・・・・・・・ com.google.appengine.tools.admin.HttpIoException: Error posting to URL: https://appengine.google.com/api/appversion/getresourcelimits?app_id=tree-maps&version=v1& 403 Forbidden You do not have permission to modify this app (app_id=u's~tree-maps'). Unable to update app: Error posting to URL: https://appengine.google.com/api/appversion/getresourcelimits?app_id=tree-maps&version=v1& 403 Forbidden You do not have permission to modify this app (app_id=u's~tree-maps').
どうもOAuthの認証周りで古いトークン情報が残ってしまっていたらしく、以下を削除する事で再びOAuthのウインドウが表示されるようになりました。
tree:local tree$ ll ~/.appcfg_oauth2_tokens_java -rw------- 1 tree staff 220B 5 17 23:00 .appcfg_oauth2_tokens_java
正常デプロイ・・・できてない!?
さて、認証も通り、エラーも無くデプロイが成功しました!やったね!
・・・あれ?「NOT FOUND」????
デプロイは正常終了し、 以下でアップロードされたファイルサイズをみても65MByteになっていて問題無さそうだし、特にエラー報告はないのにフロントは404です。
https://console.cloud.google.com/appengine
恐らくですが、Playのgae:deployの仕組みが最新のAppengineSDKに対応できておらず(フォルダ構成等)、デプロイは正常終了するのにGAEがそれを認識できずに404になった???と想像しています。
GitHub - guillaumebort/play-gae: Create play application for Google App Engine
GAEで動作するplay frameworkはv1系ですし、今更v1系、しかもGAE関連が更新される事なんて無いだろうなあ・・・・という絶望に襲われました。
だったら作り直そうぜ!!
という事で、作りなおす事にしました。
よーし、slim3で作りなお・・・せない!?
tree-tipsの方がslim3で開発していたので、とりあえずslim3でいいか〜と思い、slim3 eclipse pluginをダウンロードしようとしたら・・・404!!!
http://slim3.googlecode.com/svn/updates/
slim3のeclipse pluginはgooglecodeのsvnで管理されていたようなのですが、最近googlecodeは廃止され、githubへの移行が促されていました。しかしslim3はもう開発がストップしているようで、移行されていません。。。
もう面倒臭い・・・
面倒臭い・・・
面倒臭い・・・
面倒臭い・・・
面倒臭い・・・
面倒臭い・・・
色々面倒臭いから、もう素のservlet(v2.5)で再実装してやんよ!
フレームワーク無し、しかもservlet v2.5といえば、java-webの暗黒期じゃないか・・・死ぬなよ・・・
tree氏、playを捨てて素のservletで再実装する事を決意
doGetとかdoPostとかを見て「うわぁ・・・」と思いつつ、せっせと夜鍋して実装しました。
web.xmlのルーティングがくっそ難しくね?とか愚痴りつつも、少しづつ実装は進みます。
テンプレートエンジンが・・・
play のv1系はgroovyテンプレートになっており、汎用性がありません。これをGAE上の素のservletで動かす事はできない(できるかもしれません)ので、他のテンプレートエンジンに差し替える必要がありました。
今回はthymeleafを採用してみました。
しかしここで躓く・・・
th:each内でgetterを呼ぶと・・・エラー!
まず、↓こんなenumがあるじゃろ?
import com.google.common.base.Strings; public enum GooglemapLang { ARABIC("ar"), // BULGARIAN("bg"), // ・・・・略・・・・ CHINESE_SIMPLIFIED("zh-CN"), // CHINESE_TRADITIONAL("zh-TW"), // ; private final String name; private GooglemapLang(String name) { this.name = name; } public String getName() { return name; } }
これを、↓こんな感じでthymeleafのWebContextにsetVariableするじゃろ?
WebContext ctx = new WebContext(req, res, getServletContext(), req.getLocale()); ctx.setVariable("googlemapLangs", Lists.newArrayList(GooglemapLang.values()));
で、↓こうしてgetName()を呼ぶじゃろ?
<select id="js-select-language" name="language"> <option th:each="googlemapLang : ${googlemapLangs}" th:value="${googlemapLang.name}" th:text="${googlemapLang.name + ' / ' + googlemapLang}">ar / ARABIC</option> </select>
すると、エラーになるじゃろ?
java.lang.RuntimeException: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating OGNL expression: "googlemapLang.name" (prot/index:138) at tree.servlet.BaseServlet.renderThymeleaf(BaseServlet.java:48) at tree.servlet.ProtServlet.doGet(ProtServlet.java:34) at javax.servlet.http.HttpServlet.service(HttpServlet.java:617) at javax.servlet.http.HttpServlet.service(HttpServlet.java:717) ・・・略・・・ Caused by: ognl.OgnlException: name [java.lang.IllegalAccessException: Method [public java.lang.String tree.constants.GooglemapLang.getName()] cannot be accessed.] at ognl.ObjectPropertyAccessor.getPossibleProperty(ObjectPropertyAccessor.java:69) at ognl.ObjectPropertyAccessor.getProperty(ObjectPropertyAccessor.java:147) at ognl.OgnlRuntime.getProperty(OgnlRuntime.java:2663) at ognl.ASTProperty.getValueBody(ASTProperty.java:114) at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) at ognl.SimpleNode.getValue(SimpleNode.java:258) at ognl.ASTChain.getValueBody(ASTChain.java:141) at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) at ognl.SimpleNode.getValue(SimpleNode.java:258) at ognl.Ognl.getValue(Ognl.java:494) at ognl.Ognl.getValue(Ognl.java:458) at org.thymeleaf.standard.expression.OgnlVariableExpressionEvaluator.evaluate(OgnlVariableExpressionEvaluator.java:114) ... 82 more Caused by: java.lang.IllegalAccessException: Method [public java.lang.String tree.constants.GooglemapLang.getName()] cannot be accessed. at ognl.OgnlRuntime.invokeMethod(OgnlRuntime.java:852) at ognl.OgnlRuntime.getMethodValue(OgnlRuntime.java:1702) at ognl.ObjectPropertyAccessor.getPossibleProperty(ObjectPropertyAccessor.java:60)
なんでだよ!!
実は握りつぶされていたエラー
スタックトレースを見ると内部的にognlを使っているようで、そこでエラーが発生しているようです。
しかし実装を見た感じ、「cannot be accessed.」になる理由がさっぱり解りません。
頑張ってステップ実行して内部に潜っていくと、以下の部分で例外エラーが発生している事が解りました。
package ognl; public class OgnlRuntime { ・・・略・・・ public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException { ・・・略・・・ if (checkPermission) { try { _securityManager.checkPermission(getPermission(method)); ← ★ここでエラー!! _methodPermCache.put(method, Boolean.TRUE); } catch (SecurityException ex) { _methodPermCache.put(method, Boolean.FALSE); throw new IllegalAccessException("Method [" + method + "] cannot be accessed."); } } ・・・略・・・
このコードを見ると解るのですが、SecurityExceptionのexをIllegalAccessExceptionに渡しておらず、「アクセスできなかった」というメッセージしか伝わらないのです・・・
ここでステップ実行してexの中身を見ると、実は以下のエラーが原因で「cannot be accessed」が発生している事が解ります。
java.security.AccessControlException: access denied ("ognl.OgnlInvokePermission" "invoke.tree.constants.GooglemapLang.getName")
ここで注目するのは「java.security.AccessControlException」の部分です。javaのセキュリティ上アクセスできないが故の例外エラーである事が解ります。この情報が上位に渡っていないせいで、「cannot be accessed」???という疑問が起きていたわけです。
ognlとjava.security.AccessControlException、この単語をキーにググってみると、以下に行き着きました。
programmingpanda.blogspot.jp
この中で重要なのは以下で、Ognl側でセキュリティを設定できる事が解りました。
public void contextInitialized(ServletContextEvent servletContextEvent) { OgnlRuntime.setSecurityManager(null); }
OgnlRuntimeクラスを潜ってみると以下のようになっていたので、Ognlのデフォルト_securityManagerは、System.getSecurityManager()である事が解ります。
・・・略・・・ static SecurityManager _securityManager = System.getSecurityManager(); ・・・略・・・ public static void setSecurityManager(SecurityManager value) { _securityManager = value; }
System.getSecurityManager()が原因で「cannot be accessed」が発生するようなので、最終的に以下のfilterを作成し、web.xmlに登録する事で、getterにアクセスする事ができました。
package tree.filter; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import ognl.OgnlRuntime; public class ONGLFixListener implements ServletContextListener, HttpSessionListener, HttpSessionAttributeListener { public ONGLFixListener() {} @Override public void contextInitialized(ServletContextEvent servletContextEvent) { OgnlRuntime.setSecurityManager(null); } @Override public void attributeAdded(HttpSessionBindingEvent arg0) {} @Override public void attributeRemoved(HttpSessionBindingEvent arg0) {} @Override public void attributeReplaced(HttpSessionBindingEvent arg0) {} @Override public void sessionCreated(HttpSessionEvent arg0) {} @Override public void sessionDestroyed(HttpSessionEvent arg0) {} @Override public void contextDestroyed(ServletContextEvent arg0) {} }
<listener> <listener-class>tree.filter.ONGLFixListener</listener-class> </listener>
これでアクセスできるようになりましたが、果たして OgnlRuntime.setSecurityManager(null); こうしてしまっていいものなのか、不明です。。。
最終的な環境
環境 | Google App Engine / Java |
---|---|
JDK | v1.7.0_25 |
フレームワーク | 無し!! |
テンプレートエンジン | thymeleaf v2.1.4 |
Appengine SDK | v1.9.37 |
雑感
素のservlet超つらい
だろうね・・・・v3系なら相当違ったろうね・・・
昔の人達はよくこんなので実装してたなあ、と関心しますね。といいつつ私もstruts v1世代なので親近感はあるのですが。
結局約1週間で再実装→フロント機能復旧に漕ぎ着ける事はできました。1週間のうち半分はgae:deployが何故失敗しているのかの調査、残りのほとんどはgetterのcannot be accessedの調査をしていました。つまり・・・「実装していた時間は実は2日くらい」です。素のservletもThymeleafもそれ程時間をかけずに実装できました。
そもそもGAE/Jはスピンアップが激遅(画面表示するまでにトータル10秒以上かかる場合も多いです)なので、フレームワークを使ってGAE/Jで実装するのは極力控えた方がいいかもしれませんね。という事で、次からはGAE/Gでgolangにしたいと思っています。GAE/Gのスピンアップは50msという爆速らしいので、期待大です。ただでさえスピンアップがGAEの中で最遅なのに、フレームワーク等を使うと初期化に激しく時間がかかるので、結果としてスピンアップ後の画面表示が10秒近くかかってしまうのですよね。これはツライ。
しかし世間ではGAEの注目度は非常に低いですよね。超スピードでオートスケールしてくれるし、デプロイの仕組みも簡単なので、かなりいいと思うのですけど。やはりJDKが未だに1.7系だったり、servletもv2.5系だったり、datastoreがクッソ遅く制限が厳しすぎるのが敬遠される理由の一つとして挙げられるのかもしれませんね。
GAE/Gだけは最新版のgo v1.6に対応しており、やっぱりgolangは優遇されてるようなので?、今後はGAE/Gでいってみたいと思います!!