hekenga 2.1.0 → 2.2.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: d5a372f23cc2fe1751eb08e07e40705b0361fc609b072e14226b72819cfe9647
4
- data.tar.gz: 99228c0f076660abe23c92f37c55509b156810fc0a741764b7872d4a0c597263
3
+ metadata.gz: d81f0def52813a113005c985703355b9d207a9fb91acae8e4948d01dc43f7b33
4
+ data.tar.gz: f462545e892ababf6a251dd5d366ef59cfc0f799b1ba3456102ee0483cab652d
5
5
  SHA512:
6
- metadata.gz: e2dfea01ce0c1c17fc51ab550e1805653a255d7c65a8b8424c7a1d6e646d59b9bc70fc45cfcf558d1fd7a6df81e5a2a8230a240e8c75d578736cdf19f4e2e290
7
- data.tar.gz: 0af8e27a9f1b4c7ae490788f64457a67fae9a3fb9459821b8066bca61671ae30463971e9ec277d01ba358817992260281e5dceaaab676f9093dc05a941a520ce
6
+ metadata.gz: 62e018a5ad375a280f641847c3c85c18b190ff7ef83d558a53210b062a80eecb183168af77baac2d2a7f2b7d5d16438b2f6057faaaa7a52e410f952a46673d69
7
+ data.tar.gz: 10b34cb83cf1f8d643c3cd11f06389ae6911d84c6afa7ff7901a681a735a83bb24108178a162ba0005ef7ed1992c54448ac3eff4479498533505a7ab65a0a71e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.2.0
4
+
5
+ - Added a `hekenga watch <path_or_pkey>` CLI command to attach to a running
6
+ migration and report its status periodically. Accepts an `--interval`
7
+ option (defaults to `Hekenga.config.report_sleep`).
8
+ - Added a `hekenga failures <path_or_pkey>` CLI command to print all failed and
9
+ invalid document IDs across a migration's document tasks.
10
+ - Document tasks now support a `skip_validation!` option to write documents
11
+ without running `.valid?`.
12
+
3
13
  ## v2.1.0
4
14
 
5
15
  - `per_document` task `scope` will no longer let you specify `.only` or
data/README.md CHANGED
@@ -43,6 +43,8 @@ $ hekenga run! <path_or_pkey> # Run a specific migration
43
43
  $ hekenga run! <path_or_pkey> --test # Dry run (no writes persisted)
44
44
  $ hekenga run! <path_or_pkey> --clear # Clear logs before running
45
45
  $ hekenga recover! <path_or_pkey> # Re-process failed/invalid records
46
+ $ hekenga watch <path_or_pkey> # Attach to a running migration, report status periodically
47
+ $ hekenga failures <path_or_pkey> # Print all failed/invalid document IDs for a migration
46
48
  $ hekenga cancel # Cancel all active migrations
47
49
  $ hekenga skip <path_or_pkey> # Mark a migration as skipped
48
50
  $ hekenga clear! <path_or_pkey> # Remove all logs/failures for a migration
@@ -118,6 +120,7 @@ per_document "Process records" do
118
120
  timeless! # Don't update Mongoid timestamps
119
121
  always_write! # Write even if the document didn't change
120
122
  skip_prepare! # Skip Mongoid callbacks on load
123
+ skip_validation! # Write documents without running Mongoid validations
121
124
  use_transaction! # Wrap each batch in a MongoDB transaction
122
125
  batch_size 50 # Override migration-level batch size
123
126
  write_strategy :update # :update (default) or :delete_then_insert
@@ -143,12 +146,25 @@ Or via the CLI:
143
146
 
144
147
  $ hekenga run! <path_or_pkey> --test
145
148
 
149
+ ### Monitoring
150
+
151
+ Attach to a migration that's already running (for example one launched in another process or via a background job) and print its status on a fixed interval:
152
+
153
+ $ hekenga watch <path_or_pkey>
154
+ $ hekenga watch <path_or_pkey> --interval 5 # report every 5 seconds
155
+
156
+ The reporting interval defaults to `Hekenga.config.report_sleep`.
157
+
146
158
  ### Recovery
147
159
 
148
160
  When a migration fails (due to errors, invalid records, or write failures), Hekenga logs the failures and marks the migration as failed. You can re-process only the failed records:
149
161
 
150
162
  $ hekenga recover! <path_or_pkey>
151
163
 
164
+ To inspect exactly which documents failed or were invalid across all document tasks in a migration:
165
+
166
+ $ hekenga failures <path_or_pkey>
167
+
152
168
  ## Development
153
169
 
154
170
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/docker-compose.yml CHANGED
@@ -9,22 +9,41 @@ networks:
9
9
  services:
10
10
  mongo:
11
11
  image: mongo:6
12
- command: ["--replSet", "rs0", "--bind_ip", "localhost,mongo"]
12
+ command: ["--replSet", "rs0", "--bind_ip_all"]
13
13
  volumes:
14
14
  - mongo:/data/db
15
15
  ports:
16
16
  - 27017:27017
17
17
  networks:
18
18
  - hekenga-net
19
+ healthcheck:
20
+ # mongo:6 ships mongosh (the legacy `mongo` shell was removed in 6.0).
21
+ test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
22
+ interval: 5s
23
+ timeout: 5s
24
+ retries: 12
19
25
 
20
26
  mongosetup:
21
27
  image: mongo:6
22
28
  depends_on:
23
- - mongo
29
+ mongo:
30
+ condition: service_healthy
24
31
  restart: "no"
25
32
  entrypoint:
26
- - bash
27
- - "-c"
28
- - "sleep 3 && mongo --host mongo:27017 --eval 'rs.initiate({_id: \"rs0\", members: [{_id: 0, host: \"localhost:27017\"}]})'"
33
+ - mongosh
34
+ - "--quiet"
35
+ - "--host"
36
+ - "mongo:27017"
37
+ - "--eval"
38
+ # Idempotent: initiate the single-member replica set only if it hasn't
39
+ # been configured yet, so re-running compose doesn't error. The member
40
+ # advertises as localhost:27017 so the driver on the host can reach it
41
+ # after replica-set topology discovery (required for transactions).
42
+ - >
43
+ try { rs.status(); print('replica set already initialised'); }
44
+ catch (e) {
45
+ rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'localhost:27017'}]});
46
+ print('replica set initiated');
47
+ }
29
48
  networks:
30
49
  - hekenga-net
data/exe/hekenga CHANGED
@@ -71,6 +71,20 @@ class HekengaCLI < Thor
71
71
  end
72
72
  end
73
73
 
74
+ desc "watch PATH_OR_PKEY", "Attach to a running migration and report its status periodically."
75
+ option :interval, type: :numeric, desc: "Seconds between status reports (default: Hekenga.config.report_sleep)."
76
+ def watch(path_or_pkey)
77
+ migration = load_migration(path_or_pkey)
78
+ interval = options[:interval] || Hekenga.config.report_sleep
79
+ Hekenga::MasterProcess.new(migration).watch!(interval: interval)
80
+ end
81
+
82
+ desc "failures PATH_OR_PKEY", "Print all failed and invalid document IDs for a migration."
83
+ def failures(path_or_pkey)
84
+ migration = load_migration(path_or_pkey)
85
+ Hekenga::FailureReport.new(migration).print!
86
+ end
87
+
74
88
  desc "cancel", "Cancel all active migrations."
75
89
  def cancel
76
90
  Hekenga::Log.where(done: false).set(cancel: true)
@@ -5,7 +5,7 @@ module Hekenga
5
5
  attr_reader :ups, :downs, :setups, :filters, :after_callbacks
6
6
  attr_accessor :parallel, :scope, :timeless, :batch_size, :cursor_timeout
7
7
  attr_accessor :description, :invalid_strategy, :skip_prepare, :write_strategy
8
- attr_accessor :always_write, :use_transaction
8
+ attr_accessor :always_write, :use_transaction, :skip_validation
9
9
 
10
10
  def initialize
11
11
  @ups = []
@@ -16,6 +16,7 @@ module Hekenga
16
16
  @invalid_strategy = :continue
17
17
  @write_strategy = :update
18
18
  @skip_prepare = false
19
+ @skip_validation = false
19
20
  @batch_size = nil
20
21
  @always_write = false
21
22
  @use_transaction = false
@@ -116,6 +116,8 @@ module Hekenga
116
116
  end
117
117
 
118
118
  def validate_records
119
+ return records_to_write.concat(migrated_records) if task.skip_validation
120
+
119
121
  migrated_records.each do |record|
120
122
  if record.valid?
121
123
  records_to_write << record
@@ -56,6 +56,10 @@ module Hekenga
56
56
  @object.skip_prepare = true
57
57
  end
58
58
 
59
+ def skip_validation!
60
+ @object.skip_validation = true
61
+ end
62
+
59
63
  def setup(&block)
60
64
  @object.setups.push block
61
65
  end
@@ -0,0 +1,60 @@
1
+ module Hekenga
2
+ class FailureReport
3
+ def initialize(migration)
4
+ @migration = migration
5
+ end
6
+
7
+ # Per-task summaries for document tasks that have any failed or invalid
8
+ # document IDs.
9
+ # => [{ idx:, description:, failed_ids: [...], invalid_ids: [...] }, ...]
10
+ def tasks
11
+ @migration.tasks.each.with_index.filter_map do |task, idx|
12
+ next unless task.is_a?(Hekenga::DocumentTask)
13
+
14
+ records = @migration.task_records(idx).any_of(
15
+ { :failed_ids.ne => [] },
16
+ { :invalid_ids.ne => [] }
17
+ )
18
+
19
+ failed_ids = []
20
+ invalid_ids = []
21
+ records.pluck(:failed_ids, :invalid_ids).each do |failed, invalid|
22
+ failed_ids.concat(failed) if failed
23
+ invalid_ids.concat(invalid) if invalid
24
+ end
25
+ next if failed_ids.empty? && invalid_ids.empty?
26
+
27
+ {
28
+ idx: idx,
29
+ description: task.description,
30
+ failed_ids: failed_ids,
31
+ invalid_ids: invalid_ids
32
+ }
33
+ end
34
+ end
35
+
36
+ def print!
37
+ Hekenga.log("Failures for #{@migration.to_key}")
38
+
39
+ summaries = tasks
40
+ if summaries.empty?
41
+ Hekenga.log(" No failed or invalid documents.")
42
+ return
43
+ end
44
+
45
+ failed_ids = summaries.flat_map { |summary| summary[:failed_ids] }
46
+ invalid_ids = summaries.flat_map { |summary| summary[:invalid_ids] }
47
+
48
+ print_ids("Failed", failed_ids)
49
+ print_ids("Invalid", invalid_ids)
50
+ end
51
+
52
+ private
53
+
54
+ def print_ids(label, ids)
55
+ return if ids.empty?
56
+
57
+ Hekenga.log(" #{label} (#{ids.length}): #{ids.map { |id| "\"#{id}\"" }.join(", ")}")
58
+ end
59
+ end
60
+ end
@@ -33,18 +33,35 @@ module Hekenga
33
33
  true
34
34
  end
35
35
 
36
+ def watch!(interval: Hekenga.config.report_sleep)
37
+ if Hekenga.status(@migration) == :naught
38
+ Hekenga.log "Migration #{@migration.to_key} has not started yet."
39
+ return false
40
+ end
41
+ Hekenga.log "Watching migration #{@migration.to_key}: #{@migration.description}"
42
+ @migration.tasks.each.with_index do |task, idx|
43
+ report_while_active(task, idx, interval: interval)
44
+ rescue Hekenga::TaskFailedError
45
+ return false
46
+ end
47
+ true
48
+ end
49
+
36
50
  private
37
51
 
38
- def report_while_active(task, idx)
39
- # Wait for the log to be generated
52
+ def report_while_active(task, idx, interval: Hekenga.config.report_sleep)
53
+ # Wait for the log to be generated. When watching an out-of-process
54
+ # migration there is no @active_thread guaranteeing it, so bail out if
55
+ # the migration finished without ever reaching this task.
40
56
  until (@migration.log(idx) rescue nil)
57
+ return if %i[complete skipped].include?(Hekenga.status(@migration))
41
58
  sleep 1
42
59
  end
43
- # Periodically report on thread progress
60
+ # Periodically report on progress
44
61
  until @migration.log(idx).reload.done
45
- @active_thread.join
62
+ @active_thread&.join
46
63
  report_status(task, idx)
47
- sleep Hekenga.config.report_sleep
64
+ sleep interval
48
65
  end
49
66
  report_status(task, idx) if task.is_a?(Hekenga::DocumentTask)
50
67
  report_result(task, idx)
@@ -1,3 +1,3 @@
1
1
  module Hekenga
2
- VERSION = "2.1.0"
2
+ VERSION = "2.2.0"
3
3
  end
data/lib/hekenga.rb CHANGED
@@ -6,6 +6,7 @@ require "hekenga/config"
6
6
  require "hekenga/irreversible"
7
7
  require "hekenga/virtual_method"
8
8
  require "hekenga/scaffold"
9
+ require "hekenga/failure_report"
9
10
 
10
11
  module Hekenga
11
12
  @@load_all_mutex = Mutex.new
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hekenga
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tapio Saarinen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-23 00:00:00.000000000 Z
11
+ date: 2026-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -175,6 +175,7 @@ files:
175
175
  - lib/hekenga/failure/error.rb
176
176
  - lib/hekenga/failure/validation.rb
177
177
  - lib/hekenga/failure/write.rb
178
+ - lib/hekenga/failure_report.rb
178
179
  - lib/hekenga/id_iterator.rb
179
180
  - lib/hekenga/invalid.rb
180
181
  - lib/hekenga/irreversible.rb