executeTasksでs3のオブジェクトを高速削除

# AWS SDKを用いたオブジェクト削除
AWS S3はあらゆるサービスの基盤になっているので、S3上のオブジェクトを大量に消すコードを書いたことがある人は多いかと思います。

削除が遅いと感じたことはありませんか？
遅いのをJavaScriptのせいにしていませんか？

## DeleteObjectCommandで一個ずつ削除
DeleteObjectで一個ずつ消すことも可能ですが、これは論外ですね。

```typescript
    for await (const {Contents} of paginateListObjectsV2({
        client
    }, {
        Bucket
    })) {
        for (const {Key} of Contents ?? []) {
            await client.send(new DeleteObjectCommand({
                Bucket,
                Key
            }))
        }
    }
```

測定結果

```
あまりの遅さに大量オブジェクトで測定することを断念しました。
```

## DeleteObjectsCommandで削除

AWS SDKを使っている場合に、大量のオブジェクト削除はこのように書くことが多いと思います。

```typescript
    for await (const {Contents} of paginateListObjectsV2({
        client
    }, {
        Bucket
    })) {
        if (Contents === undefined) {
            continue
        }
        await client.send(new DeleteObjectsCommand({
            Bucket,
            Delete: {
                Quiet: true,
                Objects: Contents.map(i => ({
                    Key: i.Key
                }))
            }
        }))
    }
```

```
Create 10000 object in target bucket
batch 12273 ms 1.2273ms/key
```

pagingしながらキーを最大1000個取得して、DeleteObjectsで一括削除。何も問題ないように思われます。

秒間815件、JavaScirptならこの程度と思っていませんか？

遅い理由はシンプルで、オブジェクト削除のリクエストを並行して出すことができていません。
かといって、全部並行してPromise.allにするのはあまりにも乱暴です。


## ya-synのexecuteTasksを利用する

ya-synを使うと下記のようにコードを書き換えることができます。

最大1000個分のタスクを読み込み、SDKに合わせて最大50並列でリクエストを流します。

```typescript
    const sp = new SynchronizerProvider()
    await sp.executeTasks({
        maxTasksInFlight: 1000,
        maxTasksInExecution: 50,
        taskSource: async function* () {
            for await (const {Contents} of paginateListObjectsV2({
                client
            }, {
                Bucket
            })) {
                if (Contents === undefined) {
                    continue
                }
                yield {
                    task: Contents.map(i => i.Key!) as string[]
                }
            }
        }(),
        taskExecutor: async ({task}) => {
            await client.send(new DeleteObjectsCommand({
                Bucket,
                Delete: {
                    Quiet: true,
                    Objects: task.map(Key => ({
                        Key
                    }))
                }
            }))
        }
    })
```

実行結果

```
Create 10000 object in target bucket
batch executor 3280 ms 0.328ms/key
```

秒間3048件ぐらいなので、nodejsベースとしては悪くない数字だと思います。

maxTasksInExecutionを指定せずに、Key PrefixごとにSemaphoreを分ければもう少し高速化できる可能性もあります。

## キーごとにセマフォ分離
特定prefixへのアクセスを緩和するため、先頭４文字prefix単位での実行数を2に絞ってみました。

prefixで厳密に分けるとリクエストが増えてしまうので、最初の１件のprefixだけをみています。本気でやるなら並列度と、prefixの調整でもう少し速くなると思います。

```typescript
    const sp = new SynchronizerProvider()
    const connectionSemaphore = sp.createSynchronizer({
        maxConcurrentExecution: 50
    })
    await sp.executeTasks({
        maxTasksInFlight: 1000,
        taskSource: async function* () {
            for await (const {Contents} of paginateListObjectsV2({
                client
            }, {
                Bucket
            })) {
                if (Contents === undefined) {
                    continue
                }
                yield {
                    task: Contents.map(i => i.Key!) as string[]
                }
            }
        }(),
        taskExecutor: async ({task}) => {
            await sp.forKey(task[0].substr(0, 4), 2).synchronized(async () => {
                await connectionSemaphore.synchronized(async () => {
                    await client.send(new DeleteObjectsCommand({
                        Bucket,
                        Delete: {
                            Quiet: true,
                            Objects: task.map(Key => ({
                                Key
                            }))
                        }
                    }))
                })
            })
        }
    })
```

10万件でやってみました。4135件/秒ぐらい。
割と時間がかかる上に課金が怖いので、１回しか試してないです。

```
Create 100000 object in target bucket
batch executor 24179 ms 0.24179ms/key
```


# 自作ライブラリ ya-syn
今回紹介した実装はya-syn v1.2.0の実装の一部になっています。

https://github.com/tomohisaota/ya-syn

今回の例だとページングサイズとバッチ削除のサイズがどちらも1000なので簡単ですが、このサイズが合わない場合でもya-synなら簡単に対応できます。