In order to prevent undesirable interaction between processes, QM provides a system of locks. Consider, for example, a process that reads a record, decrements a value in that record and writes it back to the file. There is no problem here if only one process is operating. If, however, there is a second process performing a similar operation on the file, there is a danger that both processes read the record, then both write back an updated copy. Only one of the updates will actually occur as they both started from an identical version of the original data, unaware that there was another process updating the record.
Three types of lock are available on files; file locks, read locks and update locks. Each has a different role and care should be taken to use them correctly.
A file lock applies to the entire file and prevents any other user from obtaining any sort of lock on the file or the records therein. File locks are usually only needed during operations that must handle the file in a consistent "snap shot" manner when, for example, summing the values of some field from all records. A process can only acquire a file lock if no other process has any locks on the file or its records. Conversely, no other process can obtain any other sort of lock within the file while the file lock is active. Use of file locks can have a severe effect on performance and hence they should only be used where absolutely necessary.
A read lock (sometimes called a shared lock) applies to an individual record and prevents other processes from obtaining the file lock or an update lock on the same record. Any number of processes may acquire read locks on the same record at the same time. An attempt to obtain a read lock will fail if another process has the file locked or has an update lock on the record. Read locks can be used to ensure that a program sees a consistent view of a set of records, without the risk that some other process has changed any of these records.
An update lock also applies to an individual record and prevents other processes from obtaining the file lock or either type of record lock on the same record. Only one process may acquire an update lock on any individual record at a time. An attempt to obtain an update lock will fail if another process has the file is locked or has a read or update lock on the record.
A read or update lock may be acquired on a record that does not exist in the file. This provides a means of locking a record that is about to be written for the first time.
Read and update locks are obtained by the READL or READU statements (and others). These locks are released on closing the file, by the RELEASE statement or on return to the command prompt. Additionally, read and update locks are normally released by writing or deleting the locked record, however, the WRITEU and DELETEU statements provide a means of writing or deleting without releasing the lock.
Although it is possible to hold very many record locks at a time, this tends to indicate poor application design and may have an adverse effect on performance. The system wide limit on the number of concurrent locks is determined by the NUMLOCKS configuration parameter. If this limit is reached, the program will behave as though the record is locked by another user, taking the LOCKED clause of the relevant QMBasic statement with a STATUS() value of 0 or, if no LOCKED clause is present, waiting for space to become available. A message will be written to the system error log file. The MAXRLOCKS configuration parameter limits the number of record locks that a single process may hold at one time. Use of this optional limit can prevent run-away processes filling the internal lock tables.
Where an attempt to obtain a lock fails, the QMBasic language provides two methods of handling the situation. A program may either wait automatically for the lock to be released or it may regain control and take some action of its own (see the LOCKED clause of the QMBasic file handling statements). For compatibility with some other multivalue database products, the LOCK.BEEP mode of the OPTION command can be used to cause the system to emit a beep at the terminal once per second while waiting for a record lock or file lock.
A process is not affected by its own locks. Thus it is possible to take a record lock on a record from a file for which the file lock is held or, conversely, to take the file lock while holding one or more record locks. Taking a read lock on a record for which an update lock is held or vice versa will convert the lock type.
Locks are associated with the underlying operating system file, not the VOC reference to the file. QM will correctly track locks relating to the same file however it was opened, including when a file uses soft links on Linux and has multiple pathnames. Where a file has been opened more than once simultaneously by a single process, the locks are released on the final close of the file.
Locks are also associated with the particular file variable referenced when they are acquired. Thus, if an application opens the same file more than once, closing one of the file references will automatically release any locks acquired using that file variable but leave other locks in place. Similarly, use of the form of the RELEASE statement that takes only a file variable will release locks associated with that file variable. This mechanism ensures that developers do not need to be aware of how other modules within the application operate.
Lock Rule Enforcement
The lock handling operations of QMBasic only operate with correctly structured applications. The non-locking versions of the READ and WRITE operations, etc, take no part in the locking system and hence can access a file regardless of its lock state. The individual statement descriptions give more information.
A correctly written application never writes or deletes a record without locking it first, however, for compatibility with other multivalue database products, this is not enforced by default except within transactions. A poorly written program that uses READ in place of READU and then writes the record could overwrite a record that is locked by another user. The MUSTLOCK configuration parameter can be used to enforce tight control of locks, eliminating this potential problem.
Enabling lock rule enforcement may not be easy when porting existing applications to QM. Because multivalue systems have not enforced these rules in the past, programmers sometimes omitted use of locks in situations where there would never be contention. For example, overnight processing might not use locks because the developer knew that it is run at a time when there are no other users on the system. Programs that write a record that is known not to exist also appear not to need locks. Both of these examples actually represent bad programming practice as the assumption made by the developer may subsequently turn out not to be true due to changes in business operation.
A deadlock occurs when two processes are each waiting for a lock held by the other process. For example, process 1 locks record A and process 2 locks record B. Process 1 then goes on to lock record B, causing it to wait for the lock to be released by process 2. If at this stage process 2 tries to lock record A, the two processes are waiting for each other. Deadlocks can be more complex, involving multiple processes.
Deadlocks are totally avoidable by careful application design. If in the above example a rule had been adopted such that record A must always be locked before record B, a deadlock cannot occur. Setting a sequence in which locks must be acquired (e.g. order, customer, part) is gives a simple solution but may not always be practical. An order entry system in which locks on the part records are maintained until entry of the order is complete will expose the system to deadlocks if, for example, one customer orders parts 101 and 102 at the same time as another customer orders parts 102 and 101. In this case, the best solution is to devise a scheme where part records do not remain locked.
The default behaviour of QM is to allow deadlocks to happen, leaving it to the developer to analyse and resolve the cause. Setting the DEADLOCK configuration parameter to a non-zero value causes QM to check whether a deadlock would occur if the process waited for a lock that is held by another process. If a deadlock would occur, the process attempting to get the lock is aborted. A diagnostic messages showing the locks involved is displayed.
Setting the DEADLOCK configuration parameter to 2 extends the diagnostics by recording the state of the locking system in an item named deadlock.n in the temp subdirectory of the QMSYS account where n is the QM user number. The information written has four lines for each lock and a blank line between locks. Line 1 has the QM user number and user name. Line 2 has the internal file number and its pathname. Line 3 has the lock type and, for a record lock, the record id. On an ECS mode system, the record id is encoded to UTF-8 form. Line 4 shows the date/time at which the lock was acquired.
Note that resolution of a deadlock by aborting a process carries a risk of data integrity errors if the process was performing a related series of updates some of which had already been written when the deadlock occurred. Use of transaction programming would ensure that the entire update sequence could be rolled back at the abort.
QM also provides task locks, sometimes known as process synchronisation locks, that are not related to any particular file and are typically used to ensure that some task cannot be performed by two users simultaneously.
A task lock is simply an numbered entry in a 64 element locking table. A process acquires a task lock using the LOCK command or the QMBasic LOCK statement. If the lock is already owned by another user, the process either waits for it to be released or handles the situation for itself. On completion of the task, the process can release the lock using the CLEAR.LOCKS command or the QMBasic UNLOCK statement.
Task locks can be difficult to use because the lock is not related to a file, record, etc and the application designer must choose one that is not also used for some other purpose. Because task locks are shared across all accounts, this implies a possible unwanted interaction between different applications.