Commit At Most Once
===================

Commit At Most Once
CAMO機能の目的は、アプリケーションが複数回コミットしないようにすることです。

CAMOがないと、 ``COMMIT``
が送信された後にクライアントが接続を失うと、アプリケーションはサーバーから応答を受け取らない場合があるため、トランザクションがコミットしたかどうかがわかりません。

アプリケーションは、次の2つのオプションの間を簡単に決定できません。

-  場合によっては、データが2回入力される可能性があるため、同じデータでトランザクションを再試行します。

-  トランザクションを再試行せず、データがまったく処理されないリスク

これらはいずれも、高値のデータに関する重大なエラーです。

この状況を回避する1つの方法は、トランザクションの一意のインデックスを持つテーブルに少なくとも1つの\ ``INSERT``
を含めることです。ただし、これはアプリケーションのデザインに依存し、アプリケーション固有のエラー処理ロジックが必要であるため、すべての場合に効果的ではありません。

PGDのCAMO機能は、より一般的なソリューションを提供し、\ ``INSERT``
を必要としません。 ``bdr.commit_scope``
によってアクティブ化されると、アプリケーションはトランザクション識別子が既に割り当てられている場合を含むメッセージを受信します。それ以外の場合、トランザクションの最初のwriteステートメントはその情報をクライアントに送信します。

アプリケーションが明示的な\ ``COMMIT``
を送信する場合、プロトコルは、\ ``COMMIT``
が送信される前にアプリケーションがトランザクション識別子の通知を受信することを保証します。サーバーが\ ``COMMIT``
に応答しない場合、アプリケーションはトランザクション識別子を使用して別のPGDノードからトランザクションの最終ステータスを要求することにより、このエラーを処理できます。以前のトランザクションステータスがわかっている場合、アプリケーションはトランザクションを再試行するかどうかを安全に決定できます。

CAMOは、同じPGDグループからの2つのPGDノードであるパートナーノードのペアを作成することにより機能します。この操作モードでは、ペアの各ノードは、他のピアで実行された最近のトランザクションの結果を知っており、特にニーズに応じて、\ ``COMMIT``
中に切断されたトランザクションの結果を知っています。アプリケーションからトランザクションを受信するノードは「オリジン」と呼ばれ、これらのトランザクションを確認するノードは「パートナー」と呼ばれます。ただし、
CAMOペアのノードのCAMO構成に違いはありません。ペアは対称です。

..  Warning::
   CAMOでは、高度なエラー処理を利用するには、ユーザーのアプリケーションを変更する必要があります。パラメーターを有効にするだけでは保護を得るには十分ではありません。リファレンスクライアント実装は、要求に応じてお客様に提供されます。

要件
----

CAMOを使用するには、アプリケーションはマルチステートメント要求の一部としてではなく、別の要求として明示的な\ ``COMMIT``
メッセージを発行する必要があります。
CAMOは、プロシージャーまたは暗黙的なコミットを使用する単一ステートメントトランザクションから発行されたトランザクションのステータスを提供できません。

構成
----

CAMOの構成は、 :ref:`コミットスコープ <コミットスコープ>` を介して発生します。

障害シナリオ
------------

さまざまな構成では、さまざまな障害シナリオが発生します。

レシーバー側のデータの永続性
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

デフォルトでは、PGLライタはリモートノードからトランザクションを適用するときに\ ``bdr.synchronous_commit = off``
モードで動作します。これはCAMOにも当てはまります。つまり、トランザクションは、
CAMOパートナーのディスクに到達する前に、オリジンノードに確認されます。クラッシュまたはハードウェア障害が発生した場合、確認されたトランザクションはCAMOパートナーで自分自身では回復できない場合があります。これは、
CAMOパートナーノードが復旧するとトランザクションを再配布するため、
CAMO起点ノードが動作している限り問題ではありません。

これは、
CAMOが単一ノードの障害から保護できることを意味します。これは、ローカルモードおよびリモート書き込みと同様に、またはそれと組み合わせて正しいです。

CAMOペアの両方のノードの停止をカバーするには、
``bdr.synchronous_commit = local``
を使用して、コミット前の確認の前にフラッシュを強制できます。これは、リモート書き込みモードでもローカルモードでも動作せず、レイテンシーの影響を受けやすいコミットパスにあるCAMOパートナーのI/O要件により、パフォーマンスに影響を与えます。

非同期モード
^^^^^^^^^^^^

コミットスコープで\ ``DEGRADE ON ... TO ASYNC``
句が使用される場合、ノードはCAMOパートナーの準備ができているかどうかを検出します。そうでない場合、一時的に非同期ローカルモードに切り替わります。このモードの場合、ノードはCAMOモードに戻すまで、ローカルにトランザクションをコミットします。

これでは、
COMMITステータスを取得することはできませんが、一貫性よりも可用性を選択できます。このモードは、単一ノードの障害を許容できます。
CAMOペアの両方のノードに障害が発生した場合、可用性を維持するために一致しないコミット決定を選択する可能性があり、データの不一貫性が発生します。

CAMOパートナーが準備ができているように切り替えるには、接続する必要があり、推定キャッチアップインターバルは\ ``TO ASYNC``
の\ ``timeout`` 値を下回る必要があります。 ``bdr.is_camo_partner_ready``
を使用してCAMOパートナーの現在の準備状況を確認できます。\ ``bdr.node_replication_rates``
は、キャッチアップ時間の現在の推定値を提供します。

.. raw:: html

   <!-- この段落には、解決方法がわからない文の断片があります。 -->

CAMO保護から非同期モードへのスイッチは、コミットが\ ``TO ASYNC``
の\ ``timeout``
値を超えるため、またはCAMOパートナーが既に知られている場合、コミット時に切断されるため、実際のCAMOトランザクションによってのみトリガーされます。このスイッチは、推定キャッチアップインターバルとは無関係です。
CAMOペアが、現在のノードが\ ``enable_proxy_routing``
ノードグループオプションで構成されたグループの書き込みリードであることを必要とするように構成されている場合。構文については、
 :ref:`コミットスコープ <コミットスコープ>` を参照してください。これにより、分離されたノードが非同期モードに切り替わらないことによるスプリットブレイン状況を防ぐことができます。
CAMOグループに\ ``enable_proxy_routing``
が設定されていない場合、オリジンノードはすぐに非同期モードに切り替わります。

非同期モードからCAMOモードへの切り替えは、接続を開始するCAMOパートナーノードに依存します。
CAMOパートナーは、少なくとも30秒ごとに再接続を試行します。したがって、接続が再確立された後、
CAMOパートナーが元のノードに接続するまでに最大30秒かかる場合があります。
CAMOパートナーに蓄積された遅延は、
CAMO保護モードへの切り替えをさらに遅延させます。

通常のCAMOオペレーション中とは異なり、非同期モードでは、コミットオーバーヘッドの追加はありません。これは、
CAMOペアが通常処理できるよりも多くのトランザクションをノードが継続的に処理できるため、問題になる可能性があります。
CAMOパートナーが最終的に再接続してトランザクションを適用した場合でも、このような状況では遅延が増加するだけであり、
CAMO保護の再確立を妨げます。トランザクションのスループットを人為的に調整するために、PGDは\ ``bdr.camo_local_mode_delay``
設定を提供します。これにより、ローカルモードで\ ``COMMIT``
を任意の時間遅延できます。予想されるワークロード中に通常のCAMOモードでコミット時間を測定し、それに応じてこの遅延を構成することをお勧めします。デフォルトは5ミリ秒で、これは非同期ネットワークと比較的迅速なCAMOパートナーの応答を反映しています。

アーキテクチャと可用性要件を考慮して、非同期モードを許可するかどうかの選択を検討します。次の例は、詳細を提供します。

例
^^

この例では、互いにCAMOパートナーである2つのPGDノードを使用したセットアップを検討します。

edb_notranlate_0
このCAMOコミットスコープが正当であるためには、グループ内のノードの数が正確に2である必要があります。複数のノードで構成されるグループでALLまたはANY
2を使用すると、非定量化グループ式が明確なペアに解決されないため、エラーになりますノードの。

非同期モードの場合
^^^^^^^^^^^^^^^^^^

非同期モードが許可されている場合、単一障害点はありません。
1つのノードに障害が発生した場合

-  他のノードは、障害が発生したノードで\ ``COMMIT``
   中に切断されたすべてのトランザクションのステータスを判断できます。

-  新しい書き込みトランザクションが許可されます。
   2番目のノードにも障害が発生した場合、その時点でコミットされていたトランザクションの結果は不明です。

非同期モードなし
^^^^^^^^^^^^^^^^

非同期モードが許可されない場合、各ノードはトランザクションをコミットするために他のノードを必要とします。つまり、各ノードは単一障害点です。
1つのノードに障害が発生した場合

-  他のノードは、障害が発生したノードで\ ``COMMIT``
   中に切断されたすべてのトランザクションのステータスを判断できます。

-  ノードが復旧するまで、新しい書き込みトランザクションは防止されます。

アプリケーションの使用
----------------------

概要と要件
^^^^^^^^^^

CAMOは、再試行ループとクライアント側での特定のエラー処理に依存しています。それには3つの側面があります。

-  トランザクションの\ ``COMMIT``
   の結果を確認する必要があり、一時的なエラーが発生した場合、クライアントはトランザクションを再試行する必要があります。

-  ``COMMIT``
   の前に、クライアントはノードIDとトランザクションID両方32ビット整数で構成されるトランザクションのグローバル識別子を取得する必要があります。

-  トランザクションの\ ``COMMIT``
   を試行中に現在のサーバーに障害が発生した場合、アプリケーションはCAMOパートナーに接続し、そのトランザクションのステータスを取得し、応答に応じて再試行する必要があります。

アプリケーションは、 ``COMMIT``
中に切断された場合にトランザクションステータスを確認する目的でのみグローバルトランザクション識別子を保存する必要があります。特に、アプリケーションは別の永続レイヤーを必要としません。アプリケーションに障害が発生した場合、データベースの情報のみを再起動します。

CAMOパートナーの接続ステータス
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

ファンクション\ ``bdr.is_camo_partner_connected``
を使用すると、ペアモードで構成されたCAMOパートナーノードの接続ステータスを確認できます。現在、
Eager Replicationで使用されるCAMOに相当するものはありません。

概要
^^^^

.. code:: sql

   bdr.is_camo_partner_connected()

戻り値
^^^^^^

CAMOパートナーが現在ローカルノード上のWAL送信プロセスに接続しているため、トランザクションデータを受信し、確認を送り返すことができるかどうかを示すブール値。

CAMOパートナーの準備状況
^^^^^^^^^^^^^^^^^^^^^^^^

ファンクション\ ``bdr.is_camo_partner_ready``
を使用すると、ペアモードで構成されたCAMOパートナーノードの準備状況を確認できます。その下で、これはローカルモードへの、またはローカルモードからのスイッチをトリガーします。

.. _概要-1:

概要
^^^^

.. code:: sql

   bdr.is_camo_partner_ready()

.. _戻り値-1:

戻り値
^^^^^^

CAMOパートナーがタイムリーに、つまり\ ``TO ASYNC`` の\ ``timeout``
の有効期限が切れる前に、ローカルノードから発生したトランザクションを確認することが合理的に期待できるかどうかを示すブール値。

..  Note::
   このファンクションは、過去または現在の状態を照会します。正の戻り値は、 CAMOパートナーが将来のトランザクションを確認できるかどうかを示しません。

CAMOパートナーを取得する
^^^^^^^^^^^^^^^^^^^^^^^^

この機能は、ローカルノードのCAMOパートナーペアモードで構成を表示します。

.. code:: sql

   bdr.get_configured_camo_partner()

CAMOパートナーからの適用キューの消費を待機します
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

ファンクション\ ``bdr.wait_for_camo_partner_queue`` は、
CAMOパートナーノードを照会することがデフォルトの\ ``bdr.wait_for_apply_queue``
のラッパーです。ローカルノードがCAMOペアの一部でない場合、エラーを返します。

.. _概要-2:

概要
^^^^

.. code:: sql

   bdr.wait_for_camo_partner_queue()

CAMOノード間のトランザクションステータス
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

この機能により、
CAMOトランザクションが完全に解決されるまでの待機が有効になります。

.. code:: sql

   bdr.camo_transactions_resolved()

トランザクション状態照会機能
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

ノードに障害が発生したときにコミットされていたトランザクションのステータスを確認するには、アプリケーションはこのファンクションを使用する必要があります。

.. code:: sql

   bdr.logical_transaction_status(node_id OID, xid OID,
                                  require_camo_partner boolean)

ペアモードでCAMOを使用する場合、
CAMOペアの一部であるノードでのみこの機能を使用します。 Eager
Replicationと併せて、すべてのノードで使用できます。

どちらの場合も、コミットが発行されてから15分以内にファンクションを呼び出す必要があります。
CAMOパートナーはこのようなメタ情報を定期的にパージする必要があるため、古いトランザクションに正しい回答を提供できません。

トランザクションのステータスを照会する前に、このファンクションは、受信キューが消費され完全に適用されるまで待機します。このメカニズムは、受信されたがまだ適用されていないトランザクションの早期の否定的な回答を防ぎます。

その名前にもかかわらず、それは常に読み取り専用操作であるわけではありません。ステータスが不明な場合、
CAMOパートナーはトランザクションをコミットするか中止するかを決定し、その決定をローカルに保存して、今後の一貫性を保証します。

クライアントは、オリジンでコミットする前にこのファンクションを呼び出してはなりません。そうしないと、トランザクションが強制的にロールバックされる可能性があります。

.. _概要-3:

概要
^^^^

.. code:: sql

   bdr.logical_transaction_status(node_id OID, xid OID,
                                  require_camo_partner boolean DEFAULT true)

パラメーター
^^^^^^^^^^^^

-  ``node_id``
   —トランザクションが発生するPGDノードのノードID。通常はクライアントがPQパラメーター\ ``bdr.local_node_id``
   から\ ``COMMIT`` の前に取得します。

-  ``xid``
   —オリジンノードのトランザクションID。通常はクライアントがPQパラメーター
   ``transaction_id`` から\ ``COMMIT`` の前に取得します。

-  ``require_camo_partner``
   —デフォルトでtrueで、構成チェックが有効になります。これらのチェックを無効にし、
   CAMOトランザクションではないトランザクションのステータスを照会するには、falseに設定します。

.. _戻り値-2:

戻り値
^^^^^^

ファンクションは、次のいずれかの結果を返します。

-  ``'committed'::TEXT`` —トランザクションはコミットされ、
   CAMOペアの両方のノードで可視され、最終的に他のすべてのPGDノードにレプリケートされます。クライアントが再試行する必要はありません。

-  ``'aborted'::TEXT``
   —トランザクションは中止され、他のPGDノードにレプリケートされません。クライアントは再試行するか、トランザクションのコミットの失敗をエスカレートする必要があります。

-  ``'in progress'::TEXT``
   —トランザクションはこのローカルノードでまだ進行中であり、まだコミットまたは中止されていません。トランザクションはCOMMITフェーズにあり、
   CAMOパートナーがコミットを確認または拒否するのを待機している可能性があります。推奨されるクライアントの反応は、元のノードから切断し、
   CAMOパートナーに再接続して、代わりにそれを照会することです。間にロードバランサーまたはプロキシがあり、クライアントがどのノードを照会するかを制御できない場合、クライアントはステータスが\ ``'committed'``
   または\ ``'aborted'``
   に切り替わるまで、繰り返しポーリングのみできます。

Eager All-Node
Replicationの場合、ピアノードは、まだコミットまたは中止されていないトランザクションに対してこの結果を生成します。この場合、まだレプリケートされていないトランザクション、またはオリジンノードで開始されていないトランザクションでも、ピアPGDノードで\ ``in progress``
結果を生成する場合があります。ただし、クライアントは、オリジンでコミットする前にトランザクションステータスを照会してはなりません。

-  ``'unknown'::TEXT``
   —指定されたトランザクションは、将来であるか、その特定のノードにまだレプリケートされていない、または過去すぎるため不明です。このようなトランザクションのステータスはまだ知られていない、または不明です。この戻り値は、クライアントによる不適切な使用の兆候です。

クライアントは、エラーが発生した場合にファンクション呼び出しを再試行する準備をする必要があります。

接続プールとプロキシ
^^^^^^^^^^^^^^^^^^^^

CAMOクラスターをデザインするときは、接続プールとプロキシの効果を考慮してください。プロキシは、コミットグループ内のすべてのノード、つまりCAMOペアの両方のノード、またはEager
All-Node
Replicationの場合はすべてのPGDノードにトランザクションを自由に分散することができます。

アプリケーションが適切なノードIDを取得するように注意してください。セッションプーリングを使用する場合、クライアントは同じノードに接続されたままであるため、ノードIDはクライアントセッションの有効期間を通じて一定のままです。ただし、より詳細なトランザクションプーリングでは、クライアントはすべてのトランザクションのノードIDを取得する必要があります次の例のように。

PGDノードに直接接続されていないクライアントは、フェールオーバーまたはスイッチオーバーに気付かない場合があります。ただし、
``bdr.local_node_id``
パラメーターを使用して、現在接続しているノードを判断することができます。
COMMIT中の切断という重要な状況では、プロキシはその切断をエラーとしてCAMOプロトコルを適用するクライアントに適切に転送する必要があります。

``received`` モードのCAMOの場合、
CAMOペア間を切り替える可能性のあるプロキシは、
``bdr.wait_for_camo_partner_queue``
ファンクションを使用して、古い読み取りを防ぐ必要があります。

.. _例-1:

例
^^

この例は、
CAMO対応クライアントアプリケーションの再試行ループがCのような擬似コードでどのように見えるかを示しています。接続情報を提供する2つのDSN
``origin_dsn`` および\ ``partner_dsn``
を想定しています。これらは通常、\ ``bdr.create_node``
への最初の呼び出しに使用されるDSNと同じであり、 ``bdr.node_summary``
、\ ``interface_connstr`` 列でルックアップできます。

.. code:: shell

   PGconn *conn = PQconnectdb(origin_dsn);

   loop {
       // start a transaction
       PQexec(conn, "BEGIN");

       // apply transactional changes
       PQexec(conn, "INSERT INTO ...");
       ...

       // store a globally unique transaction identifier
       node_id = PQparameterStatus(conn, "bdr.local_node_id");
       xid = PQparameterStatus(conn, "transaction_id");

       // attempt to commit
       PQexec(conn, "COMMIT");
       if (PQresultStatus(res) == PGRES_COMMAND_OK)
           return SUCCESS;
       else if (PQstatus(res) == CONNECTION_BAD)
       {
           // Re-connect to the partner
           conn = PQconnectdb(partner_dsn);
           // Check if successfully reconnected
           if (!connectionEstablished())
               panic();

           // Check the attempted transactions status
           sql = "SELECT bdr.logical_transaction_status($node_id, $xid)";
           txn_status = PQexec(conn, sql);
           if (txn_status == "committed")
               return SUCCESS;
           else
               continue;   // to retry the transaction on the partner
       }
       else
       {
           // The connection is intact, but the transaction failed for some
           // other reason.  Differentiate between permanent and temporary
           // errors.
           if (isPermanentError())
               return FAILURE;
           else
           {
               // Determine an appropriate delay to back-off to account for
               // temporary failures due to congestion, so as to decrease
               // the overall load put on the servers.
               sleep(increasing_retry_delay);

               continue;
           }
       }
   }

この例は、再試行やエラー処理を含む、接続のための適切なロジックで拡張する必要があります。
PgBouncerのようなロードバランサーを使用している場合、\ ``PQreset``
を使用して再接続を実装できます。ロードバランサーがクライアントをCAMOパートナーにのみリダイレクトし、他のPGDノードではないことを確認します。

実際には、再試行の上限を設定することをお勧めします。トランザクションで実行されるアクションによっては、他の一時的なエラーが発生する可能性があります。これらは、\ ``SERIALIZABLE``
分離モードでのデッドロックまたはシリアル化障害のベストプラクティスと同様に、エラーコードに応じてトランザクションを再試行することにより処理する必要があります。

DDLおよびグローバルロックとの相互作用
-------------------------------------

CAMOによって保護されたトランザクションには、
DDL操作を含めることができます。ただし、
DDLはグローバルロックを使用します。これは既にノード間である程度の同期を提供しています。詳細については、
 :ref:`DDLロックの詳細 <DDLロックの詳細>` を参照してください。

CAMOとDDLを組み合わせると、レイテンシーが長くなり、グローバルデッドロックの可能性が高くなります。したがって、比較的低い\ ``bdr.global_lock_timeout``
を使用することをお勧めします。これはDDLを中止し、適切な時間でデッドロックを解決します。

非トランザクションDDL
^^^^^^^^^^^^^^^^^^^^^

次のDDL操作はトランザクションブロックでは許可されていないため、
CAMO保護の恩恵を受けることはできません。これらの場合、
CAMOは内部で自動的に無効になります。

-  すべての同時インデックス操作 ``CREATE`` 、\ ``DROP``
   、および\ ``REINDEX``

-  ``REINDEX DATABASE`` 、\ ``REINDEX SCHEMA``
   、および\ ``REINDEX SYSTEM``

-  ``VACUUM``

-  パラメーターなしの\ ``CLUSTER``

-  ``ALTER TABLE DETACH PARTITION CONCURRENTLY``

-  ``ALTER TYPE [enum] ADD VALUE``

-  ``ALTER SYSTEM``

-  ``CREATE`` および\ ``DROP DATABASE``

-  ``CREATE`` および\ ``DROP TABLESPACE``

-  ``ALTER DATABASE [db] TABLESPACE``

CAMOの制限
----------

CAMOの制限については、 :ref:`PGD limitations <Limitations>` ページでカバーされています。

パフォーマンスへの影響
----------------------

CAMOは、コミット時にメッセージのラウンドトリップを追加することにより、Postgresレプリケーションプロトコルを拡張します。アプリケーションのコミットレイテンシーは非同期レプリケーションの場合よりも高く、これは主に、関係するノード間の往復時間によって決まります。同時セッションの数を増やすと、並列処理が増加して、合理的なトランザクションスループットを取得できます。

トランザクションを確認するCAMOパートナーは、トランザクションの状態を保存する必要があります。非CAMOオペレーションと比較して、これは、オリジンから適用される各トランザクションの追加シークが必要になる場合があります。

クライアントアプリケーションのテスト
------------------------------------

クライアント側でCAMOを適切に使用することは簡単ではありません。ノードのクラッシュやネットワークの停止などの障害シナリオに対して、PGDクラスターでアプリケーションの動作をテストすることを強くお勧めします。
