DEV: Put a mutex around `Upload.migrate_to_new_scheme`.

DEV: Put a mutex around Upload.migrate_to_new_scheme.

This ensures that only one migration is running at any given point in time across the instances.

diff --git a/app/models/upload.rb b/app/models/upload.rb
index 0453578..ef92afe 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -205,97 +205,105 @@ class Upload < ActiveRecord::Base
   def self.migrate_to_new_scheme(limit: nil)
     problems = []
 
-    if SiteSetting.migrate_to_new_scheme
-      max_file_size_kb = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes
-      local_store = FileStore::LocalStore.new
-      db = RailsMultisite::ConnectionManagement.current_db
+    DistributedMutex.synchronize("migrate_upload_to_new_scheme") do
+      if SiteSetting.migrate_to_new_scheme
+        max_file_size_kb = [
+          SiteSetting.max_image_size_kb,
+          SiteSetting.max_attachment_size_kb
+        ].max.kilobytes
 
-      scope = Upload.by_users.where("url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/#{db}%'").order(id: :desc)
+        local_store = FileStore::LocalStore.new
+        db = RailsMultisite::ConnectionManagement.current_db
 
-      scope = scope.limit(limit) if limit
+        scope = Upload.by_users
+          .where("url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/#{db}%'")
+          .order(id: :desc)
 
-      if scope.count == 0
-        SiteSetting.migrate_to_new_scheme = false
-        return problems
-      end
+        scope = scope.limit(limit) if limit
 
-      remap_scope = nil
-
-      scope.each do |upload|
-        begin
-          # keep track of the url
-          previous_url = upload.url.dup
-          # where is the file currently stored?
-          external = previous_url =~ /^\/\//
-          # download if external
-          if external
-            url = SiteSetting.scheme + ":" + previous_url
-
-            begin
-              retries ||= 0
-
-              file = FileHelper.download(
-                url,
-                max_file_size: max_file_size_kb,
-                tmp_file_name: "discourse",
-                follow_redirect: true
-              )
-            rescue OpenURI::HTTPError
-              retry if (retires += 1) < 1
-              next
-            end
+        if scope.count == 0
+          SiteSetting.migrate_to_new_scheme = false
+          return problems
+        end
 
-            path = file.path
-          else
-            path = local_store.path_for(upload)
-          end
-          # compute SHA if missing
-          if upload.sha1.blank?
-            upload.sha1 = Upload.generate_digest(path)
-          end
+        remap_scope = nil
+
+        scope.each do |upload|
+          begin
+            # keep track of the url
+            previous_url = upload.url.dup
+            # where is the file currently stored?
+            external = previous_url =~ /^\/\//
+            # download if external
+            if external
+              url = SiteSetting.scheme + ":" + previous_url
+
+              begin
+                retries ||= 0
+
+                file = FileHelper.download(
+                  url,
+                  max_file_size: max_file_size_kb,
+                  tmp_file_name: "discourse",
+                  follow_redirect: true
+                )
+              rescue OpenURI::HTTPError
+                retry if (retires += 1) < 1
+                next
+              end
+
+              path = file.path
+            else
+              path = local_store.path_for(upload)
+            end
+            # compute SHA if missing
+            if upload.sha1.blank?
+              upload.sha1 = Upload.generate_digest(path)
+            end
 
-          # store to new location & update the filesize
-          File.open(path) do |f|
-            upload.url = Discourse.store.store_upload(f, upload)
-            upload.filesize = f.size
-            upload.save!(validate: false)
-          end
-          # remap the URLs
-          DbHelper.remap(UrlHelper.absolute(previous_url), upload.url) unless external
-
-          DbHelper.remap(
-            previous_url,
-            upload.url,
-            excluded_tables: %w{
-              posts
-              post_search_data
-            }
-          )
-
-          remap_scope ||= begin
-            Post.with_deleted
-              .where("raw ~ '/uploads/#{db}/\\d+/' OR raw ~ '/uploads/#{db}/original/\\d{1}/'")
-              .select(:raw, :cooked)
-              .all
-          end
+            # store to new location & update the filesize
+            File.open(path) do |f|
+              upload.url = Discourse.store.store_upload(f, upload)
+              upload.filesize = f.size
+              upload.save!(validate: false)
+            end
+            # remap the URLs
+            DbHelper.remap(UrlHelper.absolute(previous_url), upload.url) unless external
+
+            DbHelper.remap(
+              previous_url,
+              upload.url,
+              excluded_tables: %w{
+                posts
+                post_search_data
+              }
+            )
+
+            remap_scope ||= begin
+              Post.with_deleted
+                .where("raw ~ '/uploads/#{db}/\\d+/' OR raw ~ '/uploads/#{db}/original/\\d{1}/'")
+                .select(:raw, :cooked)
+                .all
+            end
 
-          remap_scope.each do |post|
-            post.raw.gsub!(previous_url, upload.url)
-            post.cooked.gsub!(previous_url, upload.url)
-            post.save! if post.changed?
-          end
+            remap_scope.each do |post|
+              post.raw.gsub!(previous_url, upload.url)
+              post.cooked.gsub!(previous_url, upload.url)
+              post.save! if post.changed?
+            end
 
-          upload.optimized_images.find_each(&:destroy!)
-          upload.rebake_posts_on_old_scheme
-          # remove the old file (when local)
-          unless external
-            FileUtils.rm(path, force: true)
+            upload.optimized_images.find_each(&:destroy!)
+            upload.rebake_posts_on_old_scheme
+            # remove the old file (when local)
+            unless external
+              FileUtils.rm(path, force: true)
+            end
+          rescue => e
+            problems << { upload: upload, ex: e }
+          ensure
+            file&.unlink
+            file&.close
           end
-        rescue => e
-          problems << { upload: upload, ex: e }
-        ensure
-          file&.unlink
-          file&.close
         end
       end
     end

GitHub sha: 3c9495b9