Kengo's blog

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

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

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

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を検索しても出てこないのでちょっと苦労しました。