synchronised_migration 2.2.0 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 178cde6a23ffc12e5074df16e0c8b2f5503ce460727607fc3af824c98a92bce1
4
- data.tar.gz: 0e3ebd525d0e55ffeabe3565f811ed3ff57ea272d042a3b72386ae6a627ba03c
3
+ metadata.gz: fdaaef7abd9ca7478b25d2e8ddc330db3aa699eb5a9027d3aac05b9626fc0a1b
4
+ data.tar.gz: ec0d069a1bedf336381d3241b9f869b2c41b8e25f4e6138abda5dc0b34bbdddd
5
5
  SHA512:
6
- metadata.gz: bc7a499baac225c7f3e11467f84c44eae41a2af3ef6c016b3a8b674fdd177c53930efb86ec5951da2ce5b220fb697ff87faa62ba557a3de14767293a10a71371
7
- data.tar.gz: 6d07aad5e40d68ac72c240f39875e2686416c1d2c38e1fb7555fe18626a47dbdc510116d51e6c19776ed5c22a3ef4e3bcc77b20b5c3672ed94098768dffdd35c
6
+ metadata.gz: 66a3e6dca6186f13049eae942c13cca91afa9652b6c3dfc69e3e693e5dd8433023f57f98fa3f94502cfb04edafeb323df2f90230b7b8e38f9cd48490b324744a
7
+ data.tar.gz: ae260978d7cca055fe2b361514482175c921361a4a00b944ba0bf3fecce24885f104b6bd4d3f19e205814481e7621f502f8398ad3bd85a55931b648288c87247
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Synchronised Migration
2
2
 
3
+ ## 3.0.0
4
+
5
+ - [OPS-201] Migrate library to CLI tool
6
+
3
7
  ## 2.2.0
4
8
 
5
9
  - [TT-8617] Update to build with github actions / ruby 3.0
data/README.md CHANGED
@@ -4,56 +4,67 @@
4
4
  [![Build Status](https://github.com/sealink/synchronised-migration-rb/workflows/Build%20and%20Test/badge.svg?branch=master)](https://github.com/sealink/synchronised-migration-rb/actions)
5
5
 
6
6
  This gem makes it possible to deploy multiple instances with data migration
7
- simultaneously. It uses Redis to ensure that there will be only one migration
7
+ simultaneously. It uses Redis to ensure that there will be only one migration
8
8
  running.
9
9
 
10
- This gem works out of the box with a Rails project. It should work with other
11
- Ruby projects so long as you load the rake task in Rakefile instead of relying
12
- on Railtie.
10
+ ## Usage
13
11
 
14
- This is a Ruby port of the same logic written in PHP in our [Craft
15
- Docker](https://github.com/sealink/craft-docker) project.
12
+ ### Migrating an application
16
13
 
17
- ## Usage
14
+ ```
15
+ bundle exec synchronised-migration migrate --config=migration.yml --version=1.2.3
16
+ ```
18
17
 
19
- Module `SynchronisedMigration` needs to be configured as below.
18
+ ### Checking lock stats
20
19
 
21
- ```ruby
22
- SynchronisedMigration.configure do |config|
23
- config.host = 'example.com'
24
- config.port = 6379
25
- config.db = 0
26
- end
20
+ ```
21
+ bundle exec synchronised-migration status --config=migration.yml --version=1.2.3
27
22
  ```
28
23
 
29
- Configuration can be called by using
30
- ```SynchronisedMigration.redis_config.host``` or similar.
24
+ ### Clearing locks
31
25
 
32
- You may override these settings through environment variables.
26
+ If a previous migration has failed a new one will not be allowed to start
27
+ unless it is first cleared.
33
28
 
34
29
  ```
35
- SYNCHRONISED_COMMAND=bin/launch/migrate
36
- WITH_CLEAN_BUNDLER_ENV=1 # Non-empty for true
37
- REDLOCK_TIMEOUT_MS=3600000
38
- REDLOCK_RETRY_DELAY_MS=200
39
- REDLOCK_LOCK_KEY=migration-in-progress
40
- REDLOCK_FAIL_KEY=migration-failed
30
+ bundle exec synchronised-migration clear --config=migration.yml
41
31
  ```
42
32
 
43
- Run this before you launch the application during deployment.
33
+ If you wish to clear all locks, \*including the running lock then you must also provide version
44
34
 
45
35
  ```
46
- $ rake synchronised_migrate:execute
36
+ bundle exec synchronised-migration clear --config=migration.yml --version=1.2.3 --all
37
+ ```
38
+
39
+ ### Configuration
40
+
41
+ | Key | Description |
42
+ | ------------------------ | --------------------------------------- |
43
+ | redis_uri | Redis server to use for locking |
44
+ | application | Application being migrated |
45
+ | version | Version being migrated too |
46
+ | debug | Enable additional debug information |
47
+ | command | Command to execute for the migration |
48
+ | timeout_milliseconds | Timeout to wait for the lock |
49
+ | retry_delay_milliseconds | Retry for the lock every x milliseconds |
50
+
51
+ ### Overriding config file options
52
+
53
+ You may override certain config options such as the `redis-uri` and the ```debug``` flag
54
+ by providing them via cli options.
55
+
56
+ ```
57
+ bundle exec synchronised-migration migrate --config=migration.yml --version=1.2.3 --redis_uri=redis://127.0.0.1:6379/0
47
58
  ```
48
59
 
49
60
  ## Release
50
61
 
51
62
  To publish a new version of this gem the following steps must be taken.
52
63
 
53
- * Update the version in the following files
64
+ - Update the version in the following files
54
65
  ```
55
66
  CHANGELOG.md
56
67
  lib/synchronised_migration/version.rb
57
- ````
58
- * Create a tag using the format v0.1.0
59
- * Follow build progress in GitHub actions
68
+ ```
69
+ - Create a tag using the format v0.1.0
70
+ - Follow build progress in GitHub actions
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "synchronised_migration"
6
+
7
+ Dry::CLI.new(SynchronisedMigration::Commands).call
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SynchronisedMigration
4
+ module Commands
5
+ extend Dry::CLI::Registry
6
+
7
+ class Migrate < Dry::CLI::Command
8
+ desc "Run the provided command in a migration lock"
9
+
10
+ option :config, type: :string, required: true, desc: "Configuration file location"
11
+ option :version, type: :string, required: true, desc: "Version we are migrating to"
12
+ option :application, type: :string, desc: "Application we are migrating"
13
+ option :redis_uri, type: :string, desc: "Redis Server URI"
14
+ option :debug, type: :boolean, desc: "Enable additional debugging output"
15
+
16
+ def call(**options)
17
+ abort "Config location must be provided" if options[:config].nil?
18
+ abort "Version must be provided" if options[:version].nil?
19
+
20
+ config = SynchronisedMigration::Configuration.from_cli(options)
21
+ result = SynchronisedMigration::Main.new(config).call
22
+
23
+ abort(result.error_msg) if result.failure?
24
+ $stdout.puts "Complete!"
25
+ end
26
+ end
27
+
28
+ class Clear < Dry::CLI::Command
29
+ desc "Clear the migration keys"
30
+
31
+ option :config, type: :string, required: true, desc: "Configuration file"
32
+ option :version, type: :string, required: true, desc: "Version we are migrating to"
33
+ option :application, type: :string, desc: "Application we are migrating"
34
+ option :redis_uri, type: :string, desc: "Redis Server URI"
35
+ option :debug, type: :boolean, desc: "Enable additional debugging output"
36
+ option :all, type: :boolean, default: false, desc: "Clear all keys including success and lock"
37
+
38
+ def call(**options)
39
+ abort "Config location must be provided" if options[:config].nil?
40
+
41
+ config = SynchronisedMigration::Configuration.from_cli(options)
42
+
43
+ redis_opts = {url: config.redis_uri}
44
+ redis_opts[:logger] = Logger.new($stdout) if config.debug?
45
+
46
+ redis = Redis.new(redis_opts)
47
+ redis.del(config.fail_key)
48
+
49
+ if options[:all]
50
+ abort "Version must be provided" if options[:version].nil?
51
+ redis.del(config.success_key)
52
+ redis.del(config.lock_key)
53
+ end
54
+
55
+ $stdout.puts "Complete!"
56
+ end
57
+ end
58
+
59
+ class Status < Dry::CLI::Command
60
+ desc "Shows the current status the redis failure if any"
61
+
62
+ option :config, type: :string, required: true, desc: "Configuration file"
63
+ option :version, type: :string, required: true, desc: "Version we are migrating to"
64
+ option :application, type: :string, desc: "Application we are migrating"
65
+ option :redis_uri, type: :string, desc: "Redis Server URI"
66
+ option :debug, type: :boolean, desc: "Enable additional debugging output"
67
+
68
+ def call(**options)
69
+ abort "Config location must be provided" if options[:config].nil?
70
+ abort "Version must be provided" if options[:version].nil?
71
+
72
+ config = SynchronisedMigration::Configuration.from_cli(options)
73
+
74
+ redis_opts = {url: config.redis_uri}
75
+ redis_opts[:logger] = Logger.new($stdout) if config.debug?
76
+ redis = Redis.new(redis_opts)
77
+
78
+ rows = []
79
+ fail_key = config.fail_key
80
+ rows << if redis.exists?(fail_key)
81
+ [fail_key, "true", "Failed Migration"]
82
+ else
83
+ [fail_key, "false", nil]
84
+ end
85
+
86
+ success_key = config.success_key
87
+ rows << if redis.exists?(success_key)
88
+ [success_key, "true", "successfully Migrated"]
89
+ else
90
+ [success_key, "false", nil]
91
+ end
92
+
93
+ lock_key = config.lock_key
94
+ rows << if redis.exists?(lock_key)
95
+ [lock_key, "true", "Running Migration"]
96
+ else
97
+ [lock_key, "false", nil]
98
+ end
99
+
100
+ table = TTY::Table.new(["key", "status", "message"], rows)
101
+ $stdout.puts table.render(:ascii)
102
+ end
103
+ end
104
+
105
+ register "migrate", Migrate
106
+ register "clear", Clear
107
+ register "status", Status
108
+ end
109
+ end
@@ -0,0 +1,50 @@
1
+ module SynchronisedMigration
2
+ class Configuration
3
+ attr_accessor :redis_uri
4
+ attr_accessor :command
5
+ attr_accessor :application
6
+ attr_accessor :debug
7
+ attr_accessor :version
8
+
9
+ def self.from_cli(options)
10
+ config_yml = YAML.load_file(options[:config]).transform_keys(&:to_sym).to_h
11
+ new(config_yml.merge(options.compact))
12
+ end
13
+
14
+ def initialize(options = {})
15
+ @application = options.fetch(:application, nil)
16
+ @version = options.fetch(:version, nil)
17
+ @redis_uri = options.fetch(:redis_uri, "redis://127.0.0.1:6379/0")
18
+ @debug = options.fetch(:debug, false)
19
+ @command = options.fetch(:command, "bin/launch/migrate")
20
+ @timeout_milliseconds = options.fetch(:timeout_milliseconds, 3_600_000).to_i
21
+ @retry_delay_milliseconds = options.fetch(:retry_delay_milliseconds, 3000).to_i
22
+
23
+ raise OptionParser::InvalidArgument, "Application must be configured" if @application.nil?
24
+ end
25
+
26
+ def timeout_milliseconds
27
+ @timeout_milliseconds.to_i
28
+ end
29
+
30
+ def retry_delay_milliseconds
31
+ @retry_delay_milliseconds.to_i
32
+ end
33
+
34
+ def debug?
35
+ debug
36
+ end
37
+
38
+ def success_key
39
+ "migration-success-#{application}-#{version}"
40
+ end
41
+
42
+ def fail_key
43
+ "migration-failed-#{application}"
44
+ end
45
+
46
+ def lock_key
47
+ "migration-lock-#{application}"
48
+ end
49
+ end
50
+ end
@@ -1,144 +1,84 @@
1
- require 'synchronised_migration'
2
- require 'synchronised_migration/result'
3
- require 'redis'
4
- require 'redlock'
5
- require 'singleton'
1
+ module SynchronisedMigration
2
+ class Main
3
+ FAIL_TIMEOUT = 3600 # 1 Hour
4
+ SUCCESS_TIMEOUT = 3600 * 24 * 30 # 30 dyas
6
5
 
7
- class SynchronisedMigration::Main
8
- include Singleton
9
-
10
- Result = SynchronisedMigration::Result
11
-
12
- class << self
13
- extend Forwardable
14
- def_delegators :instance, :call
15
- end
16
-
17
- def call
18
- done_or_execute
19
- end
20
-
21
- private
22
-
23
- def done_or_execute
24
- return Result.ok if migration_already_completed?
25
- lock_and_execute
26
- end
27
-
28
- def lock_and_execute
29
- redlock.lock! lock_key, timeout do
30
- execute
6
+ def initialize(config)
7
+ @config = config
31
8
  end
32
- end
33
9
 
34
- def execute
35
- return Result.ok if migration_already_completed?
36
- return Result.fail 'Halting the script because the previous migration failed.' if previous_failed?
37
- mark_failed
38
- migration_success = migrate
39
- return Result.fail 'Migration failed.' unless migration_success
40
- mark_successful
41
- remove_fail_marker
42
- return Result.ok
43
- end
10
+ def call
11
+ return Result.migration_already_completed if migration_already_completed?
44
12
 
45
- def migration_already_completed?
46
- return false if !success_key
47
- value = redis.get(success_key)
48
- not value.nil? and not value.empty?
49
- end
50
-
51
- def mark_successful
52
- if success_key
53
- redis.set success_key, timestamp, ex: 3600*24*30
13
+ lock_and_execute
54
14
  end
55
- end
56
-
57
- def previous_failed?
58
- value = redis.get(fail_key)
59
- not value.nil? and not value.empty?
60
- end
61
-
62
- def mark_failed
63
- redis.set fail_key, timestamp, ex: 3600
64
- end
65
15
 
66
- def remove_fail_marker
67
- redis.del fail_key
68
- end
69
-
70
- def migrate
71
- if with_clean_env?
72
- Bundler.with_original_env do
73
- Kernel.system target_command
16
+ private
17
+
18
+ def lock_and_execute
19
+ client = Redlock::Client.new(
20
+ [@config.redis_uri], {
21
+ retry_count: retry_count,
22
+ retry_delay: @config.retry_delay_milliseconds
23
+ }
24
+ )
25
+ client.lock!(@config.lock_key, @config.timeout_milliseconds) do
26
+ execute_within_lock
74
27
  end
75
- else
76
- Kernel.system target_command
77
28
  end
78
- $?.success?
79
- end
80
-
81
- def with_clean_env?
82
- not ENV.fetch('WITH_CLEAN_BUNDLER_ENV', '').empty?
83
- end
84
29
 
85
- def target_command
86
- ENV.fetch 'SYNCHRONISED_COMMAND', 'bin/launch/migrate'
87
- end
88
-
89
- def redis
90
- @redis ||= Redis.new(url: redis_url)
91
- end
30
+ def execute_within_lock
31
+ return Result.ok if migration_already_completed?
32
+ return Result.previous_migration_failed if previous_failed?
33
+ mark_failed
34
+ migration_success = migrate
35
+ return Result.migration_failed unless migration_success
36
+ mark_successful
37
+ remove_fail_marker
38
+ Result.ok
39
+ end
92
40
 
93
- def redlock
94
- @redlock ||= Redlock::Client.new(
95
- [ redis_url ], {
96
- retry_count: retry_count,
97
- retry_delay: retry_delay
98
- }
99
- )
100
- end
41
+ def migration_already_completed?
42
+ redis.exists?(@config.success_key)
43
+ end
101
44
 
102
- def redis_url
103
- sprintf(
104
- 'redis://%s:%s/%s',
105
- SynchronisedMigration.redis_config.host,
106
- SynchronisedMigration.redis_config.port,
107
- SynchronisedMigration.redis_config.db
108
- )
109
- end
45
+ def mark_successful
46
+ success_obj = {application: @config.application, version: @config.version, command: @config.command, timestamp: timestamp}.to_json
47
+ redis.set(@config.success_key, success_obj, ex: SUCCESS_TIMEOUT)
48
+ end
110
49
 
111
- def timestamp
112
- Time.now.to_i
113
- end
50
+ def previous_failed?
51
+ redis.exists?(@config.fail_key)
52
+ end
114
53
 
115
- def timeout
116
- ENV.fetch('REDLOCK_TIMEOUT_MS', 3_600_000).to_i
117
- end
54
+ def mark_failed
55
+ redis.set(@config.fail_key, timestamp, ex: FAIL_TIMEOUT)
56
+ end
118
57
 
119
- def retry_delay
120
- ENV.fetch('REDLOCK_RETRY_DELAY_MS', 3000).to_i
121
- end
58
+ def remove_fail_marker
59
+ redis.del(@config.fail_key)
60
+ end
122
61
 
123
- def retry_count
124
- timeout / retry_delay
125
- end
62
+ def migrate
63
+ Kernel.system @config.command
64
+ $?.success?
65
+ end
126
66
 
127
- def lock_key
128
- ENV.fetch 'REDLOCK_LOCK_KEY', 'migration-in-progress'
129
- end
67
+ def redis
68
+ @redis ||= begin
69
+ redis_opts = {url: @config.redis_uri}
70
+ redis_opts[:logger] = Logger.new($stdout) if @config.debug?
130
71
 
131
- def fail_key
132
- ENV.fetch 'REDLOCK_FAIL_KEY', 'migration-failed' + version_suffix
133
- end
72
+ Redis.new(redis_opts)
73
+ end
74
+ end
134
75
 
135
- def success_key
136
- return false if version_suffix.empty?
137
- 'migration-success' + version_suffix
138
- end
76
+ def timestamp
77
+ Time.now.to_i
78
+ end
139
79
 
140
- def version_suffix
141
- suffix = ENV.fetch 'REDLOCK_VERSION_SUFFIX', false
142
- suffix ? '-' + suffix : ''
80
+ def retry_count
81
+ @config.timeout_milliseconds / @config.retry_delay_milliseconds
82
+ end
143
83
  end
144
84
  end
@@ -1,21 +1,52 @@
1
- require 'synchronised_migration'
1
+ require "synchronised_migration"
2
2
 
3
- class SynchronisedMigration::Result
4
- attr_accessor :error
3
+ module SynchronisedMigration
4
+ MIGRATION_SUCCESS = 0
5
+ PREVIOUS_SUCCESS = 1
6
+ PREVIOUS_FAILED = 2
7
+ MIGRATION_FAILED = 3
5
8
 
6
- def initialize(error = nil)
7
- @error = error
8
- end
9
+ PREVIOUS_FAILED_MSG = "Halting the script because the previous migration failed."
10
+ MIGRATION_FAILED_MSG = "Migration command failed."
9
11
 
10
- def success?
11
- error.nil?
12
- end
12
+ class Result
13
+ attr_reader :code
13
14
 
14
- def self.ok
15
- self.new
16
- end
15
+ def initialize(code)
16
+ @code = code
17
+ end
18
+
19
+ def successful?
20
+ [MIGRATION_SUCCESS, PREVIOUS_SUCCESS].include?(code)
21
+ end
22
+
23
+ def failure?
24
+ !successful?
25
+ end
26
+
27
+ def error_msg
28
+ case code
29
+ when MIGRATION_FAILED
30
+ MIGRATION_FAILED_MSG
31
+ when PREVIOUS_FAILED
32
+ PREVIOUS_FAILED_MSG
33
+ end
34
+ end
35
+
36
+ def self.ok
37
+ new(MIGRATION_SUCCESS)
38
+ end
39
+
40
+ def self.migration_already_completed
41
+ new(PREVIOUS_SUCCESS)
42
+ end
43
+
44
+ def self.previous_migration_failed
45
+ new(PREVIOUS_FAILED)
46
+ end
17
47
 
18
- def self.fail(error)
19
- self.new error
48
+ def self.migration_failed
49
+ new(MIGRATION_FAILED)
50
+ end
20
51
  end
21
52
  end
@@ -1,3 +1,3 @@
1
1
  module SynchronisedMigration
2
- VERSION = '2.2.0'
2
+ VERSION = "3.0.0"
3
3
  end
@@ -1,22 +1,14 @@
1
1
  module SynchronisedMigration
2
- class << self
3
- attr_accessor :redis_config
4
- end
5
-
6
- def self.configure
7
- self.redis_config ||= Configuration.new
8
- yield(redis_config)
9
- end
10
-
11
- class Configuration
12
- attr_accessor :host, :port, :db
13
-
14
- def initialize
15
- @host = ''
16
- @port = 0
17
- @db = 0
18
- end
19
- end
20
2
  end
21
3
 
22
- require 'synchronised_migration/railtie' if defined?(Rails)
4
+ require "redis"
5
+ require "logger"
6
+ require "json"
7
+ require "redlock"
8
+ require "yaml"
9
+ require "dry/cli"
10
+ require "tty/table"
11
+ require "synchronised_migration/commands"
12
+ require "synchronised_migration/configuration"
13
+ require "synchronised_migration/main"
14
+ require "synchronised_migration/result"
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synchronised_migration
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alvin Yim
8
8
  - Stefan Cooper
9
9
  autorequire:
10
- bindir: bin
10
+ bindir: exe
11
11
  cert_chain: []
12
- date: 2021-01-06 00:00:00.000000000 Z
12
+ date: 2021-09-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redlock
@@ -27,6 +27,34 @@ dependencies:
27
27
  version: '0.2'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: redis
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 4.2.1
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 4.2.1
42
+ - !ruby/object:Gem::Dependency
43
+ name: dry-cli
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: tty-table
30
58
  requirement: !ruby/object:Gem::Requirement
31
59
  requirements:
32
60
  - - ">="
@@ -95,31 +123,50 @@ dependencies:
95
123
  - - ">="
96
124
  - !ruby/object:Gem::Version
97
125
  version: '3.5'
126
+ - !ruby/object:Gem::Dependency
127
+ name: standardrb
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: timecop
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
98
154
  description: Use Redis to record the data migration status
99
155
  email: support@travellink.com.au
100
- executables: []
156
+ executables:
157
+ - synchronised-migration
101
158
  extensions: []
102
159
  extra_rdoc_files: []
103
160
  files:
104
- - ".github/dependabot.yml"
105
- - ".github/workflows/release.yml"
106
- - ".github/workflows/ruby.yml"
107
- - ".gitignore"
108
- - ".ruby-version"
109
161
  - CHANGELOG.md
110
- - Gemfile
111
- - LICENSE
112
162
  - README.md
113
- - Rakefile
163
+ - exe/synchronised-migration
114
164
  - lib/synchronised_migration.rb
165
+ - lib/synchronised_migration/commands.rb
166
+ - lib/synchronised_migration/configuration.rb
115
167
  - lib/synchronised_migration/main.rb
116
- - lib/synchronised_migration/railtie.rb
117
168
  - lib/synchronised_migration/result.rb
118
169
  - lib/synchronised_migration/version.rb
119
- - lib/tasks/synchronised_migration.rake
120
- - spec/spec_helper.rb
121
- - spec/synchronised_migration/main_spec.rb
122
- - synchronised_migration.gemspec
123
170
  homepage: https://github.com/sealink/synchronised-migration-rb
124
171
  licenses: []
125
172
  metadata: {}
@@ -138,10 +185,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
185
  - !ruby/object:Gem::Version
139
186
  version: '0'
140
187
  requirements: []
141
- rubygems_version: 3.2.3
188
+ rubygems_version: 3.2.22
142
189
  signing_key:
143
190
  specification_version: 4
144
191
  summary: For deploying to multiple instances simultaneously
145
- test_files:
146
- - spec/spec_helper.rb
147
- - spec/synchronised_migration/main_spec.rb
192
+ test_files: []
@@ -1,6 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: "bundler"
4
- directory: "/"
5
- schedule:
6
- interval: "daily"
@@ -1,59 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
-
8
- jobs:
9
- build:
10
- name: Build
11
- runs-on: ubuntu-latest
12
- steps:
13
- - name: Checkout
14
- uses: actions/checkout@v2
15
- - uses: ruby/setup-ruby@v1
16
- with:
17
- bundler-cache: true
18
- - run: bundle exec rake
19
-
20
- release:
21
- needs: build
22
- name: Release
23
- runs-on: ubuntu-latest
24
- steps:
25
- - name: Checkout
26
- uses: actions/checkout@v2
27
-
28
- - name: Generate Changelog
29
- run: |
30
- # Get version from github ref (remove 'refs/tags/' and prefix 'v')
31
- version="${GITHUB_REF#refs/tags/v}"
32
- npx changelog-parser CHANGELOG.md | jq -cr ".versions | .[] | select(.version == \"$version\") | .body" > ${{ github.workflow }}-CHANGELOG.txt
33
-
34
- - name: Release
35
- uses: softprops/action-gh-release@v1
36
- with:
37
- body_path: ${{ github.workflow }}-CHANGELOG.txt
38
- env:
39
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40
-
41
- publish:
42
- needs: [build, release]
43
- name: Publish
44
- runs-on: ubuntu-latest
45
-
46
- steps:
47
- - uses: actions/checkout@v2
48
- - uses: ruby/setup-ruby@v1
49
-
50
- - name: Publish to RubyGems
51
- run: |
52
- mkdir -p $HOME/.gem
53
- touch $HOME/.gem/credentials
54
- chmod 0600 $HOME/.gem/credentials
55
- printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
56
- gem build *.gemspec
57
- gem push *.gem
58
- env:
59
- GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
@@ -1,16 +0,0 @@
1
- name: Build and Test
2
- on: [push, pull_request]
3
- jobs:
4
- test:
5
- strategy:
6
- fail-fast: false
7
- matrix:
8
- ruby: ["2.6", "2.7", "3.0"]
9
- runs-on: ubuntu-latest
10
- steps:
11
- - uses: actions/checkout@v2
12
- - uses: ruby/setup-ruby@v1
13
- with:
14
- ruby-version: ${{ matrix.ruby }}
15
- bundler-cache: true
16
- - run: bundle exec rake
data/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- /.bundle
2
- /vendor
3
- /coverage
4
- /*.gem
5
- Gemfile.lock
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.0.0
data/Gemfile DELETED
@@ -1,2 +0,0 @@
1
- source 'https://rubygems.org'
2
- gemspec
data/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2017 SeaLink Travel Group Limited
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- require "bundler/gem_tasks"
2
-
3
- desc 'Default: run specs.'
4
- task :default => :spec
5
-
6
- require 'rspec/core/rake_task'
7
-
8
- desc "Run specs"
9
- RSpec::Core::RakeTask.new do |t|
10
- t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
11
- # Put spec opts in a file named .rspec in root
12
- end
@@ -1,5 +0,0 @@
1
- class SynchronisedMigration::Railtie < Rails::Railtie
2
- rake_tasks do
3
- load 'tasks/synchronised_migration.rake'
4
- end
5
- end
@@ -1,9 +0,0 @@
1
- require 'synchronised_migration/main'
2
-
3
- namespace :synchronised_migration do
4
- task :execute do
5
- result = SynchronisedMigration::Main.call
6
- next if result.success?
7
- fail result.error
8
- end
9
- end
data/spec/spec_helper.rb DELETED
@@ -1,9 +0,0 @@
1
- require 'pry'
2
- require 'simplecov'
3
- require 'simplecov-rcov'
4
-
5
- SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
6
- SimpleCov.minimum_coverage 100
7
- SimpleCov.start do
8
- add_filter %r{^/vendor}
9
- end
@@ -1,165 +0,0 @@
1
- require 'spec_helper'
2
- require 'synchronised_migration/main'
3
-
4
- describe SynchronisedMigration::Main do
5
- subject { described_class }
6
- let(:result) { subject.call }
7
-
8
- context 'when the prerequisites are meet' do
9
- let(:redis) { double }
10
- let(:redlock) { double }
11
- let(:fail_marker_value) { nil }
12
- let(:success_marker_value) { nil }
13
- let(:version_suffix) { 'bork' }
14
- let(:set_version_suffix) { ENV['REDLOCK_VERSION_SUFFIX'] = version_suffix }
15
- let(:time_value) { double(to_i: 123456789) }
16
-
17
- before do
18
- set_version_suffix
19
-
20
- subject.instance.instance_variable_set :@redis, nil
21
- subject.instance.instance_variable_set :@redlock, nil
22
-
23
- allow(subject.instance).to receive(:execute).and_call_original
24
- allow(subject.instance).to receive(:migrate).and_call_original
25
-
26
- allow(Redis).to receive(:new).and_return(redis)
27
- allow(redis).to receive(:get) { |key|
28
- case key
29
- when "migration-failed-#{version_suffix}"
30
- fail_marker_value
31
- when "migration-failed"
32
- fail_marker_value
33
- when "migration-success-#{version_suffix}"
34
- success_marker_value
35
- when "migration-success"
36
- success_marker_value
37
- else
38
- raise "invalid key for redis get: #{key}"
39
- end
40
- }
41
- allow(redis).to receive(:set)
42
- allow(redis).to receive(:del)
43
-
44
- allow(Redlock::Client).to receive(:new).and_return(redlock)
45
- allow(redlock).to receive(:lock!) { |lock_key, timeout, &block| block.call }
46
-
47
- allow(Time).to receive(:now).and_return(time_value)
48
-
49
- allow(Kernel).to receive(:system).and_wrap_original { |method, *args|
50
- next if args == [ 'bin/launch/migrate' ]
51
- method.call *args
52
- }
53
-
54
- allow(Bundler).to receive(:with_original_env).and_call_original
55
-
56
- SynchronisedMigration.configure do |config|
57
- config.host = 'example.com'
58
- config.port = 6379
59
- config.db = 0
60
- end
61
- end
62
-
63
- context 'in the happy path' do
64
- it 'executes the migration successfully' do
65
- expect(result).to be_success
66
- expect(redlock).to have_received(:lock!)
67
- expect(redis).to have_received(:get).with('migration-failed-bork')
68
- expect(redis).to have_received(:set).with('migration-failed-bork', 123456789, ex: 3600)
69
- expect(redis).to have_received(:set).with('migration-success-bork', 123456789, ex: 3600*24*30)
70
- expect(Kernel).to have_received(:system)
71
- expect(Bundler).not_to have_received(:with_original_env)
72
- expect(redis).to have_received(:del).with('migration-failed-bork')
73
- end
74
-
75
- context 'and migration completed previously' do
76
- let(:success_marker_value) { '1' }
77
- it 'contines without executing' do
78
- expect(result).to be_success
79
- expect(redlock).not_to have_received(:lock!)
80
- end
81
- end
82
-
83
- context 'executing in lock waiter' do
84
- let(:result2) { subject.call }
85
-
86
- before do
87
- # Note: bypasses the first success flag check so it can enter lock.
88
- expect(result).to be_success
89
- allow(redis).to receive(:get).and_return(nil, '1')
90
- end
91
-
92
- it 'early exits and does not execute again', :aggregate_failures do
93
- expect(result2).to be_success
94
- expect(redlock).to have_received(:lock!).exactly(2).times
95
- expect(Kernel).to have_received(:system).exactly(1).times
96
- expect(subject.instance).to have_received(:execute).exactly(2).times
97
- expect(subject.instance).to have_received(:migrate).exactly(1).times
98
- end
99
- end
100
- end
101
-
102
- context 'when require a clean Bundler environment' do
103
- before do
104
- allow(ENV).to receive(:fetch).and_call_original
105
- allow(ENV).to receive(:fetch).with('WITH_CLEAN_BUNDLER_ENV', '').and_return('1')
106
- end
107
-
108
- it 'executes it with a clean Bundler environment' do
109
- expect(result).to be_success
110
- expect(Kernel).to have_received(:system)
111
- expect(Bundler).to have_received(:with_original_env)
112
- end
113
- end
114
-
115
- context 'after a deployment failed previously' do
116
- let(:fail_marker_value) { '1' }
117
-
118
- it "doesn't execute the migration" do
119
- expect(result).not_to be_success
120
- expect(Kernel).not_to have_received(:system)
121
- end
122
- end
123
-
124
- context 'when the task crashed' do
125
- it 'marks the failure in Redis' do
126
- fork { exit 1 }
127
- Process.wait
128
-
129
- expect(result).not_to be_success
130
- expect(redis).to have_received(:set).with('migration-failed-bork', 123456789, ex: 3600)
131
- expect(redis).not_to have_received(:del)
132
- end
133
- end
134
-
135
- context 'without version suffix' do
136
- let(:set_version_suffix) { ENV.delete 'REDLOCK_VERSION_SUFFIX' }
137
-
138
- context 'in the happy path' do
139
- it 'executes the migration successfully' do
140
- fork { exit 0 }
141
- Process.wait
142
-
143
- expect(result).to be_success
144
- expect(redlock).to have_received(:lock!)
145
- expect(redis).to have_received(:get).with('migration-failed')
146
- expect(redis).to have_received(:set).with('migration-failed', 123456789, ex: 3600)
147
- expect(Kernel).to have_received(:system)
148
- expect(Bundler).not_to have_received(:with_original_env)
149
- expect(redis).to have_received(:del).with('migration-failed')
150
- end
151
- end
152
-
153
- context 'when the task crashed' do
154
- it 'marks the failure in Redis' do
155
- fork { exit 1 }
156
- Process.wait
157
-
158
- expect(result).not_to be_success
159
- expect(redis).to have_received(:set).with('migration-failed', 123456789, ex: 3600)
160
- expect(redis).not_to have_received(:del)
161
- end
162
- end
163
- end
164
- end
165
- end
@@ -1,26 +0,0 @@
1
- lib = File.expand_path('../lib', __FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'synchronised_migration/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'synchronised_migration'
7
- spec.version = SynchronisedMigration::VERSION
8
- spec.authors = ['Alvin Yim', 'Stefan Cooper']
9
- spec.email = 'support@travellink.com.au'
10
- spec.description = 'Use Redis to record the data migration status'
11
- spec.summary = 'For deploying to multiple instances simultaneously'
12
- spec.homepage = 'https://github.com/sealink/synchronised-migration-rb'
13
-
14
- spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
15
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
- spec.require_paths = ['lib']
18
- spec.required_ruby_version = '>= 2.6.0'
19
-
20
- spec.add_dependency 'redlock', '>= 0.2'
21
- spec.add_dependency 'redis'
22
- spec.add_development_dependency 'rake'
23
- spec.add_development_dependency 'simplecov-rcov', '>= 0.2'
24
- spec.add_development_dependency 'rspec', '>= 3.6'
25
- spec.add_development_dependency 'pry-byebug', '>= 3.5'
26
- end