Transaction Pausing


Because of the potential problems caused by leaving a physical transaction open too long, IBO provides various built-in capabilities to minimise its use of physical transactions and also various methods to allow you to take precise control of the situation.

This topic discusses how to use the Pause/Resume processing now available with TIB_Transaction and refers to code from the tutorial project given in the TransactionPausing tutorial distributed with IBO.

The idea behind this concept is to pause a transaction (stop the physical/server transaction) when it is not actually in use, without clearing all the associated datasets, and then permit the transaction to be resumed when the user wishes to start actively using the data again.

This is really just the same as the TIB_Transaction.Refresh function, but split into two parts. Part 1 (Pause) closes the physical transaction without notifying the datasets, Part 2 (Resume) creates a new transaction and refreshes the associated datasets.

In the demonstration application you will find a timer which initiates the following function (the timer is set to 1 second for demonstration purposes, I would suggest that in a real application the timer could be set to once per minute or even longer).

procedure TTransactionMainForm.TransactionTimerTimer(Sender: TObject);
begin
  UpdateTransactionList;
  CheckTransactions;
end;

You can ignore the 'UpdateTransactionList' function, since it is there simply to maintain the list of transactions at the bottom of the form - which is really just for demonstration information purposes.


Pause

In the CheckTransactions procedure you will find the actual processing introduced to pause transactions when they have been running longer than desired. This function is greatly simplified from what you may use in a real-world application, but should give you an idea of what needs to be done. Following is a heavily annotated description of the function

procedure TTransactionMainForm.CheckTransactions;
var
  i: integer;
  iList: TList;
  iTran: TIB_Transaction;
  OAT: TDateTime;
  PauseEnabled: boolean;
begin
iList := TIB_Session.DefaultSession.Session_Transactions;
TIB_Session.DefaultSession gives access to the default (and only) session for the main process. Life gets more complicated in a multi-threaded application but that is a subject for some other demonstration. The TIB_Session component maintains a list of all transactions for the session as a TList called Session_Transactions.
TransactionStateGrid.RowCount := iList.Count + 1;
OAT := 0;
PauseEnabled := true;
for i := 0 to iList.Count - 1 do begin
    iTran := TIB_Transaction(iList.Items[i]);
    if OAT < iTran.TimeActive then
        OAT := iTran.TimeActive;
Above I ensure that OAT represents the TimeActive for the transaction that has been active the longest. TimeActive will return 0 if the transaction is not active, otherwise TimeActive is the difference between now and the time the transaction LastStarted. See also LastStopped for the time the transaction was last stopped.
    if iTran.IsPauseDisabled or iTran.IsPaused then
        PauseEnabled := false;
Above I try to ensure that I do not try to pause the transactions if any of the transactions are already paused or pause has been disabled.
    end;
if PauseEnabled and ((OAT * 86400) > 15) and
Above I am checking that I want to pause (no transaction was already paused or had pause disabled), and that I only try to pause if the OAT is older than 15 seconds. Obviously in a real application this duration would probably set to 15 or 30 minutes (or whatever may be appropriate to the particular application).
    (((SysUtils.Now - FLastActivity) * 86400) > 15) then begin
Above is a special check to see whether the user has been actively using the form in the last 15 seconds (in a real application this would probably be 5 or 10 minutes). This check is to try and avoid letting pause processing from interrupting the user in mid-keystroke. See the DoAppMessage procedure (assigned to Application.OnMessage), where FLastActivity is updated to the current time with every keyboard or mouse activity.
    for i := 0 to iList.Count - 1 do begin
        TIB_Transaction(iList.Items[i]).Pause( false );
Above is the call to Pause, with false indicating that any outstanding changes will be rolled back (not committed).
        end;
    PostMessage( Handle, WM_APP + 1, 0, 0 );
Rather than hold-up the response to the timer message I post a message back to this window which will result in the presentation of a modal form - see the ShowPauseMessage procedure.
    end;
end;


Resume

Once you have paused a transaction it is important to prevent the user from trying to interact with the data until the transaction has been resumed. Any attempt to interact with data on a paused transaction will result in a "Transaction Paused" exception, which is not too bad, but at the moment you may also receive various "Cursor Unknown" messages which can be confusing to the user. Eventually it may be possible to prevent these unexpected errors from occuring, but even so it is really only appropriate to ensure that it is visually obvious to the user that the data is not currently available. In the demonstration I do this with the following procedure...

procedure TTransactionMainForm.ShowPauseMessage( var Msg: TMessage );
var
  i: integer;
  iList: TList;
begin
Application.MessageBox( 'Transactions Paused.  Click OK to Resume', 
    'Paused', MB_OK );
Above I show a modal message - which will halt further processing until the user clicks the OK button. This prevents the user from trying to interact with the form data until the message has been cleared.
iList := TIB_Session.DefaultSession.Session_Transactions;
TransactionStateGrid.RowCount := iList.Count + 1;
for i := 0 to iList.Count - 1 do begin
    TIB_Transaction(iList.Items[i]).Resume( true );
Above we call Resume(true) which results in all the datasets being refreshed. You could call Resume(false) when you dont need the datasets restarted/refreshed (in which case they will be closed) - perhaps you are closing the form or something. It is safe to call Resume even when the transaction is not paused - since it will simply be ignored if not required.
    end;
end;


DisablePause and EnablePause

In most applications there will be times when you do not want a transaction to be paused. Perhaps you have a report or a long running process that you do not want interrupted. To support this requirement you can use the DisablePause and EnablePause methods of TIB_Transaction. In the demonstration application I simply use a checkbox to prevent pausing, but in real application you may have something like...

  try
    Transaction.DisablePause;
    RunLongPocedure;
  finally
    Transaction.EnablePause;
  end;

Note that you must have a call to EnablePause for every call to DisablePause - or Pause processing may never be re-enabled. For this reason you will generally use try/finally blocks as shown above.

If you call DisablePause when the transaction is already paused you will receive a "Transaction Paused" exception.


Using the Demonstration Application

I suggest that you experiment with the demonstration application, both with 'Pause Enabled' checkbox checked and cleared. This should help highlight the importance of finding some way of ensuring that physical transactions are regularly reset - and the hazards of relying upon '<default>' transactions. The Pause processing described above is just one way of achieving this. One alternative would be the simple use of the TIB_Transaction.Refresh( true/false ) which results in the physical transaction being stopped and restarted.


Real-World Applications

In a real-world application would may need to take into account the possibility that a user will leave a dataset in an Editing mode (edit/insert). In my own application I use two different timeout values. The shortest one will pause a transaction if it is only browsing (PostPendingCount = 0). I use a longer timeout value when a transaction is editing (PostPendingCount > 0) and if this timeout is reached I rollback the changes. (And all this is protected by an activity timer to ensure that I dont try to interrupt a user in the middle of typing their changes.)