Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling concurrent file access in Common Lisp

Multiple users need to access the same directory of files using an interface created in Common Lisp. Many race conditions appear when this happens. For example, when more than one user adds or deletes a file with the same time. Is there a way within lisp to "lock" a specific directory while work is being done? This would be a similar concept to the "synchronized" block in a multithreaded environment, but I have separate Lisp instances. I am using Allegro CL on Windows.

Edit: Ideas for a different solution to this problem would also be appreciated.

like image 792
ElliotPenson Avatar asked Feb 12 '23 12:02

ElliotPenson


1 Answers

OS-level

CLISP provides stream-lock and with-stream-lock which interface to fcntl or LockFileEx. These will lock both open streams and files.

You can use FFI to call those OS function in other CL implementations.

A directory is merely a (special) file, so fcntl should be able to lock it (one has to think carefully about what it means to "write to a directory" though).

Windows world is much more complicated though. I don't think it is possible to lock a directory using a library function.

App-level

You can implement collaborative locking yourself. This would mean that only the applications using your library would respect the locking, so you will be able to fix possible issues outside the app.

E.g. (untested!):

(defun file-lock (f)
  "return the name of the lock file for this file"
  (concatenate 'sting f "-my-lock-suffix")) ; or use pathname functions...
(defun lock-file-once (f)
  "try to lock file once"
  (open (file-lock f) :direction :probe :if-exists nil))
(defun lock-file (f)
  "block until the file is locked"
  (loop :until (lock-file-once f)
    :do (sleep 1)))
(defun unlock-file (f)
  "remove the lock"
  (delete-file (file-lock f)))
(defmacro with-lock-file (f &body body)
  "lock the file, run body, unlock it"
  (let ((fn (gensym "with-lock-file-f")))
    `(let ((,fn ,f))
       (unwind-protect
            (progn (lock-file ,fn)
                   ,@body)
         (unlock-file ,fn)))))

Locking whole directories would require non-trivial finesse to avoid deadlocks: locking a directory means locking all its descendants, so acquiring a lock on a file requires first locking everyting above that file, then locking the file, then unlocking everyhing above. This opens us to a race condition.

The simple solution is to have a master lock which is required for any locking operation:

(defvar *master-lock* (pathname .....))
(defun lock-file-or-directory-once (path)
  "lock file or directory or fail"
  (with-lock-file *master-lock*
    scan everything below and also above(!) path
    return nil if any relevant locks are found,
    i.e., if anything below path is locked
    or any directory above path is locked))
(defun lock-file-or-directory (path)
  "block until success"
  (loop :until (lock-file-or-directory path)
    :do (sleep 1)))
like image 162
sds Avatar answered Mar 15 '23 05:03

sds