Class: Amp::Repositories::Lock

Inherits:
Object
  • Object
show all
Defined in:
lib/amp/repository/lock.rb

Overview

Lock

Manages a given lock file, indicating that the enclosing folder should not be modified. Typically used during destructive operations on a repo (such as a commit or push).

We must be compatible with Mercurial’s lock format, unfortunately. Doesn’t life suck?

From Mercurial code, explaining their format:

lock is symlink on platforms that support it, file on others.

symlink is used because create of directory entry and contents are atomic even over nfs.

old-style lock: symlink to pid new-style lock: symlink to hostname:pid

Constant Summary collapse

@@host =
nil

Instance Method Summary collapse

Constructor Details

#initialize(file, opts = {:timeout => -1}) ⇒ Lock

Initializes the lock to a given file name, and creates the lock, effectively locking the containing directory.

Parameters:

  • file (String)

    the path to the the lock file to create

  • opts (Hash<Symbol => Object>) (defaults to: {:timeout => -1})

    the options to use when creating the lock

  • [Integer] (Hash)

    a customizable set of options

  • [Proc, (Hash)

    a customizable set of options

  • [String] (Hash)

    a customizable set of options



36
37
38
39
40
41
42
43
# File 'lib/amp/repository/lock.rb', line 36

def initialize(file, opts={:timeout => -1})
  @file = file
  @held = false
  @timeout = opts[:timeout]
  @release_fxn = opts[:release_fxn]
  @description = opts[:desc]
  apply_lock
end

Instance Method Details

#apply_lockObject

Applies the lock. Will sleep the thread for timeout time trying to apply the lock before giving up and raising an error.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/amp/repository/lock.rb', line 48

def apply_lock
  timeout = @timeout
  while true do
    begin
      # try_lock will raise of there is already a lock.
      try_lock
      return true
    rescue LockHeld => e
      # We'll put up with this exception for @timeout times, then give up.
      if timeout != 0
        sleep(1)
        timeout > 0 && timeout -= 1
        next
      end
      # Timeout's up? Raise an exception.
      raise LockHeld.new(Errno::ETIMEDOUT::Errno, e.filename, @desc, e.locker)
    end
  end
end

#make_a_lock(file, info) ⇒ Object

Creates a lock at the given location, with info about the locking process. Uses a symlink if possible, because even over NFS, creating a symlink is atomic. Nice. Otherwise, it will call make_a_lock_in_file on inferior OS’s (cough windows cough) and put the data in there.

The symlink is actually a non-working symlink - it points the filename (such as “hglock”) to the data, even though the data is not an actual file. So hglock -> “medgar:25043” is a sort-of possible lock this method would create.

Parameters:

  • file (String)

    the filename of the lock

  • info (String)

    the info to store in the lock



103
104
105
106
107
108
109
110
111
# File 'lib/amp/repository/lock.rb', line 103

def make_a_lock(file, info)
  begin
    File.symlink(info, file)
  rescue Errno::EEXIST
    raise
  rescue
    make_a_lock_in_file(file, info)
  end
end

#make_a_lock_in_file(file, info) ⇒ Object

Creates a lock at the given location, storing the info about the locking process in an actual lock file. These locks are not preferred, because symlinks are atomic even over NFS. Anyway, very simple. Create the file, write in the info, close ‘er up. That’s 1 line in ruby, folks.

Parameters:

  • file (String)

    the filename of the lock

  • info (String)

    the info to store in the lock

See Also:



122
123
124
# File 'lib/amp/repository/lock.rb', line 122

def make_a_lock_in_file(file, info)
  File.open(file, "w+") {|out| out.write info }
end

#read_lock(file) ⇒ String

Reads in the data associated with a lock file.

Parameters:

  • file (String)

    the path to the lock file to read

Returns:

  • (String)

    the data in the lock. In the format “#locking_host:#locking_pid”



131
132
133
134
135
136
137
# File 'lib/amp/repository/lock.rb', line 131

def read_lock(file)
  begin
    return File.readlink(file)
  rescue Errno::EINVAL, Errno::ENOSYS
    return File.read(file)
  end
end

#releaseObject

Releases the lock, signalling that it is now safe to modify the directory in which the lock is found.



195
196
197
198
199
200
201
202
# File 'lib/amp/repository/lock.rb', line 195

def release
  if @held
    @held = false
    @release_fxn.call if @release_fxn
    
    File.unlink(@file) rescue ""
  end
end

#test_lockObject

Text from mercurial code:

return id of locker if lock is valid, else None.

If old-style lock, we cannot tell what machine locker is on. with new-style lock, if locker is on this machine, we can see if locker is alive. If locker is on this machine but not alive, we can safely break lock.

The lock file is only deleted when None is returned.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/amp/repository/lock.rb', line 170

def test_lock
  locker = read_lock(@file)
  host, pid = locker.split(":", 1)
  return locker if pid.nil? || host != @@host
  
  pid = pid.to_i
  return locker if pid == 0
  
  return locker if test_pid pid
  
  # if locker dead, break lock.  must do this with another lock
  # held, or can race and break valid lock.
  begin
    the_lock = Lock.new(@file + ".break")
    the_lock.try_lock
    File.unlink(@file)
    the_lock.release
  rescue LockError
    return locker
  end
end

#test_pid(pid) ⇒ Boolean

Checks to see if there is a process running with id pid.

Parameters:

  • pid (Fixnum)

    the process ID to look up

Returns:

  • (Boolean)

    is there a process with the given pid?



144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/amp/repository/lock.rb', line 144

def test_pid(pid)
  return true if Platform::OS == :vms
  
  begin
    # Doesn't actually kill it
    Process.kill(0, pid)
    true
  rescue Errno::ESRCH::Errno
    true
  rescue
    false
  end
end

#try_lockObject

Attempts to apply the lock. Raises if unsuccessful. Contains the logic for actually naming the lock.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/amp/repository/lock.rb', line 71

def try_lock
  if @@host.nil?
    @@host = Socket.gethostname
  end
  lockname = "#{@@host}:#{Process.pid}"
  while !@held
    begin
      make_a_lock(@file, lockname)
      @held = true
    rescue Errno::EEXIST
      locker = test_lock
      unless locker.nil?
        raise LockHeld.new(Errno::EAGAIN::Errno, @file, @desc, locker)
      end
    rescue SystemCallError => e
      raise LockUnavailable.new(e.errno, e.to_s, @file, @desc)
    end
  end
end