Handling Concurrency

The standard http://www.strozzi.it/cgi-bin/CSA/tw7/I/en_US/CSA CSA library provides functions that are needed to handle concurrent access to the data by multiple programs, with the objective of preserving file integrity during update operations. Concurrency is handled through the use of lock-files, or semaphores. These can be created/destroyed with the relevant CSA library functions csaLock and csaUnlock. Handling locks is a tricky subject, due to the possibility of race-conditions that may lead to either data corruption or dead-locks. The former can occur whenever we have a test operation, followed by an action based on the result of the test. This test-action sequence is non-atomical, hence the potential for races.

Preventing dead-locks is another issue that the library functions try to cope with, although for this to be effective a CSA application program must be designed sensibly in the first place.

As a rule-of-thumb, semaphores MUST be set whenever any of the following conditions are met:

A program needs to write/delete data. If the operation is based on a previous read operation, the lock MUST be set before the read (reading+writing is non-atomical): in the CSA parliance this condition is called Non-Atomical Write (NAW).

A program needs to read data from multiple files, for instance as a result of a join operation between two table-files (i.e. a view). If there is a chance that one or more of the involved files can be modified while we are reading them, then all the files SHOULD be locked before we start reading from any of them. I call this condition a Non-Atomical Read (NAR). In common database terminology, we need to make sure that nobody can break the referential integrity between the data that we are about to read from multiple, phisically-indipendent but logically-related, sources.

Whatever the locking policy, it is left entirely up to the CSA application program. The CSA library merely provides the locking primitives, but whether to lock individual files, whole directories, or other application-dependent collections of resources is an application-level affair from the CSA point of view.

Explicitly releasing lock-files is usually not necessary, as they are removed automatically when a program calls csaExit or one of its variants ( csaExit.ok, csaExit.fault, etc.). Removing one or more locks with an explicit call to csaUnlock may be necessary only if a program needs to iterate multiple times before exiting, and in general when it is expected to go on for a while before calling csaExit. In such cases we SHOULD release the locks when we no longer need them, or others may time-out while waiting to try and set their own locks on the same resources.

Example

A program that wants to read from, or write to a certain file, and that wants to make sure that nobody else changes that file in the meantime, can set a lock on that file as follows:

csaLock /path/to/file || csaExit.fault 0007 /path/to/file

csaExit.ok 0028

The csaLock function will create a file named /path/to/file.lock under the directory pointed to by CSA_LOCK_ROOT, returning non-zero if the operation fails. If the lock already exists, csaLock will keep trying for a configurable number of seconds, after which it will give-up and fail with a error.

In the example, if the lock fails, an error exit is taken, with a suitable error message. If locking succeedes, csaLock returns zero and the program continues (in this case it will simply exit with a positive completion message). It is normally not necessary to release a lock when it is no longer needed, as that will occur anyway when the program terminates with any of the CSA functions of the csaExit.* family.

There is a lot more to be said to use lock-files properly. I'll eventually expand this section a bit, to try and make it more useful.