KB0122: thinkcell を使用していると一部の Excel COM Automation 呼び出しが失敗する

問題

thinkcell がインストールされていると、一部の Excel COM Automation 呼び出しが失敗します。HRESULT エラーコードは 0x8001010A RPC_E_SERVERCALL_RETRYLATER です。LotusScript では、エラーは Automation object error です。

COM サーバーには、IMessageFilter メカニズムを使用して受信呼び出しをフィルタリングする機能があります。具体的には、IMessageFilter::HandleInComingCall の実装が SERVERCALL_RETRYLATER を返すと、クライアント呼び出しは RPC_E_SERVERCALL_RETRYLATER で戻ります。

Excel は thinkcell に関係なくこのメカニズムを使用します。thinkcell が存在すると RPC_E_SERVERCALL_RETRYLATER の発生頻度が高まり、問題がより顕在化します。

このような状況でエラーが発生することが知られている製品の 1 つに、IBM Lotus Notes のスクリプトエンジンである LotusScript があります。

解決策

RPC_E_SERVERCALL_RETRYLATER に応答して、呼び出し元は短時間待機してから呼び出しを繰り返す必要があります。Visual Basic for Applications にはこの処理が組み込まれているため、LotusScript など COM をサポートする他の Windows スクリプト環境でこの処理が行われない場合は、バグと言ってよいでしょう。

スクリプト環境以外の場合、またはスクリプト環境がこの処理を行わない場合、呼び出し元は RPC_E_SERVERCALL_RETRYLATER を自分で処理する必要があります。COM では、そのように指示すればクライアントに代わって呼び出しを自動的に繰り返せるため、これは比較的簡単です。実際の呼び出しコードは変更せずに済みます。

この動作を有効にするには、クライアントが独自の IMessageFilter を実装し、dwRejectType パラメーターが SERVERCALL_RETRYLATER の場合に、メソッド IMessageFilter::RetryRejectedCall が -1 以外を返すようにする必要があります。その後、COM 呼び出しを行う各 STA スレッド で、CoRegisterMessageFilter を使用して、この実装のインスタンスを COM に登録する必要があります。

例として、C++ の ATL RAII 実装を以下に示します: もっと読む

class CHandleRetryLaterBase :
  public CComObjectRootEx<CComSingleThreadModel>,
  public IMessageFilter
{
protected:
  CComPtr<IMessageFilter> m_imessagefilterOld;

  BEGIN_COM_MAP(CHandleRetryLaterBase)
    COM_INTERFACE_ENTRY(IMessageFilter)
  END_COM_MAP()

public:
  // IMessageFilter implementation
  DWORD STDMETHODCALLTYPE HandleInComingCall(
    /* [in] */ DWORD dwCallType,
    /* [in] */ HTASK htaskCaller,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ LPINTERFACEINFO lpInterfaceInfo
  ) {
    if( m_imessagefilterOld ) {
      // pass on to old handler, no change in behavior
      return m_imessagefilterOld->HandleInComingCall(
        dwCallType,
        htaskCaller,
        dwTickCount,
        lpInterfaceInfo
      );
    } else {
      // default behavior
      return SERVERCALL_ISHANDLED;
    }
  }

  DWORD STDMETHODCALLTYPE RetryRejectedCall(
    /* [in] */ HTASK htaskCallee,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ DWORD dwRejectType
  ) {
    if( SERVERCALL_RETRYLATER==dwRejectType ) {
      // in a script, wait indefinitely for robustness under load
      return 100;
    } else if( m_imessagefilterOld ) {
      // pass on to old handler, no change in behavior
      return m_imessagefilterOld->RetryRejectedCall(
        htaskCallee, 
        dwTickCount,
        dwRejectType );			
    } else {
      // default behavior
      return (DWORD)-1;
    }
  }

  DWORD STDMETHODCALLTYPE MessagePending(
    /* [in] */ HTASK htaskCallee,
    /* [in] */ DWORD dwTickCount,
    /* [in] */ DWORD dwPendingType
  ) {
    if( m_imessagefilterOld ) {
      // pass on to old handler, no change in behavior
      return m_imessagefilterOld->MessagePending(
        htaskCallee,
        dwTickCount,
        dwPendingType );
    } else {
      // default behavior
      return PENDINGMSG_WAITDEFPROCESS;
    }\n  }
};

// Instantiate this class in your scope to enable robust IMessageFilter.
class CHandleRetryLater :
  public CHandleRetryLaterBase
{
public:
  CHandleRetryLater() {
    // set new handler and save old one
    _ASSERT( m_dwRef==0 );
    HRESULT hr=CoRegisterMessageFilter( this, &m_imessagefilterOld );
    // m_imessagefilterOld may be nullptr if there is no
    // filter previously installed
    _ASSERT( SUCCEEDED(hr) );
  }

  ~CHandleRetryLater() {
    // reset old handler
    {
      CComPtr<IMessageFilter> iMessageFilter;
      HRESULT hr=CoRegisterMessageFilter(
        m_imessagefilterOld,
        &iMessageFilter );
      _ASSERT( SUCCEEDED(hr) );
      // make sure noone replaced our IMessageFilter
      _ASSERT( iMessageFilter==static_cast<IMessageFilter*>(this) );
    } // iMessageFilter is last release
    _ASSERT( m_dwRef==0 );
  }

  STDMETHOD_(ULONG, AddRef)() throw() {
#ifdef _DEBUG
    return InternalAddRef();
#else
    return 0;
#endif
  }

  STDMETHOD_(ULONG, Release)() throw() {
#ifdef _DEBUG
    return InternalRelease();
#else
    return 0;
#endif
  }

  STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) throw() {
    return _InternalQueryInterface(iid, ppvObject);
  }
};