【Kotlin】ロックフリー・一括生成に対してソート可能なUUID v7生成器を作った

# 状況
`ID`として`UUID v4`を使っているプロジェクトで、限定的な状況で`ID`をソート可能にしたくなりました。
`ULID`や`UUID v7`の生成をサポートするいい感じのライブラリを探しましたが、要件上ロック周り[^lock]が微妙っぽかったり、同一タイムスタンプで生成した場合に確率でエラーになりそう[^kakuritu]だったりと、採用できそうなライブラリを見つけられませんでした。

[^lock]: 作った方が早そうだったので厳密な検証はしていませんが、リアクティブスタックと`ReentrantLock`の相性が悪そうという点を気にしました。

[^kakuritu]: 具体的には`java-uuid-generator`を使おうとしていましたが、[シーケンシャルずらし処理がオーバーフローしうる](https://github.com/cowtowncoder/java-uuid-generator/blob/f59ec83a984a16898cb7b2d426392b5ed4e7d4d2/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java#L136)ように見える点を気にしました。

そこで、[java\-uuid\-generator](https://github.com/cowtowncoder/java-uuid-generator)を参考に、ロックフリーかつ簡易な`UUID v7`生成器を作りました。
コードは`Kotlin`ですが、`Java`でも問題なく再現できると思います。

一応それなりに調べて作ったつもりですが、何か問題など有ればコメント頂けると嬉しいです。

## ソート可能な`UUID`の生成処理とロックの関係
ソート可能な`UUID`は、ざっくり言うと「タイムスタンプ + 乱数」で構成されます。

ここで、同一タイムスタンプで複数の生成が行われた場合、乱数部分しか違いが無くなって厳密な生成順でのソートが不可能になります。
`UUID v7`の要件的にもミリ秒精度が基本かつ同一タイムスタンプ生成内ソートをサポートするかは任意と定義されているようで、最近のマシンでは複数件一括生成するだけでこの問題に当たります。

https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-14.html#name-uuid-version-7

これを避ける方法の1つとして、生成処理毎にロックして、同一タイムスタンプ内で複数回生成された際に何らかのシーケンシャル値をインクリメントするように生成するやり方が有ります。
ただし、ロックはコードの複雑化や性能低下につながるので、今回は採用しませんでした。

# 実際に作ったもの
実際に作った生成器は以下の通りです。
なるべく実装を簡易にしたかったため、以下の制約が有ります。

- 別の一括生成とソート順が保証されない
- 4096件までしか一括生成できない
- 全て同一タイムスタンプで生成した扱いになる

```kotlin
import java.security.SecureRandom
import java.util.*

object SortableUuidGenerator {
    private const val UUID_VERSION: Long = 7 shl 12
    // randomUUIDの生成処理を参考にした
    private val numberGenerator = SecureRandom()

    /**
     * 指定された件数のUUID v7を生成し、指定されたマッパー関数で変換したリストを返す
     * @param size 生成するUUIDの件数、シーケンシャル採番の都合で最大4096（12ビットの最大値）まで
     */
    // 上位64ビット:
    //   48ビット目まで: ミリ秒精度のタイムスタンプ
    //   49 ~ 52ビット目: UUIDバージョン
    //   53 ~ 64ビット目: シーケンシャル値
    // 下位64ビット: 乱数
    fun <T> generate(size: Int, mapper: (UUID) -> T): List<T> {
        if (size < 0) throw IllegalArgumentException("サイズは0以上を指定してください $size")
        // 最大4096件に絞ったのは簡単のため
        if (4096 < size) throw IllegalArgumentException("サイズは4096までで指定してください $size")
// 52ビット目まで

        val mostSigBitsBase: Long = System.currentTimeMillis().shl(16) or UUID_VERSION 

        return (0 until size).map { seq ->
            // 下位ビットがシーケンシャルなため、単純加算で書ける
            mapper(UUID(mostSigBitsBase + seq, numberGenerator.nextLong()))
        }
    }
}
```

以下詳細な解説をまとめます。

## 作成方針
今回のユースケースでは「一括生成内での順番さえ保持されていればOK（= 別の一括生成とソート順が保証されなくてOK）」でした。
つまり、一括生成処理内でシーケンシャル値を管理すれば、ロックしなくても実現できます。

ロック周りをちゃんと作り込んで厳密にするとか、より高精度なタイムスタンプ利用も考えましたが、要件に対して過剰性能な割に実装が複雑かつ検証も大変となるので、今回は見送りました。

`mapper`を渡せるようにしているのは、`UUID`をラップする値オブジェクトを作りやすくするためです（つまり、不要なら消せます）。

## この`UUID`の構成
この生成機を使うと`018f4d64-a9e0-7000-c5f1-34fa1d006225`というような`UUID`が得られます。
これは以下のような構成になっています。

- `018f4d64-a9e0`: `System.currentTimeMillis()`で取得したミリ秒精度のタイムスタンプ
- `7000`:
  - `7`は`UUID v7`を表すバージョン
  - `000`はシーケンシャル値
- `c5f1-34fa1d006225`: 64ビット乱数

### タイムスタンプの扱い
要件的に使いまわしても困らないため、1度採番したものをそのまま使う方針としました。
値の操作は以下のコードを参考にしました。

https://github.com/cowtowncoder/java-uuid-generator/blob/f59ec83a984a16898cb7b2d426392b5ed4e7d4d2/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java#L142

### シーケンシャル値の扱い
以下の`rand_a`部分の16進数3桁を0 ~ fffのシーケンシャル値として利用しています。

https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-14.html#name-uuid-version-7

「Batch UUID creation implementations MAY utilize a monotonic counter that increments for each UUID created during a given timestamp.」となっていたので、これでも仕様は満たせている認識です。

https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-14.html#monotonicity_counters

16進数3桁をそのままカウンタにした理由は以下の通りです。

- 4096件同時生成できれば一般的にもほぼ困らない
- 2桁や4桁にすると乱数生成や桁合わせが面倒だが、3桁なら諸々簡単

### 乱数の扱い
下位64ビットが乱数になります。
`var`部分の扱いが気になりましたが、「Random data for each new UUIDv7 generated for any remaining space.」となっており、`java-uuid-generator`でも乱数で埋められていたため、問題無しと判断しました。

https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-14.html#section-5.7-3.3.1

https://github.com/cowtowncoder/java-uuid-generator/blob/f59ec83a984a16898cb7b2d426392b5ed4e7d4d2/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java#L142