filelock.rb 0.2 (c) C.Hintze 23mar2000

This module provides two classes to deal with lockfiles.

Instances of LockFile represents real lockfiles. Mostly these file have the
extension ".lock". If the instance is not any longer needed, it will delete
the lockfile during finalization.

Instances of LockedFile provides a possibility to open a certain file and
handle the lockfile mechanism transparent for the user. If a LockedFile is not
any longer needed it will be closed and the corresponding LockFile instance
will be removed during finalization.


What is it?
===========

Sometimes it is not possible to use fctl mechanism to lock/unlock files. May
it be that fctl doesn't properly work on a given UNIX, may it be that your
file resides on a mounted filesystem. UNIX mail delivery system itself use a
lock mechanism via lockfiles. For every file I want to lock a corresponding
<file>.lock file will be created. If such a lockfile already exists, than the
process have to wait until the lock was released (lockfile deleted) or have to
steal the lock to have access on that certain file.


Why you may need it?
====================

If, for example, you want to process your system mailbox it may happen that
while you are reading your mailbox, a new mail arrives. While you reading your
mailbox there is probably no problem. But if you also want to write your
mailbox this may create a big desaster.

Here you can use the LockedFile wrapper object. If you obtain the lock and
then process your mailbox you can be sure that in this very moment no write
access from your mail delivery system will occur, until you close that file
and therefore release the lock. See the function test2 at the bottom of the
file 'test.rb' to see how you can do it.


How to install?
===============

Very simple. First you have to unpack the filelock-x.y.tar.gz file. Then you
have to perform:

   ruby extconf.rb
   make
   make install

If you want to test your module after installation you could perform

   ruby lib/filelock.rb

then a small testsuite will be performed.


Algorithm:
==========

If there is no other lock, the lockfile will be created. Otherwise, it must be
decided whether the lock should be stolen or not.

If the lock should not be stolen, the process will sleep for 'sleep' seconds
and then check, whether the existant lock is vanished or not. If not it sleeps
again for 'sleep' seconds, ... etc. This all would be done for 'retry' times
or forever if 'retry' is nil.

If the lock should be stolen, then the process will first sleeps for 'steal'
seconds. Then the lockfile of the other process will be removed. After that,
the process again sleeps 'suspend' seconds, to give the process from which the
lock was stolen from, the possibility to retain its lock. If the other process
has not retained its lock, the lockfile will be created. That all would be
done for 'retry' times or forever if 'retry' is nil.

If the lock could not be obtained after 'retry' retries, an AcquireLockError
will be thrown.


Additonal Features:
===================

The methods LockedFile::open and LockFile::create_for are able to deal with 
blocks like File::open does. 

LockedFile::open will hands over the reference to the LockedFile instance as 
  parameter to the block and execute it. After executing, the LockedFile will
  be closed and the lock will be released.
  WARNING: You should not reopen the file within the block.  Doing so will
  raise a FrozenLockError.

LockFile::create_for will hands over the reference to a stolen-lock-checker
  lambda. You can invoke it with <reference>.call. It will return true, if the
  lock was stolen in the meantime; false otherwise. After the block is
  finished, the lock will be released if it was not stolen in the meantime.
  WARNING: You may unlock the lock explicity within the block, but you cannot
  obtain it again. Trying to do so will result in a FrozenLockError exception
  raised.


Examples:
=========

1. Read in a whole UNIX mailbox. During the read-in, no new mail can be
   delivered by the UNIX mailsystem.

   contents = LockedFile::open(mailbox) { |mb| mb.read() }

2. Another way to safety deal with your mailbox:

   LockFile::create_for(mailbox) do |not_ok|
      fd = open(mailbox, "r+")
      ...    # Do what you want with it. Filter mails, delete them
      ...    # It's up to you! But don't forget to call the checker
      ...    # regulary with not_ok.call
      fd.close
   end

   Here is a concrete but useless example:

   require "filelock"
   require "mailread"

   FileLock::LockFile::create_for(ARGV[0]) do |not_ok|
      fp = open(ARGV[0])
      until fp.eof? or not_ok.call
         mail = Mail.new(fp)
         print "Subj=#{mail.header['Subject']}\n"
         sleep 1
      end
      print "Lock was stolen!\n" if not_ok.call
      fp.close
   end

   But much more safety would be the following (NOT POSSIBLE USING
   Ruby 1.1c9!):

   require "filelock"
   require "mailread"

   fp = FileLock::LockedFile::open(ARGV[0])
   begin
      until fp.eof?
         mail = Mail(fp)
         print "Subj=#{mail.header['Subject']}\n"
         sleep 1
      end
   rescue FileLock::StolenLockError
      print "Lock was stolen!\n"
   ensure
      fp.close
   end
   
   Here the fact, that the lock was stolen, will be recognized on the 
   soonest time possible, whereas in the previous example, it will be only
   recognized during execution of until! That perhaps could be too late!!!


Copyright notice
================

This source is copyrighted, but you can freely use and copy it as long as you
don't change or remove the copyright notice:

Now coming to the warranty. There is no warranty! Never and in no case ;-)

I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I BE LIABLE FOR
ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
USE OR PERFORMANCE OF THIS SOFTWARE.


To-do:
    - Improve the speed, although I don't know how :-}
    - Use directories alternatively for files (mkdir is atomic)?

Bugs:
    - Slow compared with Ruby's file objects, as on every access it has to
      been checked, whether the lock is still valid yet.

    I have used the Python version of that module for some time without
    encoutering any problem. As Ruby is very new for me, and I have to adapt
    certain things to Ruby's way of doing, I don't know, if I have all done
    correct. So certainly there should be some errors. So please let me know.

                  mailto:c.hintze@gmx.net

    Describe what you have tried to do, and the contents of the
    version strings contained in the constant 'Revision' on top of the
    file 'filelock.rb' and '_filelock.c'. Please do not forget to
    attach an example, where the error occurs to enable me to
    reproduce the error.


Changes:
    0.2  Distribution include an extension written in C. It realizes the 
         createLock method as C function. This to minimize the time between
         existence check of file and file creation.
       
         Due to the new _filelock.c the distribution will now be delivered as
         .tar.gz file. It should be installed as every other distribution
         using 'mkmf.rb'.

    0.1  Creation
