Contents

Coniでのネイティブ並列ダウンロードの構築

独自の言語やツールを構築する際の素晴らしい点の一つは、操作がどのように実行されるかを再考できることです。最新のコミットでは、ビルドプロセスの大きなボトルネックであるMaven依存関係のダウンロードに取り組むことにしました。

プラットフォーム依存スクリプトの問題点

これまで、Coniのdownload-url-to-file関数は、プラットフォーム固有のツールを呼び出す(シェルアウトする)ことに依存していました:

  • Linux/macOSでは、curlプロセスを生成しました。
  • Windowsでは、System.Net.WebClientを使用する巨大なpowershellコマンドを呼び出しました。

これは機能していましたが、いくつかの欠点がありました。第一に、外部プロセスを呼び出すことは遅く、リソースを消費します。第二に、外部ツールへの依存は、curlのバージョンやWindowsのセキュリティプロトコルの微妙な違いが予期せぬ失敗を引き起こす可能性があることを意味しました。最も重要なのは、これらのシェルコマンドを使用してアーティファクトを順次(シリアルに)ダウンロードするため、大規模なMavenプロジェクトの解決に非常に時間がかかっていたことです。

ネイティブな解決策

コミット9ac76d12で、Goの評価器(エバリュエーター)内に直接実装されたネイティブの組み込み関数sys-http-downloadを導入しました。この新しい組み込み関数は、Goの標準のnet/httpクライアントを使用します。

評価器にネイティブに組み込まれているため、プロセス作成のオーバーヘッドを完全に回避できます。さらに、Maven Centralからのレート制限(認識可能なUser-Agentなしで速すぎるアクセスをした場合に頻繁に発生する「429 Too Many Requests」エラー)を処理するために、Goの実装に直接堅牢なリトライロジックを追加しました。

ゴルーチンによるスーパーチャージ

ネイティブの組み込み関数が準備できた後、Coniの並行処理プリミティブを適用したときに真の魔法が起こりました。Coniはgochan<!(チャネルからの読み取り)、>!(チャネルへの書き込み)を使用して、Goスタイルの並行処理をネイティブにサポートしています。

アーティファクトを一つずつダウンロードする代わりに、標準ライブラリの中に直接ワーカープールをセットアップしました:

(let [total-count (count missing)
      result-ch (chan total-count)
      task-ch (chan total-count)]
  ;; すべてのタスクをタスクチャネルにプッシュ
  (loop [rem missing]
    (if (not (empty? rem))
      (do 
        (>! task-ch (first rem))
        (recur (rest rem)))))
  (close! task-ch)
  
  ;; タスクを並行処理するために8つのワーカーゴルーチンを生成
  (loop [i 0]
    (if (< i 8)
      (do
        (go 
          (loop []
            (let [item (<! task-ch)]
              (if (not (nil? item))
                (do
                  (>! result-ch (download-file-single item repos))
                  (recur))))))
        (recur (+ i 1)))))

  ;; 結果を待ち、進捗を表示
  ...)

ネットワークを圧倒したり、攻撃的なレート制限を引き起こしたりするのを防ぐため、ワーカープールの最大値を8ゴルーチンに制限しています。

結果

スピードアップは劇的です。かつては長く直列化されたシェル呼び出しの連続だったものが、現在では完全にConiのランタイム内で処理される迅速な並列化操作になりました。

私たち自身の並行処理プリミティブを自分たちで使用(ドッグフーディング)することで、Coniのチャネルベースのアーキテクチャが単なる楽しいおもちゃではなく、言語自体のコアなI/Oタスクを効率的に処理するのに十分堅牢であることを証明しました。