hekenga 0.2.11 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +29 -0
- data/docker-compose.yml +30 -0
- data/exe/hekenga +25 -38
- data/hekenga.gemspec +6 -5
- 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 +26 -21
- 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 +32 -23
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", "~>
|
23
|
-
spec.add_development_dependency "rake", "
|
23
|
+
spec.add_development_dependency "bundler", "~> 2.4.6"
|
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
|