14. 複製オブジェクトの拡張機能

この章では、複製オブジェクトの拡張機能について説明します。 複製オブジェクトの拡張機能は、仕様をよく理解した上で、使用していただくことをおすすめします。

14.1. リモートメソッドコール

NetZ がリモートで DO にメソッドを呼び出す方法では、 14.1. で説明するとおり、 リモートメソッドコール(RMC)またはアクションを使用します。 RMC は、 DO マスターまたはデュプリカのオブジェクトの特定のインスタンスで呼び出されるメソッドで、値を返すことができます。 RMC を使用すると、アクションを使用するよりも、呼び出される方法に関して、また呼び出しの State と Outcome について開発者が取得する情報に関して、メソッドのコントロールが大幅に向上します。 RMC は呼び出されるステーションに対してローカルとリモートの両方で実行でき、RMC は C++ メソッドとして実装されるので、ローカルステーションに直接呼び出すこともできます。 RMC は、クラス内で、最大255個定義可能です。

15.1. に説明するとおり、RMC は DDL に宣言され、次の構文を使用して実装されます。 有効な引数は、 15.1. に示すデータセットに有効な引数と有効なカスタムデータ型と同じです。 内蔵テンプレート型(MemberVector、MemberList、MemberQueue のいずれか)が RMC の引数として定義されると、 NetZ ではユーザー定義の関数に MemberVector <DDLTYPE(type)>、MemberList <DDLTYPE(type)>、 MemberQueue <DDLTYPE(type)> のいずれかの形式のパラメータの型を使用する必要があります。

Code 14.1 RMC の宣言
returntype RMCName( [Argument List] );

例えば、 15.1. で示す車のディーラーの例では、次のように RMC は CarDealer DO クラスのメソッドとして実装されます。

Code 14.2 CarDealer 複製オブジェクトクラスでの RMC の宣言
class CarDealer : public DOCLASS(CarDealer)
{
    dohandle BestMatch(qInt imake, qInt imodel, qInt iyear, qInt iprice);
    void ListMatches(qInt imake, qInt imodel, qInt iyear,
                     qInt *piNbOfCars, dohandle *phCarArray);
    void UpdateList(dohandle *phCarArray);
};

RMC が DDL で宣言されると、NetZ はメソッドを容易に識別するために固有の MethodID をメソッドに割り当て、 NetZ の DDL コンパイラが RMC を実行するためのスタブを生成します。 スタブは DO でメソッドを呼び出すために使用されます。 スタブの名前は RMC の名前の前に Call を付けて指定します。 RMC スタブの構文を次に示します。 RMCParameters は RMC の宣言で指定された引数です。 呼び出しが成功するとスタブは真を返し、失敗するか、エラーが発生すると偽を返します。 RMC が void 以外の返り値型を指定すると、スタブは Result に追加ポインタ(pResult)を含みます。 返り値が必要ない場合は、pResult パラメータに NULL を渡すことができます。 RMC とスタブ間の引数のマッピングを Table 14.1 に示します。

Code 14.3 RMC を実行するためのスタブ
// RMC invocation stub for a method that specifies the return type
bool CallRMCName(RMCContext* pDOCallContext, type* pResult, RMCParameters)
// RMC invocation stub for a void method
bool CallRMCName(RMCContext* pDOCallContext, RMCParameters)

そのため、上の例では生成された DOCLASS(CarDealer) が次のスタブを定義します。

Code 14.4 CarDealer で定義されるスタブ
bool CallListMatches(RMCContext* pDOCallContext, qInt imake, qInt imodel,
                    qInt iyear, out qInt *piNbOfCars, out dohandle *phCarArray)
Table 14.1 リモートメソッドコールの引数型のマッピング
RMC の引数型 修飾子 RMC スタブの引数型
Simple type in simple type
Simple type out simple type へのポインタ
Simple type in out simple type へのポインタ
Simple type の配列 in simple type シーケンスの最初の要素へのポインタ
Simple type の配列 out simple type シーケンスの最初の要素へのポインタ
Simple type の配列 in out simple type シーケンスの最初の要素へのポインタ
String in qChar* シーケンスの最初の要素へのポインタ
String out qChar* シーケンスの最初の要素へのポインタ
String in out qChar* シーケンスの最初の要素へのポインタ

RMC を実行するには DOCallContext クラスを使用して RMC のコンテキストが定義されている必要があります。 このクラスでは、メソッドが呼び出される単一または複数のステーションと該当するフラグを設定します。 該当するフラグを設定すると、RMC を同期呼び出しまたは非同期呼び出しにすることや、一方向の呼び出しなどにすることができます。 また、呼び出しの State や Outcome に関する情報を取得し、呼び出しのキャンセルやリセットを可能にすることもできます。 RMC とオブジェクトのマイグレーションのコンテキストの指定に使用される呼び出しのコンテキストについては、 12.9. を参照してください。 フラグを設定する前に、一緒に設定できるフラグとできないフラグについて、 12.9. で使用可能なフラグについての説明を参照してください。 RMC が値を返すかどうか、また一方向かどうかにかかわらず、RMC はメッセージがターゲットステーションに送信されたかどうかを示す Boolean を返します。 また、返り値を持たない RMC は明確には一方向とはみなされず、それに対応する呼び出しのコンテキストが完了されます。 このため、値を返さない RMC で RetryOnTargetValidationFailure フラグを使用できます。

RMC の単一または複数のターゲットステーションは、さまざまな方法で設定できます。 RMC が返り値を持つ場合、ターゲットステーションは DOCallContext::SetTargetStation を使用して、 または DOCallContext::CallOnMaster フラグまたは DOCallContext::CallOnLocalStation フラグを使用して設定できます。 複数の同じ返り値を返す可能性を回避するため、返り値のある RMC が一度に 1 つのステーションのみで呼び出されるようにするのが理想的です。 それでも複数のステーションに RMC を呼び出す必要がある場合は、ステーションごとに繰り返して行うことができます。 ただしこの場合は、複数の返り値を自分で処理する必要があります。

反対に、RMC で値を返さないようにするには、OneWayCall フラグを設定してから DOCallContext::SetTargetStation または、 DOCallContext::CallOnMasterDOCallContext::CallOnLocalStationDOCallContext::CallOnDuplicasDOCallContext::CallOnNeighbouringStations のいずれかのフラグを使用して単一または複数のターゲットステーションを設定します。 最後の 2 つのフラグを使用すると、一度に複数のステーションに容易に、効果的にメソッドを呼び出すことができます。 オブジェクトの DO マスターのみが呼び出せる DOCallContext::CallOnDuplicas は、オブジェクトのすべてのデュプリカにメソッドを呼び出します。 DOCallContext::CallOnNeighbouringStations はローカルステーションのすべての近隣ステーション、 つまりローカルステーションが認識するセッションの全ステーションでメソッドを呼び出します。 NetZ では、各ステーションはセッションの他のすべてのステーションを認識しているので、メソッドはローカルステーション以外の、セッションの全ステーションで呼び出されます。 CallContext::OneWayCall フラグが設定されると、複数のフラグを設定してターゲットステーションを指定できますが、 CallContext::OneWayCall フラグが設定されていない場合は、DOCallContext::SetTargetStation() を使用するか、または DOCallContext::CallOnMasterDOCallContext::CallOnLocalStation のいずれかのフラグを使用して単一のターゲットのみを指定できます。 また、CallContext::OneWayCall フラグは値を返す RMC に設定できますが、返り値は無視されます。

信頼性のあるチャンネルを利用していて、複数本のReliableの利用の初期化を行っている場合には、DOCallContext::SetSubStreamID により、呼び出し時利用するSubStreamIDを指定できます。 RMCの返信の際にも同じサブストリームを利用します。 サブストリームの初期化設定は、 8.3.4. を参照にしてください。

例を完成させると、CarDealer DO クラスのイベントループの一部は次のようになります。 このケースでは、RMCContext::TargetObjectMustBeMaster フラグを設定して、RMC の ListMatches が CarDealer オブジェクトの DO マスターのみで呼び出されることを保証します。

Code 14.5 CarDealer 複製オブジェクトクラスのイベントループ(一部)
CarDealer::Ref refdealer(g_hCarDealer);
RMCContext oDOCallContext(refDealer->GetMasterStation(), true);
oDOCallContext.SetFlag(RMCContext::TargetObjectMustBeMaster);
DOHandle phCars[16];
qInt iNbCars;

qBool bResult=refDealer->CallListMatches(&oDOCallContext, BMW, M5, 2001,
                                         &iNbCars, phCars);
    if (bResult){
        for(qInt i=0; i<iNbCars; i++)
        {
            printf("Found vehicle %x", phCars[i]);
        }
    }

CallMethodOperation クラスは、埋め込まれている RMC の呼び出しが返り値を待つ間にデッドロックしないことを保証します。 これは、RMC が結果を返す準備ができていないときに RMC の呼び出しを延期して、少し経ってから RMC を再度呼び出して行います。 その間、キューにあるメッセージが処理されるので、元の RMC がキューにあるメッセージに依存している場合、そのメッセージが処理され、RMC 呼び出しが完了されます。 また、この操作クラスは RMC 呼び出しの方法、タイミング、呼び出し元を制御するためにも DOHandle を RMC の呼び出し先ステーションに返すためにも使用できます。 すべての RMC は MethodID で固有に識別されているので、RMC を呼び出すステーション、または呼び出しに関する特定の状況に従ってその RMC が呼び出されたり、拒否されたりするようにできます。 例えば、特定の呼び出しを制限して、サーバーステーションのみから、ゲームのセットアップ中にのみ、またはゲームの特定の時にのみ呼び出されるようにできます。 CallMethodOperation クラスの詳細と使用方法の例については、 14.3.3. を参照してください。

14.2. アクション

NetZ が DO にリモートでメソッドを呼び出す方法は、上述の RMC を使用する方法か、またはアクションを使用する方法です。 アクションは、 DO マスターのすべてのデュプリカにメソッドを呼び出し、 デュプリカのコピーを持つセッションに参加している各ステーションでメソッドが実行されるのを保証する最も簡単な方法です。 アクションは、常に DO クラス内で実行され、アクションが呼び出されるステーションに対してローカルでもリモートでも実行されます。 アクションは、デュプリカで呼び出され、その DO マスターにメソッドが呼び出されるか、 DO マスターに直接呼び出されます。 アクションは C++ メソッドとして実装されるので、ローカルステーションに直接呼び出すこともできます。

15.1.8. に説明するとおり、アクションは DDL に宣言され、次の構文を使用して実装されます。 有効な引数は、 15.1. に示すデータセットに有効な引数と有効なカスタムデータ型と同じです。 内蔵テンプレート型(MemberVector、MemberList、MemberQueue のいずれか)がアクションの引数として定義されると、 NetZ ではユーザー定義の関数に、MemberVector <DDLTYPE(type)>、MemberList <DDLTYPE(type)>、 MemberQueue <DDLTYPE(type)> のいずれかの形式のパラメータの型を使用する必要があります。

Code 14.6 アクションの宣言
action ActionName( [Argument List] );

例えば、SphereZ では、UpdateWeather アクションは次のように、World DO クラスのメソッドとして実装されています。

Code 14.7 SphereZ の UpdateWeather アクションの実装
class World : public DOCLASS(World)
{
    // update the weather on the World object
    void UpdateWeather()
};

アクションが DDL で宣言されると、NetZ はメソッドを容易に識別するために固有の MethodID をメソッドに割り当て、 NetZ の DDL コンパイラがアクションを実行するための 2 つのスタブを生成しますが、一度に 1 つのスタブのみが使用されます。 スタブは DO マスターまたは DO デュプリカのアクションを呼び出すために使用されます。 最初のスタブは、オブジェクトの DO マスターにアクションを呼び出し、すべてのデュプリカで実行できます。 スタブの名前は、アクションの名前に _OnMaster を付けて指定します。 上の例では、生成された DOCLASS(World) は UpdateWeather_OnMaster() というスタブを作成します。 2 つめのスタブは、オブジェクトのすべてのデュプリカにアクションを呼び出し、オブジェクトの DO マスターに実行できます。 スタブの名前は、アクションの名前に _OnDuplicas を付けて指定します。 上の例では、生成された DOCLASS(World) は UpdateWeather_OnDuplicas() というスタブを作成します。 アクションとスタブ間の引数のマッピングを次の Table 14.2 に示します。

Table 14.2 アクションの引数型のマッピング
アクションの引数型 アクションのスタブの引数型
Simple type simple type
Simple type の配列 simple type シーケンスの最初の要素へのポインタ
String qChar* シーケンスの最初の要素へのポインタ

例を完成させると、World DO クラスのイベントループの一部は次のようになります。 すべてのデュプリカで天気を更新するため、_OnDuplicas スタブが使用され、天気をローカルで更新するためには、メソッドは DO マスターに直接呼び出されます。 この例では、 DO マスターのイベントループがそのデュプリカのすべてにアクションをトリガとします。 その結果、すべてのデュプリカは、World クラスに実装される UpdateWeather メソッドを呼び出します。

Code 14.8 World 複製オブジェクトクラスのイベントループ(一部)
Session::Ref refSession(Session::GetInstanceHandle());
if(refSession.IsValid() && refSession->IsADuplicationMaster())
{
    if (!DemoScene::GetInstance()->m_bEnableFog){
        World::Ref refWorld(g_hTheWorld);
        refWorld->EnableFog();
        refWorld->UpdateWeather_OnDuplicas();
    }
    else
    {
        World::Ref refWorld(g_hTheWorld);
        refWorld->DisableFog();
        refWorld->UpdateWeather_OnDuplicas();
    }
}

DO マスターがアクションが呼び出されると実質上同時に移行した場合、アクションはデュプリカで呼び出されるか、 DO マスターの移行が障害によるものの場合は呼び出されません。 アクションは値を返さないので、この場合が発生しても通知されません。 そのため、特定のメソッドが DO マスターで呼び出されることが必須である場合、アクションではなく RMC を実装する必要があります。 これには、呼び出しの結果を返すだけでなく、RMC ではメソッドが DOCallContext::TargetObjectMustBeMaster フラグを使用して DO マスターで呼び出されるように指定できる利点があります。

14.3. システムオペレーション

オペレーションとは、システム内で発生するイベントのことで、通常はネットワークメッセージによって行われます。 NetZ で行うことができるさまざまな種類のオペレーションは DO オペレーションとセッションオペレーションに分類され、 それぞれがさらに各種類のシステムオペレーションに細分化されます DO オペレーションとセッションオペレーションは、 それぞれ特定の DO と特定のセッションで実行されるオペレーションです。 オペレーションクラスの階層の詳細を Figure 14.1 に示します。 各オペレーション中に発生する一連のイベントの詳細については、このセクションで説明します。 DuplicatedObject クラスと DataSet クラスの OperationBegin メソッドと OperationEnd メソッドは、 パラメータとしてオペレーションクラスへのポインタとともに呼び出されます。

../_images/Fig_DO_Ext_OperationClass_Hierarchy.png

Figure 14.1 オペレーションクラス階層

Operation クラスのメソッドは、特定のオペレーションの詳細を取得するのに使用でき、デバッグのためにトレースを実装する際に特に便利です。 システムトレースの実装の詳細については、 18.2. を参照してください。 実行されるオペレーションの型を取得するには、GetType メソッドと GetClassNameString メソッドを使用して、 オペレーション型をそれぞれ値または文字列として返すことができます。 SetUserData メソッドおよび GetUserData メソッドを使用すると、オペレーションにコンテキストを指定することができます。 UserContext クラスによって渡されたユーザー定義値は、unsigned long、double、 Boolean、オブジェクトへのポインタのいずれかになります。 通常は、NetZ では自動的に保持されない値で、維持することを望む値を渡すのに使用されます。 例えば、ChangeMasterStationOperation オペレーションが発生したためにオブジェクトが移行するときを知るには、次のコードを実装できます。

Code 14.9 ChangeMasterStation オペレーションによるオブジェクト移行を検知するコード
void Avatar::OperationEnd(DOOperation* pOperation)
{
    switch (pOperation->GetType()) {
        case Operation::ChangeMasterStation:
            _printf(NEX_T("The Avatar %x has migrated"),
                    pOperation->GetAssociatedDOHandle);
            break;
    }
}

ChangeMasterStation オペレーションと RemoveFromStore オペレーションは、 Scheduler::SystemLock によってシステム状態がロックされているときには DO で呼び出されません。 ロックは、例えば呼び出し中などにオブジェクトが移行しないように保証するために使用できます。

システムオペレーションがトレースされると、Operation::SetTraceFilter メソッドを使用してどのオペレーションが、 どのオブジェクトにトレースされるかを指定でき、コールバックが登録されます。 この関数がユーザーによって再定義されない場合、デフォルトでは CoreDO 以外のすべてのオブジェクトのすべてのオペレーションがトレースされます。 つまり、すべての UserDO がトレースされます。 トレースされたオペレーションとオブジェクトをフィルタリングするには、TraceFilter 関数を再定義してコールバックを登録する必要があります。 この関数は、オペレーションへのポインタを取り、Boolean を 1 つ返します。 コールバックを登録すると、SetTraceFilter メソッドを呼び出して TraceLog フラグを設定してオペレーションをトレースする必要があります。 このメソッドを呼び出して、特定のユーザーオブジェクトクラスに選択したオペレーションをトレースする方法の例を次に示します。 システムトレースの実装の詳細については、 18.2. を参照してください。

Code 14.10 特定のユーザーオブジェクトクラスに選択したオペレーションをトレースする方法の例
// Register a callback that defines how the traces will be filtered
qBool FilterAvatarTraces(const Operation* pOperation)
{
    DuplicatedObject* pAssociatedDO =NULL;

    // Select the operations to be traced
    switch (pOperation->GetType())
    {
        case Operation::UpdateDataSet:
        case Operation::ChangeMasterStation:
        case Operation::FaultRecovery:
        case Operation::AddToStore:
        case Operation::RemoveFromStore:
        case Operation::CallMethod:
        case Operation::ChangeDupSet:
            pAssociatedDO =((DOOperation*) pOperation)->GetAssociatedDO();
            break;
        default:
            break;
    }
    if (pAssociatedDO ==NULL)
    {
        // Keep traces that are not related to a specific DO.
        return true;
    }

    // Trace the operation if the object is of the Avatar class.
    if (pAssociatedDO ->IsA(DOCLASSID(Avatar)))
    {
        return true;
    }
    else
    {
        return false;
    }
}

// To trace operations, call the SetTraceFilter method and
// then set the TraceLog flag to trace operations
Operation::SetTraceFilter(FilterAvatarTraces);
TraceLog::GetInstance()->SetFlag(TRACE_OPERATION);

NetZ は、特定の各 Operation クラスの実行中のキャスティング(明確な型変換)のための Operation::DynamicCast メソッドを定義します。 このメソッドは、標準 C++ の dynamic_cast 演算子と同様に機能しますが、NetZ のランタイム情報を使用します。 例えば、次のように pOperation が RefreshOperation オブジェクトをポイントする場合には RefreshOperation オブジェクトへのポインタを返すことができます。

Code 14.11 実行中キャスティングの例
RefreshOperation * pRefreshoperation = RefreshOperation::DynamicCast(pOperation)

14.3.1. オペレーションシーケンス

特定のシステムオペレーションが発生すると、次に説明するように特定のイベントが事前に定義された順番で行われます。 間違ってプログラムをデッドロックすることのないよう、各オペレーションのイベントの順番を理解することが重要です。 NetZ では、DuplicatedObject クラスと DataSet クラスの OperationBegin と OperationEnd の呼び出しの間に、 この 2 つのメソッドの間に実行されるコードのブロックが単一のスレッドのみで呼び出され、実行中に状態が変更しないことを保証するための SafetyExecutive を使用します。

14.3.1.1. AddToStore

各ステーションは、すべてのアクティブローカル DO マスター(ローカルでパブリッシュされる)とデュプリカ(ローカルで検出される)オブジェクトのリストを持つ独自の DO ストアを維持します。 実行中にオブジェクトが作成および削除されると、オブジェクトがストアに追加または削除されます。 ストア内のオブジェクトのみがアクティブとみなされ、ローカルステーションで更新されます。 DO マスターは、ローカルステーションにある場合にアクティブとみなされます。 つまり、DuplicatedObject::Publish が呼び出され、DuplicatedObject::DeleteMainRef がまだ呼び出されていない場合にアクティブとみなされます。 デュプリカは、 DO マスターがシステムにある場合にアクティブであるとみなされます。

オブジェクトが DO マスターストアに追加されると、次の一連のイベントが行われます。

オブジェクトがデュプリカストアに追加されると、次の一連のイベントが行われます。

14.3.1.2. RemoveFromStore

各ステーションは、すべてのアクティブローカル DO マスター(ローカルでパブリッシュされる)とデュプリカ(ローカルで検出される)オブジェクトのリストを持つ独自の DO ストアを維持します。 実行中にオブジェクトが作成および削除されると、オブジェクトがストアに追加または削除されます。 ストア内のオブジェクトのみがアクティブとみなされ、ローカルステーションで更新されます。 DO マスターは、ローカルステーションにある場合にアクティブとみなされます。 つまり、DuplicatedObject::Publish が呼び出され、DuplicatedObject::DeleteMainRef がまだ呼び出されていない場合にアクティブとみなされます。 デュプリカは、 DO マスターがシステムにある場合にアクティブであるとみなされます。

DuplicatedObject::ApproveFaultRecovery メソッドが偽を返す場合、または DuplicatedObject::DeleteMainRef メソッドを使用してオブジェクトが削除される場合、 DO マスターストアからオブジェクトが削除されます。 オブジェクトが DO マスターストアから削除されると、次の一連のイベントが行われます。

  • SafetyExecutive が呼び出され、オブジェクトが削除できるかどうかを確認する。できる場合は続行し、できない場合は終了する。
  • DataSet::OperationBegin メソッドが呼び出される。
  • DuplicatedObject::OperationBegin メソッドが呼び出される。
  • DO マスターオブジェクトのデュプリカに、マルチキャストされたメッセージによる削除が通知される。
  • DO マスターストアの DO マスターへの参照が削除される。
  • DataSet::OperationEnd メソッドが呼び出される。
  • DuplicatedObject::OperationEnd メソッドが呼び出される。繰り返しが DO マスターオブジェクトでオペレーションされなくなる。
  • ローカルステーションで RefTemplate を使用して作成されたオブジェクトへの参照数が 0 になると、オブジェクトデストラクタが呼び出される。

DO マスターが削除されたことを知らせるメッセージ DO マスターからデュプリカが受信すると、デュプリカストアからオブジェクトが削除されます。 オブジェクトがデュプリカストアから削除されると、次の一連のイベントが行われます。

  • SafetyExecutive が呼び出され、オブジェクトが削除できるかどうかを確認する。できる場合は続行し、できない場合は終了する。
  • DataSet::OperationBegin メソッドが呼び出される。
  • DuplicatedObject::OperationBegin メソッドが呼び出される。
  • デュプリカストアのデュプリカへの参照が削除される。
  • DataSet::OperationEnd メソッドが呼び出される。
  • DuplicatedObject::OperationEnd メソッドが呼び出される。繰り返しがデュプリカオブジェクトでオペレーションされなくなる。
  • ローカルステーションで RefTemplate を使用して作成されたオブジェクトへの参照数が 0 になると、オブジェクトデストラクタが呼び出される。

14.3.1.3. ChangeDupSet

ChangeDupSet オペレーションは DO マスターオブジェクトのみで呼び出され、オブジェクトがデュプリカを作成または削除するたびに呼び出されます。

デュプリカが追加されると、次の一連のイベントが発生します。

  • DO マスターがデュプリカ作成のメッセージを受信する。
  • SafetyExecutive が呼び出され、オブジェクトが作成できるかどうかを確認する。できる場合は続行し、できない場合は終了する。
  • DataSet::OperationBegin メソッドが呼び出される。
  • DuplicatedObject::OperationBegin メソッドが DO セッションマスターで呼び出される。
  • マスターが、デュプリカが作成されるステーションにメッセージを送信し、そのステーションに AddToStore オペレーションをトリガする。
  • デュプリカがマスターのデュプリカロケーションセットに追加される。
  • DataSet::OperationEnd メソッドが呼び出される。
  • DuplicatedObject::OperationEnd メソッドが呼び出される。

デュプリカが削除されると、次の一連のイベントが発生します。

  • RemoveFromStore オペレーションが、デュプリカが削除されるステーションで呼び出される。
  • DO マスターがデュプリカの 1 つを削除するメッセージを受信する。
  • SafetyExecutive が呼び出され、オブジェクトが削除できるかどうかを確認する。できる場合は続行し、できない場合は終了する。
  • DataSet::OperationBegin メソッドが呼び出される。
  • DuplicatedObject::OperationBegin メソッドが DO セッションマスターで呼び出される。
  • デュプリカがマスターのデュプリカロケーションセットから削除される。
  • DataSet::OperationEnd メソッドが呼び出される。
  • DuplicatedObject::OperationEnd メソッドが呼び出される。

14.3.1.4. ChangeMasterStation

ChangeMasterStation オペレーションは DO の DO マスターのマイグレーションの結果発生します。 このオペレーションの結果として、ローカルステーションのオブジェクトのロール(マスターかデュプリカ)が変更することがあります。 オブジェクトは、デュプリカから DO マスターに昇格する場合も、 DO マスターからデュプリカに降格する場合も、デュプリカのままでいる場合もあります。

ChangeMasterStation オペレーションが呼び出されると、次の一連のイベントが発生します。

  • SafetyExecutive が呼び出され、オブジェクトが移行できるかどうかを確認する。できる場合は続行し、できない場合は終了する。
  • DataSet::OperationBegin メソッドが呼び出される。
  • DuplicatedObject::OperationBegin メソッドが呼び出される。
  • オブジェクトが新しいステーションに移行し、マスター StationID が変更される。この変更に伴うロールの変更も行われる。
  • NetZ の DO マスターとデュプリカオブジェクトのリストが更新される。
  • DataSet::OperationEnd メソッドが呼び出される。
  • DuplicatedObject::OperationEnd メソッドが呼び出される。

14.3.1.5. FaultRecovery

ステーションに障害が発生すると、障害の発生したステーションに DO マスターがある各 DO のセッションマスターで、
FaultRecovery オペレーションが次の手順で行われます。

DuplicatedObject::ApproveFaultRecovery が真を返すと、次の一連のイベントでセッションマスターが DO をコントロールします。

DuplicatedObject::ApproveFaultRecovery が偽を返すと、次の一連のイベントでオブジェクトはストアから削除されます。

  • RemoveFromStore オペレーションが行われる。
  • DuplicatedObject::OperationEnd メソッドが呼び出される。
  • ローカルステーションで RefTemplate を使用して作成されたオブジェクトへの参照数が 0 になると、オブジェクトデストラクタが呼び出される。

14.3.1.6. CallMethodOperation

CallMethodOperation は、次の一連のイベントが発生した場合に、RMC が呼び出された結果として起こります。

  • RMC が特定のステーションによって呼び出される。
  • SafetyExecutive が呼び出され、呼び出しが続行できるかどうかを確認する。できる場合は続行し、できない場合は終了する。
  • DuplicatedObject::OperationBegin メソッドが呼び出される。
  • RMC が呼び出される。
  • DuplicatedObject::OperationEnd メソッドが呼び出される。RMC が延期される場合、再度呼び出される際、オペレーションの前に SafetyExecutive によって再度渡される。

14.3.1.7. UpdateDataSet

デュプリカがその DO マスターから更新を受信すると、このオペレーションがデュプリカで呼び出され、次の一連のイベントが発生します。

14.3.1.8. JoinSession

ステーションがセッションに参加しようとすると、次の一連のイベントが発生します。

14.3.1.9. JoinStation

JoinApproval コールバックで JoinSessionOperation::Approve が返されると、次のステーションのセッション参加処理が発生します。

  • 参加するステーションのステーションオブジェクトがパブリッシュされ、JoinStationOperation が開始されます。
  • 接続ポイントのステーションが、参加リクエストが承認されたことを知らせるメッセージを参加しようとしているステーションに送信する。
  • 新しいステーションの DO マスターが接続ポイントステーションから参加するステーションに移行する。
  • JoinStationOperation が終了する。

14.3.1.10. LeaveStation

ステーションがセッションから離脱する際、次の一連のイベントが発生します。

  • RemoveFromStoreOperation によって、離脱しようとしているステーションが DO マスターから削除される。
  • LeaveStationOperation を開始する。
  • 内部クリーンアップが実行され、離脱しているステーションがセッションの一部であるとみなされないようにする。
  • LeaveStationOperation を終了する。

14.3.2. オペレーションのセキュリティ

NetZ のシステムオペレーションは、特定のオペレーションが間違って呼び出されないように、呼び出される前に承認または拒否することができます。 このタスクは SafetyExecutive クラスによって行われます。 間違った呼び出しはゲームコードのバグ、またはプレイヤーがハッキングを行って発生する場合があります。 当然、内部テストまたはベータテスト中にはチートが発生する可能性はあまりなく、間違って呼び出されたシステムオペレーションはゲームコードのバグによるものの場合がほとんどです。

SafetyExecutive::TrustLocalStation への呼び出しは、ステーションが信頼されるかどうかを設定します。 ステーションが信頼される場合、ステーションがトリガするすべてのオペレーションの有効性は、オペレーションが呼び出される前に SafetyExecutive で確認されません。 ステーションが信頼されない場合、すべてのオペレーションは呼び出される前に SafetyExecutive によって確認されます。

特定のオペレーションへの制限は、RegisterCustomOperationCheck を使用して実装されます。 このメソッドは、パラメータにユーザー定義の CustomOperationCheck 関数を取り、オペレーションが許可されるか、拒否されるかの条件を指定します。 この関数は、特定のオペレーションが間違って呼び出されるのを回避し、オペレーションが承認されると真を返し、拒否されると偽を返します。 オペレーションが拒否される場合のアクションがある場合、そのアクションは RegisterInvalidOperationCallback で登録される InvalidOperationCallback によって定義されます。 例えば、次のように Avatar オブジェクトがセッションクライアントで作成されるのを回避します。

Code 14.12 Avatar オブジェクトがセッションクライアントで作成されるのを回避する制限
// Define the CustomOperationCheck
qBool CustomOperationCheck(const Operation & oOperation)
{
    // I will not create duplicas of my Avatar on a client station.
    if (oOperation.GetType()==Operation::ChangeDupSet)
    {
        const ChangeDupSetOperation *
                    pOp=ChangeDupSetOperation::DynamicCast(&oOperation);
        if (pOp->IsADuplicaAddition() && pOp->GetAssociatedDO()->
                                                       IsA(DOCLASSID (Avatar)))
        {
            Station::Ref refStation(pOp->GetTargetStation());
            if (refStation->GetProcessType() == Station::ClientProcess)
            {
                return false;
            }
        }
    }
    return true;
}

// Then register the custom check
SafetyExecutive::GetInstance()->
            RegisterCustomOperationCheck(DenyClientAvatarCreation);

CustomOperationCheck 関数が偽を返し、オペレーションが SafetyExecutive によって拒否される場合、 NetZ はユーザー定義の InvalidOperationCallback を呼び出して行うアクションを指定します。 これは、RegisterInvalidOperationCallback() を使用して登録されています。 このコールバックでは、プレイヤーへのフラグ指定や、許可されている場合にはプレイヤーをゲームから退去させるなどのアクションを定義できます。 例えば、次のようなレポートシステムを実装できます。 ここでは、デバッグモードやテストモードでは不正なオペレーションの呼び出しはバグによるものと仮定し、これが発生するとトレースを出力します。 反対に、リリースモードでは、不正なオペレーションはチートによる可能性があると仮定し、各ステーションの統計を保持するサーバーにイベントを報告します。 ステーションにチートが発生しているとみなされると、適切なアクションを取ることができます。

Code 14.13 不正なオペレーションに対するレポートシステムの実装
// Define the pfInvalidOperationCallback
void ReportCheats(const Operation& oOperation)
{
    // For example, at this point we could report to the server
    // that the player on station 'oOperation.GetOrigin()' has
    // performed an invalid operation and is potentially cheating.
    // This event would be reported via an RMC on a well-known
    // 'CheatManager' object. The server could then keep stats on
    // the reports and if a large number of people report that the
    // same person is cheating, appropriate action be taken. Note
    // that an invalid operation may also be an indication of a bug
    // in the game and not just a cheat.
}

void TraceInvalidOperations(const Operation& oOperation)
{
    // During development testing, we know that no one cheats. If
    // an invalid operation is called, then it is due to a bug in
    // the code.
    TRACE(TRACE_ALWAYS,NEX_T(" ** An invalid operation has been performed ** "));
    SYSTEMCHECK(false); // Stop here.
}

// Register the custom callback which will be called in release mode.
SafetyExecutive::GetInstance()->RegisterInvalidOperationCallback(ReportCheats);
// and this callback which will be called when testing in internal or
// debug mode.
SafetyExecutive::GetInstance()
                    ->RegisterInvalidOperationCallback(TraceInvalidOperations);

14.3.3. CallMethodOperation

CallMethodOperation クラスの主な目的は、埋め込まれている RMC の呼び出しが、返り値を待つ間にデッドロックしないことを保証することです。 これは、RMC が結果を返す準備ができていないときに RMC の呼び出しを延期して、少し経ってから RMC を再度呼び出して行います。 その間、キューにあるメッセージが処理されるので、元の RMC がキューにあるメッセージに依存している場合、そのメッセージが処理され、RMC 呼び出しが完了されます。 RMC を延期するには、CallMethodOperation::PostponeOperation メソッドを呼び出し、 必要な時間遅延を設定します DOCallContext フラグの TargetObjectMustBeMaster または TargetObjectMustHaveAuthority のいずれかが設定されている場合、 これらのフラグはメソッドが初めて呼び出される場合にのみ真となることが保証されます。 そのため、呼び出しを延期すると、これらのフラグは再度確認されず、真にならない場合があります。 UserContext は、AttachUserContext を使用してオペレーションオブジェクトに追加したり、 GetUserContext を使用して取得したりすることができます。 GetTargetMethodID は、呼び出された RMC の MethodID と、オペレーションが RMC の呼び出しを完了させようとしたものの、 延期された回数である GetAttemptCount を返します。

次の例では、CallMethodOperation クラスが使用できる例を示します。 ここでは階乗数を演算します。

Code 14.14 CallMethodOperation クラスの使用例
// Create a specific context for the Factorial object.
struct FactorialContext: public RootObject
{
    qInt m_iResult;
    RMCContext m_oRMCContext;
};

qInt GameObject::Factorial(qInt iValue, DOHandle hS1, DOHandle hS2)
{
    // Called in the context of an RMC.
    SYSTEMCHECK(GetCurrentOperation()->GetType() == Operation::CallMethod);
    CallMethodOperation* pCurrentOp=CallMethodOperation::DynamicCast(
                                                        GetCurrentOperation());
    if (iValue==0)
    {
        SYSTEMCHECK(pCurrentOp->GetAttemptCount()==1);
        return 1;
    }
    else
    {
        FactorialContext* pContext=NULL;
        // If this is the first time the operation is called
        // we initiate the call.
        if (pCurrentOp->GetAttemptCount()==1)
        {
            // Initiate the call.
            pContext=qNew FactorialContext;
            pCurrentOp->AttachUserContext(UserContext(pContext));

            pContext->m_oRMCContext.ClearFlag(RMCContext::SynchronousCall);

            // Select worker station.
            if (Station::GetLocalStation()==hS1)
            {
                pContext->m_oRMCContext.SetTargetStation(hS2);
            }
            else
            {
                pContext->m_oRMCContext.SetTargetStation(hS1);
            }

            qBool bResult=CallFactorial(&(pContext->m_oRMCContext),
                                &(pContext->m_iResult), iValue-1, hS1, hS2);
            SYSTEMCHECK(bResult);
        }
        else
        {
            pContext=static_cast<FactorialContext*>(
                        pCurrentOp->GetUserContext().GetPointer());
            SYSTEMCHECK(pContext!=NULL);
        }

        QTRACE(TRACE_ALWAYS,(NEX_T("Testing if result of factorial(%d) is ready"),
                            iValue));
        if (pContext->m_oRMCContext.Wait(0))
        {
            // If the result is ready we return the result
            // and delete the created context.
            QTRACE(TRACE_ALWAYS,(NEX_T("Result of factorial(%d) ="), iValue,
                                pContext->m_iResult));
            qInt iResult=iValue * pContext->m_iResult;
            qDelete pContext;
            return iResult;
        }
        else
        {
            // If the result is not yet ready we postpone the
            // operation for 100 ms before recalling the method.
            pCurrentOp->PostponeOperation(100);
            return 0;
        }
    }
}

また、CallMethodOperation クラスは RMC 呼び出しの方法、タイミング、呼び出し元を制御するためにも DOHandle を RMC の呼び出し先ステーションに返すためにも使用できます。 すべての RMC は NetZ で固有に識別されているので、特定の RMC を呼び出すステーション、または呼び出しに関する特定の状況に従ってその RMC が呼び出されたり、拒否されたりするようにできます。 例えば、特定の呼び出しを制限して、サーバーステーションから、ゲームのセットアップ中、ゲームの特定の時点などに呼び出しを限定できます。 例えば、特定の RMC がサーバーステーションのみで呼び出されるようにする制限は、次のように実装します。

Code 14.15 特定の RMC がサーバーステーションでのみ呼び出される制限
qBool RestrictSpecialRMCs(const Operation & oOperation)
{
    static MethodID  idSpecialRMC =MethodIDGenerator::GetID(NEX_T("SpecialRMC"));
    // First we check that the operation being called is
    // a CallMethodOperation and that the RMC is a SpecialRMC.
    if (oOperation.GetType()==Operation::CallMethod &&
            CallMethodOperation::DynamicCast(&oOperation)->GetTargetMethodID()
            ==idSpecialRMC)
    {
        // As only server processes can call this special RMC we
        // check if the station that called it is a server.
        Station::Ref refCallOrigin(oOperation.GetOrigin());
        if (refCallOrigin.IsValid() && refCallOrigin->
                                    GetProcessType()==Station::ServerProcess)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    // All other operations are allowed.
    return true;
}

14.3.4. セッションオペレーション

SessionOperation には、JoinSessionOperationがあります。 JoinSessionOperationは、ステーションのセッションへの参加リクエストを承認または却下するのに使用sれます。

ステーションがセッションに参加しようとすると、Session::RegisterJoinApprovalCallback によって登録された JoinApproval コールバックが呼び出され、 引数のJoinSessionOperationオブジェクトで、リクエストを承認するか拒否するかが決定されます。 セッションへの参加が承認され、コールバックが JoinSessionOperation::Approve を返すと、JoinSession オペレーションが実行されます。 一方、参加が拒否され、コールバックが JoinSessionOperation::Deny を返すと、オペレーションは行われず、 参加リクエストが拒否されたことを確認するメッセージが参加ステーションに返されます。 JoinApproval コールバックが登録されていないか、コールバックが Approve または Deny を呼び出さない場合、デフォルトでは参加リクエストが承認されます。 コールバックは、JoinSessionOperation::GetApprovalState メソッドで返される参加への承認状態が Unknown または Pending の場合にのみ呼び出されます。 Pending の状態は JoinSessionOeration::PostponeDecision メソッドによって決定が延期される場合に返されます。 JoinApproval コールバックとその後の JoinSession オペレーションは、参加ステーションがセッションに参加しようとするステーションで呼び出されます。

例えば、ゲームのプレイヤー数を 4 人に制限し、秘密のパスワードを持っているプレイヤーのみに参加を許可する場合、 次のように JoinApproval コールバックを実装します。

Code 14.16 プレイヤー数を 4 人、秘密のパスワードを持っているプレイヤーのみ参加可能にするオペレーション
void CheckJoiningProcesses(JoinSessionOperation * pOperation)
{
    // Count the number of stations, but skip the
    // tool stations (e.g. ObjectMonitor)
    qUnsignedInt uiPlayerCount=0;
    Station ::SelectionIterator it;
    while (!it.EndReached())
    {
        if (it->GetProcessType()!=Station ::ToolProcess)
        {
            uiPlayerCount++;
        }
        it++;
    }

    // Approve the join if the maximum number of
    // players has not been reached and if the joining
    // player has supplied the correct password.
    if (uiPlayerCount<=4 && pOperation->GetJoiningStation()->
                            GetIdentificationToken()==NEX_T("CorrectPassword"))
    {
        pOperation->Approve();
        return;
    }
    // Always approve the join when the station is a tool process.
    // Note that for security purposes, we might want to force
    // tool processes to also use a password, or this section of
    // the code could be disabled in release mode.
    if (pOperation->GetJoiningStation()->GetProcessType()==
                                                        Station ::ToolProcess)
    {
        pOperation->Approve();
        return;
    }
    pOperation->Deny();
}

14.3.5. オペレーションコールバック

OperationCallback クラスを使用すると、システムオペレーションにユーザー定義のコールバックを呼び出すことができます。 OperationCallback クラスのサブクラスを作成し、CallMethod メソッドを実装し、 それを OperationManager::RegisterCallback() を使用して適切な OperationManager に登録して、 特定のシステムオペレーションに独自のコールバックを実装します。

OperationCallback が作成されると、-1024 ~ 1024 の範囲で、システムに使用されている順位以外の優先順位を指定する必要があります。 オペレーションコールバックは、指定された優先順位に従った数字順に呼び出されます。 優先順位は、システム内にオペレーション関連イベントが発生する順番を決める正または負の整数です。 登録されているすべてのコールバックは、優先順位の低いものから高いものの順番に呼び出され、実際のオペレーションは 0 の優先順位で行われます。 優先順位を使用すると、コールバックを OperationBegin の呼び出しの直前または直後に呼び出すことを保証する場合など、 独自のオペレーションコールバックが呼び出される順番を制御できます。 NetZ で使用される優先順位と呼び出される順番を次に示します。

../_images/Fig_DO_Ext_OperationCallbacks_CallingTimeline.png

Figure 14.2 NetZ で定義されている OperationCallbacks の呼び出し順を示すタイムライン

14.4. DOの上位互換について

DOのDDL定義プロトコルは、上位互換を取れるように設計されています。 本節では、上位互換を取る場合の留意点について記載します。

14.4.1. DOクラスの上位互換について

DOクラスは、実行時に読み込まれたDDL定義ファイルの順番、DDL定義ファイル内で定義順にクラスIDが振られます。 クラスIDが異なる場合には、追加は可能です。 しかし、追加したクラスをWellKnownオブジェクトとして扱った場合や、publishした時には正しい動作の保証ができません。

ユーザーのDOのクラスIDには、100以上が割り振られます。 上位互換性を取るために定義順ではないクラスIDを割り振る場合には、DDL内のclassidにより、クラスIDを指定することが可能です。 classidは、100以上255以下の値を設定可能です。

Code 14.17 DDLファイルでのclassidの定義例
doclass A {
};

doclass B {
    classid = 102; //自動でアサインされる101ではなく、102を指定する。
};

14.4.2. リモートメソッドコールの上位互換について

リモートメソッドコールは、DOクラスごとに定義順にIDが割り振られて、クラスIDを付加した グローバルでユニークなメソッドIDが割り振られます。 上位クラスで定義されているメソッドは、下位クラスで新たなメソッドIDをアサインせず、 上位クラスのメソッドIDをそのまま用いります。

よって、上位バージョンのアプリケーションでは、DOクラスごとに新しいリモートメソッド コールの宣言を最後尾に追加が可能です。 順番さえ合っていれば、DDL定義ファイル間でフィールド名が異なっても問題ありません。

下位バージョンのアプリケーションのDDLで定義されていない順番のメソッドを、 上位バージョンのアプリケーションから呼び出した場合には、RMCの呼び出し失敗となり、 RMCContext::GetOutcome()で、DOCallContext::ErrorRMCDispatchFailedが取得できます。

14.4.3. データセットの上位互換について

DOクラスのメンバ変数としてデータセット、データセット内のメンバ変数は、 それぞれDOクラスごと、デートセットごとにユニークなIDを定義順に割り振ります。 受信時に、DDLファイルで定義されていない順番のデータセットのデータが来た場合には、無視されます。

上位バージョンのアプリケーションは、DOクラスに新しいデータセットの宣言を、 データセットに新しいメンバ変数を、最後尾へ追加可能です。 順番さえ合っていれば、DDL定義ファイル間でメンバ変数名が異なっても問題ありません。

アプリケーションのバージョンごとに異なる定義を持つDDLのデータセットを受信したときに、 メンバ変数が送信元のDDLで有効か無効かを区別するため、データセットの宣言に識別番号としてREVISIONを 付与可能です。 REVISIONは、0からスタートして、REVISIONの番号が設定されると、それ以後の メンバー変数は、指定されたREVISIONの番号を持つものとして扱われます。 REVISIONは、データセット内で単調増加となる値を1から255まで指定可能です。

Code 14.18 DDLファイルでのREVISION定義の例
dataset Data {
    double x;
    REVISION = 1;
    int32 a;
};

doclass X {
    Data data;
};

データセットには、受信したデータセットのREVISIONを取得できるAPI(DataSet::GetRevision() ) と、メンバー変数ごとのREVISIONを取得するAPI(DATASET_VALID_REVISIONマクロ)と、 受信したデータセットが、受信したデータセットのREVISIONで有効かどうかを取得するAPI(DATASET_ISVALIDマクロ)とが定義されます。

Code 14.19 REVISION取得の例
X::SelectionIterator it;
while (!it.EndReached()) {
    QLOG(EventLog::Info,NEX_T("it->data Revision:") << it->data.GetRevision() //受信したデータセットのリビジョンを取得
         << NEX_T(" x:") << it->data.DATASET_VALID_REVISION(x) //メンバ変数xのREVISIONを取得(=0)
         << NEX_T(" valid:") << it->data.DATASET_ISVALID(x)    //メンバ変数xが受信したデータセットのリビジョンで有効か取得
         << NEX_T(" a:") << it->data.DATASET_VALID_REVISION(a) //メンバ変数aのREVISIONを取得(=1)
         << NEX_T(" valid:") << it->data.DATASET_ISVALID(a)    //メンバ変数aが受信したデータセットのリビジョンで有効か取得
    );
}