Internals
コミット
3443acd9のソースを読んだもの。ここでの主張はすべてファイルと行を指す。
コードマップ
| パス | 責務 |
|---|---|
app/src/main/java/io/apicurio/registry/rest/v3/impl/ | REST v3 リソース実装 |
app/src/main/java/io/apicurio/registry/rules/ | 階層型ルールエンジン |
app/src/main/java/io/apicurio/registry/storage/ | RegistryStorage インターフェースと DTO |
app/src/main/java/io/apicurio/registry/storage/impl/sql/ | SQL バックエンド、content ハッシュと dedup |
schema-util/util-provider/ | ArtifactTypeUtilProvider SPI |
schema-util/common/ | ContentCanonicalizer などの共有 content インターフェース |
schema-util/avro/ | Avro の canonicalizer と validator |
中核データ構造
RegistryStorage は全バックエンドが満たす境界。その createArtifact メソッドは app/src/main/java/io/apicurio/registry/storage/RegistryStorage.java:125 で宣言され、Pair<ArtifactMetaDataDto, ArtifactVersionMetaDataDto>(新 artifact のメタデータと、あれば初版のメタデータ)を返す。扱う DTO は storage/dto/ 配下にあり、KafkaSQL バックエンドが journal に書き込むため serializable でなければならない。
ArtifactTypeUtilProvider は type ごとの SPI。schema-util/util-provider/src/main/java/io/apicurio/registry/types/provider/ArtifactTypeUtilProvider.java:20 で宣言される。getArtifactType() は 22 行目、getContentCanonicalizer() は 28 行目、getContentValidator() は 32 行目、getCompatibilityChecker() は 34 行目。各スキーマ型モジュールがこのインターフェースを実装する。
ContentCanonicalizer は content を正規形に変換し、書式やフィールド順の差を比較前に消す。インターフェースは schema-util/common/src/main/java/io/apicurio/registry/content/canon/ContentCanonicalizer.java:11、唯一のメソッド canonicalize(...) は 18 行目:
public TypedContent canonicalize(TypedContent content, Map<String, TypedContent> resolvedReferences);追う価値のあるパス
面白いのは SQL バックエンドが content を保存し dedup する仕組み。保存される各 content 行は 2 つのハッシュ(生ハッシュと正規化ハッシュ)を持つ。ロジックは app/src/main/java/io/apicurio/registry/storage/impl/sql/repositories/SqlContentRepository.java:569 の ensureContentAndGetId(...) にある。
draft content かつ draft production mode 無効のとき、コードは意図的に実ハッシュを避け、content lookup で draft が見つからないようにする。SqlContentRepository.java:591:
if (isDraft && !draftProductionMode) {
contentHash = "draft:" + UUID.randomUUID().toString();
canonicalContentHash = "draft:" + UUID.randomUUID().toString();通常 content では 2 つの実ハッシュが SqlContentRepository.java:603 と SqlContentRepository.java:604 で計算される:
contentHash = utils.getContentHash(content, finalReferences);
canonicalContentHash = utils.getCanonicalContentHash(content, artifactType, finalReferences,
referenceResolver);両ハッシュ関数は app/src/main/java/io/apicurio/registry/storage/impl/sql/RegistryStorageContentUtils.java にある。getCanonicalContentHash(...) は 70 行目で、content を type 固有の canonicalizer に通し(76 行目、参照がなければ 81 行目)、DigestUtils.sha256Hex(...) でハッシュ化する(78 行目、参照がなければ 82 行目)。getContentHash(...) は 92 行目で、正規化を飛ばしてバイト列を sha256Hex で直接ハッシュ化する(96 行目、参照がなければ 99 行目)。
canonicalizer 呼び出しは SPI 経由でディスパッチされる。RegistryStorageContentUtils.java:41:
return factory.getArtifactTypeProvider(artifactType).getContentCanonicalizer()
.canonicalize(content, resolvedReferences);canonicalize が例外を投げると、メソッドは best-effort で元 content を返す(RegistryStorageContentUtils.java:43 から RegistryStorageContentUtils.java:48)。
content はハッシュでユニークに保存される。2 つの artifact version が同一 content を持つと、2 回目の insert は PK 違反になり、それを握りつぶして既存 content 行を再利用する。SqlContentRepository.java:632:
if (sqlStatements.isPrimaryKeyViolation(e)) {
log.debug("Content with content hash {} already exists: {}", finalContentHash, content);
return null;孤立した content は非同期に GC される。AbstractSqlRegistryStorage.java:510 のコメントは、dry run や create 失敗が孤立 content を残しうること、それを async プロセスが後で削除することを述べている。
読んで驚いた点
正規化ハッシュは意味的 dedup を可能にする。書式やフィールド順だけが違う 2 つのスキーマは同じ正規化ハッシュを生むので、canonical=true での lookup は生ハッシュが違っても同一 content として扱う。処理は完全に artifact type の SPI に押し込まれており、だから新しいスキーマ言語の追加は 1 つの schema-util/<type>/ モジュールに収まる。
Avro の canonicalizer がこの仕掛けを具体的に示す。schema-util/avro/src/main/java/io/apicurio/registry/avro/content/canon/AvroContentCanonicalizer.java:40 で content を JSON として読み、fields プロパティを TreeSet で並べ直してから書き戻す(AvroContentCanonicalizer.java:45 から AvroContentCanonicalizer.java:56):
JsonNode fieldsNode = root.get("fields");
if (fieldsNode != null) {
Set<JsonNode> fields = new TreeSet<>(fieldComparator);
Iterator<JsonNode> elements = fieldsNode.elements();
while (elements.hasNext()) {
fields.add(elements.next());
}
ArrayNode array = new ArrayNode(mapper.getNodeFactory());
fields.forEach(array::add);
ObjectNode.class.cast(root).replace("fields", array);
}その JSON パスが例外を投げると、Avro の Schema.Parser で parse し直し schema.toString で再シリアライズする fallback に落ちる(AvroContentCanonicalizer.java:60 から AvroContentCanonicalizer.java:67)。
draft ハッシュの逃げ道がもう 1 つの非自明な点。両ハッシュを "draft:" + UUID に置き換えること(SqlContentRepository.java:591)で draft content を content-by-hash インデックスから外し、未完成の draft が production content と衝突したり dedup されたりすることを昇格まで防ぐ。