hekenga 0.2.13 → 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.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/CHANGELOG.md +29 -0
- data/docker-compose.yml +30 -0
- data/exe/hekenga +25 -38
- data/hekenga.gemspec +5 -4
- data/lib/hekenga/context.rb +7 -6
- data/lib/hekenga/document_task.rb +9 -3
- data/lib/hekenga/document_task_executor.rb +264 -0
- data/lib/hekenga/document_task_record.rb +29 -0
- data/lib/hekenga/dsl/document_task.rb +28 -1
- data/lib/hekenga/dsl/migration.rb +3 -0
- data/lib/hekenga/failure/write.rb +1 -0
- data/lib/hekenga/iterator.rb +26 -0
- data/lib/hekenga/log.rb +14 -19
- data/lib/hekenga/master_process.rb +184 -105
- data/lib/hekenga/migration.rb +70 -330
- data/lib/hekenga/parallel_job.rb +11 -4
- data/lib/hekenga/parallel_task.rb +110 -0
- data/lib/hekenga/scaffold.rb +27 -23
- data/lib/hekenga/task_failed_error.rb +4 -0
- data/lib/hekenga/task_splitter.rb +30 -0
- data/lib/hekenga/version.rb +1 -1
- data/lib/hekenga.rb +22 -10
- metadata +22 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4befa0d3716a0609575aa42f35fe126b854365f5e348ecb6b8babff4912e2522
|
4
|
+
data.tar.gz: 674e20c142d8b8ebc0f51e75a4a372912359c196247471170c5ddae2c317c294
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6625430dc0dc24bf821e7c584fa1141362de50effdf5606ed53583b77d20b8e79b7bd17680128b25e736c24a8fb41b58169f9a0b106bd693165abfec339c49b2
|
7
|
+
data.tar.gz: 84a0af87ac9b21acc5253206d4de311adc6b5edbe47303894aeb1eb169ac89365f6338f79f67257478cc744ad98d8a23fe8002ee67c64b276f0a5e33be214685
|
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## v1.0.0
|
4
|
+
|
5
|
+
- `hekenga run!` now has a `--clear` option to clear the migration prior to running it
|
6
|
+
- (breaking) `per_document` tasks now generate `Hekenga::DocumentTaskRecord`, used for
|
7
|
+
recovery + progress tracking. `Hekenga::Failure::Cancelled` and
|
8
|
+
`Hekenga::Failure::Validation` are no longer generated.
|
9
|
+
- (breaking) Migrations will now always continue when encountering invalid records.
|
10
|
+
Recovering the migration later will reprocess the invalid records. Support
|
11
|
+
for the prompt/cancel/stop when_invalid strategies has been dropped
|
12
|
+
- `rollback`, `errors` CLI stubs have been removed as they were never
|
13
|
+
implemented
|
14
|
+
- `--edit` now no longer eats an argument when generating a migration
|
15
|
+
- Mutexes have been added to the registry to help with thread-safety. You will
|
16
|
+
still need to make sure that your application is eager loaded on workers
|
17
|
+
- (breaking) The default write strategy has been changed to use replace bulk operations
|
18
|
+
instead of a batch delete followed by a batch insert. You can swap back to
|
19
|
+
delete then insert on a per-task basis, but you probably shouldn't
|
20
|
+
- An experimental option to wrap each document batch in a transaction has been
|
21
|
+
added.
|
22
|
+
- (breaking) If a migration doesn't change a document, it will now skip writing
|
23
|
+
it by default. To always write the document, call `always_write!`
|
24
|
+
- `batch_size` can now be configured per document task
|
25
|
+
- Rather than doing a single `pluck` on the `per_document` scope, the scope will
|
26
|
+
now be iterated in subqueries of 100k IDs when retrieving IDs to queue jobs
|
27
|
+
for.
|
28
|
+
- (breaking) `Hekenga::ParallelJob` takes different arguments now.
|
29
|
+
- (breaking) some internal methods have been renamed.
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
version: "3.8"
|
2
|
+
|
3
|
+
volumes:
|
4
|
+
mongo:
|
5
|
+
|
6
|
+
networks:
|
7
|
+
hekenga-net:
|
8
|
+
|
9
|
+
services:
|
10
|
+
mongo:
|
11
|
+
image: mongo:5
|
12
|
+
command: ["--replSet", "rs0", "--bind_ip", "localhost,mongo"]
|
13
|
+
volumes:
|
14
|
+
- mongo:/data/db
|
15
|
+
ports:
|
16
|
+
- 27017:27017
|
17
|
+
networks:
|
18
|
+
- hekenga-net
|
19
|
+
|
20
|
+
mongosetup:
|
21
|
+
image: mongo:5
|
22
|
+
depends_on:
|
23
|
+
- mongo
|
24
|
+
restart: "no"
|
25
|
+
entrypoint:
|
26
|
+
- bash
|
27
|
+
- "-c"
|
28
|
+
- "sleep 3 && mongo --host mongo:27017 --eval 'rs.initiate({_id: \"rs0\", members: [{_id: 0, host: \"localhost:27017\"}]})'"
|
29
|
+
networks:
|
30
|
+
- hekenga-net
|
data/exe/hekenga
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
4
|
|
5
|
-
if File.
|
5
|
+
if File.exist?(File.expand_path("config/environment.rb"))
|
6
6
|
require File.expand_path("config/environment.rb")
|
7
7
|
end
|
8
8
|
|
@@ -11,6 +11,10 @@ require "thor"
|
|
11
11
|
require "pathname"
|
12
12
|
|
13
13
|
class HekengaCLI < Thor
|
14
|
+
def self.exit_on_failure?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
14
18
|
desc "status", "Show which migrations have run and their status."
|
15
19
|
def status
|
16
20
|
Hekenga.load_all!
|
@@ -33,31 +37,37 @@ class HekengaCLI < Thor
|
|
33
37
|
|
34
38
|
desc "run_all!", "Run all migrations that have not yet run, in date order."
|
35
39
|
def run_all!
|
36
|
-
bail_if_errors
|
37
40
|
Hekenga.load_all!
|
38
41
|
Hekenga.registry.sort_by {|x| x.stamp}.each do |migration|
|
39
42
|
if Hekenga.status(migration) == :naught
|
40
43
|
migration.perform!
|
41
|
-
bail_if_errors
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
45
47
|
|
46
48
|
desc "run! PATH_OR_PKEY --test", "Run a migration (optionally in test mode)."
|
47
49
|
option :test, default: false, type: :boolean
|
50
|
+
option :clear, default: false, type: :boolean
|
48
51
|
def run!(path_or_pkey)
|
49
|
-
|
52
|
+
clear!(path_or_pkey) if options[:clear]
|
50
53
|
migration = load_migration(path_or_pkey)
|
51
54
|
migration.test_mode! if options[:test]
|
52
55
|
migration.perform!
|
53
56
|
if options[:test]
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
57
|
+
puts "Logs have been preserved for debugging. To reset migration state run:"
|
58
|
+
puts " hekenga clear! #{path_or_pkey}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "recover! PATH_OR_PKEY", "Recover a migration"
|
63
|
+
option :test, default: false, type: :boolean
|
64
|
+
def recover!(path_or_pkey)
|
65
|
+
migration = load_migration(path_or_pkey)
|
66
|
+
migration.test_mode! if options[:test]
|
67
|
+
migration.recover!
|
68
|
+
if options[:test]
|
69
|
+
puts "Logs have been preserved for debugging. To reset migration state run:"
|
70
|
+
puts " hekenga clear! #{path_or_pkey}"
|
61
71
|
end
|
62
72
|
end
|
63
73
|
|
@@ -79,24 +89,10 @@ class HekengaCLI < Thor
|
|
79
89
|
puts "Clearing #{migration.to_key}.."
|
80
90
|
Hekenga::Log.where(pkey: migration.to_key).delete_all
|
81
91
|
Hekenga::Failure.where(pkey: migration.to_key).delete_all
|
92
|
+
Hekenga::DocumentTaskRecord.where(migration_key: migration.to_key).delete_all
|
82
93
|
puts "Done!"
|
83
94
|
end
|
84
95
|
|
85
|
-
desc "rollback", "Rollback a migration."
|
86
|
-
def rollback
|
87
|
-
todo "rollback"
|
88
|
-
end
|
89
|
-
|
90
|
-
desc "recover", "Attempt to resume a failed migration."
|
91
|
-
def recover
|
92
|
-
todo "recover"
|
93
|
-
end
|
94
|
-
|
95
|
-
desc "errors", "Print the errors associated with a failed migration."
|
96
|
-
def errors
|
97
|
-
todo "errors"
|
98
|
-
end
|
99
|
-
|
100
96
|
desc "skip PATH_OR_PKEY", "Skip a migration so that it won't run."
|
101
97
|
def skip(path_or_pkey)
|
102
98
|
migration = load_migration(path_or_pkey)
|
@@ -114,7 +110,7 @@ class HekengaCLI < Thor
|
|
114
110
|
end
|
115
111
|
|
116
112
|
desc "generate <description> --edit", "Generate a migration scaffold (and optionally edit in your editor)."
|
117
|
-
option :edit
|
113
|
+
option :edit, type: :boolean
|
118
114
|
def generate(*description)
|
119
115
|
description = description.join(" ")
|
120
116
|
scaffold = Hekenga::Scaffold.new(description)
|
@@ -130,20 +126,11 @@ class HekengaCLI < Thor
|
|
130
126
|
exec "$EDITOR #{path}"
|
131
127
|
end
|
132
128
|
end
|
129
|
+
|
133
130
|
private
|
134
131
|
|
135
|
-
def todo(op)
|
136
|
-
puts "#{op.capitalize} has not yet been implemented."
|
137
|
-
exit(99)
|
138
|
-
end
|
139
|
-
def bail_if_errors
|
140
|
-
if Hekenga.any_fatal?
|
141
|
-
puts "Refusing to run migrations while there is an existing cancelled migration."
|
142
|
-
exit(1)
|
143
|
-
end
|
144
|
-
end
|
145
132
|
def load_migration(path_or_pkey)
|
146
|
-
if File.
|
133
|
+
if File.exist?(File.expand_path(path_or_pkey))
|
147
134
|
require File.expand_path(path_or_pkey)
|
148
135
|
migration = Hekenga.registry.last
|
149
136
|
else
|
data/hekenga.gemspec
CHANGED
@@ -8,6 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Hekenga::VERSION
|
9
9
|
spec.authors = ["Tapio Saarinen"]
|
10
10
|
spec.email = ["admin@bitlong.org"]
|
11
|
+
spec.licenses = ["MIT"]
|
11
12
|
|
12
13
|
spec.summary = %q{Sophisticated migration framework for mongoid, with the ability to parallelise via ActiveJob.}
|
13
14
|
spec.homepage = "https://github.com/tzar/hekenga"
|
@@ -19,14 +20,14 @@ Gem::Specification.new do |spec|
|
|
19
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
21
|
spec.require_paths = ["lib"]
|
21
22
|
|
22
|
-
spec.add_development_dependency "bundler", "~> 2.
|
23
|
+
spec.add_development_dependency "bundler", "~> 2.4.6"
|
23
24
|
spec.add_development_dependency "rake", ">= 12.3.3"
|
24
25
|
spec.add_development_dependency "rspec", "~> 3.0"
|
25
|
-
spec.add_development_dependency "database_cleaner"
|
26
|
+
spec.add_development_dependency "database_cleaner-mongoid", "~> 2.0"
|
26
27
|
spec.add_development_dependency "pry"
|
27
28
|
spec.add_development_dependency "pry-byebug"
|
28
29
|
|
29
|
-
spec.add_runtime_dependency "mongoid", ">=
|
30
|
-
spec.add_runtime_dependency "activejob", ">=
|
30
|
+
spec.add_runtime_dependency "mongoid", ">= 6"
|
31
|
+
spec.add_runtime_dependency "activejob", ">= 5"
|
31
32
|
spec.add_runtime_dependency "thor"
|
32
33
|
end
|
data/lib/hekenga/context.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
module Hekenga
|
2
2
|
class Context
|
3
|
-
def initialize(
|
4
|
-
@
|
3
|
+
def initialize(test_mode: false)
|
4
|
+
@__test_mode = test_mode
|
5
5
|
end
|
6
6
|
|
7
|
-
def test?
|
8
|
-
!!@__test_run
|
9
|
-
end
|
10
7
|
def actual?
|
11
|
-
!@
|
8
|
+
!@__test_mode
|
9
|
+
end
|
10
|
+
|
11
|
+
def test?
|
12
|
+
!actual?
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
@@ -2,15 +2,21 @@ require 'hekenga/irreversible'
|
|
2
2
|
module Hekenga
|
3
3
|
class DocumentTask
|
4
4
|
attr_reader :ups, :downs, :setups, :filters
|
5
|
-
attr_accessor :parallel, :scope, :timeless
|
6
|
-
attr_accessor :description, :invalid_strategy, :skip_prepare
|
5
|
+
attr_accessor :parallel, :scope, :timeless, :batch_size
|
6
|
+
attr_accessor :description, :invalid_strategy, :skip_prepare, :write_strategy
|
7
|
+
attr_accessor :always_write, :use_transaction
|
8
|
+
|
7
9
|
def initialize
|
8
10
|
@ups = []
|
9
11
|
@downs = []
|
10
12
|
@setups = []
|
11
13
|
@filters = []
|
12
|
-
@invalid_strategy = :
|
14
|
+
@invalid_strategy = :continue
|
15
|
+
@write_strategy = :update
|
13
16
|
@skip_prepare = false
|
17
|
+
@batch_size = nil
|
18
|
+
@always_write = false
|
19
|
+
@use_transaction = false
|
14
20
|
end
|
15
21
|
|
16
22
|
def validate!
|
@@ -0,0 +1,264 @@
|
|
1
|
+
module Hekenga
|
2
|
+
class DocumentTaskExecutor
|
3
|
+
attr_reader :task_record
|
4
|
+
attr_reader :context, :session
|
5
|
+
|
6
|
+
def initialize(task_record, records: nil)
|
7
|
+
@task_record = task_record
|
8
|
+
@records = records
|
9
|
+
@migrated_records = []
|
10
|
+
@invalid_records = []
|
11
|
+
@records_to_write = []
|
12
|
+
@filtered_records = []
|
13
|
+
@skipped_records = []
|
14
|
+
@failed_records = []
|
15
|
+
@backed_up_records = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def run!
|
19
|
+
with_setup do |context|
|
20
|
+
with_transaction do |session|
|
21
|
+
filter_records
|
22
|
+
run_migration
|
23
|
+
validate_records
|
24
|
+
write_records
|
25
|
+
write_result unless task_record.test_mode
|
26
|
+
end
|
27
|
+
# In test mode, the transaction will be aborted - so we need to write
|
28
|
+
# the result outside of the run! block
|
29
|
+
write_result if task_record.test_mode
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_for_completion!
|
34
|
+
if migration_complete?
|
35
|
+
migration.log(task_idx).set_without_session(
|
36
|
+
done: true,
|
37
|
+
finished: Time.now,
|
38
|
+
error: migration.task_records(task_idx).failed.any?
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def migration_cancelled?
|
44
|
+
migration.log(task_idx).cancel
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
delegate :task_idx, to: :task_record
|
50
|
+
|
51
|
+
attr_reader :migrated_records, :records_to_write, :filtered_records, :invalid_records, :skipped_records, :failed_records, :backed_up_records
|
52
|
+
|
53
|
+
def migration_complete?
|
54
|
+
migration.task_records(task_idx).incomplete.none?
|
55
|
+
end
|
56
|
+
|
57
|
+
def record_scope
|
58
|
+
task.scope.klass.unscoped.in(_id: task_record.ids)
|
59
|
+
end
|
60
|
+
|
61
|
+
def records
|
62
|
+
@records ||= record_scope.to_a
|
63
|
+
end
|
64
|
+
|
65
|
+
def write_result
|
66
|
+
task_record.update_attributes(
|
67
|
+
complete: true,
|
68
|
+
finished: Time.now,
|
69
|
+
failed_ids: failed_records.map(&:_id),
|
70
|
+
invalid_ids: invalid_records.map(&:_id),
|
71
|
+
written_ids: records_to_write.map(&:_id),
|
72
|
+
stats: {
|
73
|
+
failed: failed_records.length,
|
74
|
+
invalid: invalid_records.length,
|
75
|
+
written: records_to_write.length
|
76
|
+
}
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def filter_records
|
81
|
+
records.each do |record|
|
82
|
+
if task.filters.all? {|block| context.instance_exec(record, &block)}
|
83
|
+
filtered_records << record
|
84
|
+
else
|
85
|
+
skipped_records << record
|
86
|
+
end
|
87
|
+
rescue => _e
|
88
|
+
failed_records << record
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def run_migration
|
93
|
+
filtered_records.each do |record|
|
94
|
+
backup_record(record)
|
95
|
+
task.up!(@context, record)
|
96
|
+
migrated_records << record
|
97
|
+
rescue => _e
|
98
|
+
failed_records << record
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def backup_record(record)
|
103
|
+
return unless task.write_strategy == :delete_then_insert
|
104
|
+
|
105
|
+
backed_up_records[record._id] = record.as_document.deep_dup
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate_records
|
109
|
+
migrated_records.each do |record|
|
110
|
+
if record.valid?
|
111
|
+
records_to_write << record
|
112
|
+
else
|
113
|
+
invalid_records << record
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def write_records
|
119
|
+
records_to_write.keep_if(&:changed?) unless task.always_write
|
120
|
+
return if records_to_write.empty?
|
121
|
+
return if task_record.test_mode
|
122
|
+
|
123
|
+
records_to_write.each {|record| record.send(:prepare_update) {}}
|
124
|
+
|
125
|
+
case task.write_strategy
|
126
|
+
when :delete_then_insert
|
127
|
+
delete_then_insert_records
|
128
|
+
else
|
129
|
+
replace_records
|
130
|
+
end
|
131
|
+
rescue Mongo::Error::BulkWriteError => e
|
132
|
+
# If we're in a transaction, we can retry; so just re-raise
|
133
|
+
raise if @session
|
134
|
+
# Otherwise, we need to log the failure
|
135
|
+
write_failure!(e)
|
136
|
+
end
|
137
|
+
|
138
|
+
def write_failure!(error)
|
139
|
+
log = migration.log(task_idx)
|
140
|
+
backups = records_to_write.map do |record|
|
141
|
+
failed_records << record
|
142
|
+
backed_up_records[record._id]
|
143
|
+
end.compact
|
144
|
+
@records_to_write = []
|
145
|
+
log.add_failure({
|
146
|
+
message: error.to_s,
|
147
|
+
backtrace: error.backtrace,
|
148
|
+
documents: backups,
|
149
|
+
document_ids: records_to_write.map(&:_id),
|
150
|
+
task_record_id: task_record.id
|
151
|
+
}, Hekenga::Failure::Write)
|
152
|
+
log.set_without_session({error: true})
|
153
|
+
end
|
154
|
+
|
155
|
+
def delete_then_insert_records
|
156
|
+
operations = []
|
157
|
+
operations << {
|
158
|
+
delete_many: {
|
159
|
+
filter: {
|
160
|
+
_id: {
|
161
|
+
'$in': records_to_write.map(&:_id)
|
162
|
+
}
|
163
|
+
}
|
164
|
+
}
|
165
|
+
}
|
166
|
+
records_to_write.each do |record|
|
167
|
+
operations << { insert_one: record.as_document }
|
168
|
+
end
|
169
|
+
bulk_write(operations, ordered: true)
|
170
|
+
end
|
171
|
+
|
172
|
+
def bulk_write(operations, **options)
|
173
|
+
task.scope.klass.collection.bulk_write(operations, session: session, **options)
|
174
|
+
end
|
175
|
+
|
176
|
+
def replace_records
|
177
|
+
operations = records_to_write.map do |record|
|
178
|
+
{
|
179
|
+
replace_one: {
|
180
|
+
filter: { _id: record._id },
|
181
|
+
replacement: record.as_document
|
182
|
+
}
|
183
|
+
}
|
184
|
+
end
|
185
|
+
bulk_write(operations)
|
186
|
+
end
|
187
|
+
|
188
|
+
def migration
|
189
|
+
@migration ||= Hekenga.find_migration(task_record.migration_key)
|
190
|
+
end
|
191
|
+
|
192
|
+
def with_setup(&block)
|
193
|
+
@context = Hekenga::Context.new(test_mode: task_record.test_mode)
|
194
|
+
begin
|
195
|
+
task.setups&.each do |setup|
|
196
|
+
@context.instance_exec(&setup)
|
197
|
+
end
|
198
|
+
rescue => e
|
199
|
+
fail_and_cancel!(e)
|
200
|
+
return
|
201
|
+
end
|
202
|
+
yield
|
203
|
+
ensure
|
204
|
+
@context = nil
|
205
|
+
end
|
206
|
+
|
207
|
+
def fail_and_cancel!(error)
|
208
|
+
log = migration.log(task_idx)
|
209
|
+
log.add_failure({
|
210
|
+
message: error.to_s,
|
211
|
+
backtrace: error.backtrace
|
212
|
+
}, Hekenga::Failure::Error)
|
213
|
+
log.set_without_session({cancel: true, error: true, done: true, finished: Time.now})
|
214
|
+
task_record.update_attributes(
|
215
|
+
complete: true,
|
216
|
+
finished: Time.now,
|
217
|
+
failed_ids: task_record.ids,
|
218
|
+
invalid_ids: [],
|
219
|
+
written_ids: [],
|
220
|
+
stats: {
|
221
|
+
failed: task_record.ids.length,
|
222
|
+
invalid: 0,
|
223
|
+
written: 0,
|
224
|
+
}
|
225
|
+
)
|
226
|
+
end
|
227
|
+
|
228
|
+
def with_transaction(&block)
|
229
|
+
return yield unless task.use_transaction
|
230
|
+
|
231
|
+
ensure_replicaset!
|
232
|
+
klass = task.scope.klass
|
233
|
+
# NOTE: Dummy session to work around threading bug
|
234
|
+
klass.persistence_context.client.start_session({})
|
235
|
+
klass.with_session do |session|
|
236
|
+
@session = session
|
237
|
+
@session.start_transaction
|
238
|
+
yield
|
239
|
+
if task_record.test_mode
|
240
|
+
@session.abort_transaction
|
241
|
+
else
|
242
|
+
@session.commit_transaction
|
243
|
+
end
|
244
|
+
rescue
|
245
|
+
@session.abort_transaction
|
246
|
+
raise
|
247
|
+
ensure
|
248
|
+
@session = nil
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def ensure_replicaset!
|
253
|
+
return unless task.use_transaction
|
254
|
+
|
255
|
+
unless task.scope.klass.collection.client.cluster.replica_set?
|
256
|
+
raise "MongoDB must be in a replica set to use transactions"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def task
|
261
|
+
@task ||= migration.tasks[task_idx]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hekenga
|
2
|
+
class DocumentTaskRecord
|
3
|
+
include Mongoid::Document
|
4
|
+
|
5
|
+
field :migration_key, type: String
|
6
|
+
field :task_idx, type: Integer
|
7
|
+
field :executor_key, type: BSON::ObjectId
|
8
|
+
field :finished, type: Time
|
9
|
+
field :complete, type: Boolean, default: false
|
10
|
+
field :ids, type: Array, default: []
|
11
|
+
field :id_count, type: Integer
|
12
|
+
field :test_mode, type: Boolean
|
13
|
+
|
14
|
+
field :stats, type: Hash, default: {}
|
15
|
+
field :failed_ids, type: Array, default: []
|
16
|
+
field :invalid_ids, type: Array, default: []
|
17
|
+
field :written_ids, type: Array, default: []
|
18
|
+
|
19
|
+
index(migration_key: 1, task_idx: 1, complete: 1)
|
20
|
+
index(migration_key: 1, task_idx: 1, 'stats.failed': 1)
|
21
|
+
index(migration_key: 1, task_idx: 1, ids: 1)
|
22
|
+
|
23
|
+
scope :incomplete, proc { where(complete: false) }
|
24
|
+
scope :complete, proc { where(complete: true) }
|
25
|
+
scope :failed, proc { gt('stats.failed': 1) }
|
26
|
+
|
27
|
+
before_create { self.id_count = ids.count }
|
28
|
+
end
|
29
|
+
end
|
@@ -4,7 +4,15 @@ module Hekenga
|
|
4
4
|
class DocumentTask < Hekenga::DSL
|
5
5
|
configures Hekenga::DocumentTask
|
6
6
|
|
7
|
-
INVALID_BEHAVIOR_STRATEGIES = [
|
7
|
+
INVALID_BEHAVIOR_STRATEGIES = %i[continue]
|
8
|
+
VALID_WRITE_STRATEGIES = %i[delete_then_insert update]
|
9
|
+
|
10
|
+
def batch_size(size)
|
11
|
+
unless size.is_a?(Integer) && size > 0
|
12
|
+
raise "Invalid batch size #{size.inspect}"
|
13
|
+
end
|
14
|
+
@object.batch_size = size
|
15
|
+
end
|
8
16
|
|
9
17
|
def when_invalid(val)
|
10
18
|
unless INVALID_BEHAVIOR_STRATEGIES.include?(val)
|
@@ -13,18 +21,37 @@ module Hekenga
|
|
13
21
|
@object.invalid_strategy = val
|
14
22
|
end
|
15
23
|
|
24
|
+
def write_strategy(strategy)
|
25
|
+
unless VALID_WRITE_STRATEGIES.include?(strategy)
|
26
|
+
raise "Invalid value #{strategy}. Valid values for write_strategy are: #{VALID_WRITE_STRATEGIES.join(", ")}."
|
27
|
+
end
|
28
|
+
@object.write_strategy = strategy
|
29
|
+
end
|
30
|
+
|
16
31
|
def scope(scope)
|
17
32
|
@object.scope = scope
|
18
33
|
end
|
34
|
+
|
35
|
+
def always_write!
|
36
|
+
@object.always_write = true
|
37
|
+
end
|
38
|
+
|
39
|
+
def use_transaction!
|
40
|
+
@object.use_transaction = true
|
41
|
+
end
|
42
|
+
|
19
43
|
def parallel!
|
20
44
|
@object.parallel = true
|
21
45
|
end
|
46
|
+
|
22
47
|
def timeless!
|
23
48
|
@object.timeless = true
|
24
49
|
end
|
50
|
+
|
25
51
|
def skip_prepare!
|
26
52
|
@object.skip_prepare = true
|
27
53
|
end
|
54
|
+
|
28
55
|
def setup(&block)
|
29
56
|
@object.setups.push block
|
30
57
|
end
|
@@ -11,12 +11,15 @@ module Hekenga
|
|
11
11
|
end
|
12
12
|
@object.batch_size = size
|
13
13
|
end
|
14
|
+
|
14
15
|
def created(stamp = nil)
|
15
16
|
@object.stamp = Time.parse(stamp)
|
16
17
|
end
|
18
|
+
|
17
19
|
def task(description = nil, &block)
|
18
20
|
@object.tasks.push Hekenga::DSL::SimpleTask.new(description, &block).object
|
19
21
|
end
|
22
|
+
|
20
23
|
def per_document(description = nil, &block)
|
21
24
|
@object.tasks.push Hekenga::DSL::DocumentTask.new(description, &block).object
|
22
25
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hekenga
|
2
|
+
class Iterator
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
SMALLEST_ID = BSON::ObjectId.from_string('0'*24)
|
6
|
+
|
7
|
+
attr_reader :scope, :size
|
8
|
+
|
9
|
+
def initialize(scope, size:)
|
10
|
+
@scope = scope
|
11
|
+
@size = size
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
current_id = SMALLEST_ID
|
16
|
+
base_scope = scope.asc(:_id).limit(size)
|
17
|
+
|
18
|
+
loop do
|
19
|
+
ids = base_scope.and(_id: {'$gt': current_id}).pluck(:_id)
|
20
|
+
break if ids.empty?
|
21
|
+
yield ids
|
22
|
+
current_id = ids.sort.last
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|