Featured image of post コードドクター【Iterator】急性異所性過食症〜状態の隔離と一口サイズの魔法〜

コードドクター【Iterator】急性異所性過食症〜状態の隔離と一口サイズの魔法〜

救急搬送

インフラチームから突き返された「インスタンスのメモリ64GB増設要求」の却下通知を握りしめ、俺は路地裏の雑居ビルを駆け上がった。 煤けた鉄製の扉には、飾り気のないゴシック体で「コード診療所」とだけ書かれたプレートが掛かっている。ドアノブを引いて中に飛び込むと、崩れかかったオライリー本の山の向こうから、トリプルディスプレイ越しにこちらを一瞥する男の姿があった。

「ドクター! 診断書を書いてください! インフラチームに出すやつです!」

熱でファンが唸りを上げるノートPCをカウンターに置く。

「俺のコードは完璧なのに、サーバーの胃袋が小さすぎて、毎日OOM Killerに殺されてるんです!」

ドクターは無言のまま、エスプレッソを少しだけすする。

「……過食だ」

「は? さっき昼飯は食いましたけど」

「ドクターは『サーバーの胃袋が小さいのではなく、あなたがSaaSから送られてくる大量のデータを一度に丸呑みしようとしている』とおっしゃっています」

奥から現れたのは、落ち着いた雰囲気の女性だった。助手のナナコさんだ。

「大丈夫ですよ、ここはコード診療所です。まずは患部を見せていただけますか?」

触診と画像診断

俺は舌打ちを一つして頷き、SaaSのAPIから全ユーザーデータを同期するバッチ処理のソースコードを画面に映し出した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use v5.36;

# 呼び出し側のコード(Before)
    my @all_users;
    my $current_page = 1;

    print "Fetching users (Before)...\n";

    while ($current_page) {
        my $res = $api->get_users($current_page);

        # 配列に全量詰め込む(ここで数百MB〜GBのメモリを消費する想定)
        push @all_users, $res->{items}->@*;

        $current_page = $res->{next_page};
    }

    print "Total users fetched into memory: ", scalar(@all_users), "\n";
    
    # ...この後、巨大な配列に対する処理が続く

ドクターが俺のPCをひったくり、おもむろに処理を実行した。 裏で立ち上げたアクティビティモニタのメモリ使用量グラフが、ロケットのように垂直上昇していく。PCの冷却ファンが悲鳴のような爆音を上げ始めた。

「……急性異所性過食症」

「えっ、過食症?」

「本来なら少しずつ消化すべき『100件ずつのAPIレスポンス』を、巨大な配列という胃袋に全量押し込んでいるため、破裂寸前だという診断ですね」

ナナコさんが涼しい顔で解説する。

「だって、APIが100件ずつしか返さないのが悪いんですよ! ちまちま処理してたら日が暮れるじゃないですか!」

「……状態の漏出」

ドクターが一言つぶやき、画面の while ループの条件式を指差した。

「ページネーションのトークン管理がループの外に漏れ出し、データ取得と処理のロジックがひどく癒着しています。これでは患部の切除も困難だと嘆いておられます」

消化管バイパス手術

「……咀嚼の欠如」

「無限に続くコース料理を、一口でかき込む客はいないでしょう、ということです。ドクターが今から『Iterator(反復子)』パターンによる治療を行います」

ドクターの指がHHKBの上を猛スピードで舞い始めた。 新たに作られたのは、APIのクライアント側に enumerate_users というメソッドを備えたコードだった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# APIクライアント側に追加するメソッド
use v5.36;

sub enumerate_users($self) {
    my $current_page = 1;
    my @buffer       = ();
    my $is_eof       = 0;

    # クロージャ(状態をカプセル化したサブルーチンリファレンス)を返す
    return sub {

        # バッファが空で、かつまだ終端に達していないなら次ページを取得
        if (@buffer == 0 && !$is_eof) {
            my $res = $self->get_users($current_page);

            if (scalar $res->{items}->@* == 0) {
                $is_eof = 1;    # これ以上データなし
            }
            else {
                push @buffer, $res->{items}->@*;
                
                if (defined $res->{next_page}) {
                    $current_page = $res->{next_page};
                }
                else {
                    $is_eof = 1;    # 最後まで取得完了
                }
            }
        }

        # バッファから1件ずつ取り出して返す
        return shift @buffer;
    };
}

「……クロージャによるカプセル化」

「今回は古典的なクラスベースではなく、Perlの強力なクロージャを使って『貧者のオブジェクト』を作っています。厄介なAPIページネーションの状態管理は、すべてこの小さなカプセルの中に隔離するという美しい手法ですね」

続いて、俺の書いたバッチ処理側のコードが、見違えるように書き換えられていく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use v5.36;

# 呼び出し側のコード(After)
    my $user_iterator   = $api->enumerate_users();
    my $processed_count = 0;

    print "Fetching and processing users (After)...\n";

    # イテレータから1件ずつ取り出すループ
    while (my $user = $user_iterator->()) {

        # ここで1件ずつ重い処理を行う。メモリには常に数件〜100件しか乗らない。
        $processed_count++;
    }

処置後のコードが実行される。ファンの爆音は嘘のように止み、メモリグラフは地を這うように安定した一直線を描いた。 何万件のデータが流れ込んでも、メモリ消費は一定のままだ。

安定したメモリグラフを前に驚くタケルと、落ち着いた様子のドクターとナナコ

「うわっ!? 何万件処理してるのに、メモリがピクリとも動かない……! しかも、あのごちゃごちゃだった while 文が、たった1行の綺麗なループになってる!」

術後経過

「……一口サイズ」

ドクターがキーボードから手を離し、静かに目を閉じた。

「どんなに巨大なデータストリームでも、状態を内包した Iterator を通して『一口サイズ』にすることで、安全に消化し続けることができる。暴飲暴食はやめるようにとのご忠告です」

「完全に理解しました! 俺が間違ってました!」

俺は持参していたノートPCをバタンと勢いよく閉じ、小脇に抱えた。APIの仕様が悪いだの、サーバーの胃袋が小さいだのと文句を言っていた自分がひどく恥ずかしい。

「無限ページネーションを丸呑みしようとしてた俺がバカでした! さっそく自席に戻って、あのバッチ処理をぜんぶIteratorで書き直してきます! 今すぐ!」

「ええと、実装も焦らず一口ずつ……」

ナナコさんの言葉を最後まで聞かず、俺は勢いよく踵を返した。

「診断書はもう不要です! 素晴らしい治療をありがとうございました!」

「……咀嚼を忘れるな」

ドクターの静かな声を背中に受けながら、俺は疾風のように鉄の扉を開け放ち、再び路地裏へと駆け出していった。


処方箋まとめ

症状適用すべき経過観察
全件取得によるメモリ枯渇(OOM)
ページネーション等の状態管理が呼び出し側に漏洩している
配列をそのまま返すだけの小規模なデータ

治療のステップ

  1. 状態の隔離: APIのページネーション情報(現在のページ、次のトークンなど)を専用のスコープやクラスに隠蔽する。
  2. 要素の供給口を作成: 呼び出し側には要素を「1件ずつ」返すメソッド(またはクロージャ)のみを公開する。
  3. ループの簡略化: 呼び出し側は while でその供給口を叩き続けるだけの、シンプルな記述にリファクタリングする。

助手より

ページネーションの制御が呼び出し側に散らばっていると、読みづらいだけでなく、メモリの過食症を引き起こす原因にもなります。Iterator は単なるループの代替ではなく、「状態のカプセル化」による胃腸の保護なのです。 エナジードリンクの舐め飲みはおすすめしませんが……コードの消化不良が直って何よりです。今後のご活躍をお祈りしております。

——ナナコ

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。