Buffers, Locks and Scopes in OpenEdge: A Complete Developer Guide

It is inevitable for a Progress Developer to work with buffers. In fact, it is present at every step of the coding process and therefore, we must understand entirely what happens behind the scenes. This is exactly why this article is written: for us to have a reference guide on how buffers work, why we need them, and how to have a better understanding of the code we write every day.

What is a buffer?

 

“The record buffer is a temporary storage area in memory where records are managed as they pass between the database and the statements in your code.” — Progress documentation

It may seem like a scary concept, but — believe it or not — you’ve probably used buffers from the very first day without even knowing. Let’s take a look at the line of code below:

FIND FIRST Customer EXCLUSIVE-LOCK
     WHERE Customer.Country = "FR" NO-ERROR.

This fetches the first Customer from France found in the database. Even though it is not specified in the code, a buffer that holds the record is defined by default. The buffer’s name is the same as the database table name, so it is called “Customer”. This same behaviour applies to FOR EACH, DO FOR, and REPEAT FOR.

The developer can now do whatever they intend with the record: delete it, update its fields, access the next record, and more.

That being said, best practice is always defining buffers instead of depending on the default one. The reasons for this will become obvious looking at the code below:

DEFINE BUFFER bufCustomerFrance FOR Customer.
DEFINE BUFFER bufCustomerUSA    FOR Customer.

FIND FIRST bufCustomerFrance NO-LOCK
     WHERE bufCustomerFrance.Country = "FR"  NO-ERROR.

FIND FIRST bufCustomerUSA NO-LOCK
     WHERE bufCustomerUSA.Country = "USA" NO-ERROR.

IF bufCustomerFrance.Balance > bufCustomerUSA.Balance THEN
   /* stuff */

Analysing the code, we can see the drawback of the default buffer — we can hold only one record at a time. Using the default buffer, there is no way to compare two customers. Moreover, nested loops on the same table are impossible — it even returns a syntax error in this scenario:

FOR EACH Customer NO-LOCK WHERE Customer.Country = "FR":
   FOR EACH Customer NO-LOCK WHERE Customer.Country = "USA":
      ...
   END.
END.

The syntax error makes complete sense. We have two nested loops and both of them use the same buffer. Since both loops need to hold a different record simultaneously, there is no way to make this work — the inner loop would always overwrite the outer loop’s record. Since this is fundamentally impossible to resolve at runtime, ABL catches it at compile time and throws a syntax error instead of producing unpredictable results.

The correct approach is using 2 named buffers:

 

FOR EACH bufCustomerFrance NO-LOCK
   WHERE bufCustomerFrance.Country = "FR":
   FOR EACH bufCustomerUSA NO-LOCK
      WHERE bufCustomerUSA.Country = "USA":
      ...
   END.
END.

 

Adding to the benefits already shown, named buffers make the code more explicit and the developer’s intention clear — we know exactly what the purpose of each buffer is, if it is named properly 🙂

To get the full picture, we need to look at one more concept closely tied to buffers: locking.

What is buffer locking?

 

FIND FIRST Customer EXCLUSIVE-LOCK
     WHERE Customer.Country = "FR" NO-ERROR.

You may have noticed that the code contains the keyword “EXCLUSIVE-LOCK”. The session locks the record and is the only one able to update or delete it. If other users try to acquire an exclusive lock over that record, then they will have to wait until that first session releases it. That said, other users can still read the record using “NO-LOCK”, though they cannot modify it.

In a multi-user environment, it’s very important that this exclusive lock is held for as little time as possible. Consider this scenario:

You want to send a customer a personalised email. In the background, this procedure runs:

PROCEDURE SendEmail:
   DEFINE BUFFER bufCustomer FOR Customer.

   FIND FIRST bufCustomer EXCLUSIVE-LOCK
        WHERE bufCustomer.Birthday = TODAY NO-ERROR.
   IF AVAILABLE bufCustomer THEN DO:
      DO TRANSACTION:
         /* Lock acquired - other users are now blocked */
         /* Connecting to mail server...               */
         /* Composing the email...                     */
         /* Waiting for mail server response...        */
         /* Finally done - mark email as sent          */
         bufCustomer.BirthdayEmail = TRUE.
      END.
   END.
END PROCEDURE.

The session has acquired an exclusive lock over the Customer record.

At the same time, the customer wants to update their country:

PROCEDURE UpdateCountry:
   DEFINE PARAMETER iCustNum        AS INTEGER   NO-UNDO.
   DEFINE PARAMETER cUpdatedCountry AS CHARACTER NO-UNDO.
   DEFINE BUFFER bufCustomer FOR Customer.

   FIND FIRST bufCustomer EXCLUSIVE-LOCK
        WHERE bufCustomer.CustNum = iCustNum NO-ERROR.
   IF AVAILABLE bufCustomer THEN
      bufCustomer.Country = cUpdatedCountry.
END PROCEDURE.

 

Question: What do you think will happen? Will the Customer be able to change their country successfully, or will they not acquire the lock?

The answer: both!

SendEmail holds the lock unnecessarily long. The lock is held during network calls and composing the email, therefore UpdateCountry will be blocked. Eventually UpdateCountry will be able to acquire the lock, but in this scenario it takes way too long.

Let’s see how this can be fixed:

 

PROCEDURE SendEmail:
   DEFINE BUFFER bufCustomer FOR Customer.

   FIND FIRST bufCustomer NO-LOCK
        WHERE bufCustomer.Birthday = TODAY NO-ERROR.
   IF AVAILABLE bufCustomer THEN DO:
      /* Reading without a lock - nobody is blocked */
      /* Connecting to mail server...               */
      /* Composing the email...                     */
      /* Waiting for mail server response...        */
      /* Only now do we acquire the lock            */
      FIND CURRENT bufCustomer EXCLUSIVE-LOCK NO-ERROR.
      bufCustomer.BirthdayEmail = TRUE.
   END.
END PROCEDURE.

 

This small change makes the whole difference: the lock is held for a much smaller timeframe, making the record available to other users almost immediately. Keep in mind, while this example is minimal, in a real-world application, bad locking creates the risk of multiple users queueing up for a lock and the waiting time adds up.

A practical lock example

 

To understand locks better, try running the procedure below from two sessions simultaneously and observe the outputs:

 

PROCEDURE LockTesting:
   DEFINE BUFFER bufCustomer FOR Customer.

   FIND FIRST bufCustomer EXCLUSIVE-LOCK NO-ERROR.
   IF LOCKED(bufCustomer) THEN
      MESSAGE "Someone else has access" VIEW-AS ALERT-BOX.
   ELSE IF AVAILABLE(bufCustomer) THEN
      MESSAGE "We have access" VIEW-AS ALERT-BOX.
   ELSE
      MESSAGE "No such Customer" VIEW-AS ALERT-BOX.
   PAUSE 10.
END PROCEDURE.

The LOCKED() function returns TRUE if the record is locked by another user.

The first session will lock the record. LOCKED() returns FALSE since the record is not locked by another user — it goes into the AVAILABLE branch and prints “We have access”. PAUSE 10 simply pauses the procedure for 10 seconds.

During those 10 seconds, the second session won’t be able to acquire a lock and will wait until the first procedure ends. After that, it will hold the lock and print “We have access” again.

For the second run to actually go into the LOCKED() branch, we need to tell it not to wait and simply continue its execution. For not waiting, Progress gives us a very intuitive and convenient keyword: NO-WAIT.

FIND FIRST bufCustomer EXCLUSIVE-LOCK NO-ERROR NO-WAIT.

Adding this tweak, during the second run, LOCKED() actually returns TRUE. If the LOCKED branch would’ve had an update on the record, the program would throw a runtime error, since the session skipped the FIND entirely, therefore it has no record in the buffer.

Now that we understand buffers and the philosophy behind their locking, we need to discuss one last essential concept: buffer scoping.

Buffer scoping

 

Scoping provides two valuable pieces of information: how long the buffer stays active and in what section it can be accessed. Proper scoping assures optimal locking, which is exactly what we want for our project. There are three types of scoping — let’s go through all of them.

1. Strong scope

 

PROCEDURE StrongScope:
   DEFINE BUFFER bufCustomer FOR Customer.

   DO FOR bufCustomer:
      FIND FIRST bufCustomer EXCLUSIVE-LOCK
         WHERE bufCustomer.State = "MA".
      /*...*/
   END.
END PROCEDURE.

 

This is called strong scope. Once the DO FOR block ends, two things happen immediately: the record is released from the buffer and the exclusive lock is freed, making the record available to other users. The record is strictly scoped to the DO FOR block.

2. Weak scope

 

PROCEDURE WeakScope:
   DEFINE BUFFER bufCustomer FOR Customer.

   FOR EACH bufCustomer EXCLUSIVE-LOCK
      WHERE bufCustomer.CustNum = 1:
      /*...*/
   END.
END PROCEDURE.

 

Weak scopes are interesting. The buffer is scoped to the iterating block — once the FOR EACH finishes, identical to strong scope, the record is released and the lock is freed. So what’s the difference between strong and weak scoping? For this, we need to know the last type: free scoping.

3. Free scope

 

PROCEDURE FreeScopeExample:
   DEFINE BUFFER bufCustomer FOR Customer.

   DO:
      FIND FIRST bufCustomer EXCLUSIVE-LOCK
         WHERE bufCustomer.CustNum = 1.
      /*...*/
   END.
END PROCEDURE.

 

This is free scoping. The primary attribute of a free scope is that the AVM scopes the buffer to the nearest enclosing block — in this case, the entire procedure. Once we leave the DO block, the lock is not freed and the buffer still holds the record. Both live until the procedure itself ends.

Now that we know all three scopes, let’s wrap it all up.

Rule 1 — A free scope instruction cannot exist outside of a strong scope block.

 

Think about it: the sole purpose of strong scoping is defining a clear block of code where that buffer can be accessed. A free scope instruction outside the DO FOR would link the buffer to the whole procedure, which contradicts the purpose of strong scope. This will return a compilation error:

 

PROCEDURE StrongScopeExample:
   DEFINE BUFFER bufCustomer FOR Customer.

   /* Free scope instruction OUTSIDE the DO FOR - compilation error */
   FIND FIRST bufCustomer NO-LOCK WHERE bufCustomer.CustNum = 1.

   DO FOR bufCustomer:
      FIND FIRST bufCustomer EXCLUSIVE-LOCK WHERE bufCustomer.State = "MA".
      MESSAGE bufCustomer.Name VIEW-AS ALERT-BOX.
   END.
END PROCEDURE.

Rule 2 — A weak scope instruction can exist outside of a strong scope block.

 

Since no free scope instructions can exist outside the DO FOR, the record scope will be tightly linked to each individual block of code:

 

PROCEDURE StrongScopeExample:
   DEFINE BUFFER bufCustomer FOR Customer.

   /* Strong scope block */
   DO FOR bufCustomer:
      ...
   END.

   /* Weak scope block outside the DO FOR - perfectly valid */
   FOR EACH bufCustomer:
      ...
   END.
END PROCEDURE.

 

The record will be scoped to the DO FOR block and then to the FOR EACH block.

Rule 3 — Weak scope and free scope instructions can exist inside a strong scope block.

 

It’s completely valid to have FIND and FOR EACH inside a strong scoped block:

 

DO FOR bufCustomer:
   /* Free scope instruction inside strong scope - valid */
   FIND FIRST bufCustomer EXCLUSIVE-LOCK WHERE bufCustomer.State = "MA".

   /* Weak scope instruction inside strong scope - valid */
   FOR EACH bufCustomer EXCLUSIVE-LOCK:
      ...
   END.
END.

Compile listing

 

When writing code, it can become a bit confusing to pinpoint where a buffer is scoped, especially in a project with lots of procedures, searches, and queries. Luckily, we can get a clear view at compile time by generating a listing file.

The code below will generate a .lis file. Make sure you change the path and filename according to your project and PROPATH settings:

DEFINE VARIABLE cFullPath AS CHARACTER NO-UNDO.

cFullPath = SEARCH("BufferScoping/filename.p").

COMPILE VALUE(cFullPath)
   LISTING  VALUE(REPLACE(cFullPath, "filename.p", "buffer.lis")).

As you browse the .lis file, at the very bottom you’ll see exactly what we discussed in this chapter — where each buffer is scoped:

Scope_WeakScope.p   71   Procedure   Procedure StrongScopeExample
Scope_WeakScope.p   75   Do
   Buffers: sports2000.bufCustomer
Scope_WeakScope.p   76   For
Scope_WeakScope.p   82   For
   Buffers: sports2000.bufCustomer

The output follows this format:

Filename — line number — block type — procedure name

  • Line 71: The procedure is defined.
  • Line 75: DO FOR block — contains a buffer, confirming the strong scope.
  • Line 76: FOR block — no buffer listed, meaning it inherits the buffer from the enclosing DO FOR block.
  • Line 82: FOR block — contains its own buffer, meaning the DO FOR block has ended and the buffer is now scoped to this FOR block.

Final thoughts

 

Hopefully this will serve as a useful reference guide for whenever you have uncertainties regarding buffers. I certainly did (sometimes still do!), and would’ve loved to have a source where I could find all the necessary information about buffers.

I decided to write one myself — for those future moments when I scratch my head over a DO FOR block — and maybe this will come in handy to others as well!

 

 

 

 

 

 


 

Author: Mihai Buga, Junior Developer

Mihai is passionate about understanding how things work beneath the surface, exploring even the smallest details. He enjoys breaking down complex concepts into simple, beginner-friendly explanations, showing that nothing is as difficult as it first seems.

Transactions

Transactions

A clear guide to OpenEdge ABL transactions, explaining how TRANSACTION, UNDO, and scoping keep your data consistent.

SEE HOW WE WORK.

FOLLOW US