Project

General

Profile

Transactions

Progress 4GL has a transaction processing environment integrated into the base language. While some explicit control is provided over how transactions occur, the vast majority of processing is implicit based on the operations, control flow structures, block properties and sequence of statements/blocks which is encoded by the programmer.

Transaction Definition

A transaction is a set of changes to application data, which the system either completes or discards (leaving no modifications behind). The application data protected by transactions includes the database (permanent tables, temporary tables and work tables) and undoable variables or parameters. A transaction ensures all-or-nothing processing of changes enclosed by it, which is important in managing integrity of databases. Progress discards changes to undoable variables made during a transaction if the transaction is backed out (however there are some exceptions).

All transactions are scoped to a block. The block to which a transaction is scoped defines where the transaction starts and ends. Let's take a look at this simple example:

repeat:
   create book.
   book.publisher = "Atlantis".
   update book.
end.

In this case, the transaction is scoped to the REPEAT block, which means the transaction is committed only when the flow of control passes the end of the block. In this case, since REPEAT is a loop, that commit can occur when the loop iterates OR in any circumstance where the flow of control normally exits the loop. Abnormal loop exits would cause a rollback of the changes. If the user presses F4 or CTRL-C during record update, the client abnormally ends or the server crashes at any time during cycle iteration, the current transaction is undone - this backs out the record creation and any changes to the record performed on the current iteration.

Subtransactions

Progress allows subtransactions (i.e. nested transactions). Just as transactions are scoped to specific blocks, so too are subtransactions. Subtransactions can be nested within other subtransactions to an arbitrary level (application chosen) of nesting. This nesting is a direct mapping of the transaction and subtransaction scopes to the nested block structure of the application.

If an error has occurred during subtransaction, all the work done since the beginning of the subtransaction is undone, while the work done in the parent (sub)transaction(s) can be left intact or be undone depending on the application logic. The commit of a subtransaction will not commit the buffer changes to the backing database - this will only update the parent (sub)transaction block so that, in case of a roll back, the changes done in the nested subtransaction(s) will also be undone. Changes done in a subtransaction will be committed to the backing database (and the variables will save their state) only when the root transaction block commits its changes.

At physical database level in FWD, there will always be only one transaction opened, for each full transaction block. When a subtransaction starts, the backing database does nothing; the FWD persistence layer is responsible of keeping track of all changes and act accordingly during commit or rollback. But, while a subtransaction is active, records can be flushed to the physical database, i.e. when a new record is created and the buffer switches records. In this case, if the subtransaction rolls back, the new record will be automatically removed from the backing database; else, if the subtransaction commits, the new record remains flushed to the backing database transaction and all undoable information is added to the previous (sub)transaction block.

While in a transaction, database modifications by one user cannot be seen by other database users. This is the principle of transaction isolation. Progress 4GL does support this concept, but it does so in a less than perfect manner. Because the Progress database is more of a sophisticated index engine than a true relational database, the behavior of the Progress database is highly dependent upon index processing. By its very nature, edits and additions to indexes are (and must be) made visible to other users even before the transaction is committed. Although it seems strange that any application would be dependent upon this unappealing behavior, it turns out that many applications are in fact coded to take advantage of this flaw. For this reason, the Java runtime duplicates this feature set.

Only one transaction can ever be active per user context. Nested blocks which have the transaction property, become nested subtransactions.

Example:

repeat:
   create book.
   book.book-title = "Book #1".

   inner:
   repeat:
      create book.
      book.book-title = "Book #2".
      undo inner, leave inner.
   end.

   leave.
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Init) () -> 
{
   RecordBuffer.openScope(book);
}, 
(Body) () -> 
{
   book.create();
   book.setBookTitle(new character("Book #1"));

   repeat(TransactionType.FULL, "inner", new Block((Body) () -> 
   {
      book.create();
      book.setBookTitle(new character("Book #2"));
      undoLeave("inner");
   }));

   leave("loopLabel0");
}));

Details:

In this example the transaction is scoped to the outer REPEAT block, while the subtransaction - to the inner REPEAT block. Although the converted code has the inner block marked as a full transaction, the runtime will automatically start a subtransaction, as a transaction is already active (opened by the outer block). The coded UNDO statement rolls back the subtransaction and the book with "Book #2" title is not persisted to the database, while the parent transaction remains intact and the book with "Book #1" title is committed to the database when the outer REPEAT ends.

Reading Converted Code

The converted code explicitly defines the transaction level where the original 4GL code most often leaves the transaction level as an implicit property of a block.

To emulate the 4GL's transactions mechanism, FWD uses three transaction levels. These values are used to specify transaction behavior of a block:

Transaction Level Corresponding FWD Constant Meaning
Transaction TransactionType.FULL Block should start a transaction if there is no active transaction or a subtransaction if there is an active transaction.
Subtransaction TransactionType.SUB Block should start a subtransaction if (and only if) there is an active transaction.
No transaction TransactionType.NONE Block shouldn't start a transaction or subtransaction. This transaction level doesn't appear in the converted code and is used internally by FWD in order to define default behavior of the specific block types.

The following summarizes the default transaction properties for blocks and the level to which the block can be promoted based on contained logic:

Block Type Default Transaction Level Maximum Transaction Level
external procedure SUB FULL
internal procedure SUB FULL
function SUB FULL
trigger SUB FULL
DO (without TRANSACTION keyword and without @ON { ERROR ENDKEY }@) NONE NONE
DO with @ON { ERROR ENDKEY }@ (without TRANSACTION keyword) SUB FULL
DO TRANSACTION FULL FULL
REPEAT (without TRANSACTION keyword) SUB FULL
REPEAT TRANSACTION FULL FULL
FOR (without TRANSACTION keyword) SUB FULL
FOR TRANSACTION FULL FULL
EDITING SUB SUB

The default transaction level may be overridden during the conversion process in the cases described further in this chapter. In these cases the target transaction level will be explicitly specified in the converted code for the target block. This is done using the lvl parameter available as the first argument for all BlockManager APIs to which 4GL blocks are converted (details about the lvl parameter can be found in the Blocks chapter of this book). When the BlockManager APIs are used without passing an explicit transaction level constant, the above noted defaults will apply.

Example:

/* Block of the "subtransaction" level. */
repeat:
   ...
end.

/* Block of the "transaction" level. */
repeat transaction:
   ...
end.

Converted code:

/* Block of the "subtransaction" level. */
repeat("loopLabel0", new Block((Body) () ->
{
   ...
}));

/* Block of the "transaction" level. */
repeat(TransactionType.FULL, "loopLabel1", new Block((Body) () ->
{
   ...
}));

Details:

The default transaction level of the second REPEAT block ("subtransaction") was promoted to the "transaction" level because of the TRANSACTION keyword:

Understanding Where Transactions and Subtransactions Start

When Transactions Start

A transaction begins at the start of any block while the following two conditions are true:

  1. No transaction is currently active.
  2. The block has any of these properties:
    • The explicit TRANSACTION keyword is present. This can be applied to FOR, REPEAT and DO blocks. OR
    • It is one of the following block types: DO ON { ERROR | ENDKEY }, FOR, REPEAT, external procedure, internal procedure, user-defined function AND any of the following conditions is true:
  3. The block directly contains statements that modify records of a permanent database (see information about direct use in the Code Conversion Notes sub-section): modification of database fields using the assignment operator, ASSIGN, BUFFER-COPY, CREATE, DELETE, INSERT (not supported by FWD), RAW-TRANSFER (not supported by FWD), SET, UPDATE.
  4. The block preselects (DO PRESELECT, REPEAT PRESELECT) or iterates (FOR) records of a permanent database explicitly using EXCLUSIVE-LOCK (at least one table). As a special case, in the DO PRESELECT case, there does not need to be an ON phrase for this to cause transaction support to be present.
  5. The block directly contains FIND or GET statements that read records of a permanent database explicitly using EXCLUSIVE-LOCK (see information about direct use in the Code Conversion Notes sub-section).
  6. The block directly contains GET statements that read records of a permanent database which implicitly uses EXCLUSIVE-LOCK because no locking is specified on the GET but EXCLUSIVE-LOCK is specified in the corresponding OPEN QUERY statement.

Example 1:

do transaction:
   ...
end.

Converted code:

doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () ->
{
   ...
}));

Details:

As the block has an explicit TRANSACTION clause, the converted code will mark the block so that it starts a full transaction. This can also be applied to FOR and REPEAT blocks.

Example 2:

repeat:
   create book.
   ...
end.

repeat:
   find first book.
   book.publisher = "Atlantis".
   ...
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Body) () ->
{
   RecordBuffer.create(book);
   ...
}));

repeat(TransactionType.FULL, "loopLabel1", new Block((Body) () ->
{
   new FindQuery(book, (String) null, null, "book.bookId asc").first();
   book.setPublisher(new character("Atlantis"));
}));

Details:

As the each of the REPEAT blocks directly contains statements which change database records, the two blocks will be automatically marked as a full transaction by the conversion rules. Although in 4GL the transaction level is determined at compile time, the FWD approach calculates the block's transaction level at conversion time and then encodes any deviation from the default (for that block type) explicitly in the resulting Java source code.

Example 3:

do preselect each book exclusive-lock
      on error undo, leave:
   ...
end.

Converted code:

PreselectQuery query0 = new PreselectQuery();

OnPhrase[] onPhrase0 = new OnPhrase[]
{
   new OnPhrase(Condition.ERROR, Action.LEAVE, "blockLabel0")
};

doBlock(TransactionType.FULL, "blockLabel0", onPhrase0, new Block((Init) () -> 
{
   RecordBuffer.openScope(book);
   query0.initialize(book, ((String) null), null, "book.bookId asc", LockType.EXCLUSIVE);
}, 
(Body) () -> 
{
   ...
}));

Details:

Whenever a block obtains an exclusive lock for the iterated records, the transaction level of such blocks is automatically upgraded to full transaction. In this case, both the EXCLUSIVE-LOCK and the ON ERROR clause trigger the transaction upgrade, for the DO block.

Example 4:

for each person no-lock, each book exclusive-lock:
  ...
end.

Converted code:

CompoundQuery query0 = new CompoundQuery();

forEach(TransactionType.FULL, "loopLabel0", new Block((Init) () -> 
{
   RecordBuffer.openScope(person, book);
   query0.initialize(false, false);
   query0.addComponent(new AdaptiveQuery().initialize(person, ((String) null), null, "person.siteId asc, person.empNum asc", LockType.NONE));
   query0.addComponent(new AdaptiveQuery().initialize(book, ((String) null), null, "book.bookId asc", LockType.EXCLUSIVE));
}, 
(Body) () -> 
{
   query0.iterate();
   ...
}));

Details:

When a FOR block iterates the records from multiple tables, it is enough to obtain an EXCLUSIVE-LOCK for records in only one table, to trigger the transaction upgrade to full transaction for this block.

Example 5:

repeat:
   find first book exclusive-lock.
   ...
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Init) () -> 
{
   RecordBuffer.openScope(book);
}, 
(Body) () -> 
{
   new FindQuery(book, (String) null, null, "book.bookId asc", LockType.EXCLUSIVE).first();
   ...
}));

Details:

Obtaining an EXCLUSIVE-LOCK for a record will upgrade the transaction level for the enclosing block to full transaction. Note that a full transaction will be started by the runtime only if one is not already active.

When Subtransactions Start

A subtransaction (a nested transaction) begins at the start of any block while the following two conditions are true:

  1. A transaction is currently active.
  2. The block is of the following types: DO ON { ERROR | ENDKEY }, DO TRANSACTION, FOR, REPEAT, internal procedure, external procedure, user-defined function, trigger or EDITING block.

Example 6:

repeat transaction:
   repeat:
      ...
   end.
   ...
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Body) () -> 
{
   repeat("loopLabel1", new Block((Body) () -> 
   {
      ...
   }));

   ...
}));

Details:

The runtime will automatically start a subtransaction when the inner REPEAT block is entered, as a transaction is already active (opened by the outer REPEAT block).

Example 7:

repeat transaction:
   do transaction:
      ...
   end.
   ...
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Body) () -> 
{
   doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () -> 
   {
      ...
   }));

   ...
}));

Details:

If two nested blocks are both marked with a full transaction level, then the runtime will always start a subtransaction for the inner block. The outer block will start a full transaction only if one is not already active.

Example 8:

repeat transaction:
   do on error undo, leave:
      ...
   end.
   ...
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Body) () -> 
{
   OnPhrase[] onPhrase0 = new OnPhrase[]
   {
      new OnPhrase(Condition.ERROR, Action.LEAVE, "blockLabel0")
   };

   doBlock(TransactionType.SUB, "blockLabel0", onPhrase0, new Block((Body) () -> 
   {
      ...
   }));

   ...
}));

Details:

The ON ERROR clause for the DO block will result in upgrading the transaction level for the DO block from no transaction to subtransaction and not full transaction, as there is an outer block (REPEAT in this case) which starts a full transaction.

Code Conversion Notes

In order for a statement to be considered as "directly" used inside a block (and affect transaction processing as described in the When Transactions Start section), the language statement must not be contained in a nested block unless that nested block is of a type that cannot start a subtransaction, i.e. is a DO block without the ON { ERROR | ENDKEY } phrase or TRANSACTION specifications.

The assignment operator can only be used without a runtime error on database records that have SHARE-LOCK or EXCLUSIVE-LOCK. If the record is opened with NO-LOCK, then an ERROR will be raised at runtime. The NO-ERROR keyword can be used on an assignment to suppress the error generation but this won't make the update successful. This means that at runtime, one cannot successfully assign to a database record that is NO-LOCK but at compilation time (or conversion time for FWD) this (pointless) use of the assignment operator will cause the contained block to be a transaction.

Usage of a BROWSE widget is a "black-box" language statement which reads from/writes to the database on behalf of the programmer. Any BROWSE that uses the ENABLE keyword is not read-only (in Progress terms it is "updatable") and it will update/modify the database. All transaction support for updatable BROWSE is hidden inside the FWD runtime. For this reason, the transaction property is not attached to the containing block.

BROWSE widgets that are read-only (no use of ENABLE) can still generate reads with EXCLUSIVE-LOCK (this can be set in the DEFINE BROWSE statement). If the DEFINE BROWSE does not explicitly use the EXCLUSIVE-LOCK keyword, then even if the query was opened EXCLUSIVE-LOCK this is overridden to NO-LOCK (by default) or SHARE-LOCK (if specified). Only a browse defined explicitly with EXCLUSIVE-LOCK will open the query as EXCLUSIVE-LOCK. However, since such an approach is only allowed inside a block which already has transaction support (it must be placed inside a DO TRANSACTION or REPEAT TRANSACTION), the containing block will already have transaction support. For this reason, browse usage of EXCLUSIVE-LOCK is ignored during conversion.

The REPOSITION statement fetches a record when the query is associated with a BROWSE widget. Since the transaction processing associated with a browse in hidden inside the browse's "black-box", so also is any effect of the hidden/implicit record reading that occurs with this statement. Although REPOSITION can have an implicit record read associated, there is no case where it causes the containing block to become a transaction.

Reading (with exclusive-lock) or modification of records of a temporary database does not affect transaction / subtransaction scopes (while reading with exclusive lock or modification of records of a permanent database does affect them).

The Progress 4GL documentation is unclear in regards to which ON phrases cause a DO block to become transaction-eligible. In some places, only ON ERROR is mentioned. In other places, the list includes ON ERROR and ON ENDKEY. The current FWD implementation actually honors subtransaction support for all ON phrases at this time. This is not documented above, but it has successfully worked in real applications. However, this may be a fluke that is caused by the fact that it is rare for an ON phrase to use STOP or QUIT when the DO block does not also include ERROR or ENDKEY. This needs to be tested to confirm if this deviation is correct or not.

FOR block transaction anomaly: there is a case where reading records of a permanent database using a FOR block with exclusive lock will not create a transaction. This behavior is documented in the Progress Knowledge Base article KB13974. It only happens if the following conditions are both true:

  • The buffer scope for the buffer specified in the FOR record phrase has been expanded beyond the FOR block (there is a free reference outside that block). If the FOR block iterates several tables using exclusive lock, then all of the buffers which are iterated using exclusive lock must be referenced outside the block in order to trigger this anomaly. If even one buffer referenced (in the list of tables being iterated) is scoped to the FOR block, then the use of EXCLUSIVE-LOCK will still be treated as a transaction.
  • There are no data editing or other statements directly contained in the FOR block that will cause the start of a transaction.

EDITING blocks are currently hard coded to be a subtransaction only (in FWD). There are cases that have not been tested which could possibly lead to needing full transaction support. See the Transactions in EDITING Blocks section below for more details.

Example 9:

repeat:
   create book.
   ...
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Init) () -> 
{
   RecordBuffer.openScope(book);
}, 
(Body) () -> 
{
   book.create();
}));

Details:

As this is a "direct" use of the CREATE statement inside a REPEAT block, the REPEAT block will start a transaction.

Example 10:

repeat:
   do i = 1 to 3:
      create book.
      ...
   end.
   ...
end.

Converted code:

repeat(TransactionType.FULL, "loopLabel0", new Block((Init) () -> 
{
   RecordBuffer.openScope(book);
}, 
(Body) () -> 
{
   loopLabel1:
   for (i.assign(1); _isLessThanOrEqual(i, 3); i.increment())
   {
      book.create();
      ...
   }
   ...
}));

Details:

As the CREATE statement is nested in a DO block without transaction properties, this is considered to be a direct use inside the outer REPEAT block - consequently, the outer REPEAT block will start a transaction.

Example 11:

define temp-table tt
   field f1 as integer.

repeat:
   create tt.
   ...
end.

Converted code:

repeat("loopLabel0", new Block((Body) () -> 
{
   tt.create();
}));

Details:

Creating temporary records in a block does not affect the transaction property of that block, as transactions can be automatically started only when a record for a table in a permanent database is changed or created.

Example 12:

find first book.         /* Free reference for book */
find first address.      /* Free reference for address */

for each book exclusive-lock,   /* Should have an outside free reference to trigger anomaly*/
  each pers-addr,               /* Iterated using shared lock, ignored */
  first address exclusive-lock: /* Should have an outside free reference to trigger anomaly*/
  ...
end.

Converted code:

new FindQuery(book, (String) null, null, "book.bookId asc").first();
new FindQuery(address, (String) null, null, "address.addrId asc").first();

CompoundQuery query0 = new CompoundQuery();

forEach("loopLabel0", new Block((Init) () -> 
{
   RecordBuffer.openScope(persAddr);
   query0.initialize(false, false);
   query0.addComponent(new AdaptiveQuery().initialize(book, ((String) null), null, "book.bookId asc", LockType.EXCLUSIVE));
   query0.addComponent(new AdaptiveQuery().initialize(persAddr, ((String) null), null, "persAddr.siteId asc, persAddr.empNum asc"));
   query0.addComponent(new RandomAccessQuery().initialize(address, ((String) null), null, "address.addrId asc", LockType.EXCLUSIVE), QueryConstants.FIRST);
}, 
(Body) () -> 
{
   query0.iterate();
   ...
}));

Details:

This example shows the FOR block anomaly. As an EXCLUSIVE-LOCK is acquired for the book and address records and for both these buffers there is a free reference outside the FOR block, then the block will not start a transaction, unless is explicitly specified.

Conversion and Runtime Transaction Handling

In order to duplicate the 4GL transaction management, FWD uses two steps of processing: at conversion time and at runtime. Although the transaction property of a block can definitely be determined by the conversion rules (in cases when i.e. a buffer field updates are directly nested in a block), in other cases, depending on the code path, the conversion rules are not enough to decide whether the runtime needs to start a transaction or a subtransaction. To handle these cases, the runtime will read each block's transaction level emitted during the conversion process and, depending on the actual transaction state, will act accordingly.

Conversion Processing

The conversion step detects static statements and statement options that affect transaction processing. The conversion process explicitly specifies a transaction level if it does not match the default one (TransactionType.SUB for FOR and REPEAT blocks, internal and external procedures, and TransactionType.NONE for DO blocks). Promotion occurs to:

  • TransactionType.FULL if it was detected that a block should start a transaction (if there is no active transaction) or subtransaction (if there is an active transaction).
  • TransactionType.SUB if it was detected that a DO block should start a subtransaction.

This promotion is performed when at least one of the following conditions were detected:

  • The explicit TRANSACTION clause was used in the 4GL code.
  • The block iterates records of a permanent database using exclusive lock.
  • The block contains statements that modify records of a permanent database or reads them using exclusive lock.

More precise rules are described in the Understanding Where Transactions and Subtransactions Start section.

Runtime Processing

The rules defining a subtransaction are a super-set of the rules defining a transaction. Since a given block can be invoked with a call stack that is different depending on the path taken through the program, when a particular code block is invoked it is possible that sometimes a transaction will be active and other times not. To duplicate this behavior, the determination of whether a block opens a transaction or a subtransaction must be handled at runtime.

The same block will process differently depending on runtime conditions. The following cases are possible:

  1. The start of the block will start either a transaction or a subtransaction, depending only on whether a transaction is already active. This occurs with use of TransactionType.FULL.
  2. The start of the block will start a subtransaction if a transaction is already active, but otherwise a transaction will not be started. This occurs with use of TransactionType.SUB.
  3. The start of the block has no transaction or subtransaction implications (any DO block without the ON or TRANSACTION specifications). This is the equivalent of TransactionType.NONE, although in practice there is no need for this to be explicitly specified in the converted code (and thus it will not appear in converted code). The nature of the block itself (a converted DO block) and the lack of an explicit override is enough to tell the runtime than the TransactionType.NONE is in force.

Example 1:

B0:
do transaction:
  B1:
  repeat transaction:  /* Starts a subtransaction */
     message "message".
     leave.
  end.
end.

B2:
repeat transaction:    /* Starts a transaction */
   message "message".
   leave.
end.

Converted code:

doBlock(TransactionType.FULL, "b0", new Block((Body) () -> 
{
   repeat(TransactionType.FULL, "b1", new Block((Body) () -> /* Starts a subtransaction */
   {
      message("message");
      leave("b1");
   }));
}));

repeat(TransactionType.FULL, "b2", new Block((Body) () -> /* Starts a transaction */
{
   message("message");
   leave("b2");
}));

Details:

As the transaction is started at block B0, block B1 will start a subtransaction, regardless the fact that it has specified the TRANSACTION option (and thus TransactionType.FULL in the converted code). As no transaction is active and block B2 has also the TRANSACTION option, it will start a transaction.

Example 2:

B0:
do transaction:
  B1:
  repeat:                   /* Starts the subtransaction */
     message "message".
     leave.
  end.
end.

B2:
repeat:                     /* Does not start a transaction */
   message "message".
   leave.
end.

Converted code:

doBlock(TransactionType.FULL, "b0", new Block((Body) () -> 
{
   repeat("b1", new Block((Body) () -> /* Starts the subtransaction */
   {
      message("message");
      leave("b1");
   }));
}));

repeat("b2", new Block((Body) () -> /* Does not start a transaction */
{
   message("message");
   leave("b2");
}));

Details:

A subtransaction is started by block B1 as block B0 has started a full transaction. Block B2 will not start a subtransaction, as no transaction is active in its scope. By default, the runtime knows that a TransactionType.SUB is in force for a REPEAT block. Any explicit encoding of a different transaction level will override this.

Example 3:

B0:
do transaction:
  B1:
  do i = 1 to 3:            /* Does not start a subtransaction */
     message "message".
     leave.
  end.
end.

B2:
do i = 1 to 3:              /* Does not start a transaction */
   message "message".
   leave.
end.

Converted code:

doBlock(TransactionType.FULL, "b0", new Block((Body) () -> 
{
   b1:
   for (i.assign(1); _isLessThanOrEqual(i, 3); i.increment()) /* Does not start a subtransaction */
   {
      message("message");
      break b1;
   }
}));

b2:
for (i.assign(1); _isLessThanOrEqual(i, 3); i.increment()) /* Does not start a transaction */
{
   message("message");
   break b2;
}

Details:

As block B1 has no transaction support, it will not start a subtransaction, even if a transaction is active (started by block B0). This applies for block B2 too - as it doesn't have transaction support, it can not start a subtransaction or transaction. By default, the runtime knows that a TransactionType.NONE is in force for a DO block. Any explicit encoding of a different transaction level will override this.

Transactions with Procedures and Functions

Transactions for external procedures, internal procedures and user-defined functions are treated in the same way as regular blocks. A block of this type will always start a subtransaction by default. The contained logic may cause the procedure or function to open a transaction as well. If a transaction was open in the calling code, it remains active in the called code. The precise rules are listed in the Understanding Where Transactions and Subtransactions Start section. A subtle yet important point is that when considering the contained logic, the only logic that matters is code that is not nested inside an inner block which has block properties. Simple DO blocks (which by definition do not have any of the traditional 4GL behavior or properties) don't count in this calculation. Any code directly contained inside a simple DO block is considered to be directly contained inside the nearest enclosing non-simple DO block.

Example 1:

parent-proc.p:

do transaction:   /* Start of the transaction */
   ...
   run proc.p
end.              /* End of the transaction */

proc.p (called by parent-proc.p):

[proc.p]
/* External procedure proc.p starts subtransaction #1 */
...
find first book exclusive-lock.
repeat:           /* Start of subtransaction #2 */
   ...
end.              /* End of subtransaction #2 */

/* Subtransaction #1 ends with proc.p */

Converted code:

ParentProc.java:

doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () ->  /* Start of the transaction */
{
   ...
   Proc proc0 = new Proc();
   proc0.execute();
}));                                                      /* End of the transaction */

Proc.java (called by ParentProc.java):

public void execute()
{
   externalProcedure(TransactionType.FULL, /* External procedure proc.p starts subtransaction #1 */
   new Block((Body) () ->
   {
      new FindQuery(book, (String) null, null, "book.bookId asc", LockType.EXCLUSIVE).first();
      repeat("loopLabel0", new Block((Body) () ->  /* Start of subtransaction #2 */
         {
            ...
         }));                               /* End of subtransaction #2 */
   }));                                     /* Subtransaction #1 ends with proc.p */
}

Details:

The parent-proc.p procedure has no directly contained logic which would make it a transaction. It result in a TransactionType.SUB. The transaction is opened by the nested DO block which has the TRANSACTION keyword.

The proc.p procedure has a direct use of reading with exclusive-lock. This overrides the default and makes the external procedure itself open with TransactionType.FULL.

Just as with inner blocks, if an external procedure is invoked while the caller has an active transaction, the called procedure will always start a sub-transaction, even if it directly uses a statement which upgrades the block's default transaction level to full transaction during the conversion process. That decision is made at runtime. The source code above has comments which mark when each of the (sub)transaction(s) start.

Example 2:

parent-proc2.p:

...
run proc2.p

proc2.p (called by parent-proc2.p):

/* Procedure proc2.p starts the transaction because
   1. there is no active transaction
   2. the procedure directly contains the statement that reads a record of
      a permanent database using exclusive lock   */

find first book exclusive-lock.

repeat:           /* Start of subtransaction */
   ...
end.              /* End of subtransaction */

/* The transaction ends with proc2.p */

Converted code:

ParentProc2.java:

...
Proc2 proc20 = new Proc2();
proc20.execute();

Proc2.p (called by ParentProc2.java):

public class Proc2
{
   ...

   public void execute()
   {
      externalProcedure(TransactionType.FULL, new Block((Body) () ->  /* Start of transaction */
      {
         new FindQuery(book, (String) null, null, "book.bookId asc", LockType.EXCLUSIVE).first();
         repeat("loopLabel0", new Block((Body) () ->                 /* Start of subtransaction */
         {
            ...
         }));                                              /* End of subtransaction */
      }));                                                 /* End of transaction */
   }
}

Details:

In cases when there a transaction is not already active, the runtime will start a transaction if the invoked external procedure was marked to open a full transaction by the conversion rules. Here, the called procedure starts a transaction because proc2.p directly contains a statement that reads a record of a permanent database using exclusive lock (the conversion rules will set its transaction level to TransactionType.FULL).

Example 3:

parent-proc3.p:

...
run proc3.p

proc3.p (called by parent-proc3.p):

/* The procedure proc3.p does NOT start a transaction or a subtransaction */

repeat:           /* Start of the transaction */
   create book.
   ...
end.              /* End of the transaction */

Converted code:

ParentProc3.java:

...
Proc3 proc30 = new Proc3();
proc30.execute();

Proc3.java (called by ParentProc3.java):

public class Proc3
{
   ...
   public void execute()
   {
      externalProcedure(new Block((Body) () ->
      {
         repeat("loopLabel0", new Block( /* Start of transaction */
         {
            (Init) () ->
            {
               RecordBuffer.openScope(book);
            }, 
            (Body) () ->
            {
               RecordBuffer.create(book);
               ...
            }
         }));                              /* End of transaction */
      });
   }
}

Details:

It is important to note that even though a procedure may not open a transaction directly, a nested block may still open a transaction. For example, a nested REPEAT that updates the database will have a transaction scoped to the REPEAT block rather than to the containing procedure.

Example 4:

/* This will be a full transaction. */
function full-trans returns int ():
   find first book exclusive-lock.
   return book-id.
end.

/* This will be a sub-transaction. */
function sub-trans returns int ():
   return 0.
end.

Converted code:

/**
 * This will be a full transaction.
 */
@LegacySignature(type = Type.FUNCTION, name = "full-trans", returns = "INTEGER")
public integer fullTrans()
{
   return function(this, "full-trans", integer.class, TransactionType.FULL, new Block((Body) () -> 
   {
      new FindQuery(book, (String) null, null, "book.bookId asc", LockType.EXCLUSIVE).first();
      returnNormal(book.getBookId());
   }));
}

/**
 * This will be a sub-transaction.
 */
@LegacySignature(type = Type.FUNCTION, name = "sub-trans", returns = "INTEGER")
public integer subTrans()
{
   return function(this, "sub-trans", integer.class, new Block((Body) () -> 
   {
      returnNormal(0);
   }));
}

Details:

User-defined functions work just like internal procedures. If they have directly contained logic which would make it a transaction, the result will be a TransactionType.SUB. That transaction level will be used by default in the subTrans() method. When they directly contain logic which does cause a full transaction (such as the direct use of reading with exclusive-lock in the fullTrans() method), then this overrides the default and makes the function open with TransactionType.FULL.

Transactions with Triggers

The transaction mechanism for triggers is the same as for regular blocks:

  • A trigger can start a transaction if there is no active transaction and it directly contains statements that modify records of a permanent database or reads them using exclusive lock (the full list of statements that can perform it can be found in the Understanding Where Transactions and Subtransactions Start section). If there is an active transaction started in the main code, a subtransaction is started, even if the conversion rules have marked the trigger as a full transaction.
  • If a transaction was started by a trigger, it cannot extend beyond the end of the trigger, as well as a transaction started outside the trigger cannot be committed inside it. A trigger block is essentially a nested block that is dynamically called in the runtime code at an arbitrary location based on an event.
  • If the trigger does not directly contain statements that modify records of a permanent database or read them using exclusive lock then the following behavior takes place: if there is an active transaction started in the main code then the trigger starts a subtransaction, otherwise it does not start a transaction or subtransaction. In other words, trigger blocks are TransactionType.SUB by default.
  • If a transaction hasn't been started in the main code or by a trigger, blocks inside the trigger can still start a transaction.

This section will not cover how the trigger gets converted. For these kind of details, please see the Triggers section in the Blocks chapter of this book.

Example 1:

on f anywhere do:                  /* start of the transaction */
  find first book exclusive-lock.
end.                               /* end of the transaction */

wait-for close of current-window.

Converted code:

public void execute()
{
   externalProcedure(Test.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(book);

      registerTrigger(new EventList("f", true), 
                      Test.this, 
                      true,  // "trans" parameter is true because the trigger should start a transaction
                      new Trigger((Body) () -> 
      {
         new FindQuery(book, (String) null, null, "book.bookId asc", LockType.EXCLUSIVE).first();
      }));

      LogicalTerminal.waitFor(null, new EventList("close", currentWindow()));
   }));
}

Details:

When a trigger may start a full transaction, the converted code will have the trans parameter of the LogicalTerminal.registerTrigger function set to true. This corresponds to FWD transaction level TransactionType.FULL.

Example 2:

on f anywhere do:                     /* start of the subtransaction */
  find first book.
end.                                  /* end of the subtransaction */

do transaction:                       /* start of the transaction */
   wait-for close of current-window.
end.                                  /* end of the transaction */

Converted code:

public void execute()
{
   externalProcedure(Test.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(book);

      registerTrigger(new EventList("f", true), Test.this, new Trigger((Body) () ->  /* "trans" parameter is set to its default value - false */
      {
         new FindQuery(book, (String) null, null, "book.bookId asc").first();
      }));

      doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () -> 
      {
         LogicalTerminal.waitFor(null, new EventList("close", currentWindow()));
      }));
   }));
}

Details:

When a trigger may start a subtransaction, the converted code will have the trans parameter of the LogicalTerminal.registerTrigger function set to false. This corresponds to FWD transaction level TransactionType.SUB.

Example 3:

on f anywhere do:
  do transaction:                    /* start of the transaction */
     ...
  end.                               /* end of the transaction */
end.

wait-for close of current-window.

Converted code:

public void execute()
{
   externalProcedure(Test.this, new Block((Body) () -> 
   {
      registerTrigger(new EventList("f", true), Test.this, new Trigger((Body) () ->  /* "trans" parameter is false because the trigger should not start a transaction */
      {
         doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () -> 
         {
            ...
         }));
      }));

      LogicalTerminal.waitFor(null, new EventList("close", currentWindow()));
   }));
}

Details:

If a transaction hasn't been started in the main code or by a trigger, blocks inside the trigger can still contain a transaction.

Transactions with EDITING Blocks

EDITING blocks are a special kind of looping block that is nested inside of a PROMPT-FOR, SET or UPDATE statement. Actually, the use in a PROMPT-FOR is undocumented in the Progress references but has been found to really work in practice.

These blocks are defined as part of one of those data editing statements. When the associated statement executes, the statement's list of fields will be enabled for interaction and the editing block will execute. Inside the editing block there must be a READKEY and usually some form of APPLY statement. The READKEY will block until the user presses a key, then the READKEY will return. The key press can be read using LASTKEY. The programmer can make decisions, access program state and do any other kind of logic. Then when the end of the editing block is reached, the block loops around and processes again, blocking on the READKEY. When a condition is raised or a GO event occurs, the editing block loop will exit and the data editing statement (e.g. UPDATE) will finish.

Since editing blocks are always part of a data editing statement like SET or UPDATE, if the data editing statement references a permanent database field, then the containing block will already be a TransactionType.FULL. Although Progress doesn't document the PROMPT-FOR case, even so, the PROMPT-FOR is usually accompanied by an ASSIGN statement which (if it references a permanent database field) would cause the containing block to be a TransactionType.FULL.

FWD encodes all editing blocks as TransactionType.SUB. This is hard coded in the BlockManager worker methods that process editing blocks, so it is not exposed to the programmer.

It is possible that Progress would actually allow an editing block to become TransactionType.FULL, but this case has not been tested at this time. An example case would be if a PROMPT-FOR that referenced permanent database fields was not accompanied by an ASSIGN statement and instead the editing block itself contained some kind of direct modification of the database fields. Another example would be where the data editing statement did not reference any permanent database field but the editing block directly contained statements that modified the database. These cases have not been seen in 4GL code and they have not yet been tested to determine if the unconditional use of TransactionType.SUB by FWD is flawed.

This section will not cover how editing blocks convert. For more details, please see the Blocks chapter and the Editing chapter of this book.

Undo Processing

Traditional relational databases provide transaction management including a mechanism to commit or rollback database changes. The Progress 4GL language design is tightly integrated into the Progress database commit/rollback, such that the basic language features naturally implement database transactions. This processing is the equivalent of traditional database transaction management (with one notable flaw - see "dirty sharing" in the Advanced Transaction Features section below). Interestingly, the Progress 4GL language also extends the concept of commit/rollback of changes to normal program variables. In Progress terms, this can be generally described as the language's support for UNDO.

The Java runtime fully supports all of these features. The runtime is responsible for tracking variable and record state and for managing undo processing. This includes taking a snapshot of the state of variables and records on the first entry to any block (with properties) and taking an updated snapshot on every iteration of those blocks. When a transaction is active AND a condition is raised for which an undo must occur, the runtime uses the collected state to bring all undoable variables and records to the correct previous state. This is equivalent to a rollback in traditional database terms. Conversely, when the flow of control normally exits a block that opened the active transaction, then those undoable variable and record changes are all committed.

Depending on the application logic, the memory footprint used by the runtime to gather all the undo-related data may increase when using large transaction scopes (with deeply nested subtransactions). Even if data changes are done in subtransactions, committing a subtransaction will not commit the changes; instead, information about each change will be collected at the transaction block and will be discarded or accepted only when the transaction block rolls back or commits the current iteration. Consequently, one should be aware of the amount of data changed in a transaction block, when using subtransaction blocks.

Undo (rollback) action can take place in the following cases:

  • As a reaction to an ERROR, ENDKEY, STOP or QUIT condition. Although each block type may have some implicit behavior, the corresponding control flow action can be explicitly specified for inner blocks using the ON phrase. Regardless of the control flow action, each of these events cause an UNDO to occur. These conditions can occur if:
    • The user presses a specific key which generates an event which is associated with one of the above conditions. For example, by default F4 (the END-ERROR key) will generate an ENDKEY or ERROR condition and CTRL-C will generate a STOP condition. In the example below, if user presses F4 then the record creation is undone:
do transaction:
  create book.
  pause message "press F4 to undo".
end.
  • The runtime raises a condition, typically because of encountering some kind of failure. Most often, an ERROR is raised, but the other conditions can also be generated (e.g. ENDKEY when a query goes "off end"). In the example below record creation is undone at the ASSIGN statement if book-id is an unique field:
find first book.
id = book.book-id.

do on error undo, leave:
  create book.
  assign book.book-id = id.
  pause message "book created".  /* unreachable if book-id is an unique field */
end.
  • A STOP statement has been executed (the STOP condition is raised in this case). In the example below, the record creation is undone:
do transaction:
  create book.
  stop.
end.
  • A QUIT statement has been executed (the QUIT condition is raised in this case) AND it has been done inside a containing block that has an explicitly specified ON QUIT UNDO phrase (the UNDO keyword here is necessary). The default behavior for QUIT is to commit transactions. See below for more details. In this example, the record creation is undone:
do transaction ON QUIT UNDO, LEAVE:
  create book.
  quit.
end.
  • If an explicit UNDO statement has been executed. In the example below, the record creation is undone:
do transaction:
  create book.
  undo, leave.
end.

The cause of an undo can always be traced back to the specific line of code that was executing when the condition was raised or when the UNDO statement was executed.

When an undo is caused, two things occur. First, the undo itself will occur. This undo (or rollback) operation will be targeted at one of the blocks that encloses the line of code that caused the undo. This can be the immediately enclosing block OR it can be a more outer enclosing block. Second, after the rollback occurs, the program's control flow is changed. This control flow action will either target the same block that was rolled back OR it can target a block that encloses the rolled back block. The target block for the control flow change can never be enclosed by the block that was undone (rolled back). It will always be the same or a more enclosing block.

The control flow action that is taken depends on the action specified by that target block's explicit ON phrases (or by implicit block properties) or by the action specified in an explicit UNDO statement. The action can be LEAVE, NEXT, RETRY or RETURN.

For full details on how the target blocks are determined for the UNDO operation and for the control flow change action, please see the section entitled Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY section of the Blocks chapter. Of course, if the control flow action is RETURN, then that causes an exit from the nearest enclosing top-level block. In that case, the target is always easy to identify.

The LEAVE, RETURN and NEXT statements do not raise conditions because these are treated as the "normal" exit from a block or loop. For this reason, undo processing does not execute and any transaction will commit.

The QUIT statement is special. It is the only way to generate a QUIT condition. It is similar to the STOP statement, in that it explicitly raises something that is treated like a condition (ON phrases can "catch" this condition at particular blocks with programmer controlled behavior). However, the ON QUIT phrase is unique in that the UNDO operation is optional. For other ON phrases, the UNDO operation is required and implicit (it cannot be avoided). In the absence of any explicitly specified ON QUIT UNDO phrase (in any containing block), the default response to a QUIT condition is to commit the active transaction (and then exit the application). Even in the presence of an explicitly specified ON QUIT phrase in a containing block, if the UNDO keyword is missing, then the behavior will be to commit any transaction or subtransaction that has occurred as if this was a normal exit from the contained blocks. UNDO processing is only triggered if the containing block has an explicitly specified ON QUIT UNDO phrase.

More info about the ON phrase and implicit block properties can be found in the Blocks chapter of this book. The UNDO statement and the STOP, QUIT, LEAVE, RETURN and NEXT statements are explained in the Control Flow chapter of this book.

Undo for Permanent Tables

Any changes made to records of a permanent database in a transaction or subtransaction block are undone whenever a transaction or subtransaction is backed out. Tables are restored to the state they had at the beginning of the transaction or subtransaction that is undone.

Example 1:

/* clear permanent table */
for each book:
  delete book.
end.

/* create initial records */
create book. book.book-id = 1.
create book. book.book-id = 2.

repeat:                              /* start the subtransaction */
  /* record modification */
  find book where book.book-id = 1.
  book.book-id = 3.

  /* record creation */
  create book.
  book.book-id = 4.

  /* record deletion */
  find book where book.book-id = 2.
  delete book.

  undo, leave.                       /* undo the subtransaction */
end.

/* output records */
for each book:
   message string(book.book-id).
end.

Output:

1
2

Details:

Changes performed for the book table in the subtransaction were undone by the undo, leave command.

Undo for Variables and Temporary Tables

Any changes made to variables or records of a temporary table in a transaction or subtransaction block are undone whenever a transaction or subtransaction is backed out (although statements that modify records of a temporary table or variables do not start a transaction by themselves). The variables and temporary tables are restored to the state they had at the beginning of the transaction or subtransaction that is undone.

Example 2:

def var i as integer.

i = 1.

do transaction:       /* start the transaction */
  i = 2.
  undo, leave.        /* undo the transaction */
end.

message string(i).

Output:

1

Details:

Change of i variable value from 1 to 2 was undone by undo, leave command.

Example 3:

def temp-table tt
   field f1 as integer.

/* create initial records */
create tt. tt.f1 = 1.
create tt. tt.f1 = 2.

do transaction:                 /* start the transaction */
  /* record modification */
  find tt where tt.f1 = 1.
  tt.f1 = 3.

  /* record creation */
  create tt.
  tt.f1 = 4.

  /* record deletion */
  find tt where tt.f1 = 2.
  delete tt.

  undo, leave.                /* undo the transaction */
end.

/* output records */
for each tt:
   message string(tt.f1).
end.

Output:

1
2

Details:

Change performed for the tt table were undone by undo, leave command. Variables and temporary tables specifically defined as NO–UNDO are not undone in this case. You can read about the NO-UNDO option below, in the next sub-section.

NO-UNDO Option

By default most variables and all temporary tables are undoable. Progress 4GL allows variables and temporary tables to be used as if no transaction is active, even when one is started. This feature is implemented using the NO-UNDO option. Whenever this is used, it will ignore any undo processing, for the targeted variable or temporary table.

The NO-UNDO option can be specified in temp-table definitions and in many forms of variable definition. For full details on which variable definition forms can be NO-UNDO and how UNDO registration (and in some cases deregistration) works, please see the sections on UNDO, Shared Variables and DEFINE PARAMETER of the Data Types chapter.

If a condition occurs that forces undo, then variables, temporary tables and parameters specifically defined as NO–UNDO are not rolled back to their previous value.

The NO-UNDO option is reflected in the converted code in the different ways, depending on the element:

  • Undo behavior for variables is implemented by registering the associated variable in the set of undoables elements. For this purpose, the variable is defined using UndoableFactory instead of TypeFactory, which is emitted in Java class or as a local var for the Java method of the top-level block in which the undoable variable is defined. If the variable is marked as NO-UNDO, then TypeFactory will not includ it in the list of undoable variables passed to the TransactionManager.registerUndo or TransactionManager.register function. As in Progress 4GL variables are undoable by default (unless NO-UNDO is specified explicitly), in the Java converted code they will be defined using UndoableFactory APIs.
  • For NO-UNDO tables, the undoable parameter is set to false in the corresponding TemporaryBuffer.define call, when the buffer for the temporary table is instantiated in the converted code.
  • Internal procedure and external procedure parameters can optionally be marked as NO-UNDO. Any such parameters that are OUTPUT or INPUT-OUTPUT will have some kind of variable or field reference passed by the caller (and which must be modified by the function). If that passed reference is registered for undo processing, then it will be temporarily removed (by the FWD runtime) from the undoables set before the internal procedure or user-defined function begins.
  • User-defined function parameters are always NO-UNDO. Any such parameters that are OUTPUT or INPUT-OUTPUT will have some kind of variable or field reference passed by the caller (and which must be modified by the function). If that passed reference is registered for undo processing, then the FWD runtime will temporarily remove it from the undoables. Otherwise even though there won't be a registration call, since they are already in the list from further up the call stack, they will get undone when the 4GL would not do so.

Example 4:

def var i as integer no-undo.
def var j as integer.

i = 1.
j = 1.

do transaction:
  i = 2.
  j = 2.
  undo, leave.
end.

Converted code:

@LegacySignature(type = Type.VARIABLE, name = "i")
integer i = TypeFactory.integer();

@LegacySignature(type = Type.VARIABLE, name = "j")
integer j = UndoableFactory.integer();

@LegacySignature(type = Type.MAIN, name = "test.p")
public void execute()
{
   externalProcedure(Test.this, new Block((Body) () -> 
   {
      i.assign(1);
      j.assign(1);

      doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () -> 
      {
         i.assign(2);
         j.assign(2);
         undoLeave("blockLabel0");
      }));
   }));
}

Details:

Only the j variable is undoable - so the TransactionManager.registerUndo call will not register variable i, as it was marked as NO-UNDO. At the end of the DO block, i will hold value 2 and j will be rolled back to value 1.

Example 5:

def temp-table t1 field f1 as integer.
def temp-table t2 field f2 as integer no-undo.

Converted code:

T1_1_1.Buf t1 = TemporaryBuffer.define(T1_1_1.Buf.class,
                                       "t1",
                                       "t1",
                                       false);

T2_1_1.Buf t2 = TemporaryBuffer.define(T2_1_1.Buf.class,
                                       "t2",
                                       "t2",
                                       false,
                                       false);  /* "undoable" parameter is false */

Details:

In the converted code for NO-UNDO temporary tables the undoable parameter is set to false in the corresponding TemporaryBuffer.define call. Any temporary tables which are not marked as NO-UNDO (as it table t1 is), the undoable parameter is skipped and defaults to true.

Example 6:

def input-output parameter i as integer no-undo.

Converted code:

   integer i;

   @LegacySignature(type = Type.MAIN, name = "test.p", parameters = 
   {
      @LegacyParameter(name = "i", type = "INTEGER", mode = "INPUT-OUTPUT")
   })
   public void execute(final integer _i)
   {
      integer i = TypeFactory.initInputOutput(_i);

      externalProcedure(Test.this, new Block((Init) () -> 
      {
         Test.this.i = i;
      }));
   }

Details:

Any procedure NO-UNDO parameters (output or input-output) are defined via TypeFactory APIs instead of UndoableFactory. This also applies to internal procedures and function parameters.

Undo and Commit Processing Notes

UNDO can only be done when a transaction is active. When a transaction is not active, UNDO is a null operation, so changes to variables, procedure parameters and temporary tables made outside a transaction are never undone since only transaction and subtransaction blocks can be undone.

Changes to permanent tables can only be performed inside a transaction. If developer does not specify the transaction scope explicitly, the conversion process will detect statements that modify records of a permanent database and will encode the proper transaction level at the proper block.

The scope of the rollback is either explicitly defined by the programmer or is implicit, but in either case a specific rollback scope is targeted. A rollback target scope is inherently specified at any block that defines a transaction or subtransaction. For details on how the target of a rollback operation is calculated, please see the Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY section of the Blocks chapter.

After the rollback there is also a control flow action that will be taken. This control flow action is RETRY, LEAVE, NEXT or RETURN. These actions can be explicitly specified by the ON phrase for the block enclosing the line causing the rollback. Or that enclosing block can have an implicit default. Or the statement that generated the rollback can specify the action. That control flow action will target a block that is the same as the block rolled back by the UNDO or it will enclose that block. For full details, please see the Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY section of the Blocks chapter.

There is no way to disable undo for a block, although the definitions of individual variables, temporary tables and procedure parameters can be specified as NO-UNDO, which will exclude them from the transaction rollback.

Changes to permanent tables are always rolled back. There is no way to disable undo for permanent tables.

Once the successful end of a transaction block is encountered, the runtime commits all changes (permanently) to variables and tables of permanent and temporary databases. In the case of tables this process includes execution of validation rules, inserts (of new records), updates (of current records), deletes (of current records) and the release (or downgrade) of held locks.

Reaching the END statement for a block or encountering a LEAVE, NEXT or RETURN statement are all considered as normal ends of a block. This means each of these cases will cause a commit to occur. This is different from a control flow action that occurs as a result of a condition being raised. The LEAVE, NEXT and RETURN control flow actions operate with the same behavior as their statement equivalents, except that the actions occur after the rollback and thus have no impact on the decision of whether to undo or not. The RETRY control flow action has no direct statement counterpart, but the closest equivalent is the UNDO statement where RETRY is a possible action. The UNDO statement is never considered a normal exit from a block. Similarly, there is no ambiguity about RETRY, which is just a control flow action that occurs after a rollback executes.

When a subtransaction scope ends successfully, all changes made during that scope are effectively added to the "transaction set". This transaction set can be undone as a set or committed as a set but once the end of a subtransaction occurs, one loses the ability to undo only that sub-set of the parent transaction.

Once a transaction is active, the state of all in-scope undoable resources is tracked. For both looping and non-looping blocks that have the transaction or subtransaction property, the original state of each undoable resource is captured on first entry to the block/loop. That snapshot is what will be restored on abnormal exit from the block/loop.

In the case of a looping block, the normal end of each iteration of the block causes a transaction or subtransaction to commit. If the loop is not ending, then it is as if the block was being entered again from the top and thus the "backup set" of all undoable resources (variables, records, parameters) is updated at this point. In other words, the snapshot of each undoable resource's latest state is re-captured on each iteration. This means that if an undoable resource is defined as external to the (sub)transaction scope and the first iteration of the given (sub)transaction loop updates its value and then commits (i.e. UNDO was not performed), this new value will be result of any UNDO that occurs until the next (sub)transaction commit.

In the example below we have variable i with the initial value 1. When the REPEAT loop is entered, the backup value of i becomes 1. On the first iteration i is incremented to 2 and this change is committed at the end of the loop iteration, so the backup value is updated to 2. On the second iteration i is incremented to 3, but this change is undone by the UNDO, LEAVE command, therefore i is rolled back to the most recent backup value, i.e. 2.

Example 7:

i = 1.
repeat transaction:
   i = i + 1.
   if i = 3 then undo, leave.
end.

message string(i).

Output:

2

The Progress documentation recommends the use of NO-UNDO option when possible for better performance. But in FWD, use of NO-UNDO option for temporary tables leads to worse performance because of how the rollback is implemented. When no-undo temporary tables are rolled back, it first performs a database-level rollback, to discard all the changes done in all the temporary tables, in the current database-level transaction. After this, if there were temporary tables which changes' should have not been rolled back (as they were marked as NO-UNDO), FWD must ensure that these temp tables do behave as always-commit: for them, FWD collects undoable information about the changed records, but this is done not for rollback purposes, but to reinstate the records in their proper state, after the database-level transaction has been rolled back.

In brief, this translates to the fact that FWD collects undoable data for any temporary table, even if its NO-UNDO, and using an unnecessary NO-UNDO for a temporary table results in an overhead, as the changes to that temporary table are first rolled back and then re-applied.

Advanced Transaction Features

Progress supports two-phase commit across multiple databases in a single transaction. That mechanism ensures that any transaction is either committed to all affected databases or to none. Two phase commit is not supported by FWD at this time.

Progress has a mechanism of sharing uncommitted changes across sessions (i.e., "dirty sharing"), which is supported by FWD. At a high level, changes (inserts, updates, deletes) to a permanent database that affect one or more indexes may be immediately visible in the other sessions (and not only after they have been committed). This is a purely runtime feature, there are no code conversion differences due to this anomaly. The FWD runtime does fully support this set of features. Further details are beyond of the scope of this book.

Determining When Transactions Are Active

You can determine if there is an active transaction by using the TRANSACTION function, which returns true when there is an active transaction. It is converted to a TransactionManager.isTransaction function call (which returns a boolean value) or to a TransactionManager.isTransactionActive function call (which returns a logical value), depending on the code where it is used.

Example:

message string(transaction).

do transaction:                     /* start of the transaction */
  message string(transaction).

  repeat:                           /* start of the subtransaction */
     message string(transaction).
     if transaction then leave.
  end.                              /* end of the subtransaction */
end.                                /* end of the transaction */

Output:

no
yes
yes

Converted code:

message(valueOf(TransactionManager.isTransactionActive()));

doBlock(TransactionType.FULL, "blockLabel0", new Block((Body) () ->  /* start of the transaction */
{
   message(valueOf(TransactionManager.isTransactionActive()));

   repeat("loopLabel0", new Block((Body) () ->                       /* start of the subtransaction */
   {
      message(valueOf(TransactionManager.isTransactionActive()));

      if (TransactionManager.isTransaction())
      {
         leave("loopLabel0");
      }
   }));                                                              /* end of the subtransaction */
}));                                                                 /* end of the transaction */

Transaction Management

Most 4GL code has implicit transaction boundaries. The Progress 4GL compiler/runtime and in FWD, the conversion process calculates the blocks associated with full transactions and subtransactions. It is possible to explicitly specify transaction scopes.

The ability to have nested subtransactions allows the block-structuring of 4GL code to be mapped such that transaction processing (commit/rollback) can be associated with the level of granularity desired by the developer.

Expanding Transaction Scope

You may what to expand transaction scope in order to undo a larger set of work in the case of an error. Consider the case of a procedure that creates a set of book records:

repeat:
  create book.
  update book with frame f1.
end.

In this procedure if a system crash occurs or a STOP event is raised by the user, only the most recent book record is rolled back. To change the code such that all created records are rolled back, one can wrap the code in a DO TRANSACTION block:

do transaction:
   repeat:
     create book.
     update book with frame f1.
   end.
end.

As mentioned in the Undo Processing section, expanding the transaction scope increases memory footprint, as the runtime will need to hold in memory every record change done in a nested subtransaction, even after that subtransaction commits.

Reducing Transaction Scope

One may need to reduce the transaction scope in order to preserve some subset of work in the case of an error. Consider a procedure that creates a set of person records and a set of pers-addr records for each created person record:

repeat:                               /* Start of transaction #1 */
   create person.
   update person with frame f1.

   repeat:                            /* Start of subtransaction #2 */
      create pers-addr.
      update pers-addr with frame f2. /* End of subtransaction #2 */
   end.
End.                                  /* End of transaction #1 */

In this code if a system crash occurs or a STOP event is raised by the user, the most recent person record is rolled back, as the transaction is scoped to the outer REPEAT block. To change the code such that if a condition is raised during update of a pers-addr record, the most recent person record will be preserved, one would wrap person-related code in a@ DO TRANSACTION block:

repeat:
   do transaction:                    /* Start of transaction #1 */
      create person.
      update person with frame f1.
   end.                               /* End of transaction #1 */

   repeat:                            /* Start of transaction #2 */
      create pers-addr.
      update pers-addr with frame f2.
   end.                               /* End of transaction #2 */
end.

In this case there are two separate transactions inside the outer REPEAT block.

Transactions for Temporary Tables and Variables

By default statements that change records in temporary tables or which change variables do not start a transaction. This means that changes made to them are irreversible by default. But if you want to be able to undo some changes to temporary tables or variables then you can explicitly start a transaction by specifying the TRANSACTION keyword for the block you want to be able to undo (if you do not already have the specific block, you can wrap the targeted code in a DO/REPEAT TRANSACTION block). In the example below, record creation is undone if user decides not to commit it:

define temp-table tt
     field f1 as integer.

do transaction:
   create tt.
   message "Commit new record?" update commit as logical.
   if not commit then undo, leave.
end.

© 2004-2022 Golden Code Development Corporation. ALL RIGHTS RESERVED.