tapsoob 0.6.1 → 0.7.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.
@@ -0,0 +1,241 @@
1
+ # -*- encoding : utf-8 -*-
2
+ #
3
+ # Ruby/ProgressBar - a text progress bar library
4
+ #
5
+ # Copyright (C) 2001-2005 Satoru Takabayashi <satoru@namazu.org>
6
+ # All rights reserved.
7
+ # This is free software with ABSOLUTELY NO WARRANTY.
8
+ #
9
+ # You can redistribute it and/or modify it under the terms
10
+ # of Ruby's license.
11
+ #
12
+
13
+ module Tapsoob
14
+ module Progress
15
+ class Bar
16
+ VERSION = "0.9"
17
+
18
+ def initialize (title, total, out = STDOUT, title_width = nil)
19
+ @title = title
20
+ @total = total
21
+ @out = out
22
+ @terminal_width = 80
23
+ @bar_mark = "="
24
+ @current = 0
25
+ @previous = 0
26
+ @finished_p = false
27
+ @start_time = ::Time.now
28
+ @previous_time = @start_time
29
+ # Set title width: use provided width, or accommodate the title, with a minimum of 14
30
+ @title_width = title_width || [title.length, 14].max
31
+ @format = "%-#{@title_width}s %3d%% %s %s"
32
+ @format_arguments = [:title, :percentage, :bar, :stat]
33
+ clear
34
+ show
35
+ end
36
+ attr_reader :title
37
+ attr_reader :current
38
+ attr_reader :total
39
+ attr_accessor :start_time
40
+
41
+ private
42
+ def fmt_bar
43
+ bar_width = do_percentage * @terminal_width / 100
44
+ sprintf("|%s%s|",
45
+ @bar_mark * bar_width,
46
+ " " * (@terminal_width - bar_width))
47
+ end
48
+
49
+ def fmt_percentage
50
+ do_percentage
51
+ end
52
+
53
+ def fmt_stat
54
+ if @finished_p then elapsed else eta end
55
+ end
56
+
57
+ def fmt_stat_for_file_transfer
58
+ if @finished_p then
59
+ sprintf("%s %s %s", bytes, transfer_rate, elapsed)
60
+ else
61
+ sprintf("%s %s %s", bytes, transfer_rate, eta)
62
+ end
63
+ end
64
+
65
+ def fmt_title
66
+ @title[0,(@title_width - 1)] + ":"
67
+ end
68
+
69
+ def convert_bytes (bytes)
70
+ if bytes < 1024
71
+ sprintf("%6dB", bytes)
72
+ elsif bytes < 1024 * 1000 # 1000kb
73
+ sprintf("%5.1fKB", bytes.to_f / 1024)
74
+ elsif bytes < 1024 * 1024 * 1000 # 1000mb
75
+ sprintf("%5.1fMB", bytes.to_f / 1024 / 1024)
76
+ else
77
+ sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024)
78
+ end
79
+ end
80
+
81
+ def transfer_rate
82
+ bytes_per_second = @current.to_f / (::Time.now - @start_time)
83
+ sprintf("%s/s", convert_bytes(bytes_per_second))
84
+ end
85
+
86
+ def bytes
87
+ convert_bytes(@current)
88
+ end
89
+
90
+ def format_time (t)
91
+ t = t.to_i
92
+ sec = t % 60
93
+ min = (t / 60) % 60
94
+ hour = t / 3600
95
+ sprintf("%02d:%02d:%02d", hour, min, sec);
96
+ end
97
+
98
+ # ETA stands for Estimated Time of Arrival.
99
+ def eta
100
+ if @current == 0
101
+ "ETA: --:--:--"
102
+ else
103
+ elapsed = ::Time.now - @start_time
104
+ eta = elapsed * @total / @current - elapsed;
105
+ sprintf("ETA: %s", format_time(eta))
106
+ end
107
+ end
108
+
109
+ def elapsed
110
+ elapsed = ::Time.now - @start_time
111
+ sprintf("Time: %s", format_time(elapsed))
112
+ end
113
+
114
+ def eol
115
+ if @finished_p then "\n" else "\r" end
116
+ end
117
+
118
+ def do_percentage
119
+ if @total.zero?
120
+ 100
121
+ else
122
+ @current * 100 / @total
123
+ end
124
+ end
125
+
126
+ def get_width
127
+ # FIXME: I don't know how portable it is.
128
+ default_width = 80
129
+ begin
130
+ tiocgwinsz = 0x5413
131
+ data = [0, 0, 0, 0].pack("SSSS")
132
+ if @out.ioctl(tiocgwinsz, data) >= 0 then
133
+ rows, cols, xpixels, ypixels = data.unpack("SSSS")
134
+ if cols > 0 then cols else default_width end
135
+ else
136
+ default_width
137
+ end
138
+ rescue Exception
139
+ default_width
140
+ end
141
+ end
142
+
143
+ def show
144
+ arguments = @format_arguments.map {|method|
145
+ method = sprintf("fmt_%s", method)
146
+ send(method)
147
+ }
148
+ line = sprintf(@format, *arguments)
149
+
150
+ width = get_width
151
+ if line.length == width - 1
152
+ @out.print(line + eol)
153
+ @out.flush
154
+ elsif line.length >= width
155
+ @terminal_width = [@terminal_width - (line.length - width + 1), 0].max
156
+ if @terminal_width == 0 then @out.print(line + eol) else show end
157
+ else # line.length < width - 1
158
+ @terminal_width += width - line.length + 1
159
+ show
160
+ end
161
+ @previous_time = ::Time.now
162
+ end
163
+
164
+ def show_if_needed
165
+ if @total.zero?
166
+ cur_percentage = 100
167
+ prev_percentage = 0
168
+ else
169
+ cur_percentage = (@current * 100 / @total).to_i
170
+ prev_percentage = (@previous * 100 / @total).to_i
171
+ end
172
+
173
+ # Use "!=" instead of ">" to support negative changes
174
+ if cur_percentage != prev_percentage ||
175
+ ::Time.now - @previous_time >= 1 || @finished_p
176
+ show
177
+ end
178
+ end
179
+
180
+ public
181
+ def clear
182
+ @out.print "\r"
183
+ @out.print(" " * (get_width - 1))
184
+ @out.print "\r"
185
+ end
186
+
187
+ def finish
188
+ @current = @total
189
+ @finished_p = true
190
+ show
191
+ end
192
+
193
+ def finished?
194
+ @finished_p
195
+ end
196
+
197
+ def file_transfer_mode
198
+ @format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer]
199
+ end
200
+
201
+ def format= (format)
202
+ @format = format
203
+ end
204
+
205
+ def format_arguments= (arguments)
206
+ @format_arguments = arguments
207
+ end
208
+
209
+ def halt
210
+ @finished_p = true
211
+ show
212
+ end
213
+
214
+ def inc (step = 1)
215
+ @current += step
216
+ @current = @total if @current > @total
217
+ show_if_needed
218
+ @previous = @current
219
+ end
220
+
221
+ def set (count)
222
+ if count < 0 || count > @total
223
+ raise "invalid count: #{count} (total: #{@total})"
224
+ end
225
+ @current = count
226
+ show_if_needed
227
+ @previous = @current
228
+ end
229
+
230
+ def inspect
231
+ "#<Tapsoob::Progress::Bar:#{@current}/#{@total}>"
232
+ end
233
+ end
234
+
235
+ class ReversedBar < Bar
236
+ def do_percentage
237
+ 100 - super
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,190 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module Tapsoob
4
+ module Progress
5
+ # MultiBar manages multiple progress bars in parallel with a clean interface:
6
+ # - N progress bar lines (constantly updating)
7
+ # - 1 separator line
8
+ # - 1 info message line (shows latest INFO, gets replaced)
9
+ class MultiBar
10
+ def initialize(max_bars = 4)
11
+ @max_bars = max_bars
12
+ @bars = []
13
+ @mutex = Mutex.new
14
+ @active = true
15
+ @out = STDOUT
16
+ @last_update = Time.now
17
+ @max_title_width = 14 # Minimum width, will grow with longer titles
18
+ @initialized = false
19
+ @total_lines = 0 # Total lines: max_bars + separator + info line
20
+ @info_message = "" # Current info message to display
21
+ @start_time = Time.now # Track total elapsed time
22
+ @terminal_width = get_terminal_width
23
+ end
24
+
25
+ # Get terminal width, default to 160 if can't detect
26
+ def get_terminal_width
27
+ require 'io/console'
28
+ IO.console&.winsize&.[](1) || 160
29
+ rescue
30
+ 160
31
+ end
32
+
33
+ # Create a new progress bar and return it
34
+ def create_bar(title, total)
35
+ @mutex.synchronize do
36
+ # Initialize display area on first bar creation
37
+ unless @initialized
38
+ @total_lines = @max_bars + 2 # bars + separator + info line
39
+ @total_lines.times { @out.print "\n" }
40
+ @out.flush
41
+ @initialized = true
42
+ end
43
+
44
+ # Remove any existing bar with the same title to prevent duplicates
45
+ @bars.reject! { |b| b.title == title }
46
+
47
+ # Update max title width to accommodate longer titles
48
+ @max_title_width = [@max_title_width, title.length].max
49
+
50
+ bar = ThreadSafeBar.new(title, total, self)
51
+ @bars << bar
52
+ bar
53
+ end
54
+ end
55
+
56
+ # Update the info message line (called from outside for INFO logs)
57
+ def set_info(message)
58
+ @mutex.synchronize do
59
+ return unless @active
60
+ @info_message = message
61
+ redraw_all if @initialized
62
+ end
63
+ end
64
+
65
+ # Get the current maximum title width for alignment
66
+ # Note: Always called from within synchronized methods, so no mutex needed
67
+ def max_title_width
68
+ @max_title_width
69
+ end
70
+
71
+ # Called by individual bars when they update
72
+ def update
73
+ @mutex.synchronize do
74
+ return unless @active
75
+ return unless should_redraw?
76
+
77
+ @last_update = Time.now
78
+ redraw_all
79
+ end
80
+ end
81
+
82
+ # Finish a specific bar - mark it as completed
83
+ def finish_bar(bar)
84
+ @mutex.synchronize do
85
+ return unless @active
86
+
87
+ bar.mark_finished
88
+
89
+ # Respect throttle when finishing to avoid spamming redraws
90
+ if should_redraw?
91
+ @last_update = Time.now
92
+ redraw_all
93
+ end
94
+ # If throttled, the next regular update will show the finished state
95
+ end
96
+ end
97
+
98
+ # Stop all progress bars and clear them from display
99
+ def stop
100
+ @mutex.synchronize do
101
+ return unless @active # Already stopped
102
+ @active = false
103
+
104
+ # Clear all lines (progress bars + separator + info line)
105
+ if @total_lines > 0 && @initialized
106
+ # Move cursor up to first line
107
+ @out.print "\e[#{@total_lines}A"
108
+
109
+ # Clear each line
110
+ @total_lines.times do
111
+ @out.print "\r\e[2K\n"
112
+ end
113
+
114
+ # Move cursor back to start
115
+ @out.print "\e[#{@total_lines}A\r"
116
+ end
117
+
118
+ @out.flush
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # Check if enough time has passed to redraw (throttle to 10 updates/sec)
125
+ def should_redraw?
126
+ Time.now - @last_update >= 0.1
127
+ end
128
+
129
+ def redraw_all(force = false)
130
+ return unless @active
131
+ return if @bars.empty?
132
+
133
+ render_active_display
134
+ end
135
+
136
+ # Format elapsed time as HH:MM:SS
137
+ def format_elapsed_time
138
+ elapsed = Time.now - @start_time
139
+ hours = (elapsed / 3600).to_i
140
+ minutes = ((elapsed % 3600) / 60).to_i
141
+ seconds = (elapsed % 60).to_i
142
+ sprintf("%02d:%02d:%02d", hours, minutes, seconds)
143
+ end
144
+
145
+ # Render the complete display: progress bars + separator + info line
146
+ def render_active_display
147
+ return if @total_lines == 0
148
+
149
+ # Show the last N bars (finished or not) - creates a rolling window effect
150
+ # As new tables start, old completed ones scroll off the top
151
+ bars_to_draw = @bars.last(@max_bars)
152
+
153
+ # Move cursor up to first line
154
+ @out.print "\e[#{@total_lines}A"
155
+
156
+ # Draw progress bars (they handle their own width)
157
+ @max_bars.times do |i|
158
+ @out.print "\r\e[K"
159
+ bars_to_draw[i].render_to(@out) if i < bars_to_draw.length
160
+ @out.print "\n"
161
+ end
162
+
163
+ # Draw separator line using box drawing character
164
+ @out.print "\r\e[K"
165
+ @out.print "─" * @terminal_width
166
+ @out.print "\n"
167
+
168
+ # Draw info message line with elapsed time on the right
169
+ @out.print "\r\e[K"
170
+ unless @info_message.empty?
171
+ elapsed_str = "Elapsed: #{format_elapsed_time}"
172
+ # Calculate space to right-align elapsed time
173
+ available_width = @terminal_width - @info_message.length - elapsed_str.length - 2
174
+ if available_width > 0
175
+ @out.print @info_message
176
+ @out.print " " * available_width
177
+ @out.print elapsed_str
178
+ else
179
+ # If too long, just show message
180
+ @out.print @info_message[0...(@terminal_width - elapsed_str.length - 2)]
181
+ @out.print " " + elapsed_str
182
+ end
183
+ end
184
+ @out.print "\n"
185
+
186
+ @out.flush
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,87 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module Tapsoob
4
+ module Progress
5
+ # Thread-safe progress bar that reports to a MultiBar
6
+ class ThreadSafeBar < Bar
7
+ attr_reader :title
8
+
9
+ def initialize(title, total, multi_progress_bar)
10
+ @multi_progress_bar = multi_progress_bar
11
+ @out = STDOUT # Need this for get_width to work
12
+ # Don't call parent initialize, we'll manage output ourselves
13
+ @title = title
14
+ @total = total
15
+ @terminal_width = 80
16
+ @bar_mark = "="
17
+ @current = 0
18
+ @previous = 0
19
+ @finished_p = false
20
+ @start_time = ::Time.now
21
+ @previous_time = @start_time
22
+ @format_arguments = [:title, :percentage, :bar, :stat]
23
+ end
24
+
25
+ # Override show to notify multi-progress instead of direct output
26
+ def show
27
+ @previous_time = ::Time.now # Update to prevent time-based refresh spam
28
+ @multi_progress_bar.update
29
+ end
30
+
31
+ # Render this bar to the given output stream
32
+ def render_to(out)
33
+ # Get dynamic title width from MultiBar for consistent alignment
34
+ # Store as instance variable so parent class fmt_* methods can use it
35
+ @title_width = @multi_progress_bar.max_title_width
36
+
37
+ # Recalculate terminal width to handle resizes and use full width
38
+ width = get_width
39
+ # Calculate bar width: total_width - fixed_elements - padding
40
+ # Fixed: title(variable) + " "(1) + percentage(4) + " "(1) + "|"(1) + "|"(1) + " "(1) + timer(15) = title_width + 25
41
+ # Padding: +3 for timer fluctuations and safety
42
+ fixed_chars = @title_width + 28
43
+ @terminal_width = [width - fixed_chars, 20].max
44
+
45
+ # Build format string with dynamic title width
46
+ format = "%-#{@title_width}s %3d%% %s %s"
47
+ arguments = @format_arguments.map { |method| send("fmt_#{method}") }
48
+ line = sprintf(format, *arguments)
49
+
50
+ # Ensure line doesn't exceed terminal width to prevent wrapping
51
+ # Leave 2 chars margin for safety
52
+ line = line[0, width - 2] if line.length > width - 2
53
+
54
+ out.print(line)
55
+ end
56
+
57
+ # Override clear to do nothing (managed by MultiBar)
58
+ def clear
59
+ # no-op
60
+ end
61
+
62
+ # Mark this bar as finished (for tracking)
63
+ def mark_finished
64
+ @finished_p = true
65
+ end
66
+
67
+ # Override to use the same @finished_p flag
68
+ def finished?
69
+ @finished_p
70
+ end
71
+
72
+ # Override finish to notify multi-progress
73
+ def finish
74
+ @current = @total
75
+ @multi_progress_bar.finish_bar(self)
76
+ end
77
+
78
+ # Override inc to check if we need to update
79
+ def inc(step = 1)
80
+ @current += step
81
+ @current = @total if @current > @total
82
+ show_if_needed
83
+ @previous = @current
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,11 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # Progress bar module for Tapsoob
4
+ # Provides progress tracking for database operations with support for:
5
+ # - Single progress bars (Bar)
6
+ # - Multiple parallel progress bars (MultiBar)
7
+ # - Thread-safe progress bars (ThreadSafeBar)
8
+
9
+ require 'tapsoob/progress/bar'
10
+ require 'tapsoob/progress/multi_bar'
11
+ require 'tapsoob/progress/thread_safe_bar'
@@ -0,0 +1,109 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'json'
3
+
4
+ module Tapsoob
5
+ module ProgressEvent
6
+ @last_progress_time = {}
7
+ @progress_throttle = 0.5 # Emit progress at most every 0.5 seconds per table
8
+ @enabled = false # Only emit when CLI progress bars are disabled
9
+
10
+ def self.enabled=(value)
11
+ @enabled = value
12
+ end
13
+
14
+ def self.enabled?
15
+ @enabled
16
+ end
17
+
18
+ # Emit structured JSON progress events to STDERR for machine parsing
19
+ # Only emits when enabled (typically when CLI progress bars are disabled)
20
+ def self.emit(event_type, data = {})
21
+ return unless @enabled
22
+ event = {
23
+ event: event_type,
24
+ timestamp: Time.now.utc.iso8601
25
+ }.merge(data)
26
+
27
+ STDERR.puts "PROGRESS: #{JSON.generate(event)}"
28
+ STDERR.flush
29
+ end
30
+
31
+ # Check if enough time has passed to emit a progress event for this table
32
+ def self.should_emit_progress?(table_name)
33
+ now = Time.now
34
+ last_time = @last_progress_time[table_name]
35
+
36
+ if last_time.nil? || (now - last_time) >= @progress_throttle
37
+ @last_progress_time[table_name] = now
38
+ true
39
+ else
40
+ false
41
+ end
42
+ end
43
+
44
+ # Clear throttle state for a table (call when table completes)
45
+ def self.clear_throttle(table_name)
46
+ @last_progress_time.delete(table_name)
47
+ end
48
+
49
+ # Schema events
50
+ def self.schema_start(table_count)
51
+ emit('schema_start', tables: table_count)
52
+ end
53
+
54
+ def self.schema_complete(table_count)
55
+ emit('schema_complete', tables: table_count)
56
+ end
57
+
58
+ # Data events
59
+ def self.data_start(table_count, record_count)
60
+ emit('data_start', tables: table_count, records: record_count)
61
+ end
62
+
63
+ def self.data_complete(table_count, record_count)
64
+ emit('data_complete', tables: table_count, records: record_count)
65
+ end
66
+
67
+ # Table-level events
68
+ def self.table_start(table_name, record_count, workers: 1)
69
+ clear_throttle(table_name) # Reset throttle for new table
70
+ emit('table_start', table: table_name, records: record_count, workers: workers)
71
+ end
72
+
73
+ def self.table_progress(table_name, current, total)
74
+ # Throttle progress events to avoid spam
75
+ return unless should_emit_progress?(table_name)
76
+
77
+ percentage = total > 0 ? ((current.to_f / total) * 100).round(1) : 0
78
+ emit('table_progress', table: table_name, current: current, total: total, percentage: percentage)
79
+ end
80
+
81
+ def self.table_complete(table_name, record_count)
82
+ clear_throttle(table_name) # Clean up throttle state
83
+ emit('table_complete', table: table_name, records: record_count)
84
+ end
85
+
86
+ # Index events
87
+ def self.indexes_start(table_count)
88
+ emit('indexes_start', tables: table_count)
89
+ end
90
+
91
+ def self.indexes_complete(table_count)
92
+ emit('indexes_complete', tables: table_count)
93
+ end
94
+
95
+ # Sequence events
96
+ def self.sequences_start
97
+ emit('sequences_start')
98
+ end
99
+
100
+ def self.sequences_complete
101
+ emit('sequences_complete')
102
+ end
103
+
104
+ # Error events
105
+ def self.error(message, context = {})
106
+ emit('error', { message: message }.merge(context))
107
+ end
108
+ end
109
+ end
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Tapsoob
3
- VERSION = "0.6.1".freeze
3
+ VERSION = "0.7.0".freeze
4
4
  end
@@ -19,7 +19,7 @@ namespace :tapsoob do
19
19
  FileUtils.mkpath "#{dump_path}/indexes"
20
20
 
21
21
  # Run operation
22
- Tapsoob::Operation.factory(:pull, database_uri, dump_path, opts).run
22
+ Tapsoob::Operation::Base.factory(:pull, database_uri, dump_path, opts).run
23
23
 
24
24
  # Invoke cleanup task
25
25
  Rake::Task["tapsoob:clean"].reenable
@@ -53,7 +53,7 @@ namespace :tapsoob do
53
53
  end
54
54
 
55
55
  # Run operation
56
- Tapsoob::Operation.factory(:push, database_uri, dump_path, opts).run
56
+ Tapsoob::Operation::Base.factory(:push, database_uri, dump_path, opts).run
57
57
  end
58
58
 
59
59
  desc "Cleanup old dumps"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapsoob
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Félix Bellanger
@@ -102,11 +102,22 @@ files:
102
102
  - lib/tapsoob/cli/schema.rb
103
103
  - lib/tapsoob/config.rb
104
104
  - lib/tapsoob/data_stream.rb
105
+ - lib/tapsoob/data_stream/base.rb
106
+ - lib/tapsoob/data_stream/file_partition.rb
107
+ - lib/tapsoob/data_stream/interleaved.rb
108
+ - lib/tapsoob/data_stream/keyed.rb
109
+ - lib/tapsoob/data_stream/keyed_partition.rb
105
110
  - lib/tapsoob/errors.rb
106
111
  - lib/tapsoob/log.rb
107
- - lib/tapsoob/multi_progress_bar.rb
108
112
  - lib/tapsoob/operation.rb
109
- - lib/tapsoob/progress_bar.rb
113
+ - lib/tapsoob/operation/base.rb
114
+ - lib/tapsoob/operation/pull.rb
115
+ - lib/tapsoob/operation/push.rb
116
+ - lib/tapsoob/progress.rb
117
+ - lib/tapsoob/progress/bar.rb
118
+ - lib/tapsoob/progress/multi_bar.rb
119
+ - lib/tapsoob/progress/thread_safe_bar.rb
120
+ - lib/tapsoob/progress_event.rb
110
121
  - lib/tapsoob/railtie.rb
111
122
  - lib/tapsoob/schema.rb
112
123
  - lib/tapsoob/utils.rb