gamma_replication 0.1.1 → 0.1.3

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: b68929290d56de956de9597cfbdb9c6c7e2574f26489b5b55fc0223aa7d3c6ad
4
- data.tar.gz: 550aa21f4800a9da12f9d4f7b5bb4565a999321623d700cb74ae360bac461ae5
3
+ metadata.gz: 6fcf79da2a605e4984ba168b86117cc087f4da9414fc3075949ec3de0e4150e0
4
+ data.tar.gz: 369e57afcf359c2f148a0a3c985641ccf5beaa3706aade4c48e210827d5c04ec
5
5
  SHA512:
6
- metadata.gz: 73720919700ce8c4158bee20a58f375fe5a160ba1e44aa0f4348ed3f7ba0ba60d75c3224563579c65c5616dabd4f764b997c936dee029c12a2bade898f410028
7
- data.tar.gz: 10cf80dc382210270080adf30148a7556938bb164633bce1336484f3fdf22be571622ab711e2289aefc7486b8d1c3ea54e12932a117af77540eda7ba73045023
6
+ metadata.gz: c669c00890ad60fdfe242ad8e472f4f7b63154e4755d22bd798223c47536384f94f51dc8ae6bb2cd8aef83c386f5ac3f72906dcb4b4cccb709beb5282df9a2fa
7
+ data.tar.gz: f7ff79e999e05d3dabe4e5bd73fc9c0d8bf1939d0faef44391ba212eb5c8516809e65c50c5ee972e044cb6b7d68805c172ad18924f01b53644e2146a68a761ce
@@ -12,6 +12,8 @@ class GammaReplicationCLI < Thor
12
12
  option :data, aliases: "-d", desc: "Table Sync Settings yaml", required: true
13
13
  option :hook_dir, aliases: "-h", desc: "Hook script directory", default: "."
14
14
  option :maxwell_config, aliases: "-m", desc: "Maxwell configuration file path", required: true
15
+ option :enable_stats, type: :boolean, desc: "Enable statistics output (default: true)"
16
+ option :stats_interval, type: :numeric, desc: "Statistics output interval in seconds (default: 10800)"
15
17
  def start
16
18
  GammaReplication::Command::Start.new(options).execute
17
19
  end
@@ -21,6 +23,8 @@ class GammaReplicationCLI < Thor
21
23
  option :data, aliases: "-d", desc: "Table Sync Settings yaml", required: true
22
24
  option :hook_dir, aliases: "-h", desc: "Hook script directory", default: "."
23
25
  option :maxwell_config, aliases: "-m", desc: "Maxwell configuration file path", required: true
26
+ option :enable_stats, type: :boolean, desc: "Enable statistics output (default: true)"
27
+ option :stats_interval, type: :numeric, desc: "Statistics output interval in seconds (default: 10800)"
24
28
  def dryrun
25
29
  GammaReplication::Command::Dryrun.new(options).execute
26
30
  end
@@ -8,6 +8,9 @@ module GammaReplication
8
8
  setup_database(opts)
9
9
  setup_parser(opts)
10
10
  setup_maxwell(opts)
11
+ initialize_stats
12
+ @enable_stats = opts[:enable_stats].nil? ? true : opts[:enable_stats]
13
+ @stats_interval = (opts[:stats_interval] || 3 * 60 * 60).to_i # Default: 3 hours in seconds
11
14
  end
12
15
 
13
16
  def execute
@@ -18,6 +21,9 @@ module GammaReplication
18
21
  hash[table.table_name] = table
19
22
  end
20
23
  before_start if respond_to?(:before_start)
24
+
25
+ setup_stats_reporter if @enable_stats
26
+
21
27
  @maxwell_client.start do |data|
22
28
  process_maxwell_data(data)
23
29
  end
@@ -25,6 +31,52 @@ module GammaReplication
25
31
 
26
32
  private
27
33
 
34
+ def initialize_stats
35
+ @stats = {
36
+ total: { insert: 0, update: 0, delete: 0 },
37
+ by_table: {}
38
+ }
39
+ @stats_mutex = Mutex.new
40
+ end
41
+
42
+ def setup_stats_reporter
43
+ @stats_thread = Thread.new do
44
+ loop do
45
+ sleep @stats_interval
46
+ output_stats
47
+ reset_stats
48
+ end
49
+ end
50
+ end
51
+
52
+ def update_stats(table_name, operation)
53
+ return unless @enable_stats
54
+
55
+ @stats_mutex.synchronize do
56
+ @stats[:total][operation] += 1
57
+ @stats[:by_table][table_name] ||= { insert: 0, update: 0, delete: 0 }
58
+ @stats[:by_table][table_name][operation] += 1
59
+ end
60
+ end
61
+
62
+ def reset_stats
63
+ @stats_mutex.synchronize do
64
+ initialize_stats
65
+ end
66
+ end
67
+
68
+ def output_stats
69
+ @stats_mutex.synchronize do
70
+ total_stats = "Total[ins:#{@stats[:total][:insert]} | upd:#{@stats[:total][:update]} | del:#{@stats[:total][:delete]}]"
71
+
72
+ table_stats = @stats[:by_table].map do |table, counts|
73
+ "#{table}[ins:#{counts[:insert]} | upd:#{counts[:update]} | del:#{counts[:delete]}]"
74
+ end.join("\n")
75
+
76
+ logger.info("STATS | #{total_stats}\n#{table_stats}")
77
+ end
78
+ end
79
+
28
80
  def setup_database(opts)
29
81
  @database_settings = GammaReplication::DatabaseSettings.new(opts[:settings])
30
82
  @in_client = GammaReplication::DatabaseConnector::MysqlConnector.new(@database_settings.in_database)
@@ -48,7 +100,8 @@ module GammaReplication
48
100
 
49
101
  process_data_by_type(data)
50
102
  rescue StandardError => e
51
- logger.error(e)
103
+ error_message = e.message.to_s.split("\n").first
104
+ logger.error("Error: #{error_message.gsub(/\s+/, " ")}")
52
105
  end
53
106
 
54
107
  def should_process_data?(data)
@@ -64,15 +117,24 @@ module GammaReplication
64
117
  table_setting = @table_settings[data["table"]]
65
118
  case data["type"]
66
119
  when "insert"
120
+ return unless data["data"].present?
121
+
122
+ update_stats(data["table"], :insert)
67
123
  process_insert(table_setting, data)
68
124
  when "update"
125
+ return unless data["data"].present?
126
+
127
+ update_stats(data["table"], :update)
69
128
  process_update(table_setting, data)
70
129
  when "delete"
130
+ return unless data["old"].present?
131
+
132
+ update_stats(data["table"], :delete)
71
133
  process_delete(table_setting, data)
72
134
  end
73
135
  rescue StandardError => e
74
- logger.error(e)
75
- # Nothing
136
+ error_message = e.message.to_s.split("\n").first
137
+ logger.error("Error processing #{data["type"]} for table #{data["table"]}: #{error_message.gsub(/\s+/, " ")}")
76
138
  end
77
139
 
78
140
  def process_insert(table_setting, data)
@@ -100,8 +162,9 @@ module GammaReplication
100
162
 
101
163
  def process_delete(table_setting, data)
102
164
  old_record = data["old"]
103
- where_clause = build_where_clause(old_record, nil, table_setting.primary_key)
165
+ return unless old_record.present? && old_record[table_setting.primary_key].present?
104
166
 
167
+ where_clause = build_where_clause(old_record, nil, table_setting.primary_key)
105
168
  query = "DELETE FROM #{table_setting.table_name} WHERE #{where_clause}"
106
169
  execute_query(query)
107
170
  end
@@ -120,7 +183,8 @@ module GammaReplication
120
183
  elsif new_record.present? && new_record[primary_key].present?
121
184
  "`#{primary_key}` = #{format_value(new_record[primary_key])}"
122
185
  else
123
- raise "Primary key not found in record. old_record: #{old_record.inspect}, new_record: #{new_record.inspect}"
186
+ logger.error("Primary key not found. old_record: #{old_record.inspect}, new_record: #{new_record.inspect}")
187
+ raise "Primary key '#{primary_key}' not found in record"
124
188
  end
125
189
  end
126
190
 
@@ -133,7 +197,34 @@ module GammaReplication
133
197
  when Time
134
198
  "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
135
199
  else
136
- "'#{@out_client.client.escape(value.to_s)}'"
200
+ if json_column?(value)
201
+ sanitized_value = sanitize_json(value)
202
+ "'#{@out_client.client.escape(sanitized_value)}'"
203
+ else
204
+ "'#{@out_client.client.escape(value.to_s)}'"
205
+ end
206
+ end
207
+ end
208
+
209
+ def json_column?(value)
210
+ value.is_a?(String) && (value.start_with?("{") || value.start_with?("["))
211
+ end
212
+
213
+ def sanitize_json(value)
214
+ JSON.parse(value)
215
+ value
216
+ rescue JSON::ParserError => e
217
+ sanitized = value.gsub(/([{,]\s*)(\w+)(\s*:)/) do
218
+ "#{::Regexp.last_match(1)}\"#{::Regexp.last_match(2)}\"#{::Regexp.last_match(3)}"
219
+ end
220
+ sanitized = sanitized.gsub(/:\s*([^",\s\d\[\]{}-].*?)(,|\}|$)/) do
221
+ ": \"#{::Regexp.last_match(1)}\"#{::Regexp.last_match(2)}"
222
+ end
223
+ begin
224
+ JSON.parse(sanitized)
225
+ sanitized
226
+ rescue JSON::ParserError
227
+ value.to_json
137
228
  end
138
229
  end
139
230
 
@@ -19,8 +19,9 @@ module GammaReplication
19
19
  io.each do |line|
20
20
  data = JSON.parse(line.strip)
21
21
  block.call(data) if block_given?
22
- rescue JSON::ParserError => e
23
- logger.error("Failed to parse Maxwell output: #{e.message}")
22
+ rescue JSON::ParserError
23
+ # Ignore Maxwell's non-JSON output (startup messages, etc)
24
+ next
24
25
  end
25
26
  end
26
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GammaReplication
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gamma_replication
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shinsuke Nishio
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-22 00:00:00.000000000 Z
11
+ date: 2025-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport