1 /*
   2  * Copyright (c) 2013, Joyent, Inc. All rights reserved.
   3  */
   4 // vim: set sts=4 sw=4 et:
   5 
   6 // Ensure we're using the platform's node
   7 require('/usr/node/node_modules/platform_node_version').assert();
   8 
   9 var mod_path = require('path');
  10 var mod_fs = require('fs');
  11 var mod_assert = require('assert');
  12 
  13 var mod_lockfd = require('/usr/node/0.8/node_modules/lockfd');
  14 
  15 // If we fail to lock (i.e. the call to fcntl() in node-lockfd), and
  16 // the errno is in this list, then we should back off for some delay
  17 // and retry.  Note that an EDEADLK, in particular, is not necessarilly
  18 // a permanent failure in a program using multiple lock files through
  19 // multiple threads of control.
  20 var RETRY_CODES = [
  21     'EAGAIN',
  22     'ENOLCK',
  23     'EDEADLK'
  24 ];
  25 var RETRY_DELAY = 250; // ms
  26 
  27 var LOCKFILE_MODE = 0644;
  28 
  29 var LOCKFILES = [];
  30 var NEXT_HOLDER_ID = 1;
  31 
  32 function lockfile_create(path) {
  33     path = mod_path.normalize(path);
  34 
  35     mod_assert.strictEqual(lockfile_lookup(path), null);
  36 
  37     var lf = {
  38         lf_path: path,
  39         lf_state: 'UNLOCKED',
  40         lf_cbq: [],
  41         lf_fd: -1,
  42         lf_holder_id: -1
  43     };
  44 
  45     LOCKFILES.push(lf);
  46 
  47     return (lf);
  48 }
  49 
  50 function lockfile_lookup(path) {
  51     path = mod_path.normalize(path);
  52 
  53     for (var i = 0; i < LOCKFILES.length; i++) {
  54         var lf = LOCKFILES[i];
  55 
  56         if (lf.lf_path === path)
  57             return (lf);
  58     }
  59 
  60     return (null);
  61 }
  62 
  63 // Make an unlock callback for this lockfile to hand to the waiter for whom
  64 // we acquired the lock:
  65 function lockfile_make_unlock(lf) {
  66     var holder_id;
  67 
  68     mod_assert.strictEqual(lf.lf_holder_id, -1);
  69 
  70     lf.lf_holder_id = holder_id = ++NEXT_HOLDER_ID;
  71 
  72     return (function __unlock(ulcb) {
  73         mod_assert.strictEqual(lf.lf_holder_id, holder_id,
  74             'mismatched lock holder or already unlocked');
  75         lf.lf_holder_id = -1;
  76 
  77         mod_assert.strictEqual(lf.lf_state, 'LOCKED');
  78         mod_assert.notStrictEqual(lf.lf_fd, -1);
  79 
  80         lf.lf_state = 'UNLOCKING';
  81 
  82         mod_fs.close(lf.lf_fd, function (err) {
  83             lf.lf_state = 'UNLOCKED';
  84             lf.lf_fd = -1;
  85 
  86             ulcb(err);
  87 
  88             lockfile_dispatch(lf);
  89         });
  90     });
  91 }
  92 
  93 function lockfile_dispatch(lf) {
  94     if (lf.lf_state !== 'UNLOCKED')
  95         return;
  96 
  97     if (lf.lf_cbq.length === 0) {
  98         // No more waiters to service for now.
  99         return;
 100     }
 101 
 102     lockfile_to_locking(lf);
 103 }
 104 
 105 function lockfile_to_locking(lf) {
 106     mod_assert.strictEqual(lf.lf_state, 'UNLOCKED');
 107     mod_assert.strictEqual(lf.lf_fd, -1);
 108 
 109     lf.lf_state = 'LOCKING';
 110 
 111     // Open the lock file, creating it if it does not exist:
 112     mod_fs.open(lf.lf_path, 'w+', LOCKFILE_MODE, function __opencb(err, fd) {
 113         mod_assert.strictEqual(lf.lf_state, 'LOCKING');
 114         mod_assert.strictEqual(lf.lf_fd, -1);
 115 
 116         if (err) {
 117             lf.lf_state = 'UNLOCKED';
 118 
 119             // Dispatch error to the first waiter
 120             lf.lf_cbq.shift()(err);
 121             lockfile_dispatch(lf);
 122             return;
 123         }
 124 
 125         lf.lf_fd = fd;
 126 
 127         // Attempt to get an exclusive lock on the file via our file
 128         // descriptor:
 129         mod_lockfd.lockfd(lf.lf_fd, function __lockfdcb(_err) {
 130             mod_assert.strictEqual(lf.lf_state, 'LOCKING');
 131 
 132             if (_err) {
 133                 var do_retry = (RETRY_CODES.indexOf(_err.code) !== -1);
 134 
 135                 // We could not lock the file, so we should close our fd now:
 136                 mod_fs.close(lf.lf_fd, function __closecb(__err) {
 137                     // It would be most unfortunate to fail here:
 138                     mod_assert.ifError(__err);
 139 
 140                     lf.lf_fd = -1;
 141                     lf.lf_state = 'UNLOCKED';
 142 
 143                     if (do_retry) {
 144                         // Back off and try again.
 145                         setTimeout(function __tocb() {
 146                             lockfile_dispatch(lf);
 147                         }, RETRY_DELAY);
 148                         return;
 149                     }
 150 
 151                     // Report the condition to the first waiter:
 152                     lf.lf_cbq.shift()(_err);
 153 
 154                     lockfile_dispatch(lf);
 155                 });
 156                 return;
 157             }
 158 
 159             lf.lf_state = 'LOCKED';
 160 
 161             // Dispatch locking success to first waiter, with unlock callback:
 162             lf.lf_cbq.shift()(null, lockfile_make_unlock(lf));
 163         });
 164     });
 165 }
 166 
 167 exports.lock = function (path, callback) {
 168     var lf = lockfile_lookup(path);
 169     if (!lf) {
 170         lf = lockfile_create(path);
 171     }
 172 
 173     lf.lf_cbq.push(callback);
 174 
 175     lockfile_dispatch(lf);
 176 
 177 };