Skip to content

Commit 436daa2

Browse files
committed
[GitRepository] Add #git_transaction method
And also defines the following private helper methods and constants: - `LOCKFILE_DIR` # Where the locks are stored - `#git_lock_filename` # name of the lockfile for this record - `#acquire_git_lock` # fetches a lock for the instance (one per) - `#release_git_lock` # releases existing lock for other processes `#git_transaction` is meant to be a multi-purpose method for dealing with any file system changes so that only one process is able to interact with it at a time. According to the docs (`ri File#flock`), `File#flock(FILE::LOCK_EX)` should block until the lock is available for the process, so the first process to acquire the lock will process first. The method is also designed so that nested `#git_transaction` calls will no-op and not release the lock pre-maturely. This is important for running multiple actions that by design need to happen by a single process, but the sub actions are also configured to use the lock. Implementation of this method will be done in a followup commit, but the method is also meant to be a public interface so that when existing methods implement this method, locks will work just fine with user defined `git_transaction` blocks as well as with GitRepository method API.
1 parent f136c1a commit 436daa2

File tree

1 file changed

+51
-0
lines changed

1 file changed

+51
-0
lines changed

app/models/git_repository.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class GitRepository < ApplicationRecord
44
include AuthenticationMixin
55

66
GIT_REPO_DIRECTORY = Rails.root.join('data/git_repos')
7+
LOCKFILE_DIR = GIT_REPO_DIRECTORY.join("locks")
78

89
validates :url, :format => URI::regexp(%w(http https file)), :allow_nil => false
910

@@ -86,6 +87,26 @@ def update_repo
8687
@updated_repo = true
8788
end
8889

90+
# Configures a file lock in LOCKFILE_DIR so that only a single process has
91+
# access to make changes to the `GitWorktree` at a time. Assumes the record
92+
# has been saved, since there is no way store (clone, fetch, pull, etc.) the
93+
# git data to disk if there isn't a `id`.
94+
#
95+
# Only a single `@git_lock` can be aquired per-process, and do avoid
96+
# deadlocks, his method is just a passthrough if `@git_lock` has already been
97+
# defined (another method has already started a `git_transaction`.
98+
#
99+
# This means that you can surround a couple of actions with this method, and
100+
# the lock will only be enforced on the top level.
101+
#
102+
# Return value is the result of the yielded block
103+
def git_transaction
104+
should_unlock = acquire_git_lock
105+
yield
106+
ensure
107+
release_git_lock if should_unlock
108+
end
109+
89110
private
90111

91112
def ensure_refreshed
@@ -155,6 +176,36 @@ def handling_worktree_errors
155176
raise MiqException::Error, err.message
156177
end
157178

179+
def git_lock_filename
180+
@git_lock_filename ||= LOCKFILE_DIR.join(id.to_s)
181+
end
182+
183+
def acquire_git_lock
184+
return false if defined? @git_lock || !@git_lock.nil?
185+
186+
FileUtils.mkdir_p(LOCKFILE_DIR)
187+
188+
@git_lock = File.open(git_lock_filename, File::RDWR | File::CREAT, 0o644)
189+
@git_lock.flock(File::LOCK_EX) # block waiting for lock
190+
@git_lock.write("#{Process.pid} - #{Time.zone.now}\n") # for debugging
191+
@git_lock.flush # write current data
192+
@git_lock.truncate(@git_lock.pos) # clean up remaining chars
193+
194+
true
195+
end
196+
197+
def release_git_lock
198+
return unless defined? @git_lock || @git_lock.nil?
199+
200+
@git_lock.flock(File::LOCK_UN)
201+
@git_lock.close
202+
203+
# TODO: unlink? Unsure how to handle this if other processes are waiting
204+
# on this though...
205+
206+
@git_lock = nil
207+
end
208+
158209
def worktree_params
159210
params = {:path => directory_name}
160211
params[:certificate_check] = method(:self_signed_cert_cb) if verify_ssl == OpenSSL::SSL::VERIFY_NONE

0 commit comments

Comments
 (0)