**同期バリアを使ったPostgreSQL のレースコンディション検証**

2026/02/17 5:23

**同期バリアを使ったPostgreSQL のレースコンディション検証**

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

要約

この記事では、決定的同期バリア(deterministic synchronization barriers)が、同時実行データベース操作をテストする際に通常の逐次テストスイートでは検出できないレースコンディション・バグを露呈させる方法について説明しています。

createBarrier(count)
ヘルパーを挿入すると、複数のゴルーチンがすべて期待されるリクエストが同じポイントに到達するまで停止し、制御されたインターレイ(interleaving)を強制します。

クレジットアカウント例では、初期

SELECT
(読み取り)とその後の
UPDATE
(書き込み)の間にバリアを配置すると決定的な失敗が発生します。ロックなしで最終残高が $200 になるはずなのに、両方のゴルーチンが同じ古い値 ($100) を読み取り、$50 を上書きするため最終残高が $150 になってしまいます。
PostgreSQL のデフォルトの READ COMMITTED 隔離レベルでは、単にトランザクションで操作を包むだけではこの問題は防げません。

SELECT … FOR UPDATE
を使用すると行レベルロックが取得できますが、バリアを SELECT の に配置した場合、両方のトランザクションが同時にバリアに到達するとデッドロックになる可能性があります。
バリアを
BEGIN
の直後(すなわち SELECT の前)に置くと、最初のトランザクションがロックを取得し、2 つ目は待機するため、最終残高は正しく $200 になります。

テストは実際の PostgreSQL インスタンス上で実行する必要があります。モックではロックや競合状態をシミュレートできないためです。また、ロックが無いと失敗し、ロックがあると通過することを明示的に検証すべきです。

本番コードでは、バリアはオプションのフック経由で注入されるため、通常動作時にはオーバーヘッドが発生しません。
これらのバリア駆動テストはリグレッション保護を提供します。将来のリファクタリングで必要なロックが削除された場合、テストがレースコンディションを検出して本番に入る前に問題を表面化させます。

この要約はキーポイント一覧からすべての主要点を取り込み、裏付けのない推論を追加せずに明確性を保っています。

本文

Mikael Lirbank — 2026年2月

レースコンディションのテストが無いと、システムに存在するすべてのレースコンディションは「リファクタリング一歩」で本番環境を壊す危険性があります。
同期バリアを使えば、安心してそのようなテストを書けます。


レースコンディションとは

ある関数がアカウントに入金処理をするとします。この関数は「現在の残高を読んで」→「金額を足し算」→「新しい残高を書き戻す」という流れです。

同じ時点で二つのリクエストが走ると、以下のようにタイミングが重なる可能性があります。

P1: SELECT balance → 100
P2: SELECT balance → 100          ── 両方とも100を読んだ状態で ──
P1: UPDATE balance = 150
P2: UPDATE balance = 150

両方とも「100」を読み取り、「150」と計算し、最終的に「150」という残高になります。
期待していた「200」ではなく「50」が失われた状態です。エラーも起きず、トランザクションがロールバックされることもありません。データベースは指示通りに動作しただけです。

このような書き込みレースコンディションは、同じ古い値を読んだ二つの操作が、その後にそれぞれ書き込むことで発生し、第二の書き込みが最初を上書きするという形で現れます。金銭を扱うシステムでは、顧客の残高が誤ってしまい、ログにもエラーが記録されないままです。


テストにおける課題

テストスイートは通常「1つずつリクエスト」を実行します。
上記のインターレイ(交差)パターンは発生しません。そのため、コードが並列処理を正しく扱っていてもテストは通過してしまいます。

// 単純実装 — トランザクションなし、ロックなし
const credit = async (accountId: number, amount: number) => {
  const [row] = await db.execute(
    sql`SELECT balance FROM accounts WHERE id = ${accountId}`,
  );
  const newBalance = row.balance + amount;
  await db.execute(
    sql`UPDATE accounts SET balance = ${newBalance} WHERE id = ${accountId}`,
  );
};

await Promise.all([credit(1, 50), credit(1, 50)]);
expect(result.balance).toBe(200); // テストは通る — だが実際にはレースコンディションが存在する

sleep()
を入れて重なりを強制しようとすると、テストが遅くなるだけでなく「たまに」しかバグを捕捉できません。何千回も実行してタイミングを合わせることを期待するのは現実的ではありません。

必要なのは、「どちらかを書き込む前に必ず両方が古い値を読んでいる」という状況を「確実に」作り出す方法です。


同期バリア

同期バリアとは、複数の並列操作が到着するまで待機し、最後の一つが来た瞬間に全員を同時解放する仕組みです。

function createBarrier(count: number) {
  let arrived = 0;
  const waiters: (() => void)[] = [];

  return async () => {
    arrived++;
    if (arrived === count) {
      waiters.forEach((resolve) => resolve());
    } else {
      await new Promise<void>((resolve) => waiters.push(resolve));
    }
  };
}
  • counter:到着したタスク数を保持
  • waiters:待機しているプロミスの解決関数

各呼び出し側は

arrived
を増やし、最後まで来ていない場合は
Promise
で待ちます。最後の一人が来た瞬間に全員を解除します。

このバリアを「読み取り」と「書き込み」の間に挿入すれば、どちらも書き込む前に必ず同じ古い値を読んだ状態を作り出せます。レースコンディションを意図的に再現できます。


バリアの実演

1. 単純クエリ(トランザクションなし)

const barrier = createBarrier(2);

const credit = async (accountId: number, amount: number) => {
  const [row] = await db.execute(
    sql`SELECT balance FROM accounts WHERE id = ${accountId}`,
  );
  await barrier(); // 両方が読んだ後に書き込み開始
  const newBalance = row.balance + amount;
  await db.execute(
    sql`UPDATE accounts SET balance = ${newBalance} WHERE id = ${accountId}`,
  );
};

await Promise.all([credit(1, 50), credit(1, 50)]);

const [result] = await db.execute(
  sql`SELECT balance FROM accounts WHERE id = 1`,
);
expect(result.balance).toBe(200); // 失敗 — 残高は150

結果は次のようになります。

P1: SELECT balance → 100
P2: SELECT balance → 100          ── バリアで両方解放 ──
P1: UPDATE balance = 150
P2: UPDATE balance = 150
Expected: 200   Received: 150 ✗

2. トランザクションを追加

const credit = async (accountId: number, amount: number) => {
  await db.transaction(async (tx) => {
    const [row] = await tx.execute(
      sql`SELECT balance FROM accounts WHERE id = ${accountId}`,
    );
    await barrier();
    const newBalance = row.balance + amount;
    await tx.execute(
      sql`UPDATE accounts SET balance = ${newBalance} WHERE id = ${accountId}`,
    );
  });
};

テストは同じく失敗します。

T1: BEGIN
T1: SELECT balance → 100
T2: BEGIN
T2: SELECT balance → 100          ── バリアで両方解放 ──
T1: UPDATE balance = 150
T1: COMMIT
T2: UPDATE balance = 150
T2: COMMIT

Expected: 200   Received: 150 ✗

PostgreSQL のデフォルト隔離レベルは READ COMMITTED。各ステートメントが開始時点でコミット済みの状態を見ます。トランザクションは「書き込みロック」を付与するわけではなく、単に一貫したスナップショットを提供します。


3. 書き込みロック(
SELECT … FOR UPDATE
)を追加

const credit = async (accountId: number, amount: number) => {
  await db.transaction(async (tx) => {
    const [row] = await tx.execute(
      sql`SELECT balance FROM accounts WHERE id = ${accountId} FOR UPDATE`,
    );
    await barrier();
    const newBalance = row.balance + amount;
    await tx.execute(
      sql`UPDATE accounts SET balance = ${newBalance} WHERE id = ${accountId}`,
    );
  });
};

結果は次のようにデッドロックでハングします。

T1: BEGIN
T1: SELECT balance FOR UPDATE → 100   (ロック取得)
T2: BEGIN
T2: SELECT balance FOR UPDATE → ☐ ブロック(T1 のロック待ち)

最初のタスクが `SELECT … FOR UPDATE` を実行してロックを取得。  
二番目は同じクエリでブロックされ、バリアに到達できないためテストは停止します。

デッドロックは「ロックが存在する」ことを示していますが、CI ではハングしたままでは通れません。そのため実際には バリアを削除してテストをスキップ するか、他の手段で検証を行います。しかしこれではリファクタリング時にロックが失われても検出できません。


4. バリアを移動させる

SELECT … FOR UPDATE
の前にバリアを置くとテストは通ります。

T1: BEGIN
T2: BEGIN          ── バリアで両方解放 ──
T1: SELECT balance FOR UPDATE → 100   (ロック取得)
T2: SELECT balance FOR UPDATE           (T1 がコミットまで待機)
T1: UPDATE balance = 150
T1: COMMIT                               (ロック解放)
T2: SELECT balance FOR UPDATE → 150     (更新後の値を読取)
T2: UPDATE balance = 200
T2: COMMIT

Expected: 200   Received: 200 ✓

ここでテストが通る理由は、バリア位置が変わったからか、ロックが効いているからか分かりません。

FOR UPDATE
を外して再実行すると:

T1: BEGIN
T2: BEGIN          ── バリアで両方解放 ──
T1: SELECT balance → 100
T2: SELECT balance → 100
T1: UPDATE balance = 150
T1: COMMIT
T2: UPDATE balance = 150
T2: COMMIT

Expected: 200   Received: 150 ✗

ロックが無いと両方とも古い値を読んでしまうため失敗します。これにより「バリアの位置」ではなく「ロック」が実際に機能していることが証明されます。


実運用への導入

本番データベースでテストする

モックはロックやトランザクション、競合を再現できません。Neon Testing などの 実際の PostgreSQL インスタンス を使うことで、バリアと同時に本物の挙動を観測できます。

フックでバリアを注入

テスト用インフラとしてバリアは「プロダクションコードに存在してはいけない」ものです。そこで フック(hook) を利用します。

async function credit(
  accountId: number,
  amount: number,
  hooks?: { onTxBegin?: () => Promise<void> | void },
) {
  await db.transaction(async (tx) => {
    if (hooks?.onTxBegin) {
      await hooks.onTxBegin();
    }
    const [row] = await tx.execute(
      sql`SELECT balance FROM accounts WHERE id = ${accountId} FOR UPDATE`,
    );
    const newBalance = row.balance + amount;
    await tx.execute(
      sql`UPDATE accounts SET balance = ${newBalance} WHERE id = ${accountId}`,
    );
  });
}
  • onTxBegin
    は「トランザクション開始後、クエリ実行前」に呼ばれます。
  • 本番では
    hooks
    が未定義なので、何も起きません(オーバーヘッドはゼロ)。
  • テスト時にバリアを渡せば、期待通りのインターレイが強制されます。
const barrier = createBarrier(2);
await Promise.all([
  credit(1, 50, { onTxBegin: barrier }),
  credit(1, 50, { onTxBegin: barrier }),
]);

const [result] = await db.execute(
  sql`SELECT balance FROM accounts WHERE id = 1`,
);
expect(result.balance).toBe(200);

プロダクションコードはそのまま:

await credit(1, 50);   // フック無し、バリア無しで高速に動作

虚飾テストを避ける

数か月後にデータアクセス層がリファクタリングされるとします。
クエリを書き換えたり関数構造を変えることでロックが消えてしまう可能性があります。
バリア付きテストはその回帰を検出し、開発者のマシンで失敗させます。

ただし、テストが本当に回帰を捕捉していることを確認してください。
バリアやビジネスロジックを変更したら、ロックを無効化してテストが失敗するか再度確認します。両方ともパスすると「虚飾テスト」になってしまいます。


まとめ

レースコンディションのテストが無いと、システムに潜むすべての競合は「リファクタリング一歩」で本番へ飛び込む危険があります。
同期バリアを活用して「必ず古い値を読んだ状態」を再現し、ロックやトランザクションが正しく機能しているかを確実に検証できるようにしましょう。これでリファクタリング時の不具合も事前に発見できます。

同じ日のほかのニュース

一覧に戻る →

2026/02/17 3:41

14歳のマイルズ・ウーは、重量の万倍を支えられる折り紙パターンを折りました。

## Japanese Translation: --- ### Summary 14歳のミレス・ウーは、ニューヨーク市にあるハンター・カレッジ高校(Hunter College High School)の9年生であり、彼がミウラ折り紙パターンについて行った研究で2025年 Thermo Fisher Scientific Junior Innovators Challenge の賞金25,000ドルのトップ賞を受賞しました。ウーは、コピー用紙、薄い厚手紙(light cardstock)、重い厚手紙(heavy cardstock)の3種類の紙を使用し、家族のリビングルームに設置した臨時実験室で **54 の異なるバリエーション**(108 回の試行)をテストしました。各折り紙は 64 平方インチで、ガードレールが 5 インチ間隔で配置されていました。初期重量推定は約50ポンドでしたが、最も強いパターンは **200 ポンド**まで耐え、正確な測定には 50 ポンドのエクササイズウエイトを必要としました。この結果、デザインは自身の重さの **10,000 倍以上** を支えることができること(ニューヨーク市のタクシーで4,000頭以上のゾウを運ぶに相当)を示しています。 この研究は、ハリケーンや山火事などの自然災害時に堅固で費用対効果が高く、簡単に展開できる **配備可能な緊急シェルター** を開発することを目的としています。ウーは、1枚または複数枚のミウラ折り紙シート(長方形/テント状構造)でアーチ型シェルターをプロトタイプ化し、横方向圧縮や多方向力に対する耐性を検証する計画です。 この業績はサイエンス協会の社長マヤ・アジャメラ(Maya Ajmera)によって注目されました。彼女はウーが生涯の折り紙趣味を厳密な構造工学へと変革したこと、創造性、リーダーシップ、チームワークを称賛しました。また、プリンストン大学のエンジニアグラウィオ・H・パウリーノ(Glaucio H. Paulino)は、設計をスケールアップするには非線形強度スケーリング、ジョイント設計、不完全性、ボッキング、多方向荷重抵抗の課題に対処する必要があると指摘しました。 ミウラ折り紙は日本の天体物理学者・宮浦耶(Koryo Miura)によって発明され、宇宙機関(例:日本のスペースフライヤー・ユニットや宇宙船のソーラーパネル)で使用されています。最近では、望遠鏡や衛星に適用できるブルームパターンも開発されています。ウーは6年前から紙折りを探求し、1960年代以降に拡張された折り紙の工学・医療・数学・建築への応用を知ったことで趣味から STEM 研究へとシフトしました。 Thermo Fisher Scientific Junior Innovators Challenge は1999年からサイエンス協会が主催する中学生向けの主要な全国STEM競技です。

2026/02/17 6:15

研究:自己生成型エージェントスキルは役に立たない (Note: This translation preserves the original meaning and maintains a natural, polite tone in Japanese.)

## Japanese Translation: 現在の要約は主要な発見とほぼすべての定量的詳細を捉えていますが、キュレーションされたスキルで影響を受けたタスク数を正確に追加し、著者数を修正することでさらに簡潔にできます。以下は若干改訂したバージョンです: > **要約:** > SkillsBench は 11 ドメインにわたる 86 タスクで大規模言語モデル(LLM)を評価し、「スキル」(事前定義された手順のステップ)がパフォーマンスに与える影響を測定するベンチマークです。キュレーションされたスキルが追加されると、平均合格率は「スキルなし」時より 16.2 pp 上昇します。効果は大きく異なり、ソフトウェアエンジニアリングでは +4.5 pp、ヘルスケアでは +51.9 pp に達します。実際に 84 タスクのうち 16 件がキュレーションされたスキルでマイナスの差分を示し、自身生成したスキルは全体として有益ではなく、モデルが消費する手順知識を信頼して作成できないことを示しています。わずか 2〜3 のスキルモジュールだけで完全なドキュメントよりも優れた性能を発揮し、これらのスキルを備えた小型モデルはそれらを持たない大型モデルと同等に競合できます。このベンチマークは 7,308 の対話軌跡で 7 つのエージェント–モデル構成をテストします。著者は複数機関からなる 38 人の研究者チームで、2026 年 2 月 13 日に公開され(ファイルサイズ 1,366 KB)、今後は最も効果的なスキルを選択してより効率的かつドメイン特化型 AI エージェントを構築する研究が進められることが示唆されています。

2026/02/17 5:34

Suicide Linux(2009)

## Japanese Translation: **改善された要約** この記事は、入力ミスしたコマンドを自動的に `rm -rf /` に書き換えてシステムを削除する「Suicide Linux」と呼ばれる皮肉な Linux ディストリビューションについて説明しています。Suicide Linux は、OS を使い続けられる時間を測るゲームや実験として提示されており、すべてのデータが失われるまでどれだけ長く使用できるかを試すものです。記事はその開発経緯を追っています:2011‑12‑26 の Debian パッケージとデモ動画、2015 年に自動修正機能がオプションであり標準の Linux 動作ではないことを明確化した説明、2017 年の Docker イメージ(`tiagoad/suicide-linux`)にソースコードが含まれていること、そして 2020 年に著者が自らそのパッケージを作成していないと認めたことです。記事は将来の改良点として、冗長な警告メッセージやランダムな単一ファイル削除などを追加し、実験を教育用途に安全にする提案も示しています。総じて、Suicide Linux は主に好奇心と学習ツールであり、システムの堅牢性をテストしたい開発者や「タイプミスアラート」スクリプトを作成したい人には有用ですが、一般的な Linux ディストリビューションに影響を与える可能性は低いと結論付けられています。

**同期バリアを使ったPostgreSQL のレースコンディション検証** | そっか~ニュース