We’re going to start off with a simple, non-MTS-aware project, called SimpleNegotiator
, and import it into MTS and see what happens. We’ll build it as an in-process DLL, despite the fact that we’ll want to be able to access it remotely. This is because all MTS objects run inside transaction server proxies. This means that everything that the object does is wrapped up inside MTS control logic. We use the ATL object wizard to create our single object, called Negotiator
(with the one interface, INegotiator
) and we add the method and two properties that I’ve specified above.
As with DBManager
, we need to include the ADO headers from the MTS installation in our source, and add the library to the link section of the project settings.
Starting off with our two properties, the code for these is very simple; all we’re really doing here is parameterizing the object:
STDMETHODIMP CNegotiator::put_Hostage(BSTR newVal)
{
m_bstrHostage = newVal;
return S_OK;
}
STDMETHODIMP CNegotiator::put_Account(BSTR newVal)
{
m_bstrAccount = newVal;
return S_OK;
}
We need to declare the two private CComBSTR variables, m_bstrHostage
and m_bstrAccount
, in Negotiator.h
:
private:
...
CComBSTR m_bstrHostage;
CComBSTR m_bstrAccount;
This is what the code for our DealWithKidnappers()
method looks like:
STDMETHODIMP CNegotiator::DealWithKidnappers
(int amount, VARIANT_BOOL* pbFree)
{
HRESULT hResult = TakeCashFromAccount(amount, pbFree);
if (FAILED(hResult))
return hResult;
if (*pbFree == VARIANT_TRUE)
return ReleaseHostage(amount, pbFree);
return S_OK;
}
Again, this is very simple; the real action is in the two local method calls TakeCashFromAccount()
and ReleaseHostage()
. However, it’s worth noting at this stage how the two halves of the transaction, represented by these local methods, have been separated out from each other; we’ll see later on how they get tied back together.
Inside TakeCashFromAccount()
, we hit ADO for the first time:
HRESULT CNegotiator::TakeCashFromAccount(int amount, VARIANT_BOOL* pbFree)
{
// Find recordset for this account
CComPtr<ADOConnection> pConnection = OpenDatabase(L"kidnap2");
if (pConnection == NULL)
return E_FAIL;
CComPtr<ADORecordset> pRecordset = FindRecordset(pConnection, L"accounts",
L"name", m_bstrAccount);
if (pRecordset == NULL)
return E_FAIL;
// Extract current data from fields
CComPtr<ADOFields> pFields;
pRecordset->get_Fields(&pFields);
long count;
pFields->get_Count(&count);
long lBalance = 0;
for (long field = 0; field < count; field++)
{
CComVariant vField(field);
CComPtr<ADOField> pField;
pFields->get_Item(vField, &pField);
CComBSTR bstrName;
pField->get_Name(&bstrName);
CComVariant vValue;
pField->get_Value(&vValue);
if (!wcscmp(bstrName, L"balance"))
{
lBalance = V_I4(&vValue);
break;
}
}
*pbFree = VARIANT_FALSE;
HRESULT hResult = S_OK;
if (amount <= lBalance)
{
// Set up variant containing change to database
CComVariant vFields(L"balance");
CComVariant vValues(lBalance - amount);
// Update record
hResult = pRecordset->Update(vFields, vValues);
if (FAILED (hResult))
Log(hResult, "Update record", pConnection);
else
*pbFree = VARIANT_TRUE;
}
pRecordset->Close();
return hResult;
}
That shouldn’t look that much different from the logic that we had in the DBManager
application. The code for the OpenDatabase()
and FindRecordset()
member functions is identical, bar the use of CComPtr
s, to that used in DBManager
, so I won’t repeat it here. They too are declared as private member variables of CNegotiator
.
All we are actually doing is opening up a connection to the SQL Server kidnap2
database, getting hold of the record in the accounts
table for the specified account, and decrementing the amount
column by the specified amount — if there are sufficient funds to enable us to do so.
If the TakeCashFromAccount()
method is successful, we invoke the ReleaseHostage()
method:
HRESULT CNegotiator::ReleaseHostage(int amount, VARIANT_BOOL* pbFree)
{
// Find recordset for this hostage
CComPtr<ADOConnection> pConnection = OpenDatabase(L"kidnap1");
if (pConnection == NULL)
return E_FAIL;
CComPtr<ADORecordset> pRecordset = FindRecordset(pConnection, L"hostages",
L"name", m_bstrHostage);
if (pRecordset == NULL)
return E_FAIL;
// Extract current data from fields
CComPtr<ADOFields> pFields;
pRecordset->get_Fields(&pFields);
long count;
pFields->get_Count(&count);
long lRansom = 0;
for (long field = 0; field < count; field++)
{
CComVariant vField(field);
CComPtr<ADOField> pField;
pFields->get_Item(vField, &pField);
CComBSTR bstrName;
pField->get_Name(&bstrName);
CComVariant vValue;
pField->get_Value(&vValue);
// If hostage is already free, don't do anything else
if (!wcscmp (bstrName, L"free"))
if ((vValue.vt == VT_BOOL) && (vValue.boolVal == VARIANT_TRUE))
{
pRecordset->Close();
return E_INVALIDARG;
}
// Extract ransom
if (!wcscmp (bstrName, L"ransom"))
{
lRansom = V_I4(&vValue);
break;
}
}
// Set up variant array containing changes to database
*pbFree = VARIANT_FALSE;
HRESULT hResult = S_OK;
if ((amount >= lRansom)) && (lRansom != 0))
{
CComVariant vFields(L"free");
CComVariant vValues(true);
hResult = pRecordset->Update(vFields, vValues);
if (FAILED (hResult))
Log(hResult, "Update record", pConnection);
else
*pbFree = VARIANT_TRUE;
}
pRecordset->Close();
return hResult;
}
This method opens up a connection to the kidnap1
database, extracts the record for the specified hostage from the hostages
table, and checks to see if the incoming funds are greater than the required ransom amount. If they are, the free
column in the record is set to 1, to indicate that the hostage has been successfully freed.
Before we test it, we need a simple Visual Basic application. We’ll call this NegotiatorClient
, and here’s the form:
In this application, we simply set up the account name, the hostage name and the attempted ransom amount, and try to deal. Here’s the complete source:
Private Sub cmdDeal_Click()
Dim neg As Negotiator
Dim result As Boolean
Set neg = New Negotiator
neg.Hostage = txtHostage.Text
neg.Account = txtAccount.Text
On Error GoTo failed
result = neg.DealWithKidnappers(txtAmount.Text)
If result = True Then
MsgBox ("Freed!")
Else
MsgBox ("Still captive")
End If
Exit Sub
failed:
If (Err.Number = 5) Then
MsgBox ("Already free")
Else
MsgBox (Err.Description)
End If
End Sub
Try setting up a few accounts and hostages using DBManager
. Then run the client, try to make a deal and see what happens. You should see that whatever the result of the attempt to release the hostage, any changes made to the account still stand. This is because outside of MTS, no attempt is made to enforce the transaction logic. In other words, the kidnappers always get to keep the ransom, even if you didn’t stump up enough. This isn’t wholly satisfactory from the point of view of the negotiator. Furthermore, if something horrendous goes wrong during the negotiation process (for example, a software failure), the system may end up in an indeterminate state. Try, for example, inserting a spurious divide by zero somewhere in the DealWithKidnappers()
method, and see what happens.