Skip to content

Commit 8d41f64

Browse files
authored
Merge pull request #19074 from NickLaMuro/ansible-runner-use-flock-with-git-repositories
[GitRepository] Use multi-process file locking for git actions
2 parents 45da279 + b38bfb3 commit 8d41f64

File tree

2 files changed

+68
-10
lines changed

2 files changed

+68
-10
lines changed

app/models/git_repository.rb

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ 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")
8+
9+
attr_reader :git_lock
710

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

@@ -86,6 +89,31 @@ def update_repo
8689
@updated_repo = true
8790
end
8891

92+
# Configures a file lock in LOCKFILE_DIR so that only a single process has
93+
# access to make changes to the `GitWorktree` at a time. Assumes the record
94+
# has been saved, since there is no way store (clone, fetch, pull, etc.) the
95+
# git data to disk if there isn't a `id`.
96+
#
97+
# Only a single `@git_lock` can be aquired per-process, and do avoid
98+
# deadlocks, his method is just a passthrough if `@git_lock` has already been
99+
# defined (another method has already started a `git_transaction`.
100+
#
101+
# This means that you can surround a couple of actions with this method, and
102+
# the lock will only be enforced on the top level.
103+
#
104+
# NOTE: However, it is worth noting that if two threads in the same process
105+
# try to share the same instance while using `git_transation` is not thread
106+
# safe, so avoid sharing `GitRepository` objects across multiple threads
107+
# (chances are you won't run into this scenario, but commenting just in case)
108+
#
109+
# Return value is the result of the yielded block
110+
def git_transaction
111+
should_unlock = acquire_git_lock
112+
yield
113+
ensure
114+
release_git_lock if should_unlock
115+
end
116+
89117
private
90118

91119
def ensure_refreshed
@@ -110,7 +138,7 @@ def refresh_branches
110138
end
111139

112140
def refresh_tags
113-
with_worktree do
141+
with_worktree do |worktree|
114142
current_tags = git_tags.index_by(&:name)
115143
worktree.tags.each do |tag|
116144
info = worktree.tag_info(tag)
@@ -128,7 +156,7 @@ def refresh_tags
128156

129157
def worktree
130158
@worktree ||= begin
131-
clone_repo unless Dir.exist?(directory_name)
159+
clone_repo_if_missing
132160
fetch_worktree
133161
end
134162
end
@@ -137,13 +165,17 @@ def fetch_worktree
137165
GitWorktree.new(worktree_params)
138166
end
139167

140-
def clone_repo
141-
handling_worktree_errors do
142-
message = "Cloning #{url} to #{directory_name}..."
143-
_log.info(message)
144-
GitWorktree.new(worktree_params.merge(:clone => true, :url => url))
145-
@updated_repo = true
146-
_log.info("#{message}...Complete")
168+
def clone_repo_if_missing
169+
git_transaction do
170+
unless Dir.exist?(directory_name)
171+
handling_worktree_errors do
172+
message = "Cloning #{url} to #{directory_name}..."
173+
_log.info(message)
174+
GitWorktree.new(worktree_params.merge(:clone => true, :url => url))
175+
@updated_repo = true
176+
_log.info("#{message}...Complete")
177+
end
178+
end
147179
end
148180
end
149181

@@ -155,6 +187,32 @@ def handling_worktree_errors
155187
raise MiqException::Error, err.message
156188
end
157189

190+
def git_lock_filename
191+
@git_lock_filename ||= LOCKFILE_DIR.join(id.to_s)
192+
end
193+
194+
def acquire_git_lock
195+
return false if git_lock
196+
197+
FileUtils.mkdir_p(LOCKFILE_DIR)
198+
199+
@git_lock = File.open(git_lock_filename, File::RDWR | File::CREAT, 0o644)
200+
@git_lock.flock(File::LOCK_EX) # block waiting for lock
201+
@git_lock.write("#{Process.pid} - #{Time.zone.now}\n") # for debugging
202+
@git_lock.flush # write current data
203+
@git_lock.truncate(@git_lock.pos) # clean up remaining chars
204+
205+
true
206+
end
207+
208+
def release_git_lock
209+
return if git_lock.nil?
210+
211+
@git_lock.flock(File::LOCK_UN)
212+
@git_lock.close
213+
@git_lock = nil
214+
end
215+
158216
def worktree_params
159217
params = {:path => directory_name}
160218
params[:certificate_check] = method(:self_signed_cert_cb) if verify_ssl == OpenSSL::SSL::VERIFY_NONE

spec/models/git_repository_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
tag_info_hash[name]
9898
end
9999

100-
expect(repo).to receive(:clone_repo).once.with(no_args).and_call_original
100+
expect(repo).to receive(:clone_repo_if_missing).once.with(no_args).and_call_original
101101
expect(GitWorktree).to receive(:new).with(anything).and_return(gwt)
102102
expect(gwt).to receive(:fetch_and_merge).with(no_args)
103103

0 commit comments

Comments
 (0)