Kengo's blog

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

「ドメイン駆動設計入門」付録のGradle向け解釈

ITエンジニア本大賞2021で紹介されていた「ドメイン駆動設計入門」(以下、本書と呼ぶ)が、DDDを学ぶ上でわかりやすかったです。一応他のDDD本も数冊読んではいたのですが、どうしてもユビキタス言語や境界づけられたコンテキストなど”場合による”ものが頻出してしまい、結局どうすればいいんだ……となって手が動きにくかったのです。よくわからない概念の上にさらにわからない概念を積み重ねるので、どんどん混乱する感覚がありました。

反面、本書では副題の通りボトムアップ形式を採っているため、実際にどう手を動かせば良いのか細かく確認できました。また不明点を最小化しながら進むだけでなく、その概念を導入することでどんな問題が解決されるのかが実例をもって明示されており、私のような独学派にはとてもありがたかったです。

さて本書ではC#を使っています。Javaとたいして変わらないとはいえ細かいところは違ってきますので、ここでは付録で紹介されたソリューション(プロジェクト)構築方法をGradleユーザ向けに解釈したものを紹介します。なおウェブアプリケーションフレームワークとしてspring-bootを利用するものとしますが、他のフレームワークでも大差ないはずです。

サブプロジェクトによる分割

Mavenではサブモジュール、Gradleではサブプロジェクトを使ってプロジェクトを分割します。本書ではプロジェクト分割の方針を2つ紹介していますが、ここでは簡単のためすべてのレイヤが固有サブプロジェクトを持つ構成を説明します。

domainサブプロジェクトには値オブジェクトやエンティティ、ドメインサービス、仕様やリポジトリインタフェースを配置します。ほぼPOJOで済むため、依存はspring-contextのようなアノテーションのみとなるでしょう。

ドメインオブジェクトの振る舞いの多くがここに配置されるため、Javadocやunit testを優先的に充実させる必要があるでしょう。また equals()hashCode()を手書きするとコードやテストカバレッジの見通しが悪くなることから、Immutablesのようなコードジェネレータによって対策することも考えられます。

infrastrutureサブプロジェクトはdomainサブプロジェクトに含まれるリポジトリインタフェースの実装を配置します。このサブプロジェクトはJDBCドライバのような依存先ミドルウェア固有の依存を持ちます。productionで利用する実装に加え、オンメモリで動作する実装も別途用意することで、unit testを書きやすくできます。

このサブプロジェクトではクラスをpublicにする必要はありません。Springが自動的に@Repositoryで修飾されたクラスを見つけてインスタンスを作るためです。他サブプロジェクトのunit testから呼び出す必要がある場合のみ、publicにすると良いでしょう。

applicationサブプロジェクトはdomainサブプロジェクトとinfrastructureサブプロジェクトの双方に依存します。アプリケーションサービスによるユースケースの記述をメインとするサブプロジェクトです。

本書の方針に従うならば、このサブプロジェクトに含まれるクラスやメソッドのAPIにはドメインオブジェクトを露出させないことが望ましいと言えます。このため他サブプロジェクトには api configuration ではなく implementation configuration を使って依存します。これによりドメインオブジェクトの露出をコンパイルエラーとして発見できる可能性が上がります。詳細は後述します。

presentationサブプロジェクトはapplicationサブプロジェクトにのみ依存します。後述するGradleの機能により、presentationサブプロジェクト内ではドメインオブジェクトやRepositoryの実装に触れない状態を担保できます。MockMvcなどを使うことでunit testが重くなり、またこのサブプロジェクトをビルドしている際はGradleのworkerが空きがちなので、maxParallelForksオプションでテストを並列実行することの恩恵が大きいでしょう。

apiとimplementationの使い分け

ここまでで2つ、詳細を後述すると述べたことがあります:

  1. presentationサブプロジェクトに対するドメインオブジェクトの露出をコンパイルエラーとして発見する方法
  2. presentationサブプロジェクト内ではドメインオブジェクトやRepositoryの実装に触れない状態を担保する方法

これらは表現こそ異なりますがひとつの課題を示しています。すなわち「ドメインオブジェクトをpresentationサブプロジェクトに露出させたくない」です。 もちろん依存先のpublicフィールドや戻り値がドメインオブジェクトでないことをArchUnitなどで確認しても良いですが、Gradleならこれらをコンパイルエラーとして発見する方法があります。

API and implementation separationがこの課題に対する解決になります。これはAPIとして外部に露出している依存と、そうではない内部利用している依存とを明確に分けて定義するものです。つまりサブプロジェクトが内部利用している依存は、そのサブプロジェクトの利用者に対して開示する必要はないという考えです。

これにより、applicationサブプロジェクトが依存しているdomainサブプロジェクトやinfrastructureサブプロジェクトを、presentationサブプロジェクトのコンパイル時クラスパスに含めずプロジェクトをビルドすることができます。 依存に色を付ける必要があるという意味で手間は増えますが、そもそもAPIとして露出している依存は減らすべき(JLBP-2)という話もありますので”依存がAPIとして露出しているのか?”を意識することは有用です。Gradle公式ドキュメントによるとコンパイルのパフォーマンス改善も期待できるケースがあるそうです。

例えば以上のサブプロジェクトの依存関係を図示すると、以下のようになります。

f:id:eller:20210131174527p:plain
com.savvasdalkitsis.module-dependency-graph によって生成

api configurationを使うのは1箇所だけです。infrastructureサブプロジェクトに含まれるクラスはdomainサブプロジェクトに含まれるリポジトリインタフェースを実装しており、またドメインオブジェクトをその引数や戻り値に使っているため、api configurationを使って依存します。その他はすべて implementation configurationが使えます。

補足: アセット生成を独立したサブプロジェクトの責務とする理由

前出の図に含まれているfrontendサブプロジェクトは、HTMLやJSなどのアセットを生成するためのものです。ドメイン駆動からは外れますが解説します。

このサブプロジェクトだけ若干特殊で、npmやyarnのプロジェクトをfrontend-gradle-pluginを通じてビルドする作りになります。ReactやVueの開発をGradleで無理やり実現するのではなくnpmあるいはyarnを呼び出す形を取ることで、JavaScript開発の知見を無理なく導入するとともにフロントエンド開発者にGradleやJavaの学習を強要する必要性を減らせます。

ちなみにビルド性能の向上も期待できます。これはfrontend-gradle-pluginのタスク、特にinstallFrontendタスクが重いことと、Gradleは異なるプロジェクトに属するタスクだけparallel buildできることから、説明できます。なおプロジェクトと同数以上のworkerがいないと性能が出ないので、--max-workersオプションを使うかorg.gradle.workers.maxプロパティを使って明示的にワーカー数を引き上げておくことが望ましいです。以下に手元の環境でhyperfineした結果を貼っておきます:

Benchmark #分割前: ./gradlew clean build
  Time (mean ± σ):     28.356 s ±  2.624 s    [User: 1.419 s, System: 0.145 s]
  Range (min … max):   25.914 s … 33.624 s    10 runs

Benchmark #分割後 (4 workers): ./gradlew clean build
  Time (mean ± σ):     27.161 s ±  3.987 s    [User: 1.388 s, System: 0.137 s]
  Range (min … max):   23.202 s … 33.721 s    10 runs

Benchmark #分割後 (5 workers): ./gradlew clean build --max-workers=5
  Time (mean ± σ):     24.442 s ±  2.916 s    [User: 1.321 s, System: 0.135 s]
  Range (min … max):   21.799 s … 31.388 s    10 runs

まとめ

ドメイン駆動設計入門」付録のGradle向け解釈を述べました。本書は私のような、ドメイン駆動を手を動かしつつ独学したい方におすすめします。

本投稿が本書の内容を実践する上でGradleユーザの参考になれば幸いです。またGitHubに私の書いたプロジェクトを置いてありますので、具体的にプロジェクト設定を見てみたい方は参照ください。

最近見かける新しいライセンスについて

Elastic社のブログをきっかけに、最近見かける新しいライセンスについて個人的に調べてみた。私は専門家ではないので要注意。公開情報も隅々まで追えているわけではないし。

なお一部ライセンスはOpen Source Initiative (OSI)による承認を受けていないので、ここではオープンソースライセンスではなく単に「ライセンス」と書くことにする。

新しいライセンスが誕生している背景

  • 従来のオープンソースライセンスが再頒布以外の利用をあまり想定していなかった。
  • Open-core modelないし完全オープンソース戦略を採る企業が自衛策を必要とした。
  • 既存のライセンスが難解なため、理解しやすいライセンスが求められた。
  • OSS活動を収入に繋げるためのモデルが試行錯誤されている。

新しいライセンスを導入しているプロジェクト(一例)

プロジェクト ライセンス
Elastic SSPLと独自ライセンス
MongoDB SSPL
CockroachDB BSL *1
Redis Modules 独自ライセンス
MariaDB BSL
Artifex AGPL3と独自ライセンス
Husky v5 License Zero Parity 7.0.0 and MIT (contributions) with exception License Zero Patron 1.0.0

各種ライセンス

GNU Affero General Public License (AGPL3)

言うほど新しくない。2010年に公開されたnippondanji氏のスライドを参照。

Business Source License (BSL)

MariaDBが作成、v1.1が最新。以下説明文をサイトから引用:

BSL is a new alternative to Closed Source or Open Core licensing models. Under BSL, the source code is freely available from the start and it is guaranteed to become Open Source at a certain point in time (i.e., the Change Date). Usage below a specific level in the BSL is always completely free. Usage above the specified level requires a license from the vendor until the Change Date, at which point all usage becomes free.

一定期間後にオープンソースになる(MaxScaleならGPLのv2以降になる)ということは、旧バージョンのサポートをユーザ自身が行うことも他の企業が請け負うことも可能ということ。最新バージョンの開発に注力したいLicensorと、サポートが切れたときの対応を懸念するユーザとの双方に利がありそう。以下のQ&Aはこれを意図しての記載と思われる。

Q: What are the main benefits for a company using BSL for their product?

A: BSL allows anyone to work on the code, both for their own benefit and that of others. It also generates trust in the BSL product, as there is no lock-in to a single software vendor.

このときusage limitsの定義はLicensorに委ねられている。例えばMaxScaleだとサーバ台数で絞っていて、2台までならどのような用途でも制限なく利用可能。このため”自衛策”になると期待される。

ちょっと気になるのはChange Dateの変更タイミングで、FAQには以下のようにメジャーバージョンごとに設定するものとある。マイナーバージョンごとにEOLを設定したいケースもあると思うのだけど。

Q: Will the Change Date remain constant?

A: No. Each new major version of the software will have its own Change Date.

Server Side Public License (SSPL)

今回Elasticが採用したライセンスで、MongoDBも利用している。GPLv3をベースにしているらしい

AGPLv3をベースにしなかった理由として、AGPLv3のRemote Network Interactionの定義が曖昧で市場に混乱を生んでいたと書いてある。GNUのFAQを見ても、確かに厳密性は低いと感じる。ユーザのリクエストを直接受けない、ユーザにレスポンスを直接返さないと解釈・主張できるケースは多数ありそう。なおAGPLv3とSSPLの違いがMongoDB公式サイトで配布されているので、具体的な違いを容易に確認できる。

ソースコードを変更することなく自社サービスのコンポーネントとして提供する場合は、特に制約はない。MongoDBのFAQにも以下の記載がある:

Q: What are the implications of this new license on applications built using MongoDB and made available as a service (SaaS)?

A: The copyleft condition of Section 13 of the SSPL applies only when you are offering the functionality of MongoDB, or modified versions of MongoDB, to third parties as a service. There is no copyleft condition for other SaaS applications that use MongoDB as a database.

プログラムの機能自体をサービスとして提供する場合は、そのサービスのソースコードを公開する必要がある。ここでリンクの有無やユーザにレスポンスを返すかとかは関係なく、"the Service Source Code"すべてが対象になっているのに注目。MongoDBのFAQ "What specifically is the difference between the GPL and the SSPL?" では以下が対象になると書いてある:

the software it uses to offer such service under the terms of the SSPL, including the management software, user interfaces, application program interfaces, automation software, monitoring software, backup software, storage software and hosting software, all such that a user could run an instance of the service using the source code made available.

無理ゲー感満載である。もしここで言うsoftwareがクラウドベンダーが持つIaaS/PaaSをも対象とするなら、事実上不可能と言えそう。

この制約はオープンソースライセンスとしては強すぎるようで、OSIからOpen Software Definitionに明確に不適合だと言われているそう。 21日にはOSIからThe SSPL is Not an Open Source Licenseという声明も出ている。

Parity Public License

Husky v5が採用しているライセンス。7.0.0が最新。GitHubで各種ドキュメントが公開されている。あまりアクティブじゃなさそうというか、さほど導入実績がなさそうというか、Huskyもよく使う気になったなぁという感じ。READMEにはアーリーアクセスが完了したらMITに戻すとも書いてあるので、試用という立ち位置なのだろうか。

無償で自由に使えるし共有も可能だが、ライセンスされたソフトウェアを使ってビルドされたソフトウェア(software that builds on it)もまた同様に公開されなければならない。このbuilds on itという表現が珍しい。Huskyで使われているということは、ライブラリとしてリンクしたソフトウェアだけではなく、このソフトウェアをdevDependenciesに入れて利用したすべてのソフトウェアに対して効力があるものと推測される。ホントか?

またライセンスにDon’t make any legal claimという文が入っているのも気になる。Facebookが”BSD + Patents”でやらかしたのと何が違うんだろう。ここまでくるとIANALなのでさっぱりわからない。

Patron License

Husky v5が採用しているライセンス。1.0.0が最新。他のライセンスと組み合わせて使う。 支払いを行うことで他のライセンスが持つnoncommercial or copyleftの制約を外すことができる。つまり商用利用やソースコード改変がしたかったら支払いをするように、ということ。

Husky v5は自身のライセンスを「License Zero Parity 7.0.0 and MIT (contributions) with exception License Zero Patron 1.0.0」としているが、整理すると:

ということらしい。Huskyは2年前からnpm install時に寄付を呼びかけていたので、金銭的支援を得ることがライセンス導入の主目的と思われる。サービスではないOSSが選択可能な収益につながるライセンスは現状他になさそうなので、Huskyが先鞭をつけてくれるといいなぁ。

結局こうしたライセンスは勝手に "as a Service" されることへの自衛策になるのか

  • BSLを適用することで、サーバ台数などに制限をかけることができそう。Elasticの語る将来の計画を見る限りでは、有償モジュールの代替機能開発・利用を禁止する拘束力を持たせることも可能と見ているのかも。
  • 完全にソースコードを公開しているプロジェクトの場合、AGPLを適用しても"as a Service"への抑止力は得られないが、サービス提供側に改変内容を公開する必要性が生じる。それでも大規模なインフラを持っているクラウドベンダーに目をつけられると競争力を発揮できなさそう。
  • Open-core戦略を採るプロジェクトの場合、AGPLを適用しても"as a Service"への抑止力は得られないが、サービス提供側に改変内容を公開する必要性が生じる。非公開モジュールと同じ機能を持つモジュールをクラウドベンダーが実装し"as a Service"を実現することに対する抑止力をどう持たせるかが課題か?
  • SSPLを適用することで、プログラムをコンポーネントとして使うユーザの自由を確保しつつ、"as a Service"への抑止力を得ることができる。
  • Patron Licenseは商用利用自体を制限するライセンスと組み合わせなければならず、"as a Service"だけを制限したい場合には使いにくそう。

揺らぐオープンソースの定義

これらのライセンスを適用すると厳密な意味でのオープンソースではなくなるわけだが、より強力かつ実情に沿ったコピーレフトを実現するという意味でも、オープンソースの定義自体が更新を必要としていると見ることもできそう。"as a Service"を自由に提供できるのも、ひとつのopennessではあるわけで。

ライセンスが乱立しても良いことはないので、OSIなどの団体がいずれBSLなどを追認するような流れになると良いなぁと思う。

以上。間違っている箇所があれば、はてなブックマークTwitterでご指摘いただけるとありがたい。

更新履歴

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するようにすればできるはず

2021年8月29日更新:下記、2つやり方をまとめました。

技術書「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