←Part 31  Part 33→

The HP e3000--For Complete Novices
Part 32: IMAGE/SQL Application Logic—LOCKING examples

by George Stachnik

Welcome to part 32 of this series of articles on the HP e3000. We are nearly at an end of our discussion of the IMAGE/SQL database management system. We've been discussing ways of preventing database corruption, including good database locking strategies. Poorly implemented locking strategies are perhaps the most common sources of database corruption and poor performance in the HP e3000 world. Now that we have learned how to set up a good locking strategy, we're going to focus on writing the code that implements that strategy.

Two instrinsics are involved in locking strategies: DBLOCK and DBUNLOCK. Not surprisingly, it's DBLOCK that is used to lock a database (or a part of a database), and DBUNLOCK that releases the locks obtained by DBLOCK. One key difference between these two intrinsics is in their scope. When you call DBLOCK, you must tell it precisely which database (or parts of a database) you want to lock. But when you call DBUNLOCK, you will release all the locks that you're currently holding. One of the most dangerous things that an application program can do is to "forget" that it's holding a lock. If the program continues processing for a long time while holding a lock, it can cause serious problems for other users who want to lock the resource it is holding. Locking "best practices" can be summarized easily: whenever you lock a database (or a part of a database), do whatever you need to do and then release the lock quickly. Holding onto locks longer than necessary can be an invitation to disaster.

Calling DBLOCK

We've seen how programmers use parameters to tell IMAGE/SQL's intrinsics what to do. For example, when we learned how to call DBOPEN, we used parameters to tell DBOPEN which database to open. Similarly, when we call DBLOCK, we're going to use parameters to tell DBLOCK what to lock.

When you call DBLOCK, you must pass it a string of four parameters:

Base This is the name of an array that contains the name of the database you want to lock. Typically, this will be the same array that you passed to DBOPEN when you opened the database. You cannot lock a database unless you open it first. If you aren't planning to lock the entire database (i.e., if you're going to ask for a dataset-level or a data-entry level lock), you still must specify the Base parameter. Each call to DBLOCK can only lock data in a single database.

Qualifier This parameter tells DBLOCK which part (or parts) of the database you wish to lock. The precise meaning of the data in this parameter depends on the value in the mode parameter—so let's look at that before we go into the details of Qualifier.

Mode This parameter tells DBLOCK what kind of lock you want, and at what level you want the lock applied.

Status This parameter contains the status array. It's identical to the status array used by all the other IMAGE intrinsics that we've seen in this series of articles. Ordinarily, the first element of the status array will contain a zero when DBLOCK returns control to the calling program. This indicates that the lock that was requested has been obtained. But if an error or an exceptional condition occurs, such as failure to obtain the lock, DBCLOSE will place error codes here.

The first parameter we're going to focus on is the mode (parameter 3 above). This parameter is an integer value that specifies the lock type and the lock level. There are only two kinds of locks—conditional and unconditional.

If you specify unconditional locking, DBLOCK will not return control to the calling program until it is able to obtain the lock that it asked for. The only exception to that rule is in the case in which DBLOCK detects an error, but we'll discuss errors later on.

If you specify conditional locking, DBLOCK will return control to the calling program immediately after it tries to obtain the lock, regardless of whether it was successful or not.

Using Lock Types

For example, suppose you call DBLOCK and tell it (via the parameters) that you want to lock an entire database called REALTY. If nobody else has the database opened at the time, then DBLOCK will almost surely be able to obtain the lock. Even if there are other users logged onto your system who have the database open, as long as none of them is holding any locks on any portion of the REALTY database, DBLOCK should be able to obtain the lock and return control to you (the calling program).

But now suppose that DBLOCK is not able to obtain the lock that you asked for. Suppose, for example, that another user has opened the REALTY database and obtained a lock on some part of it. DBLOCK will not be able to obtain the database-level lock that you asked for. What happens next depends on whether you specified a conditional or an unconditional lock.

If you specify an unconditional lock, there are two possibilities. If DBLOCK detects an error (e.g., if you spelled the database name wrong), it will return control immediately and put a non-zero value in the status array. But what if you spell everything correctly? If there is no error, then DBLOCK will simply wait until it is able to obtain the lock that you wanted. Effectively, your application program will be "hung up" until the lock that you asked for becomes available. Depending on who (or what) is holding the lock that's blocking you, you could be waiting for a long time.

On the other hand, if you specify a conditional lock, then DBLOCK will return control to the calling program immediately—even if it is not able to obtain the lock you wanted. In that case it will place a non-zero integer in the first element of the status array, which your program would be responsible for handling.

For example, suppose you asked for a conditional database-level lock. If DBLOCK determines that the database (or some portion of it) is already locked by another user, then DBLOCK will return control to the calling program, and place an error code of 20 in the status array.

The number 20 is used only for failed conditional database-level lock requests. There are other error codes that you may see if DBLOCK fails to obtain dataset-level or data entry level-locks on your behalf. There is a complete list of possible error codes in the TurboIMAGE/XL Library Procedures Manual, which is available on the Web from HP. Some of the most common errors that can be returned by DBLOCK are summarized in Table 1.

When should you use conditional locks (as opposed to unconditional locks)? It depends on your application program logic. As we mentioned above, asking for an unconditional lock on a database that is being used by other users means running the risk that your program could "hang" until the other users release whatever locks they are holding.

In an earlier article in this series, we warned against the programming practice of "locking around a user dialogue." In other words, you don't want application programs to lock all (or part) of a database, and then prompt the end user for additional information. By doing so, you run the risk that the program will hold those locks for a long time while the user tries to figure out how to respond to your prompt. If the user goes to lunch or goes home for the day, then the locks could be tied up for hours, or even days. That means that other programs could get hung up for hours waiting for locks, especially if they're using unconditional locking.

If you are working in an environment in which locks can sometimes be held for a long time, then you should avoid unconditional locking. Instead, use conditional locking. If DBLOCK returns one of the errors shown in Table 1, your program should pass that information back to the user who is running it. (You might display an error message such as "Item number xxxxx is busy right now. Try later.")

Locking Levels

In addition to the lock type, the mode parameter is also used to specify the locking level. There are three locking levels-database-level locks, dataset-level locks and data entry-level locks. These were discussed in an earlier article, so we'll just review them briefly.

A database-level lock locks an entire database. If user A is holding a database-level lock against a particular database, then no other user will be able to obtain any lock (at any level) against that database until A releases the lock that he's holding.

A dataset-level lock locks up one (or more) datasets in a particular database. If user A is holding a dataset-level lock against a dataset called DS1, then no other user will be able to obtain a lock on DS1, or a lock on any of the data entries (i.e., records) in DS1 until A releases the lock he's holding. Of course, other users can lock other datasets in the same database, or lock data entries in those other datasets.

A data entry-level lock locks up one (or more) data entries (or records) in a particular dataset. If user A is holding a data entry-level lock against record number 123 in dataset DS1, then no other user can lock that particular record until A releases it. Similarly, no other user can lock dataset DS1, or the database containing DS1. However, other users can lock other records in DS1 or in other datasets in the database.

Given the fact that there are three locking levels and two locking types, it should come as no surprise that there are only six possible values that you can pass to DBLOCK in the mode parameter. The six locking modes are summarized in Table 2. Let's use these to analyze some coding examples.

Dataset-Level Locking

Here is a fragment of a COBOL program that's doing a dataset-level lock:
001890 DATA DIVISION.
001900 WORKING-STORAGE SECTION.
001910
001920 01  DB-MODE                     PIC S9(04) COMP.
001930 01  DB-NAME                     PIC X(10) VALUE SPACES.

002360 01  SET-NAME                    PIC X(16).
002370
002380 01  STATUS-ARRAY.
002390     05  CONDITION-CODE          PIC S9(4) COMP.
002400     05  ENTRY-LENGTH            PIC S9(4) COMP.
002410     05  RECORD-NUMBER           PIC S9(9) COMP.
002420     05  CHAIN-LENGTH            PIC S9(9) COMP.
002430     05  BACKWARD-POINTER        PIC S9(9) COMP.
002440     05  FORWARD-POINTER         PIC S9(9) COMP.
002450

-------------------------------------------------------------------

089800 LOCK-ALL-PARTS.
092000
092600     MOVE "PARTS;"  TO SET-NAME.
092700     MOVE 3         TO DB-MODE.
092800
093100     CALL INTRINSIC "DBLOCK" USING DB-NAME,
093200                         SET-NAME,
093300                         DB-MODE,
093400                         STATUS-ARRAY.
093500
093900     IF CONDITION-CODE NOT= 0
094000       PERFORM IMAGE-ERROR.
094100
094200     DISPLAY "All Parts successfully locked!".
094300     DISPLAY SPACE.
094400
At the top we see the parts of the program's DATA DIVISION that are referenced by the call to DBLOCK. At the bottom are the parts of the PROCEDURE DIVISION that set up and perform the lock.

Statement number 092600 moves the string "PARTS;" to a variable called SET-NAME. This variable is being used as the qualifier, and it's passed to DBLOCK in statement 93200. DBLOCK expects the dataset name to be left-justified and terminated by a semicolon or blank. It's worth noting in passing that DBLOCK can also accept the dataset number instead of the dataset name. The dataset number can be obtained from the DBINFO intrinsic, and using it instead of the dataset name will make your program just a tiny bit more efficient. If you're going to be calling DBLOCK a lot, it's worth considering.

The statement number 092700 moves the integer 3 to DB-MODE. When this value is passed to DBLOCK in statement 93300, it tells the intrinsic that we are asking for an unconditional lock of the PARTS dataset (see Table 2.)

Because this is an unconditional lock, DBLOCK will not return control to this program until it has successfully obtained the lock we asked for (unless there's an error). Therefore, we are not expecting any value other than zero in the condition code. When statement number 093900 tests the condition code, a non-zero value there would indicate that something catastrophic has gone wrong. For example, a condition code of -11 would be returned if the value in DB-NAME did not match the name of any database that was currently opened by this program. Such an error is indicative of some logic error in our application program (or untested coding by the programmer). The IMAGE-ERROR routine that our sample code performs in such a case (statement 094000) displays an error message and then aborts the application.

Entry-Level Locking

In our next example, we're using DBLOCK to get an unconditional entry-level lock:
001890 DATA DIVISION.
001900 WORKING-STORAGE SECTION.
001910
001920 01  DB-MODE                     PIC S9(04) COMP.
001930 01  DB-NAME                     PIC X(10) VALUE SPACES.
001970
001980 01  LOCK-DESCRIPTOR.
001990     05  NUMBER-OF-LOCKS         PIC S9(4) COMP.
002000     05  LOCK1-LENGTH            PIC S9(4) COMP.
002010     05  LOCK1-SET-NAME          PIC X(16).
002020     05  LOCK1-ITEM-NAME         PIC X(16).
002030     05  LOCK1-RELOP             PIC X(02).
002040     05  LOCK1-ITEM-VALUE        PIC X(10).
002050     05  LOCK2-LENGTH            PIC S9(4) COMP.
002060     05  LOCK2-SET-NAME          PIC X(16).
002070     05  LOCK2-ITEM-NAME         PIC X(16).
002080     05  LOCK2-RELOP             PIC X(02).
002090     05  LOCK2-ITEM-VALUE        PIC X(12).
002100

---------------------------------------------------------------

094900 LOCK-AN-ORDER.
096700     DISPLAY SPACE.
097200     DISPLAY "Please enter the Order Number to lock on: "
097300        WITH NO ADVANCING.
097400     ACCEPT LOCK1-ITEM-VALUE FREE.
098300
098400     MOVE  5               TO DB-MODE.
098500     MOVE 1                TO NUMBER-OF-LOCKS.
098600     MOVE 23               TO LOCK1-LENGTH.
098700     MOVE "ORDER-DETAIL;"  TO LOCK1-SET-NAME.
098800     MOVE "ORDER-NO "      TO LOCK1-ITEM-NAME.
098900     MOVE "="              TO LOCK1-RELOP.
099000
099400     CALL INTRINSIC "DBLOCK" USING DB-NAME,
099500                         LOCK-DESCRIPTOR,
099600                         DB-MODE,
099700                         STATUS-ARRAY.
100100
100200     IF CONDITION-CODE NOT= 0
100300       PERFORM IMAGE-ERROR.
100400
100500     DISPLAY "The Order was successfully locked!".
100600     DISPLAY SPACE.
100700
The code is pretty similar that that which we saw in the first example. The chief difference is that we're passing a more complicated array in the qualifier parameter.

Statement number 094900 names this routine LOCK-AN-ORDER. The first thing that the code does is prompt the user for the order number that we want to lock (statement number 097400). Note that we aren't holding any locks yet-so holding this brief dialogue with the user at this time is OK.

The next six statements (98400 to 98900) are building a lock descriptor that we're going to use to tell DBLOCK which record we want to lock. The value of 5 that we place in DB-MODE (statement 98400) tells DBLOCK that this is to be an unconditional lock request for an entry-level lock. (See Table 2). DBLOCK will not return control to this program until it obtains all the locks that we ask for unless that catastrophic error that we've been worrying about happens.

Statement 098500 moves the value 1 to a variable called NUMBER-OF-LOCKS. As you can see by looking at the top portion of the listing (statement 1990), NUMBER-OF-LOCKS is part of the LOCK-DESCRIPTOR array. In this sample, we are only trying to lock a single record (data entry) in the database. If you wanted to lock more than one data entry, you could do that by describing them all in the LOCK-DESCRIPTOR array, in which case NUMBER-OF-LOCKS would indicate how many data entries you are trying to lock. In this way, you can lock all the records you need in a single call to DBLOCK. This is very important because it provides you with a way to avoid the pitfalls of the "deadly embrace" scenario that we discussed last time. (More about that in a moment.)

The next thing to do (statement 98600) is to tell DBLOCK the length of our lock descriptor. The value that we're passing (23) specifies that this descriptor is 23 words long. Each word is 16 bits, or 2 bytes long. So what we're actually saying in this statement is that our descriptor is 46 bytes long. If you add up the lengths (in bytes) of LOCK1-LENGTH, LOCK1-SET-NAME, LOCK1-ITEM-NAME- LOCK1-RELOP, and LOCK1-ITEM-VALUE, they do indeed add up to 46 bytes. The fact that this value is specified in halfwords is a holdover from the HP 3000's classic architecture, which was based on a 16-bit word size. Although the HP e3000 is now based on a 32/64-bit PA-RISC architecture, this field was left "as-is" in order to maintain compatibility with the older systems.

Next we need to specify the name of the dataset that contains the record we want to lock. Statement number 98700 initializes LOCK1-SET-NAME to "ORDER-DETAIL;". Once again, the dataset name must be left justified, and terminated with a semi-colon or blank. And once again, we could have used a dataset number (obtained from DBINFO) instead for slightly greater efficiency.

The next two statements (98800 and 98900) are being used to create a relational operator that specifies which record we're interested in locking. Assume that the records in the ORDER-DETAIL dataset have a search item named ORDER-NO. The lock-descriptor specifies that we're interested in locking the record with ORDER-NO equal to the value found in LOCK1-ITEM-VALUE. (Remember that we prompted the user for this value in statement 97400, so it's already been initialized for us.)

Now that the lock descriptor has been built, we're ready to call DBLOCK, which happens in statements 99400 through 99700. Since this is an unconditional lock, we know that DBLOCK won't return any non-zero condition codes to the calling program unless there's been a catastrophic error, which we test for in statement 100200.

Avoiding the Deadly Embrace

We mentioned that we could have used code very similar to this if we had wanted to lock two or more records. Suppose we wanted to lock two records. All we would have had to do differently would have been to initialize NUMBER-OF-LOCKS to a value of 2, and describe the second lock descriptor in the variables shown in statements number 2050 through 2090. The important thing to understand is that, because we are asking for both locks at the same time, we avoid running the risk of falling into the "deadly embrace" trap that we discussed last time. Let's briefly review what we said about deadly embraces in our previous article.

A "deadly embrace" can happen when two processes ask to lock the same two records, but in different order. Let's look at a scenario which can lead to a deadly embrace. Suppose user A asks to lock record 1, and when he gets it, calls DBLOCK again in order to try to lock record 2. Note that A is calling DBLOCK twice-once to get the lock on record 1, and then again to get another lock for record 2. In order to understand what's so bad about doing this, suppose that user B does the same thing, but in the opposite order: B asks to lock record 2, and then calls DBLOCK again to lock record 1.

This is the classic deadly embrace scenario. User B has record 2 locked, but cannot proceed until he is able to lock record 1. User A has record 1 locked, but cannot proceed until he is able to lock record 2. But A can't lock record 2 because it is already locked by user B. A and B are waiting on one another, and will do so forever (or at least until the system is rebooted).

The best way to prevent deadly embraces is for users A and B to ask for both locks at the same time. By passing two lock descriptors to DBLOCK, they are effectively saying, "We need both of these locks, and won't be able to proceed until you can give us both of them."

You might be wondering how DBLOCK prevents itself from getting into the same trouble. After all, when A and B each ask to lock records 1 and 2, what's to prevent DBLOCK from creating the same situation? What if user A's lock descriptor describes record 1 followed by record 2, while user B's lock descriptor has them in the opposite order? Won't DBLOCK get into the same trouble that A and B got into when they were calling DBLOCK multiple times?

The answer is no, and the reason lies is in the fact that DBLOCK does not process the locks in the order in which they are supplied by the calling program. DBLOCK will, in fact, process the requests in a random order, but (and this is the important part) it will use the same order on behalf of any user that calls it. The problem that led to the deadly embrace was rooted in the fact that A and B asked for their locks in a different order. A asked for 1 followed by 2–where B asked for 2 followed by 1.

If A and B had both asked for 1 followed by 2, then the deadly embrace could not have happened. And that's exactly how DBLOCK avoids the error. Suppose both A and B call DBLOCK once, each passing it lock descriptors specifying both record 1 and record 2 in any order. If DBLOCK locks 1 before 2 on behalf of A, then it will process B's lock requests in exactly the same order, and the deadly embrace will be avoided.

In spite of these safeguards, there is still one situation that programmers must watch out for in order to avoid deadlocks such as the deadly embrace. The code shown in example was for an unconditional lock (mode 5) of a single record. What if we had elected to use mode 6, which is for unconditional locking and specified multiple records?

When called in mode 6, DBLOCK will return control to the calling program whether or not it is able to obtain the locks that were requested. If you requested locks for multiple entries, then DBLOCK will return control to the calling program, holding whatever locks it was able to get. In other words, if you ask to lock five records, and DBLOCK is only able to lock four of them, it will return control to you and report an error. But you will still be holding the four locks that it was able to get. And it's your responsibility to release them.

In order to avoid deadlocks, avoid calling DBLOCK while you're already holding locks. Think back to the "best practices" that we discussed at the beginning of this article. When you obtain a lock, do whatever you need to do and release it—quickly. If a program calls DBLOCK while it's already holding a lock, the consequences can be disastrous. In order to discourage programmers from doing this, MPE will only allow a process to do so if the program was linked with MR capability. Access to this capability is typically controlled by the system manager. In any case remember, if you're calling DBLOCK in mode 6 with multiple entries, and DBLOCK returns an error code of 25 (the entry or entries are already locked—see Table 1), it's critically important that you release any locks you might be holding before attempting to call DBLOCK again.

Releasing locks is achieved by calling DBUNLOCK:
101200 RELEASE-A-LOCK.
103000     DISPLAY SPACE.
103100
103900     MOVE 1    TO DB-MODE.
104200
104300     CALL INTRINSIC "DBUNLOCK" USING DB-NAME,
104400                           IGNORED-PARM,
104500                           DB-MODE,
104600                           STATUS-ARRAY.
105000
105100     IF CONDITION-CODE NOT= 0
105200       PERFORM IMAGE-ERROR.
105300
105400     DISPLAY "The Lock was successfully released!".
105500     DISPLAY SPACE.
105600
A call to DBUNLOCK will release all locks that are currently being held by the calling process, so there's no need to tell it which locks you wish to unlock. One good programming practice that will go a long way toward preventing deadly embrace situations is to call DBUNLOCK immediately after determining that a CONDITIONAL lock request has failed. And of course, you should always release any locks that you're holding as soon as you're done with the record, dataset, or database that you're locking.


George Stachnik works in Hewlett-Packard's Commercial Systems Division in Cupertino, California. He is responsible for the development and delivery of e3000 training.
  ←Part 31  Part 33→
     [3khat]3kRanger   [3khat]3kMail   Updated