Kengo's blog

Technical articles about original projects, JVM, Static Analysis and JavaScript.

2020年のFOSS活動状況まとめ

昨年のに引き続きFOSS活動状況をまとめます。2020年12月21日時点の情報です。

概要:昨年比23%増

GitHubのプロファイルページによると今年のpublic contributionsは1,440で、昨年が1,166だったので約23%増です。commit 69%のpull requests 16%なので、引き続き手を動かしてコードを書けたと思います。

f:id:eller:20201221181440p:plain

また今年からGitHub Enterprise Cloud(GHEC)の業務利用を始めたのですが、publicとprivatecontributions比はだいたい趣味:業務が7:3でした。業務の方に手を動かす余地があると考えるべきか、マネジメントなのにこんなに手を動かしてることを反省点として受け止めるか……。

今年の主なリリースはspotbugs-gradle-plugin 4.0.0~4.6.0、SpotBugs の4.0.0 beta5~4.2.0、あといくつかGitHub ActionをTypeScriptで実装しました

SpotBugs周りの開発

今年はSpotBugs v4の安定版を出したことが1番大きいリリースでした。加えてGradleの非公開APIに依存してしまってメンテナンス性と性能の双方に問題が出ていたGradleプラグインをスクラッチで書き直したのも大きなプロジェクトだったと言えます。

SpotBugs v4がFindBugsよりも性能上で改善されていることはマイクロベンチで確認しましたし、MavenプラグインやGradleプラグインに加えてGitHub Actionもあるらしいので、特にFindBugsで困ってないという方も一度アップグレードを検討してみると良いと思います。

その他

この夏ごろにSLF4Jがメンテナンスされるのか知る目的で、チケットやPRの整理をしたりMLにメールを送ったりという活動をしばらく続けていました。私個人の結論としては1.8は使うべきではないし2.0も先行き不透明なので、今後は極力Log4j 2をLogging Facadeとして使うべきだろうと考えています。

OASISの定めるSARIFに興味を持ってSpotBugsレポートとしての実装を進めています。JSONスキーマの制約から望ましい実装ができていない状態が続いているので、いずれテコ入れしたいところです。

TypeScriptでGitHub Actionを書くときのTips

actions-setup-docker-compose, sonar-update-center-actionsauce-connect-actionなど5つくらいActionを実装したので、Tipsをここにまとめます。

公式テンプレートを使う

TypeScriptでGitHub Actionを書くためのテンプレートが公開されています。とりあえずこのテンプレートを使うのがおすすめです。

github.com

使っているのはnpm, jest, @vercel/ncc, eslint, prettier とオーソドックスなもの。jest が気に入らなければ置き換えても構わないでしょう。

公式ドキュメントとライブラリに目を通す

さらっとで良いので以下のページは読んでおくといいです。どういった情報がどこにあるのか、これをするために何を使えばいいのか、情報の場所を掴んでおきます。

特に以下は必要になる知識だと思われます:

Fixtureの作成と読込

単体テストでは api.github.com との通信を再現してコードの挙動を見ていくことになります。一応 docs.github.com に期待されるステータスコードやそのペイロードが書かれていますが、まだ内容が間違っていることがあったので実際にコードを回して確認することを薦めます。ドキュメントよりは @octokit/rest の型情報のほうが信用できます。

レスポンスを再現するためのFixtureは、JSON.stringify(response.data)JSONファイルに保存して作成します。tsconfig.json"resolveJsonModule": trueを設定すればJSONファイルをそのままオブジェクトとしてimportできるので便利です。

// quoted from https://git.io/JIchk
import releases from './fixtures/sonarqube-releases.json'

const token = process.env.GITHUB_TOKEN
if (!token) {
  throw new Error('No GITHUB_TOKEN env var found')
}

test('searchLatestMinorVersion()', async () => {
  const scope = nock('https://api.github.com')
    .get('/repos/SonarSource/sonarqube/releases')
    .reply(200, releases)
  expect(await searchLatestMinorVersion(token)).toBe('8.5.*')
})

こうしたテストの面倒を見てくれる@octokit/fixtures もあるようですが、私はまだnockしか使っていないので詳しいことは不明です。

Changelog Blogを購読する

Actions周りはまだ動きが活発で、最近も add-path コマンドなどが削除されたばかりです。 変更に追従するためにもChangelog Blogは購読をすると良いでしょう。

TBU: まだ自動化できていないこと

  • dependabot が依存を更新したら dist ディレクトリ以下のファイルも更新する
    • pull_request イベントでActionを発火させて、 npm run all した結果をcommit & pushするようにすればできるはず

技術書「JavaのビルドとCIのキホン」を公開しました

zenn.dev がホットなのでフォロワーの皆様にアンケートを取った結果、「JavaのビルドとCIのキホン」が5票を獲得したので書きました。

書籍はこちらです。

zenn.dev

zenn.dev上では約26266文字と言われてるんですが、いや流石にそんなに書いてないはず。まえがきと付録を除いて6章構成です。 初心者向けを意図していますが、経験者でないとわからない論理的飛躍というか、結果ありきの書き方になっているところが残っているかもしれません。しばらくは継続的に更新します。

マイクロサービス時代のアプリケーションサーバ実装について

4年前の下書きが出てきたので、供養のために置いておきます。


SpringOne Platform 2016KeynoteとSessionを、特にProject Reactor周りについて確認した。

これらに最近考えていたことを加えて、マイクロサービスやサーバサイドリアクティブについて(利点や必要性は一旦論点から外したうえで)実現するためのあるべき論をざっくり整理したい。プライベートプロジェクトで検証済みではあるがチーム開発でも通用するかは不明である。

なお簡単のためRxJava用語のみ記述するが、Project Reactor等の用語で置き換えても良い(SingleMono, ObservableFlux)。

あるべき論

APIサーバ&BFF共通

  • Repository(DAO)
    • 境界を超えないデータストアに対するアクセスを担うレイヤのこと。
    • Repositoryのメソッドは非同期I/Oを呼び出すことが期待される。よってSingleあるいはObservableを返すべき。
    • 戻り値がない場合(例えば更新処理)でも、内部の処理が正常に終了したかどうかを表現するために Single<Void> を返すべき。
    • 非同期I/Oを呼び出さないことが明らかな場合のみ、同期的にインスタンスを返しても良い。
  • Service(ビジネスロジック
    • ServiceのメソッドはDAOを通じて、あるいは境界外へアクセスするクライアントを通じて非同期I/Oを呼び出すことが期待される。よってSingleあるいはObservableを返すべき。
    • 戻り値がない場合(例えばジョブキューにジョブを登録する場合)でも、内部の処理が正常に終了したかどうかを表現するために Single<Void> を返すべき。
    • Repositoryから渡されたSingle/Observableのエラー処理(ログ出力等)は、
      • その Single/Observable がControllerから利用されるのであれば、Service内で行っても行わなくても良い。
      • その Single/Observable がControllerから利用されないのであれば、Service内で行う。
    • Serviceは冪等性を保つ必要がある。UUID version 1でID採番する場合などは、処理結果にランダム失敗した性が含まれるので扱いに注意する(Eventual Consistencyを意識する)。
  • Client(境界外アクセス)
    • RestTemplate等を直接呼び出す実装が多く見受けられるが、サーキットブレーカーや監視や自動テスト容易性を考えると一枚抽象化を入れた方が良いと思われる。
    • 各マイクロサービス提供者がEntityと共にユーザに提供する。Serviceから呼び出され、HTTP等によるRPCを行う。よってSingleあるいはObservableを返すべき。
    • コレオグラフィ優先採用するならば、基本的には使用しない方が良い。
  • EventBus
    • Serviceによって購読ないしpublishされる。
  • Controller
    • モデルとしてSingleObservableをテンプレートエンジンに渡す。テンプレートエンジンの機能に応じて、SingleObservableを変換するかもしれない。CompletableFutureあたりが有望か。
    • 通信先サービスが正常動作しなかった場合、CircuitBreakerが落ちている場合のUIも考えておく。

BFFサーバ の実装

WebAPIを多数コールして1つのレスポンスにまとめるには、大きく分けて2つ実装パターンが考えられる。

  1. サーバ内ですべてのサービスからのレスポンスを束ね、1枚のHTMLページやJSONデータをつくり上げる
  2. サービスから逐次受け取ったデータをクライアントに横流しして、クライアント側で組み立てる(Server-sent Event, WebSocket, Big pipe etc.)
    • 2020年夏時点ではJSON listをうまく扱うJS手法はまだなさそう?Streams APIの安定化を待つ必要があるのでは。

完全な2だとBFFを作る意味が無いので、1を主体として見た目に影響の薄い部分で2のような遅延処理をを採用することが多いはず。しかし1のレスポンスを束ねる部分で各サービスに線形的に問い合わせてはレイテンシ低下が容易に発生するため、非同期I/Oを使った並行問い合わせが必要になる。

サーバ間連携

  • メッセージキューには永続性が必要
    • コレオグラフィを志向してサービスを実装するためには「イベントを永続化してサブスクライバが何度も読みに行く」か「サービスを冪等にしてパブリッシャが何度もリトライする」かのどちらかになる。後者はパブリッシャがイベント発火によって行われる処理を知っている必要があるので、基本的には前者が好ましいと思われる(Webアプリだとパブリッシャが何度もリトライするような書き方だとユーザへのレスポンスが遅れるのも痛い)。
      • 2020年追記:一見意味不明だが、ここで言うサブスクライバとパブリッシャはサービス単位を指している(同一JVM内にあるインスタンスを比較しているわけではない)と思われる。前者はイベントが永続化することで、同じイベントを何度も聞きに行くケースのこと。複数種類のサブスクライバがいたり、サブスクライバが常時起動でなかったりするとこの必要性が生じるはず。後者は非同期処理を依頼する、例えば送信処理や投機的実行のケースで事実上リアルタイム性を求めているケースのこと。当時使ってたVertxがデフォルト設定で利用するhazelcastのイメージに引っ張られてそう。
    • このため、キューに記録されたメッセージは1回以上必ず読まれること、つまりロストがないことを保証したい。そのためにはキューに永続性が必要。
  • サブスクライバごとにイベントの既読管理を行う
    • コレオグラフィを採用するならば、1イベントにサブスクライバが多数いることも予想されるため、メッセージ(イベント)がConsumeされたかどうかはサブスクライバごとに管理する必要がある。
    • 例えばKafkaなら、イベント種別=Topic、サブスクライバ=Consumer Groupとすることでサブスクライバごとの管理が可能。

備考

RxJava v2, reactive-streams そして Java9 Flow API について

現状では気にしないので良さそうな印象。

operatorすら用意されていないシンプルなreactive-streamsが、それだけ見ていれば詳細実装を気にしないで良いレベルのインタフェースになることは当面ないだろうし、そもそもそこを目指していないように見受けられる。ライブラリ提供者にとっては各プラットフォームを公平にサポートするための良いインタフェースになるだろうが、サービス開発者にとってはRxJavaやProject Reactorに直接依存・利用したほうがコードもシンプルになるし利用できる機能も多く便利なはず。

参考:

CompletableFutureの何が嬉しいか、そしてReactorへ……

DBにクエリを2つ投げて合成する処理

例えばユーザーIDから所有するリソースを検索してその情報を返すAPI Stream<Resource> find(long userId) を実装するとします。Resourceは大量に帰ってくる可能性があるので、List<Resource>ではなくStream<Resource>として扱います。実装はどうなるでしょうか:

// blocking API (wrong)
try (UserDao userDao = daoFactory.createUserDao();
    ResourceDao resourceDao = daoFactory.createResourceDao();) {
  Stream<Long> resourceIds = userDao.search(userId); // ページング処理を隠蔽
  return resourceIds.map(resourceId -> {
    Resource resource = resourceDao.find(resourceId);
    return resource;
  }); // Stream<Result>
}

これは正常に動作しません。メソッドを抜ける前にtry-with-finallyブロックがDAOを閉じてしまうからです。 呼び出し元がStream<Resource>を閉じてくれることを期待して、以下のようなコードになるでしょう:

// blocking API
UserDao userDao = daoFactory.createUserDao();
ResourceDao resourceDao = daoFactory.createResourceDao();
Stream<Long> resourceIds = userDao.search(userId); // ページング処理を隠蔽

// Streamが閉じられたらDAOも閉じる
resourceIds.onClose(userDao::close);
resourceIds.onClose(resourceDao::close);

return resourceIds.map(resourceId -> {
  Resource resource = resourceDao.find(resourceId);
  return resource;
}); // Stream<Result>

このように処理を遅延するコードはFutureでは書けません。Java8で登場したCompletableFutureを使うことになります。例えばUserResourceがひとつだけ結びつく状態なら以下のように書けます:

// CompletableFuture
UserDao userDao = daoFactory.createUserDao();
ResourceDao resourceDao = daoFactory.createResourceDao();
CompletableFuture<Long> future = userDao.search(userId).whenComplete( ... );

return future.thenApply(resourceId -> {
  Resource resource = resourceDao.find(resourceId);
  return resource;
}).whenComplete( ... ); // CompletableFuture<Result>

が、Streamのように複数の値を返すモノに使うのは複雑な工夫が必要ですし、やりようによってはすべての値を一度にオンメモリに乗せてしまいます。 私の知る限りではReactorフレームワークやRxJavaの採用がこの問題へのスマートな解になります。例えばFlux#using()を使うと以下のように書けます:

// Reactor
Flux<Long> resourceIds = Flux.using(daoFactory::createUserDao, userDao -> {
  return userDao.search(userId);
}, UserDao::close);

return resourceIds.flatMap(resourceId -> {
  Mono<Resource> resource = Mono.using(daoFactory::createResourceDao, resourceDao -> {
    resourceDao.find(resourceId);
  }, ResourceDao::close);
  return resource; // Flux<Resource>
});

関連記事

blog.kengo-toda.jp

Project Reactorでページング処理を書くにはFlux#expand()を使う

Bing検索で見つけるのが難しかったのでメモ。Project Reactorでページング処理を書く方法について。

例えばこういうAPIがあったときに、どう実装するか?

class Foo { ... }

class FooPage {
  @NonNull
  Foo[] getEntities();
  Optional<Integer> getNextPageNumber();
}

interface FooRepository {
  /** @param page 0-indexed page number */
  @NonNull
  Mono<FooPage> loadPage(int page);
}

class FooService {
  @Inject // use constructor injection instead in prod.
  FooRepository repository;

  @NonNull
  Flux<Foo> loadAll() {
   // TODO
  }
}

以下のようにFlux#expand()を利用する必要があります。引数には前回ロード時の値が入っているので、そこから次回のリクエストに使用するパラメータを算出します。大抵はサーバのレスポンスに次のページ番号が入っていたり、検索パラメータとして使用すべきトークンやIDが入っているはずなので、それを引き回す形になるでしょう。

  Flux<Foo> loadAll() {
    Flux<FooPage> fluxForPage = repository.loadPage(0).expand(prevPage -> {
      return prevPage.getNextPage().map(repository::loadPage).orElse(Mono.empty());
    });
    Flux<Foo> fluxForEntity = fluxForPage.flatMap(page -> Flux.fromArray(page.getEntities()));
    return fluxForEnttiy;
  }

Reactorはリソースの開放に using() を使うのもわかりにくかったですが、ページング処理もJavadocGitHubを検索しても出てこないのでちょっと苦労しました。

SLF4Jの1.8は安定版が出ないので2.0を使おうという話

前回の投稿で、SLF4Jには現在1.7、1.8、2.0の3バージョンがあるという話をしました。

これについてSLF4Jのuser向けメーリングリストで質問をしたところ、1.8の開発中にFluent Logging APIを追加するために2.0へとメジャーバージョンを上げたという旨の回答を得られました。つまり1.8は今後開発されず、安定版も出ないことになります。

[slf4j-user] Will version 1.8 be released in future?

よって現時点では1.8を利用する積極的な理由はあまりないと思われます。ライブラリ開発者としては、JPMS(Jigsaw)あるいはFluent Logging APIが必要なら2.0、そうでないなら1.7を選択することが良いのではないでしょうか。