はじめに
ZooKeeperは、ノードの階層セット内のデータを表すフォールトトレラントな分散データベースです。 ノードは、作成、変更、削除、存在の確認、およびノードへのアクセス権の管理ができます。 一部の操作では追加のオプションを使用できます。たとえば、コマンドを適用するノードのバージョンを指定できます。 この記事で説明するマルチスレッドZooKeeperクライアントは、2つの追加ストリームを作成します。1つはすべてのI / O操作を実行し、もう1つはユーザーコールバックとモニターを実行します。 モニターは、ノードの状態が変化したときにクライアントによって呼び出される機能です。 たとえば、ノードが存在するかどうかを確認し、ノードが消えたり表示されたりしたときに呼び出される関数を渡すことができます。 記事を理解するために必要な残りの詳細は、必要に応じて提供します。
複数のデータセンターの多くのマシンによるタスクの実行を調整するために、ZooKeeperが必要でした。
C ++ライブラリの作業を開始したので、ノード上の各操作に1つのメソッドを提供する1つの大きな
ZooKeeper
      
      クラスを持つJavaクライアントAPIにできるだけ近いAPIを作成することにしました 。 しかし、このアプローチの欠点はすぐに明らかになりました。
各コマンドにいくつかのオプションが必要でした。
- 標準非同期:要求パラメーターとコールバック関数(コールバック)をクライアントに渡します。 操作が完了すると、クライアントは、ZooKeeperクライアントによって作成された別のスレッドで提供された関数を呼び出します。
-  非同期、 std::future
 
 
 
 オブジェクトを返します。 クライアントに要求パラメーターを渡すと、クライアントは非同期計算を表すオブジェクトを返します。std::future::get
 
 
 
 メソッドを呼び出すと、操作が完了した後に制御が返されます。 操作が失敗した場合、std::future::get
 
 
 
 呼び出しで例外がスローされます。
- 同期。 操作が完了するまで、コールはブロックされます。 エラーは例外に変換されます。
操作にN個の可能なオプション(モニターあり/なし、バージョンあり/なしなど)およびMオプションがある場合、N * Mメソッドを記述してサポートする必要があります。 たとえば、Javaクライアントには4つの
exists
      
      メソッド
exists
      
      ます。
 Stat exists(String path, boolean watch) void exists(String path, boolean watch, AsyncCallback.StatCallback cb, Object ctx) Stat exists(String path, Watcher watcher) void exists(String path, Watcher watcher, AsyncCallback.StatCallback cb, Object ctx)
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      future
      
      を返すオプションが必要な場合は、さらに2つのメソッドを追加する必要があります。 全部で6つのメソッドがあり、これは1つの操作のみです! これは受け入れられないことがわかりました。
急ぎのタイプ
明らかなパスの無益さに気付いた後、APIを再構築するというアイデアを思いつきました。コマンドがチーム自体から実行される方法の分離を最大化する必要があります。 各コマンドは、個別のタイプ(パラメーターのコンテナー)の形式で発行する必要があります。
この場合、クライアントでは、コマンドの非同期実行のためのメソッドを1つだけ実装する必要があります。
 void run(Command cmd, Callback callback);
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      基本的な非同期実行オプションのみを実装すれば十分です。他のすべてのオプションは、カプセル化を害することなく 、非同期インターフェースをベースとしてクライアントの外部で実装できます。
そのため、操作ごとに個別のクラスがあります。
-  CreateCmd
 
 
 
 
-  DeleteCmd
 
 
 
 
-  ExistsCmd
 
 
 
 
- ...
各クラスには、コマンドの実行に必要なすべてのパラメーターが格納されます。 たとえば、
delete
      
      操作は必要なパスを取り、オプションで適用可能なバージョンのデータを受け入れることができます。
exists
      
      操作にはパスも必要であり、オプションでノードが削除/作成されたときに呼び出される関数を受け入れることができます。
ここでは、いくつかのテンプレートを選択できます-たとえば、すべてのコマンドにはノードへのパスが含まれている必要があり、一部は特定のバージョンに適用できます(
delete
      
      、
setACL
      
      、
setData
      
      )、一部は追加のコールバックモニターを受け入れるか、セッションコールバックモニターにイベントを発行できます。 これらの「テンプレート」 をmixinの形式で実装できます。このテンプレートから、レンガとしてチームを編成します。 合計で、3つの不純物を見つけることができました。
-   Pathable
 
 
 
 コンストラクターでパスを取得し、パスを取得するメソッドを提供します。
-  バージョン管理可能-バージョンをVersionable
 
 
 
 、バージョンを指定して指定されたバージョンを取得するためのメソッドを提供します。
-   Watchable
 
 
 
 可能-ノードの状態が変化したときに呼び出されるコールバックを保存し、決定できるようにします。
たとえば、
Versionable
      
      不純
Versionable
      
      次のとおり
Versionable
      
      。
 template <typename SelfType> struct Versionable { explicit Versionable(Version version = AnyVersion) : version_(version) {} SelfType & setVersion(Version version) { this->version_ = version; return static_cast<SelfType &>(*this); } Version version() const { return this->version_; } private: Version version_; };
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      setVersion
      
      基本クラス
Versionable
      
      型
setVersion
      
      返すために、
setVersion
      
      では不思議な繰り返しテンプレートパターン技術が使用されています。 コマンドに不純物を追加する方法は次のとおりです。
 struct DeleteCmd : Pathable, Versionable<DeleteCmd> { explicit DeleteCmd(std::string path); // other methods };
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      次のステップでは、各チームに対応するコールバックのタイプを決定します。これは、異なるチームの作業が完了すると、異なるタイプの値がコールバックに転送されるためです。 合計で、7種類のコールバックを区別できます。
 using VoidCallback = std::function<void(std::error_code const&)>; using StatCallback = std::function<void(std::error_code const&, Stat const&)>; using ExistsCallback = std::function<void(std::error_code const&, bool, Stat const&)>; using StringCallback = std::function<void(std::error_code const&, std::string)>; using ChildrenCallback = std::function<void(std::error_code const&, Children, Stat const&)>; using DataCallback = std::function<void(std::error_code const&, std::string, Stat const&)>; using AclCallback = std::function<void(std::error_code const&, AclVector, Stat const&)>;
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
        統計とは何ですか?   
        
        
        
      
    
        
        
        
      
      
        
        
        
      
構造には、UNIXシステムの
        
        
        
      
    
        
        
        
      
      
        
        
        
      
構造と同様に、ツリーノードに関するメタ情報が含まれています。 たとえば、この構造には、最終変更の仮想時間、ノードに格納されているデータのサイズ、子孫の数などが含まれます。
        
        
        
      
    
      Stat
      
      構造には、UNIXシステムの
stat
      
      構造と同様に、ツリーノードに関するメタ情報が含まれています。 たとえば、この構造には、最終変更の仮想時間、ノードに格納されているデータのサイズ、子孫の数などが含まれます。
コマンドをコールバックにバインドする最も簡単な方法は、対応するネストされた
CallbackType
      
      タイプを定義するために各コマンドを要求することです。 チームはコールバックと非同期に実行されると推測し始めるため、これは完全に美しいわけではありません。これはまさに避けようとしたものです。 それにもかかわらず、この特定の実装オプションを選択したのは、その単純さと、非同期実行オプションが基本であり、残りのオプションがその上のアドオンになるという事実のためです。
次に、コマンドを非同期に実行するコードを記述する必要があります。 最も簡単なオプションは、パラメーターのパッケージ化とコマンドの非ブロッキング開始の責任をコマンド自体のクラスに割り当てることです。 これは、受け入れられている哲学ともわずかに矛盾しますが、コマンドの非同期処理のすべてのロジックを1か所に保持できます。 ZooKeeperの次のバージョンに新しいコマンドが含まれている場合、ライブラリにクラスを1つだけ追加するだけで十分です。変更は非常にローカルで下位互換性があります。
コマンドインターフェイスの統一のために、抽象型
Handle
      
      を導入することにしました-ライブラリクライアントからすべての実装の詳細を隠す低レベルの記述子(たとえば、コマンドの実行に
libzookeeper
      
      ライブラリが使用されるという事実)。 C / C ++では、タイプを宣言することでこれを実現できますが、パブリックライブラリヘッダーファイルで定義することはできません。
 class Handle;
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      Handle
      
      クラスの実装方法は、それほど重要ではありません。 簡単にするために、これは実際には
libzookeeper
      
      ライブラリの
libzookeeper
      
      であり、コマンドをユーザーから密かに実装すると、不完全型へのポインターが
zhandle_t
      
      へのポインターに
zhandle_t
      
      れると
zhandle_t
      
      ます。
したがって、コマンドを表す各クラスには、オーバーロードされた呼び出しステートメントが表示されます。
 struct SomeCmd { using CallbackType = SomeCallbackType; void operator()(Handle *, CallbackType) const; };
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      コマンドパラメータをパックするためのコードは提供しません。 それはかなりかさばり、記事の本質に関係のない多くの中間コードと詳細が含まれています。
クライアントクラスでコマンドを実行する方法は非常に簡単になります。
 class Session { public: // other methods template <typename CmdType> void run(CmdType const& cmd, typename CmdType::CallbackType callback) { cmd(this->getHandle(), std::move(callback)); } private: Handle * getHandle(); };
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      ここでは、基本的にコマンドの起動をチーム自体に委任し、低レベルの記述子を渡します。
オーバーロードされた呼び出し演算子が一定であることが重要です-チームは呼び出されたときに状態を変更しないでください。 まず、これにより、次の形式のコードで一時コマンドオブジェクトを使用できるようになります。
 session.run(DeleteCmd("path").setVersion(knownVersion), myCallback);
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
      
      値によってコマンドを送信し、 move-semanticsに依存することで同じ効果を実現できますが、これにより、場合によっては冗長コピーを作成する必要が生じます。
第二に、これは、同じコマンドを繰り返し実行しても、リポジトリの構造の変更に関連しない副作用につながるべきではないことをコードを読んでいる人(および一部はコンパイラ)に通知する方法です。
これで、可能なすべてのオプションを使用してすべての操作を非同期的に実行できます。これには、クライアントで1つのメソッド
run
      
      のみが必要です。
  std::future
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
    でコマンドの非同期実行を追加std::future
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      だから、今はすべてが何のために始まったのかを実装する番です-代替オプション。
std::future
      
      オブジェクトを使用したコマンドの非同期実行を有効にするには、次のシグネチャを持つ関数が必要です。
 template<class CmdType> std::future<ResultOf<CmdType>> runAsync(Session &, CmdType const&);
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      この関数は、セッションとコマンドを入力として受け入れ、コマンドの非同期実行の結果を表す
std::future
      
      オブジェクトを返します。
最初に、コマンドのコールバックパラメーターを1つの値に合わせる方法を理解する必要があります。 これが
ResultOf
      
      メタ関数の
ResultOf
      
      です。 関数パラメーターと戻り値の対応を示す方法はいくつかありますが、最も単純なものを選択し
DeduceResult
      
      。テンプレートクラス
DeduceResult
      
      個別の特殊化として、考えられるすべてのケースを
DeduceResult
      
      です。
 template <typename CallbackType> struct DeduceResult; template <> struct DeduceResult<std::function<void(std::error_code const&)>> { using type = void; }; template <typename T> struct DeduceResult<std::function<void(std::error_code const&, T)>> { using type = typename std::decay<T>::type; }; template <typename T> struct DeduceResult<std::function<void(std::error_code const&, T, Stat const&)>> { using type = std::pair<typename std::decay<T>::type, Stat>; }; template <typename CmdType> using ResultOf = typename DeduceResult<typename CmdType::CallbackType>::type;
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      DeduceResult
      
      は単純です。
-  エラーコードのみがコールバックに送信される場合、操作の結果はvoid
 
 
 
 です。
- エラーコードに加えて、追加のパラメーターがコールバックに渡される場合、操作の結果はこのパラメーターになります。
-   Stat
 
 
 
 オブジェクトへの参照もコールバックに送信される場合、結果とStat
 
 
 
 オブジェクトはstd::pair
 
 
 
 パックされます。
ResultOf
      
      は、テンプレート同義語( エイリアステンプレート 、C ++ 11の優れた機能の1つ)で、コマンドで定義された
DeduceResult
      
      タイプを
DeduceResult
      
      ます。
std::decay
      
      メタ関数の使用は注目に値します-いくつかのパラメーターは参照によってコールバックに渡されますが、値によってクライアントに返したいので、 オブジェクトはスタック上に存在できます。オブジェクトへのリンクを別のストリームに転送すると、クライアントがオブジェクトを読み取るまでにオブジェクトは既に破棄されています。
これで、
runAsync
      
      関数の実装を開始できます。 実装はほとんど明白です:必要なタイプの
std::promise
      
      オブジェクトを作成し、そこから
std::future
      
      オブジェクトを取得する(
std::promise::get_future()
      
      メソッドを
std::promise::get_future()
      
      ことで、
std::promise
      
      オブジェクトを受け取る特別なコールバックを作成し、コールバック実行の結果またはエラーをそこに入れます。 次に、コールバックを使用して標準セッションインターフェイスからコマンドを実行するだけです。 コールバックは
promise
      
      オブジェクトを所有する必要があるため、コールバックをフィールドとして
promise
      
      を含む関数オブジェクトにすることは論理的です。 結果の
runAsync
      
      関数コードは次のとおりです。
 template <typename CmdType> std::future<ResultOf<CmdType>> runAsync(Session & session, CmdType const& cmd) { FutureCallback<ResultOf<CmdType>> cb; auto f = cb.getPromise().get_future(); session.run(cmd, std::move(cb)); return f; }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      FutureCallback
      
      関数
FutureCallback
      
      の実装は、多くの点で
ResultOf
      
      メタ関数に埋め込まれたロジックを反映しています。 予想される操作のタイプに基づいて、入力引数をオブジェクトにパックし、このオブジェクトを
promise::set_value
      
      または
promise::set_exception
      
      介して共通の(
future
      
      オブジェクトの)状態に渡す関数を生成します。
 template <typename T> void setError(std::promise>T> & p, std::error_code const& ec) { //    codeToExceptionPtr  . //     ,    //     exception_ptr,   //  . p.set_exception(codeToExceptionPtr(ec)); } template <typename T> struct CallbackBase { //       promise   ,  //   std::function -    . // . n337 (20.8.11.2.1) using PromisePtr = std::shared_ptr<std::promise<T>>; PromisePtr promisePtr; CallbackBase() : promisePtr(std::make_shared<std::promise<T>>()) {} std::promise<T> & getPromise() { return *promisePtr.get(); } }; template <typename T> struct FutureCallback : CallbackBase<T> { void operator()(std::error_code const& ec, T value) { if (ec) { setError(this->getPromise(), ec); } else { this->getPromise().set_value(std::move(value)); } } }; template <> struct FutureCallback<void> : CallbackBase<void> { void operator()(std::error_code const& ec) { if (ec) { setError(this->getPromise(), ec); } else { this->getPromise().set_value(); } } }; template >typename T> struct FutureCallback<std::pair<T, Stat>> : CallbackBase<std::pair<T, Stat>> { void operator()(std::error_code const& ec, T data, Stat const& stat) { if (ec) { setError(this->getPromise(), ec); } else { this->getPromise().set_value( std::make_pair(std::move(data), stat)); } } };
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      これで、次の方法で関数を使用できます。
 std::vector<std::future<std::string>> nodeNameFutures; //     for (const auto & name : {"/node1", "/node2", "/node3"}) { nodeNameFutures.emplace_back(runAsync(session, CreateCmd(name))); } //     for (auto & f : nodeNameFutures) { f.wait(); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      コールバック関数を使用してこのようなコードを書くことは良いことではありません。
std::future
      
      メカニズムを使用すると、このようなタスクが大幅に簡素化されます。 たとえば、このメカニズムを使用して、サブツリーを再帰的に削除する機能を実装しました。
その他の実施形態
ほぼ無料でコマンド実行の同期バージョンを取得します。
 template <typename CmdType> ResultOf<CmdType> runSync(Session & session, CmdType const& cmd) { return runAsync(session, cmd).get(); }
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      実行のためのより多くの異なるオプションを思い付くことができます。 たとえば、接続エラーが発生したときにコマンドを再実行する関数を非常に簡単に記述できます。また、デバッグおよびパフォーマンス分析のために、コマンド実行のパラメーター、コマンド実行の開始と終了を記録します(たとえば、 trace_eventchrom APIを使用 )。
実際、原始的で非常に限定されたバージョンのアスペクト指向のパラダイムを手に入れました。 コマンドを開始する前と完了した後に追加のアクションを実行し、1つの機能のフレームワーク内のロジックをローカライズすることができます-「アスペクト」。
おわりに
リポジトリの操作をメソッドからオブジェクトに変換すると、コードの一貫性が低下し、コードの数が大幅に削減されたため、実装とデバッグの労力が大幅に節約されました。
もちろん、説明したアプローチは根本的に新しいものではありません。 少なくとも、 HBase Javaクライアントでは同様の方法が使用されます 。
このメソッドには欠点もあります-かなりの数のクラスを生成し、クライアントがライブラリインターフェイスを調べるのが少し難しくなります-操作はクラスインターフェイスで一緒に収集されず、異なるタイプに分離されます。 同じ理由で、クライアントがAPIを調査することは困難です。ビデオとプレゼンテーションは、IDEのオートコンプリートを介してリンクを介して利用できます(ただし、少なくともドキュメントを読んでください)。 したがって、このインターフェイスの構築では、詳細なドキュメントとライブラリの使用例が必要です。
UPD:記事で紹介された資料は、レポート「データウェアハウスの実用的なAPI」の基礎となりました。著者は、7月4日にニジニノヴゴロドのC ++ユーザーグループで講演しました。 リンクは、pdf形式のビデオおよびプレゼンテーションで入手できます。