@@ -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
0 commit comments