rails_async_migrations 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +138 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +171 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +162 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/generators/rails_async_migrations/install_generator.rb +22 -0
  14. data/lib/generators/rails_async_migrations/templates/create_async_schema_migrations.rb +11 -0
  15. data/lib/rails_async_migrations/config.rb +13 -0
  16. data/lib/rails_async_migrations/connection/active_record.rb +59 -0
  17. data/lib/rails_async_migrations/migration/check_queue.rb +72 -0
  18. data/lib/rails_async_migrations/migration/fire_migration.rb +56 -0
  19. data/lib/rails_async_migrations/migration/lock.rb +84 -0
  20. data/lib/rails_async_migrations/migration/method_added.rb +24 -0
  21. data/lib/rails_async_migrations/migration/overwrite.rb +28 -0
  22. data/lib/rails_async_migrations/migration/run.rb +73 -0
  23. data/lib/rails_async_migrations/migration/unlock.rb +42 -0
  24. data/lib/rails_async_migrations/migration.rb +23 -0
  25. data/lib/rails_async_migrations/models/async_schema_migration.rb +18 -0
  26. data/lib/rails_async_migrations/mutators/base.rb +9 -0
  27. data/lib/rails_async_migrations/mutators/trigger_callback.rb +57 -0
  28. data/lib/rails_async_migrations/mutators/turn_async.rb +22 -0
  29. data/lib/rails_async_migrations/mutators.rb +24 -0
  30. data/lib/rails_async_migrations/railtie.rb +7 -0
  31. data/lib/rails_async_migrations/tracer.rb +23 -0
  32. data/lib/rails_async_migrations/version.rb +3 -0
  33. data/lib/rails_async_migrations/workers/sidekiq/check_queue_worker.rb +16 -0
  34. data/lib/rails_async_migrations/workers/sidekiq/fire_migration_worker.rb +18 -0
  35. data/lib/rails_async_migrations/workers.rb +58 -0
  36. data/lib/rails_async_migrations.rb +35 -0
  37. data/lib/tasks/rails_async_migrations.rake +6 -0
  38. data/rails_async_migrations.gemspec +30 -0
  39. metadata +193 -0
@@ -0,0 +1,72 @@
1
+ # we check the state of the queue and launch run worker if needed
2
+ module RailsAsyncMigrations
3
+ module Migration
4
+ class CheckQueue
5
+ def initialize
6
+ end
7
+
8
+ def perform
9
+ Tracer.new.verbose 'Check queue has been triggered'
10
+
11
+ return if has_failures?
12
+ return if has_on_going?
13
+ return if no_migration?
14
+
15
+ pending!
16
+ fire_migration
17
+ end
18
+
19
+ private
20
+
21
+ def fire_migration
22
+ Tracer.new.verbose "Migration `#{current_migration.id}` will now be processed"
23
+ Workers.new(:fire_migration).perform(current_migration.id)
24
+ end
25
+
26
+ def pending!
27
+ current_migration.update state: 'pending'
28
+ end
29
+
30
+ def current_migration
31
+ created_migration
32
+ end
33
+
34
+ def processing_migration
35
+ @processing_migration ||= AsyncSchemaMigration.processing.first
36
+ end
37
+
38
+ def pending_migration
39
+ @pending_migration ||= AsyncSchemaMigration.pending.first
40
+ end
41
+
42
+ def created_migration
43
+ @created_migration ||= AsyncSchemaMigration.created.first
44
+ end
45
+
46
+ def no_migration?
47
+ unless current_migration
48
+ Tracer.new.verbose 'No available migration in queue, cancelling check'
49
+ true
50
+ end
51
+ end
52
+
53
+ def has_on_going?
54
+ if pending_migration || processing_migration
55
+ Tracer.new.verbose 'Another migration under progress, cancelling check'
56
+ true
57
+ end
58
+ end
59
+
60
+ def has_failures?
61
+ if failed_migration
62
+ Tracer.new.verbose 'Failing migration blocking the queue, cancelling check'
63
+ true
64
+ end
65
+ end
66
+
67
+ def failed_migration
68
+ @failed_migration ||= AsyncSchemaMigration.failed.first
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,56 @@
1
+ # we check the state of the queue and launch run worker if needed
2
+ module RailsAsyncMigrations
3
+ module Migration
4
+ class FireMigration
5
+ attr_reader :migration
6
+
7
+ def initialize(migration_id)
8
+ @migration = AsyncSchemaMigration.find(migration_id)
9
+ end
10
+
11
+ def perform
12
+ return if done?
13
+
14
+ process!
15
+ run_migration
16
+ done!
17
+
18
+ check_queue
19
+ end
20
+
21
+ private
22
+
23
+ def check_queue
24
+ Workers.new(:check_queue).perform
25
+ end
26
+
27
+ def run_migration
28
+ Migration::Run.new(migration.direction, migration.version).perform
29
+ rescue Exception => exception
30
+ failed_with! exception
31
+ raise
32
+ end
33
+
34
+ def done?
35
+ if migration.state == 'done'
36
+ Tracer.new.verbose "Migration #{migration.id} is already `done`, cancelling fire"
37
+ return
38
+ end
39
+ end
40
+
41
+ def process!
42
+ migration.update! state: 'processing'
43
+ end
44
+
45
+ def done!
46
+ migration.update! state: 'done'
47
+ Tracer.new.verbose "Migration #{migration.id} was correctly processed"
48
+ end
49
+
50
+ def failed_with!(error)
51
+ migration.update! state: 'failed'
52
+ Tracer.new.verbose "Migration #{migration.id} failed with exception `#{error}`"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,84 @@
1
+ # locks any class methods depending on a configuration list
2
+ # this allow us to ignore migration without making
3
+ # a parallel pipeline system
4
+ module RailsAsyncMigrations
5
+ module Migration
6
+ class Lock
7
+ attr_reader :resource_class, :method_name
8
+
9
+ def initialize(resource_class, method_name)
10
+ @resource_class = resource_class
11
+ @method_name = method_name
12
+ end
13
+
14
+ def perform
15
+ return false unless locked_method?
16
+ return false if unlocked?
17
+
18
+ preserve_method_logics
19
+
20
+ suspend_lock do
21
+ overwrite_method
22
+ end
23
+ end
24
+
25
+ def suspend_lock(&block)
26
+ unlock
27
+ yield if block_given?
28
+ lock
29
+ end
30
+
31
+ private
32
+
33
+ def unlocked?
34
+ locked == false
35
+ end
36
+
37
+ def clone_method_name
38
+ "async_#{method_name}"
39
+ end
40
+
41
+ def preserve_method_logics
42
+ resource_class.define_method(clone_method_name, &captured_method)
43
+ end
44
+
45
+ def captured_method
46
+ resource_class.new.method(method_name).clone
47
+ end
48
+
49
+ def overwrite_method
50
+ resource_class.define_method(method_name, &overwrite_closure)
51
+ end
52
+
53
+ def overwrite_closure
54
+ proc do
55
+ Overwrite.new(self, __method__).perform
56
+ end
57
+ end
58
+
59
+ def lockable?
60
+ unlocked? && locked_method?
61
+ end
62
+
63
+ def locked_method?
64
+ RailsAsyncMigrations.config.locked_methods.include? method_name
65
+ end
66
+
67
+ def lock
68
+ self.locked = true
69
+ end
70
+
71
+ def unlock
72
+ self.locked = false
73
+ end
74
+
75
+ def locked=(value)
76
+ resource_class.instance_variable_set(:@locked, value)
77
+ end
78
+
79
+ def locked
80
+ resource_class.instance_variable_get(:@locked)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,24 @@
1
+ # any method added within the synchronous migration
2
+ # with asynchronous directive will trigger this class
3
+ module RailsAsyncMigrations
4
+ module Migration
5
+ class MethodAdded
6
+ attr_reader :resource_class, :method_name
7
+
8
+ def initialize(resource_class, method_name)
9
+ @resource_class = resource_class
10
+ @method_name = method_name
11
+ end
12
+
13
+ def perform
14
+ lock_and_overwrite
15
+ end
16
+
17
+ private
18
+
19
+ def lock_and_overwrite
20
+ Lock.new(resource_class, method_name).perform
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ module RailsAsyncMigrations
2
+ module Migration
3
+ class Overwrite
4
+ attr_reader :instance, :method_name
5
+
6
+ def initialize(instance, method_name)
7
+ @instance = instance
8
+ @method_name = method_name
9
+ end
10
+
11
+ def perform
12
+ dispatch_trace
13
+ trigger_callback
14
+ end
15
+
16
+ private
17
+
18
+ def dispatch_trace
19
+ Tracer.new.verbose "#{instance.class}\##{method_name} was called in a locked state"
20
+ end
21
+
22
+ def trigger_callback
23
+ instance.send(:trigger_callback, method_name)
24
+ rescue NoMethodError
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,73 @@
1
+ # to run actual migration we need to require the migration files
2
+ module RailsAsyncMigrations
3
+ module Migration
4
+ class Run
5
+ attr_reader :direction, :version, :migration
6
+
7
+ def initialize(direction, version)
8
+ @direction = direction
9
+ @version = version
10
+ @migration = migration_from version
11
+
12
+ ensure_data_consistency
13
+ require "#{Rails.root}/#{migration.filename}" if defined? Rails
14
+ end
15
+
16
+ def perform
17
+ unlock_migration_methods
18
+ delete_migration_state
19
+ run_migration
20
+ delete_migration_state
21
+ lock_migration_methods
22
+ end
23
+
24
+ private
25
+
26
+ def migration_from(version)
27
+ Connection::ActiveRecord.new(direction).migration_from version
28
+ end
29
+
30
+ def run_migration
31
+ migrator_instance.migrate
32
+ end
33
+
34
+ def migrator_instance
35
+ @migrator_instance ||= ::ActiveRecord::Migrator.new(direction.to_sym, [migration])
36
+ end
37
+
38
+ def schema_migration
39
+ @schema_migration ||= ActiveRecord::SchemaMigration.find_by(version: version)
40
+ end
41
+
42
+ def delete_migration_state
43
+ schema_migration&.delete
44
+ end
45
+
46
+ def class_name
47
+ migration.name.constantize
48
+ end
49
+
50
+ def locked_methods
51
+ RailsAsyncMigrations.config.locked_methods
52
+ end
53
+
54
+ def unlock_migration_methods
55
+ locked_methods.each do |method_name|
56
+ Migration::Unlock.new(class_name, method_name).perform
57
+ end
58
+ end
59
+
60
+ def lock_migration_methods
61
+ locked_methods.each do |method_name|
62
+ Migration::Lock.new(class_name, method_name).perform
63
+ end
64
+ end
65
+
66
+ def ensure_data_consistency
67
+ unless migration
68
+ raise RailsAsyncMigrations::Error, "No migration from version `#{version}`"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,42 @@
1
+ module RailsAsyncMigrations
2
+ module Migration
3
+ class Unlock
4
+ attr_reader :resource_class, :method_name
5
+
6
+ def initialize(resource_class, method_name)
7
+ @resource_class = resource_class
8
+ @method_name = method_name
9
+ end
10
+
11
+ def perform
12
+ restore_original_method
13
+ end
14
+
15
+ private
16
+
17
+ def restore_original_method
18
+ if valid?
19
+ Lock.new(resource_class, method_name).suspend_lock do
20
+ resource_class.define_method(method_name, &method_clone)
21
+ end
22
+ end
23
+ end
24
+
25
+ def valid?
26
+ temporary_instance.respond_to? clone_method_name
27
+ end
28
+
29
+ def clone_method_name
30
+ "async_#{method_name}"
31
+ end
32
+
33
+ def method_clone
34
+ temporary_instance.method(clone_method_name).clone
35
+ end
36
+
37
+ def temporary_instance
38
+ @temporary_instance ||= resource_class.new
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ require 'rails_async_migrations/migration/check_queue'
2
+ require 'rails_async_migrations/migration/fire_migration'
3
+ require 'rails_async_migrations/migration/lock'
4
+ require 'rails_async_migrations/migration/method_added'
5
+ require 'rails_async_migrations/migration/overwrite'
6
+ require 'rails_async_migrations/migration/run'
7
+ require 'rails_async_migrations/migration/unlock'
8
+
9
+ # when included this class is the gateway
10
+ # to the method locking system
11
+ module RailsAsyncMigrations
12
+ module Migration
13
+ def self.included(base)
14
+ base.extend ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+ def method_added(name)
19
+ MethodAdded.new(self, name).perform
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ class AsyncSchemaMigration < ActiveRecord::Base
2
+ validates :version, presence: true
3
+ validates :state, inclusion: { in: %w[created pending processing done failed] }
4
+ validates :direction, inclusion: { in: %w[up down] }
5
+
6
+ after_save :trace
7
+
8
+ scope :created, -> { where(state: 'created').by_version }
9
+ scope :pending, -> { where(state: 'pending').by_version }
10
+ scope :processing, -> { where(state: 'processing').by_version }
11
+ scope :done, -> { where(state: 'done').by_version }
12
+ scope :failed, -> { where(state: 'failed').by_version }
13
+ scope :by_version, -> { order(version: :asc) }
14
+
15
+ def trace
16
+ RailsAsyncMigrations::Tracer.new.verbose "Asynchronous migration `#{id}` is now `#{state}`"
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ module RailsAsyncMigrations
2
+ module Mutators
3
+ class Base
4
+ def migration_class
5
+ instance.class
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ module RailsAsyncMigrations
2
+ module Mutators
3
+ class TriggerCallback < Base
4
+ attr_reader :instance, :method_name
5
+
6
+ def initialize(instance, method_name)
7
+ @instance = instance
8
+ @method_name = method_name
9
+ end
10
+
11
+ # this method can be called multiple times (we should see what manages this actually)
12
+ # if you use up down and change it'll be called 3 times for example
13
+ def perform
14
+ unless active_record.allowed_direction?
15
+ Tracer.new.verbose "Direction `#{direction}` not allowed."
16
+ return
17
+ end
18
+
19
+ enqueue_asynchronous unless already_enqueued?
20
+ check_queue
21
+ end
22
+
23
+ private
24
+
25
+ def enqueue_asynchronous
26
+ AsyncSchemaMigration.create!(
27
+ version: active_record.current_version,
28
+ direction: active_record.current_direction,
29
+ state: 'created'
30
+ )
31
+ end
32
+
33
+ def already_enqueued?
34
+ AsyncSchemaMigration.find_by(
35
+ version: active_record.current_version,
36
+ direction: active_record.current_direction
37
+ )
38
+ end
39
+
40
+ def check_queue
41
+ Workers.new(:check_queue).perform
42
+ end
43
+
44
+ def active_record
45
+ @active_record ||= Connection::ActiveRecord.new(direction)
46
+ end
47
+
48
+ def direction
49
+ if instance.reverting?
50
+ :down
51
+ else
52
+ :up
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,22 @@
1
+ module RailsAsyncMigrations
2
+ module Mutators
3
+ class TurnAsync < Base
4
+ attr_reader :instance
5
+
6
+ def initialize(instance)
7
+ @instance = instance
8
+ end
9
+
10
+ def perform
11
+ Tracer.new.verbose '`turn_async` has been triggered'
12
+ alter_migration
13
+ end
14
+
15
+ private
16
+
17
+ def alter_migration
18
+ migration_class.include RailsAsyncMigrations::Migration
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ require 'rails_async_migrations/mutators/base'
2
+ require 'rails_async_migrations/mutators/turn_async'
3
+ require 'rails_async_migrations/mutators/trigger_callback'
4
+
5
+ # this is the entry point of the gem as it adds methods to the current migration class
6
+ # the `self` represents the class being ran so we have to be careful as not to conflict
7
+ # with the original ActiveRecord names
8
+ module RailsAsyncMigrations
9
+ module Mutators
10
+ private
11
+
12
+ def turn_async
13
+ TurnAsync.new(self).perform
14
+ end
15
+
16
+ # # the following methods are internal mechanics
17
+ # # do not overwrite those methods if you don't know
18
+ # # exactly what you're doing
19
+
20
+ def trigger_callback(method)
21
+ TriggerCallback.new(self, method).perform
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ module RailsAsyncMigrations
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ load 'tasks/rails_async_migrations.rake'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # log things and dispatch them wherever
2
+ # depending on the context mode
3
+ module RailsAsyncMigrations
4
+ class Tracer
5
+ def initialize
6
+ end
7
+
8
+ def verbose(text)
9
+ return unless verbose?
10
+ puts text
11
+ end
12
+
13
+ private
14
+
15
+ def verbose?
16
+ mode == :verbose
17
+ end
18
+
19
+ def mode
20
+ RailsAsyncMigrations.config.mode
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module RailsAsyncMigrations
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,16 @@
1
+ # we check the state of the queue and launch run worker if needed
2
+ module RailsAsyncMigrations
3
+ class Workers
4
+ module Sidekiq
5
+ class CheckQueueWorker
6
+ include ::Sidekiq::Worker
7
+
8
+ sidekiq_options queue: :default
9
+
10
+ def perform
11
+ Migration::CheckQueue.new.perform
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # we check the state of the queue and launch run worker if needed
2
+ module RailsAsyncMigrations
3
+ class Workers
4
+ module Sidekiq
5
+ class FireMigrationWorker
6
+ include ::Sidekiq::Worker
7
+
8
+ sidekiq_options queue: :default
9
+
10
+ def perform(migration_id)
11
+ Migration::FireMigration.new(
12
+ migration_id
13
+ ).perform
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,58 @@
1
+ # we check the state of the queue and launch run worker if needed
2
+ module RailsAsyncMigrations
3
+ class Workers
4
+ ALLOWED = [:check_queue, :fire_migration].freeze
5
+ attr_reader :called_worker
6
+
7
+ def initialize(called_worker)
8
+ @called_worker = called_worker # :check_queue, :fire_migration
9
+ ensure_worker_presence
10
+ end
11
+
12
+ def perform(args = nil)
13
+ return unless ALLOWED.include? called_worker
14
+ self.send called_worker, *args
15
+ end
16
+
17
+ private
18
+
19
+ def check_queue(*args)
20
+ case workers_type
21
+ when :sidekiq
22
+ Workers::Sidekiq::CheckQueueWorker.perform_async(*args)
23
+ when :delayed_job
24
+ ::Delayed::Job.enqueue Migration::CheckQueue.new
25
+ end
26
+ end
27
+
28
+ def fire_migration(*args)
29
+ case workers_type
30
+ when :sidekiq
31
+ Workers::Sidekiq::FireMigrationWorker.perform_async(*args)
32
+ when :delayed_job
33
+ ::Delayed::Job.enqueue Migration::FireMigration.new(*args)
34
+ end
35
+ end
36
+
37
+ def workers_type
38
+ RailsAsyncMigrations.config.workers
39
+ end
40
+
41
+ def ensure_worker_presence
42
+ case workers_type
43
+ when :sidekiq
44
+ require 'sidekiq'
45
+ require 'rails_async_migrations/workers/sidekiq/check_queue_worker'
46
+ require 'rails_async_migrations/workers/sidekiq/fire_migration_worker'
47
+
48
+ unless defined? ::Sidekiq::Worker
49
+ raise Error, 'Please install Sidekiq before to set it as worker adapter'
50
+ end
51
+ when :delayed_job
52
+ unless defined? ::Delayed::Job
53
+ raise Error, 'Please install Delayed::Job before to set it as worker adapter'
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end