flydata 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -2
- data/VERSION +1 -1
- data/flydata-core/lib/flydata-core/errors.rb +12 -0
- data/{lib/flydata/mysql/mysql_util.rb → flydata-core/lib/flydata-core/mysql/command_generator.rb} +39 -6
- data/flydata-core/lib/flydata-core/mysql/compatibility_checker.rb +222 -0
- data/flydata-core/lib/flydata-core/query_job.rb +7 -0
- data/flydata-core/lib/flydata-core/query_job/redshift.rb +27 -0
- data/{spec/flydata/mysql/mysql_util_spec.rb → flydata-core/spec/mysql/command_generator_spec.rb} +22 -3
- data/flydata-core/spec/mysql/compatibility_checker.rb +9 -0
- data/flydata-core/spec/query_job/redshift_spec.rb +34 -0
- data/flydata.gemspec +14 -9
- data/lib/flydata/api/data_entry.rb +9 -0
- data/lib/flydata/command/mysql.rb +2 -1
- data/lib/flydata/command/sync.rb +62 -31
- data/lib/flydata/compatibility_check.rb +33 -152
- data/lib/flydata/fluent-plugins/in_mysql_binlog_flydata.rb +4 -4
- data/lib/flydata/mysql/table_ddl.rb +2 -2
- data/lib/flydata/parser/mysql/dump_parser.rb +2 -2
- data/spec/flydata/command/sync_spec.rb +17 -15
- data/spec/flydata/compatibility_check_spec.rb +13 -12
- data/spec/flydata/mysql/table_ddl_spec.rb +1 -1
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83cb9df24d4fe0b983b20bfd30d1798aa4cbecdd
|
4
|
+
data.tar.gz: a00a24a84acf688553503bf97b156acdd57fc8ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 335e28547f000af29cf5df8d7a28632fe8fa5356298016f80819622c2a8299ae8469aa46a4e2ec8aa6179c472d151ea90583da1a7ab5f9367aec7668200c9ebc
|
7
|
+
data.tar.gz: 78049be459795e1eccb4b486c55e74989b8f2fae7cce9e5a8ce79236a12ad54d990550b30e71f9027e3ff299598d9f586a63fff8911ae159c6c0935f95e2bfeb
|
data/Gemfile
CHANGED
@@ -13,7 +13,7 @@ gem "slop", '~> 3.4', '>= 3.4.6'
|
|
13
13
|
gem "treetop", '~> 1.5', '>= 1.5.3'
|
14
14
|
gem "sys-filesystem", '~> 1.1', '>= 1.1.3'
|
15
15
|
gem "io-console", '~> 0.4.2', '>= 0.4.2'
|
16
|
-
gem "kodama", '~> 0.1.2', '>= 0.1.
|
16
|
+
gem "kodama", '~> 0.1.2', '>= 0.1.6'
|
17
17
|
gem "serverengine", '~> 1.5'
|
18
18
|
|
19
19
|
group :development do
|
data/Gemfile.lock
CHANGED
@@ -61,7 +61,7 @@ GEM
|
|
61
61
|
rdoc
|
62
62
|
json (1.8.1)
|
63
63
|
jwt (1.0.0)
|
64
|
-
kodama (0.1.
|
64
|
+
kodama (0.1.6)
|
65
65
|
ruby-binlog (~> 1.0, >= 1.0.4)
|
66
66
|
method_source (0.8.2)
|
67
67
|
mime-types (2.3)
|
@@ -135,7 +135,7 @@ DEPENDENCIES
|
|
135
135
|
io-console (~> 0.4.2, >= 0.4.2)
|
136
136
|
jeweler (~> 1.8, >= 1.8.8)
|
137
137
|
json (~> 1.8, >= 1.8.0)
|
138
|
-
kodama (~> 0.1.2, >= 0.1.
|
138
|
+
kodama (~> 0.1.2, >= 0.1.6)
|
139
139
|
mysql2 (~> 0.3, >= 0.3.17)
|
140
140
|
protected_attributes (~> 1.0, >= 1.0.8)
|
141
141
|
pry
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.4.
|
1
|
+
0.4.1
|
@@ -304,6 +304,18 @@ class TableDefError < StandardError
|
|
304
304
|
end
|
305
305
|
end
|
306
306
|
|
307
|
+
## Compatibility check error
|
308
|
+
|
309
|
+
class CompatibilityError < StandardError
|
310
|
+
end
|
311
|
+
|
312
|
+
class AgentCompatibilityError < StandardError
|
313
|
+
end
|
314
|
+
|
315
|
+
class MysqlCompatibilityError < StandardError
|
316
|
+
end
|
317
|
+
|
318
|
+
|
307
319
|
## Error container
|
308
320
|
|
309
321
|
class DataDeliveryErrorThreadContext
|
data/{lib/flydata/mysql/mysql_util.rb → flydata-core/lib/flydata-core/mysql/command_generator.rb}
RENAMED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'open3'
|
2
2
|
require 'flydata-core/table_def/mysql_table_def'
|
3
3
|
|
4
|
-
module
|
4
|
+
module FlydataCore
|
5
5
|
module Mysql
|
6
|
-
class
|
6
|
+
class CommandGenerator
|
7
7
|
DEFAULT_MYSQL_CMD_OPTION = "--default-character-set=utf8 --protocol=tcp"
|
8
8
|
|
9
9
|
# Generate mysql/mysqldump command with options
|
@@ -76,7 +76,34 @@ module Flydata
|
|
76
76
|
generate_mysql_cmd(opt)
|
77
77
|
end
|
78
78
|
|
79
|
-
def self.each_mysql_tabledef(tables,
|
79
|
+
def self.each_mysql_tabledef(tables, options, &block)
|
80
|
+
tables = tables.clone
|
81
|
+
missing_tables = []
|
82
|
+
begin
|
83
|
+
if tables.to_s == '' || tables.to_s == '[]'
|
84
|
+
raise ArgumentError, "tables is nil or empty"
|
85
|
+
end
|
86
|
+
_each_mysql_tabledef(tables, options, &block)
|
87
|
+
rescue TableMissingError => e
|
88
|
+
tables.delete e.table
|
89
|
+
missing_tables << e.table
|
90
|
+
return missing_tables if tables.empty?
|
91
|
+
retry
|
92
|
+
end
|
93
|
+
missing_tables
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
class TableMissingError < RuntimeError
|
99
|
+
def initialize(message, table)
|
100
|
+
super(message)
|
101
|
+
@table = table
|
102
|
+
end
|
103
|
+
attr_reader :table
|
104
|
+
end
|
105
|
+
|
106
|
+
def self._each_mysql_tabledef(tables, option)
|
80
107
|
command = generate_mysql_ddl_dump_cmd(option.merge(tables: tables))
|
81
108
|
|
82
109
|
create_opt = {}
|
@@ -100,14 +127,20 @@ module Flydata
|
|
100
127
|
errors = ""
|
101
128
|
while !stderr.eof?
|
102
129
|
line = stderr.gets.gsub('mysqldump: ', '')
|
103
|
-
|
130
|
+
case line
|
131
|
+
when /Couldn't find table: "([^"]+)"/
|
132
|
+
missing_table = $1
|
133
|
+
raise TableMissingError.new(line, missing_table)
|
134
|
+
when /Warning: Using a password on the command line interface can be insecure./
|
135
|
+
# Ignore
|
136
|
+
else
|
137
|
+
errors << line
|
138
|
+
end
|
104
139
|
end
|
105
140
|
raise errors unless errors.empty?
|
106
141
|
end
|
107
142
|
end
|
108
143
|
|
109
|
-
private
|
110
|
-
|
111
144
|
def self.convert_keys_to_sym(hash)
|
112
145
|
hash.inject(hash.dup) do |ret, (k, v)|
|
113
146
|
if k.kind_of?(String) && ret[k.to_sym].nil?
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'flydata-core/errors'
|
2
|
+
require 'mysql2'
|
3
|
+
|
4
|
+
module FlydataCore
|
5
|
+
module Mysql
|
6
|
+
class CompatibilityChecker
|
7
|
+
def initialize(option = {})
|
8
|
+
@option = option || {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def do_check(option = @option, &block)
|
12
|
+
result = block.call create_query(option)
|
13
|
+
check_result(result, option)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Override
|
17
|
+
#def create_query(option = @option)
|
18
|
+
#end
|
19
|
+
|
20
|
+
# Override
|
21
|
+
#def validate_result(result, option = @option)
|
22
|
+
#end
|
23
|
+
end
|
24
|
+
|
25
|
+
class MysqlCompatibilityChecker < CompatibilityChecker
|
26
|
+
def do_check(option = @option, &block)
|
27
|
+
query = create_query(option)
|
28
|
+
result = if block
|
29
|
+
block.call query
|
30
|
+
else
|
31
|
+
exec_query(query)
|
32
|
+
end
|
33
|
+
check_result(result, option)
|
34
|
+
end
|
35
|
+
|
36
|
+
def exec_query(query)
|
37
|
+
begin
|
38
|
+
client = Mysql2::Client.new(@option)
|
39
|
+
client.query(query)
|
40
|
+
ensure
|
41
|
+
client.close rescue nil if client
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class SyncPermissionChecker < MysqlCompatibilityChecker
|
47
|
+
def create_query(option = @option)
|
48
|
+
"SHOW GRANTS"
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_result(result, option = @option)
|
52
|
+
databases = ['mysql', @option[:database]]
|
53
|
+
get_grant_regex = /GRANT (?<privs>.*) ON (`)?(?<db_name>[^`]*)(`)?\.\* TO '#{@option[:username]}/
|
54
|
+
necessary_permission_fields = ["SELECT","RELOAD","LOCK TABLES","REPLICATION SLAVE","REPLICATION CLIENT"]
|
55
|
+
all_privileges_field = ["ALL PRIVILEGES"]
|
56
|
+
|
57
|
+
found_priv = Hash[databases.map {|d| [d,[]]}]
|
58
|
+
missing_priv = {}
|
59
|
+
|
60
|
+
result.each do |res|
|
61
|
+
# SHOW GRANTS should only return one column
|
62
|
+
res_value = res.values.first
|
63
|
+
matched_values = res_value.match(get_grant_regex)
|
64
|
+
next unless matched_values
|
65
|
+
line_priv = matched_values["privs"].split(", ")
|
66
|
+
if matched_values["db_name"] == "*"
|
67
|
+
return true if (all_privileges_field - line_priv).empty?
|
68
|
+
databases.each {|d| found_priv[d] << line_priv }
|
69
|
+
elsif databases.include? matched_values["db_name"]
|
70
|
+
if (all_privileges_field - line_priv).empty?
|
71
|
+
found_priv[matched_values["db_name"]] = necessary_permission_fields
|
72
|
+
else
|
73
|
+
found_priv[matched_values["db_name"]] << line_priv
|
74
|
+
end
|
75
|
+
end
|
76
|
+
missing_priv = get_missing_privileges(found_priv, necessary_permission_fields)
|
77
|
+
return true if missing_priv.empty?
|
78
|
+
end
|
79
|
+
error_text = "The user '#{@option[:username]}' does not have the correct permissions to run FlyData Sync\n"
|
80
|
+
error_text << " * These privileges are missing...\n"
|
81
|
+
missing_priv.each_key {|db| error_text << " for the database '#{db}': #{missing_priv[db].join(", ")}\n"}
|
82
|
+
raise MysqlCompatibilityError, error_text
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def get_missing_privileges(found_priv, all_priv)
|
88
|
+
return_hash = {}
|
89
|
+
found_priv.each_key do |key|
|
90
|
+
missing_priv = all_priv - found_priv[key].flatten.uniq
|
91
|
+
return_hash[key] = missing_priv unless missing_priv.empty?
|
92
|
+
end
|
93
|
+
return_hash
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
module MysqlVariablesHandling
|
98
|
+
def create_query(option = @option)
|
99
|
+
"SHOW VARIABLES;"
|
100
|
+
end
|
101
|
+
|
102
|
+
def convert_result_to_hash(result)
|
103
|
+
ret = {}
|
104
|
+
result.each do |record|
|
105
|
+
k = record['Variable_name']
|
106
|
+
v = record['Value']
|
107
|
+
ret[k] = v
|
108
|
+
end
|
109
|
+
ret
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class BinlogParameterChecker < MysqlCompatibilityChecker
|
114
|
+
include MysqlVariablesHandling
|
115
|
+
|
116
|
+
SYS_VAR_TO_CHECK = {
|
117
|
+
# parameter => expected value
|
118
|
+
'binlog_format'=>'ROW',
|
119
|
+
'binlog_checksum'=>'NONE',
|
120
|
+
'log_bin_use_v1_row_events'=>'ON',
|
121
|
+
'log_slave_updates'=>'ON'
|
122
|
+
}
|
123
|
+
|
124
|
+
def check_result(result, option = @option)
|
125
|
+
errors = {}
|
126
|
+
param_hash = convert_result_to_hash(result)
|
127
|
+
|
128
|
+
SYS_VAR_TO_CHECK.each_key do |sys_var|
|
129
|
+
if param_hash.has_key?(sys_var)
|
130
|
+
actual_val = param_hash[sys_var]
|
131
|
+
expected_val = SYS_VAR_TO_CHECK[sys_var]
|
132
|
+
unless actual_val == expected_val
|
133
|
+
errors[sys_var] = actual_val
|
134
|
+
end
|
135
|
+
elsif not %w(binlog_checksum log_bin_use_v1_row_events).include?(sys_var)
|
136
|
+
# Mark variables that do not exist as error
|
137
|
+
errors[sys_var] = false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
unless errors.empty?
|
142
|
+
error_explanation = ""
|
143
|
+
errors.each_key do |err_key|
|
144
|
+
error_explanation << "\n * #{err_key} is #{errors[err_key]} but should be #{SYS_VAR_TO_CHECK[err_key]}"
|
145
|
+
end
|
146
|
+
raise FlydataCore::MysqlCompatibilityError,
|
147
|
+
"These system variable(s) are not the correct value: #{error_explanation}\n" +
|
148
|
+
" Please change these system variables for FlyData Sync to run correctly"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class TableTypeChecker < MysqlCompatibilityChecker
|
154
|
+
SELECT_TABLE_INFO_TMPLT =
|
155
|
+
"SELECT table_name, table_type, engine " +
|
156
|
+
"FROM information_schema.tables " +
|
157
|
+
"WHERE table_schema = '%s' and table_name in (%s)"
|
158
|
+
|
159
|
+
# option[:client] : Mysql2::Client object
|
160
|
+
# option[:tables] : target table list
|
161
|
+
def create_query(option = @option)
|
162
|
+
SELECT_TABLE_INFO_TMPLT % [
|
163
|
+
Mysql2::Client.escape(option[:database]),
|
164
|
+
option[:tables].collect{|t| "'#{Mysql2::Client.escape(t)}'"}.join(", ")
|
165
|
+
]
|
166
|
+
end
|
167
|
+
|
168
|
+
def check_result(result, option = @option)
|
169
|
+
invalid_tables = []
|
170
|
+
result.each do |r|
|
171
|
+
invalid_tables.push(r['table_name']) if r['table_type'] == 'VIEW' || r['engine'] == 'MEMORY'
|
172
|
+
end
|
173
|
+
|
174
|
+
unless invalid_tables.empty?
|
175
|
+
raise FlydataCore::MysqlCompatibilityError,
|
176
|
+
"FlyData does not support VIEW and MEMORY ENGINE table. " +
|
177
|
+
"Remove following tables from data entry: #{invalid_tables.join(", ")}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class NonRdsRetentionChecker < MysqlCompatibilityChecker
|
183
|
+
include MysqlVariablesHandling
|
184
|
+
|
185
|
+
BINLOG_RETENTION_HOURS = 24
|
186
|
+
EXPIRE_LOGS_DAYS_LIMIT = BINLOG_RETENTION_HOURS / 24
|
187
|
+
|
188
|
+
def check_result(result, option = @option)
|
189
|
+
param_hash = convert_result_to_hash(result)
|
190
|
+
|
191
|
+
if param_hash["expire_logs_days"].to_s != '0' &&
|
192
|
+
param_hash["expire_logs_days"].to_i <= EXPIRE_LOGS_DAYS_LIMIT
|
193
|
+
raise FlydataCore::MysqlCompatibilityError,
|
194
|
+
"Binary log retention is too short\n " +
|
195
|
+
" We recommend the system variable '@@expire_logs_days' to be either set to 0 or at least #{EXPIRE_LOGS_DAYS_LIMIT} day(s)"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class RdsRetentionChecker < MysqlCompatibilityChecker
|
201
|
+
BINLOG_RETENTION_HOURS = NonRdsRetentionChecker::BINLOG_RETENTION_HOURS
|
202
|
+
|
203
|
+
def create_query(option = @option)
|
204
|
+
"call mysql.rds_show_configuration;"
|
205
|
+
end
|
206
|
+
|
207
|
+
def check_result(result, option = @option)
|
208
|
+
if result.first["name"] == "binlog retention hours"
|
209
|
+
if result.first["value"].nil? ||
|
210
|
+
result.first["value"].to_i <= BINLOG_RETENTION_HOURS
|
211
|
+
raise FlydataCore::MysqlCompatibilityError,
|
212
|
+
"Binary log retention is too short\n" +
|
213
|
+
" We recommend setting RDS binlog retention " +
|
214
|
+
"to be at least #{BINLOG_RETENTION_HOURS} hours. To do this, " +
|
215
|
+
"run this on your RDS MySQL database:\n" +
|
216
|
+
" $> call mysql.rds_set_configuration('binlog retention hours', 94);"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# QueryJob is a library provding an interfact to the Query Job Processor (also
|
2
|
+
# known as Copy Handler)
|
3
|
+
# SimpleDB related classes and methods defined in flydata and flydata-web will
|
4
|
+
# be eventually consolidated into this library.
|
5
|
+
Dir[File.join(File.dirname(__FILE__), "query_job/**/*.rb")].each do |f|
|
6
|
+
require f
|
7
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'flydata-core/table_def/redshift_table_def'
|
2
|
+
|
3
|
+
module FlydataCore
|
4
|
+
module QueryJob
|
5
|
+
|
6
|
+
class Redshift
|
7
|
+
# returns a list of table names used in the Query Job system.
|
8
|
+
def self.target_table_names(flydata_table_names)
|
9
|
+
flydata_table_names = [ flydata_table_names ] unless flydata_table_names.kind_of?(Array)
|
10
|
+
|
11
|
+
flydata_table_names.collect {|flydata_table_name|
|
12
|
+
redshift_table_name = TableDef::RedshiftTableDef.convert_to_valid_table_name(flydata_table_name)
|
13
|
+
table_names = [ redshift_table_name ]
|
14
|
+
if redshift_table_name != flydata_table_name
|
15
|
+
# for backward compatibility
|
16
|
+
# Old implementation used FlyData table names. To support it, the
|
17
|
+
# method adds the flydata_table_name as well.
|
18
|
+
table_names << flydata_table_name
|
19
|
+
end
|
20
|
+
|
21
|
+
table_names
|
22
|
+
}.flatten
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/{spec/flydata/mysql/mysql_util_spec.rb → flydata-core/spec/mysql/command_generator_spec.rb}
RENAMED
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'flydata/mysql/
|
2
|
+
require 'flydata-core/mysql/command_generator'
|
3
3
|
|
4
|
-
module
|
4
|
+
module FlydataCore
|
5
5
|
module Mysql
|
6
6
|
|
7
|
-
describe
|
7
|
+
describe CommandGenerator do
|
8
8
|
let(:default_option) { {
|
9
9
|
command: 'mysqldump',
|
10
10
|
host: 'test-host',
|
@@ -139,6 +139,25 @@ module Flydata
|
|
139
139
|
'mysql -h test-host -P 3306 -utestuser -p"testpassword" --default-character-set=utf8 --protocol=tcp -e "SHOW GRANTS;" testdb'
|
140
140
|
) }
|
141
141
|
end
|
142
|
+
|
143
|
+
describe '.each_mysql_tabledef' do
|
144
|
+
context 'with empty tables' do
|
145
|
+
subject { described_class.each_mysql_tabledef([], nil) }
|
146
|
+
it do
|
147
|
+
expect{subject}.to raise_error(
|
148
|
+
ArgumentError,"tables is nil or empty")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when all tables are missing' do
|
153
|
+
before do
|
154
|
+
allow(described_class).to receive(:_each_mysql_tabledef).
|
155
|
+
and_raise(CommandGenerator::TableMissingError.new('table, missing error', 'table1'))
|
156
|
+
end
|
157
|
+
subject { described_class.each_mysql_tabledef(%w(table1), nil) }
|
158
|
+
it { is_expected.to eq(%w(table1)) }
|
159
|
+
end
|
160
|
+
end
|
142
161
|
end
|
143
162
|
end
|
144
163
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'flydata-core/query_job/redshift'
|
3
|
+
|
4
|
+
module FlydataCore::QueryJob
|
5
|
+
|
6
|
+
describe Redshift do
|
7
|
+
describe '.target_table_names' do
|
8
|
+
subject { described_class.target_table_names(flydata_table_names) }
|
9
|
+
|
10
|
+
context 'with a single table name' do
|
11
|
+
let(:flydata_table_names) { "my_table" }
|
12
|
+
|
13
|
+
it { is_expected.to eq [ flydata_table_names ] }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'with multiples table names' do
|
17
|
+
let(:flydata_table_names) { %w(my_table1 my_table2) }
|
18
|
+
|
19
|
+
it { is_expected.to eq flydata_table_names }
|
20
|
+
end
|
21
|
+
context 'with a mixed case table name' do
|
22
|
+
let(:flydata_table_names) { "MyTable" }
|
23
|
+
|
24
|
+
it { is_expected.to eq [ flydata_table_names.downcase, flydata_table_names ] }
|
25
|
+
end
|
26
|
+
context 'with table names including mixed case one' do
|
27
|
+
let(:flydata_table_names) { %w(MyTable my_table2) }
|
28
|
+
|
29
|
+
it { is_expected.to eq %w(mytable MyTable my_table2) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|