|
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.")
Table 1:
Missing stachtb1.jsp |
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.
Table 2:
Missing stachtb2.jsp |
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.
|