Humanity

Edit the world by your favorite way

GitLab 運用して起こったエラー対処法

tyru.hatenablog.com

tyru.hatenablog.com

の後日録です。

あと、今日ちょうど GitLab の新バージョンが出たようです。

GitLab 8.13 Released with Multiple Issue Boards and Merge Conflict Editor | GitLab

同時に git push した場合のエラー

gitlab.com

自宅サーバをジョブで一斉に yum check-update; yum upgrade -y *1 した後、GitLab の Activity を見てみると設定が push されていないノードがあった。 その際の出力には下記のようなエラーが出ていた。

GitLab: Failed to authorize your Git request: internal API unreachable
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

同時に push されるとよく起こるっぽい。 現在6台のノードを管理していて、その内3台の git push が失敗していた。

ということで冒頭のチケット通りに /etc/gitlab/gitlab.rb を以下のように変更して対処した。

unicorn['worker_processes'] = 2

/etc/gitlab/gitlab.rb を変更した後は gitlab-ctl reconfigure を忘れずに(Omnibus package 版の場合)。

バックアップスクリプトがエラーになる問題

gitlab.com

これは結構以前からエラーになっていた。 チケットの以前の締切は 9/22 だったけど、それが 10/22 (今日)に変更された。 私は上記チケットにもコメントした通り対処したので今でも起きるかは確認してないけど、 チケットがクローズされていないので直ってないはず。

このエラーが起きる条件として、バックアップ先を WebDAV でマウントしたパスに設定したことだった。

gitlab_rails['backup_path'] =  "<WebDAV(davfs2) mounted path>"

これを初期値のパスに戻して(変更した後は gitlab-ctl reconfigure を(ry)、

gitlab_rails['backup_path'] =  "/var/opt/gitlab/backups"

rsync で転送するようにした。

# root で実行
sudo -u git -H gitlab-rake gitlab:backup:create
rsync -rlv -delete /var/opt/gitlab/backups/ /path/to/remote/backup/path/

転送先のディスク容量が溢れないよう --delete を付けているけど、 安全のために転送先のファイルサーバには1か月前までのバックアップを残すとかしてもいいかも。 ただ GitLab を動かしてる VM にあまり容量がないので、ローカルには7日間しか残していない。

# root で実行
sudo -u git -H gitlab-rake gitlab:backup:create
rsync -rlv /var/opt/gitlab/backups/ /path/to/remote/backup/path/
find /path/to/remote/backup/path/ -type f -name '*.tar' -mtime +30 -delete

むしろ転送したらローカルには何も残さない方がいい気がする?(ファイルサーバには30日間残す)

# root で実行
sudo -u git -H gitlab-rake gitlab:backup:create
rsync -rlv /var/opt/gitlab/backups/ /path/to/remote/backup/path/ && rm -f /var/opt/gitlab/backups/*.tar
find /path/to/remote/backup/path/ -type f -name '*.tar' -mtime +30 -delete

*1:ちゃんと運用するなら検証環境をアップグレードした後、一定期間問題が無ければ本番環境をアップグレードした方がいいんだろうけど

Spring でリクエストボディの JSON に対してバリデーションを行う

問題

Spring でリクエストボディの JSON に対して共通のバリデーション処理を行うに当たって、まずは問題がある。

@ControllerAdvice を使うと、コントローラーが実行される前にバリデーションなどの共通処理を行うことができるが、 @ControllerAdvice で実行できるメソッドに渡ってくるのはリクエストボディのストリームである。 これは元の HttpServletRequest#getInputStream() がそうだから。 それの何が問題なのかと言うと、一度ストリームを読んでしまうと、Stream は通常元に戻せない *1 ため、 @ControllerAdvice のメソッドでバリデーションするために読み込んでしまうとコントローラーが実行されるタイミングで EOF (Stream closed) となってしまいます。

実はこの問題は Spring に限りません。 先程 HttpServletRequest#getInputStream() 由来の問題であると述べたように、Servlet の filter などで前後処理を行う際もこの問題は起き得ます。

解決策

では Spring はこれに対して対策をしていないのかというと、もちろんしています。 @ControllerAdvice アノテーションが付いていて、かつ RequestBodyAdvice インターフェースを実装したクラスは RequestBodyAdvice#beforeBodyRead() で新たな InputStream を含む HttpInputMessage クラスのインスタンスを返すことができます。 これにより、一度リクエストボディを文字列として読み込んでバリデーションなどの前処理を行ったあと、そのボディの文字列をまた InputStream に変換してやり、それを HttpInputMessage クラスのインスタンスに持たせて返してやればいいのです。

サンプルコード

  1. beforeBodyRead()
  2. initBinder()

の順番に呼ばれます。

github.com

ちなみに、上記クラスの使用方法は、プロジェクトの README.md の examples にも書いてあるように以下のように使います。

ちなみに

ちなみに RequestBodyAdvice の似たメソッド afterBodyRead() は何をするためのメソッドかと言うと、このメソッドの第1引数にはリクエストボディが変換された後の POJO クラスのインスタンスが渡される。

どういうことかというと、

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<HelloReqBean> addMsg(@RequestBody HelloReqBean hello) {
        service.addMsg(hello);
    }

コントローラーのクラスにこのようなエンドポイントのメソッドがあるとして、この @RequestBody が付いた引数 HelloReqBean クラスのインスタンスが上記で言った POJO クラスのインスタンスにあたる。 ちなみに @PostMapping@RequestMapping(method=RequestMethod.POST)エイリアス@RequestMapping(method=RequestMethod.POST, value="/path/to/resource") の代わりに @PostMapping("/path/to/resource") みたいにも書ける。

この変数は Object 型として渡ってくるが、あくまでメソッドシグニチャのためであり、HelloReqBean 型にキャストできる。

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
            Class<? extends HttpMessageConverter<?>> converterType) {
        HelloReqBean bean = (HelloReqBean) body;
        // ...
    }

また @InitBinder が付けられたメソッドで取得できる WebDataBinder クラスのインスタンスのメソッド getTarget() でもこの POJO クラスのインスタンスを取得することができるようだ。

    @InitBinder
    private void initBinder(WebDataBinder binder) throws IOException {
        HelloReqBean bean = (HelloReqBean) binder.getTarget();
        // ...
    }

afterBodyRead()@InitBinderPOJO クラスに対して共通の処理を行うなら便利そうだけど、今回はリクエストボディの JSON 文字列を取得しないといけなかったので見送った。

まとめ

  • HttpServletRequest#getInputStream() で2回読む事はできない(IOException: Stream closed と言われる)
  • リクエストボディを文字列で取得したければ @ControllerAdvice アノテーションを付け、かつ RequestBodyAdvice インターフェースを実装し、beforeBodyRead() メソッドでストリームからリクエストボディを読み込む
  • 読み込んだ後、後続のコントローラー等の処理もストリームを読めるようにするため、読み込んだバイト列なり文字列を InputStream にまた変換してやり、HttpInputMessage クラスのインスタンスに渡して返す
  • コントローラーに引数で渡すクラスのインスタンスに対して共通処理を行うには RequestBodyAdvice#afterBodyRead() または WebDataBinder#getTarget() が使える

関連リンク

Spring MVC(+Spring Boot)上でのリクエスト共通処理の実装方法を理解する - Qiita

@ControllerAdvice ではなく Spring AOPAspectJ)を使う場合のコードはこちら。 ただ、InputStream の問題は同様に存在します。

参考リンク

1と2のアーキテクチャ概要図がソースコードを追う時にすごく参考になった。

  1. 01.基本概念:全体的な処理フロー - soracane
    • 詳しすぎて何者感ある資料。
  2. 2.2. Spring MVCアーキテクチャ概要 — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.2.0.RELEASE documentation
  3. Spring Framework Reference Documentation
    • 公式資料だけど見づらい…

コードリーディングメモ

Spring のソースコード追っかけてた時の自分用メモ。

  • org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(HttpInputMessage, MethodParameter, Type)

でそれぞれの ControllerAdvice を呼んでいる。

  • org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage.EmptyBodyCheckingHttpInputMessage(HttpInputMessage)

PushBackStream 初めて知った。 そういや InputStream の mark も使ったことないな…

一応 JAX-RS、Spring MVC の他に素の HttpServletRequest 対応も考えてるので入れたい。 一応クラスは入れてあるものの、HttpServletRequst#getInputStream() を直接呼んでるので2回呼べない。 これじゃ前処理に使えない…

java-json-hyper-schema-validator/ServletJSONRequest.java at 1f4e78fccf60eaa615c1f2dfcb0b365513bb5dee · tyru/java-json-hyper-schema-validator · GitHub

色々調べたけど、どうやら元々の HttpServletRequst#getInputStream() を2回呼べない問題は単純に2回読んだ場合はどうしても解決できないっぽいので、Spring みたく Chain of Responsibility パターンで解決するか、他の何らかの方法で対策をする必要があるのか…

そういや JAX-RS の実装も ContainerRequestContext#getEntityStream() で InputStream 取得して中身読んでたけど普通にコントローラーちゃんと呼べてたなぁ… 対策してあるのか分からんけど、もしくは未定義動作…?

java-json-hyper-schema-validator/JaxrsJSONRequest.java at 1f4e78fccf60eaa615c1f2dfcb0b365513bb5dee · tyru/java-json-hyper-schema-validator · GitHub

  • org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(NativeWebRequest, ModelAndViewContainer, Object...)
    • DefaultParameterNameDiscoverer
      • Default implementation of the ParameterNameDiscoverer strategy interface, using the Java 8 standard reflection mechanism (if available)
    • HandlerMethodArgumentResolverComposite
      • Resolves method parameters by delegating to a list of registered HandlerMethodArgumentResolvers.

コントローラーや initBinder() の引数はこれでインスタンスを生成して渡しているっぽい。

上記メソッドにブレークポイントを置くと、コントローラーに inject する引数を生成するために再帰的に @InitBinder のメソッドを読んでいるのが分かる。

f:id:tyru:20161013121558p:plain f:id:tyru:20161013121601p:plain

まずコントローラーの引数を ServletInvocableHandlerMethod#getMethodArgumentValues(NativeWebRequest, ModelAndViewContainer, Object...) で解決しようとする。 次に InvocableHandlerMethod.java 161行目の

args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

により @ControllerAdvice の @InitBinder が呼び出される。

これ Spring じゃないけど、HttpServletRequest をラップするとしたらこんな感じのコードがよさそう。 まぁこのクラスに HttpServletRequest のインスタンスを渡したらその時点で HttpServletRequest#getInputStream() が呼ばれて読み込まれるから、 元の HttpServletRequest は使えなくなるけど…

あくまで本記事のようにリクエストボディのみではなく HttpServletRequest クラスをラッパークラスに渡して、そのサブクラスとして扱いたい場合は、HttpServletRequest のインスタンスを上記のクラスのコンストラクタに渡して、以降はそのラッパークラスのインスタンスを使い回すのがいいかな?(細かく見てないので間違ってるかも)

*1:InputStream#markSupported() が true を返す実装クラスのインスタンスと PushBackStream は除く

Java のマルチスレッドに関する本を読んだ

並行処理に限る訳ではないけど、ここに挙げた本読めば Java でそこそこ安全にマルチスレッドなプログラム書けそうな気がしてくる。 まぁ実際並行・並列処理は量書いて身につけないとしょうがないけど…

ぶっちゃけて言えば、読む前から変わらなかった意見は「並行・並列処理書くならメッセージパッシングの言語機能なりライブラリを持った言語で書くのが一番良いだろうな」だった。Go とか Scala とか Erlang とか。

まぁでも Stream API とか使えば結構カジュアルに並列処理できて便利という事が分かったので便利。 Java 8 になって Concurrency Utilities も色々強化されてるみたいなのでおさらいすると楽しそう。

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Java仮想マシン仕様 (The Java series)

Java仮想マシン仕様 (The Java series)

  • 作者: ティムリンドホルム,フランクイェリン,Tim Lindholm,Frank Yellin,村上雅章
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2001/05
  • メディア: 単行本
  • 購入: 5人 クリック: 98回
  • この商品を含むブログ (35件) を見る

Java 仮想マシン仕様は飛ばし読み(ニーモニック一覧の所とか)。 並行・並列処理に関しては8章のスレッドの部分。

あと定番のやつ。 もう一度読み直したい。

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

ちなみに

並行性と並列性について簡潔な説明が Oracle の公式ドキュメントにあるのを見つけた。

並行性と並列性 (マルチスレッドのプログラミング)

さらに

もっと詳しく知りたい人は以下の記事が公式ドキュメントへのリンクとその内容を例を交えて分かりやすく解説してくれてらっしゃる神記事なのでどうぞ。

マルチコア時代に備えて本気でメモリモデルを理解しておこう - リオーダー & finalフィールド 編 - - じゅんいち☆かとうの技術日誌

関連記事