flydata 0.1.8 → 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/VERSION +1 -1
- data/flydata.gemspec +8 -3
- data/lib/flydata/cli.rb +7 -1
- data/lib/flydata/command/base.rb +3 -1
- data/lib/flydata/command/sender.rb +57 -46
- data/lib/flydata/command/setup.rb +84 -17
- data/lib/flydata/command/start.rb +4 -1
- data/lib/flydata/command/sync.rb +130 -55
- data/lib/flydata/helpers.rb +12 -3
- data/lib/flydata/table_def/mysql_table_def.rb +7 -4
- data/lib/flydata/table_def/redshift_table_def.rb +13 -6
- data/spec/flydata/cli_spec.rb +172 -0
- data/spec/flydata/command/sender_spec.rb +39 -1
- data/spec/flydata/command/sync_spec.rb +85 -14
- data/spec/flydata/table_def/mysql_table_def_spec.rb +21 -4
- data/spec/flydata/table_def/mysqldump_test_unsigned.dump +47 -0
- data/spec/flydata/table_def/redshift_table_def_spec.rb +45 -0
- metadata +22 -4
@@ -1,8 +1,11 @@
|
|
1
1
|
module Flydata
|
2
2
|
module Command
|
3
3
|
class Start < Base
|
4
|
+
def self.slop
|
5
|
+
Flydata::Command::Sender.slop_start # Needs options for Sender#start
|
6
|
+
end
|
4
7
|
def run
|
5
|
-
sender = Flydata::Command::Sender.new
|
8
|
+
sender = Flydata::Command::Sender.new(opts)
|
6
9
|
sender.start
|
7
10
|
end
|
8
11
|
end
|
data/lib/flydata/command/sync.rb
CHANGED
@@ -18,6 +18,17 @@ module Flydata
|
|
18
18
|
STATUS_COMPLETE = 'COMPLETE'
|
19
19
|
|
20
20
|
def run(*tables)
|
21
|
+
sender = Flydata::Command::Sender.new
|
22
|
+
if (sender.process_exist?)
|
23
|
+
if tables.empty?
|
24
|
+
# full sync
|
25
|
+
puts "FlyData Agent is already running. If you'd like to restart FlyData Sync from scratch, run 'flydata sync:reset' first."
|
26
|
+
else
|
27
|
+
# per-table sync
|
28
|
+
puts "Flydata Agent is already running. If you'd like to Sync the table(s), run 'flydata flush' first."
|
29
|
+
end
|
30
|
+
exit 1
|
31
|
+
end
|
21
32
|
de = retrieve_data_entries.first
|
22
33
|
raise "There are no data entry." unless de
|
23
34
|
case de['type']
|
@@ -36,6 +47,12 @@ module Flydata
|
|
36
47
|
end
|
37
48
|
|
38
49
|
def reset
|
50
|
+
return unless ask_yes_no("This resets the current Sync. Are you sure?")
|
51
|
+
sender = Flydata::Command::Sender.new
|
52
|
+
sender.flush_client_buffer # TODO We should rather delete buffer files
|
53
|
+
# TODO Reset server when API becomes available
|
54
|
+
sender.stop
|
55
|
+
|
39
56
|
de = retrieve_data_entries.first
|
40
57
|
sync_fm = Flydata::FileUtil::SyncFileManager.new(de)
|
41
58
|
[
|
@@ -51,16 +68,36 @@ module Flydata
|
|
51
68
|
end
|
52
69
|
end
|
53
70
|
|
71
|
+
def wait_for_server_data_processing
|
72
|
+
state = :PROCESS
|
73
|
+
puts "Processing data..."
|
74
|
+
sleep 10
|
75
|
+
status = nil
|
76
|
+
while (status = check)
|
77
|
+
if state == :PROCESS && status['state'] == 'uploading'
|
78
|
+
puts " -> Done"
|
79
|
+
state = :UPLOAD
|
80
|
+
puts "Uploading data to Redshift..."
|
81
|
+
end
|
82
|
+
print_progress(status)
|
83
|
+
sleep 10
|
84
|
+
end
|
85
|
+
if (state == :PROCESS)
|
86
|
+
# :UPLOAD state was skipped due to no data
|
87
|
+
puts " -> Done"
|
88
|
+
puts "Uploading data to Redshift..."
|
89
|
+
end
|
90
|
+
puts " -> Done"
|
91
|
+
end
|
92
|
+
|
54
93
|
def check
|
55
94
|
de = retrieve_data_entries.first
|
56
95
|
retry_on(RestClient::Exception) do
|
57
|
-
|
58
|
-
if
|
59
|
-
|
60
|
-
true
|
96
|
+
status = do_check(de)
|
97
|
+
if status['complete']
|
98
|
+
nil
|
61
99
|
else
|
62
|
-
|
63
|
-
false
|
100
|
+
status
|
64
101
|
end
|
65
102
|
end
|
66
103
|
end
|
@@ -93,6 +130,11 @@ module Flydata
|
|
93
130
|
flydata.data_entry.buffer_stat(de['id'], env_mode)
|
94
131
|
end
|
95
132
|
|
133
|
+
def print_progress(buffer_stat)
|
134
|
+
message = buffer_stat['message']
|
135
|
+
puts message unless message.nil? || message.empty?
|
136
|
+
end
|
137
|
+
|
96
138
|
DDL_DUMP_CMD_TEMPLATE = "mysqldump --protocol=tcp -d -h %s -P %s -u %s %s %s %s"
|
97
139
|
def do_generate_table_ddl(de)
|
98
140
|
if `which mysqldump`.empty?
|
@@ -110,10 +152,11 @@ module Flydata
|
|
110
152
|
|
111
153
|
command = DDL_DUMP_CMD_TEMPLATE % params
|
112
154
|
|
113
|
-
|
155
|
+
Open3.popen3(command) do |stdin, stdout, stderr|
|
156
|
+
stdin.close
|
114
157
|
create_flydata_ctl_table = mp['initial_sync']
|
115
|
-
while !
|
116
|
-
mysql_tabledef = Flydata::TableDef::MysqlTableDef.create(
|
158
|
+
while !stdout.eof?
|
159
|
+
mysql_tabledef = Flydata::TableDef::MysqlTableDef.create(stdout)
|
117
160
|
if mysql_tabledef.nil?
|
118
161
|
# stream had no more create table definition
|
119
162
|
break
|
@@ -122,6 +165,10 @@ module Flydata
|
|
122
165
|
puts Flydata::TableDef::RedshiftTableDef.from_flydata_tabledef(flydata_tabledef, flydata_ctl_table: create_flydata_ctl_table)
|
123
166
|
create_flydata_ctl_table = false
|
124
167
|
end
|
168
|
+
while !stderr.eof?
|
169
|
+
line = stderr.gets
|
170
|
+
$stderr.print line unless /Warning: Using a password on the command line interface can be insecure./ === line
|
171
|
+
end
|
125
172
|
end
|
126
173
|
end
|
127
174
|
|
@@ -131,7 +178,7 @@ module Flydata
|
|
131
178
|
|
132
179
|
# Check client condition
|
133
180
|
if File.exists?(sync_fm.binlog_path) and de['mysql_data_entry_preference']['initial_sync']
|
134
|
-
raise "Already synchronized. If you want to do initial sync,
|
181
|
+
raise "Already synchronized. If you want to do initial sync, run 'flydata sync:reset'"
|
135
182
|
end
|
136
183
|
|
137
184
|
# Copy template if not exists
|
@@ -141,7 +188,7 @@ module Flydata
|
|
141
188
|
|
142
189
|
if generate_mysqldump(de, sync_fm)
|
143
190
|
sync_fm.save_sync_info(de['mysql_data_entry_preference']['initial_sync'], de['mysql_data_entry_preference']['tables'])
|
144
|
-
|
191
|
+
parse_mysqldump_and_send(dp, de, sync_fm)
|
145
192
|
complete
|
146
193
|
end
|
147
194
|
end
|
@@ -156,32 +203,40 @@ module Flydata
|
|
156
203
|
end
|
157
204
|
end
|
158
205
|
|
159
|
-
puts "Running mysqldump... host:#{de['mysql_data_entry_preference']['host']} " +
|
160
|
-
"username:#{de['mysql_data_entry_preference']['username']} " +
|
161
|
-
"database:#{de['mysql_data_entry_preference']['database']}"
|
162
|
-
if de['mysql_data_entry_preference']['data_servers']
|
163
|
-
puts "Send to Custom Data Servers: #{de['mysql_data_entry_preference']['data_servers']}"
|
164
|
-
end
|
165
|
-
|
166
|
-
if de['mysql_data_entry_preference']['tables']
|
167
|
-
puts " target tables: #{de['mysql_data_entry_preference']['tables']}"
|
168
|
-
else
|
169
|
-
puts " target tables: <all-tables>"
|
170
|
-
end
|
171
|
-
|
172
206
|
fp = sync_fm.dump_file_path
|
173
207
|
if File.exists?(fp) and File.size(fp) > 0 and not overwrite
|
174
208
|
puts " -> Skip"
|
175
209
|
return fp
|
176
210
|
end
|
177
211
|
|
178
|
-
|
179
|
-
|
212
|
+
tables = de['mysql_data_entry_preference']['tables']
|
213
|
+
tables ||= '<all tables>'
|
214
|
+
data_servers = de['mysql_data_entry_preference']['data_servers'] ? "\n data servers: #{de['mysql_data_entry_preference']['data_servers']}" : ""
|
215
|
+
|
216
|
+
confirmation_text = <<-EOM
|
217
|
+
|
218
|
+
FlyData Sync will start synchonizing the following database tables
|
219
|
+
host: #{de['mysql_data_entry_preference']['host']}
|
220
|
+
port: #{de['mysql_data_entry_preference']['port']}
|
221
|
+
username: #{de['mysql_data_entry_preference']['username']}
|
222
|
+
database: #{de['mysql_data_entry_preference']['database']}
|
223
|
+
tables: #{tables}#{data_servers}
|
224
|
+
dump file: #{fp}
|
225
|
+
|
226
|
+
Dump file saves contents of your tables temporarily. Make sure you have enough disk space.
|
227
|
+
EOM
|
228
|
+
|
229
|
+
print confirmation_text
|
230
|
+
|
231
|
+
if ask_yes_no('Start Sync?')
|
232
|
+
puts "Exporting data from database..."
|
180
233
|
Flydata::Mysql::MysqlDumpGeneratorNoMasterData.new(de['mysql_data_entry_preference']).dump(fp)
|
181
234
|
else
|
182
235
|
newline
|
183
|
-
puts "You can change the
|
184
|
-
puts
|
236
|
+
puts "You can change the dump file path with 'mysqldump_path' property in the following conf file."
|
237
|
+
puts
|
238
|
+
puts " #{Flydata::Preference::DataEntryPreference.conf_path(de)}"
|
239
|
+
puts
|
185
240
|
return nil
|
186
241
|
end
|
187
242
|
puts " -> Done"
|
@@ -208,8 +263,8 @@ module Flydata
|
|
208
263
|
# <- checkpoint
|
209
264
|
#...
|
210
265
|
#CREATE TABLE ...
|
211
|
-
def
|
212
|
-
puts "
|
266
|
+
def parse_mysqldump_and_send(dp, de, sync_fm)
|
267
|
+
puts "Sending data to FlyData Server..."
|
213
268
|
|
214
269
|
# Prepare forwarder
|
215
270
|
de_tag_name = de["tag_name#{env_suffix}"]
|
@@ -250,15 +305,13 @@ module Flydata
|
|
250
305
|
ret = flydata.redshift_cluster.run_query(sql)
|
251
306
|
if ret['message'].index('ERROR:')
|
252
307
|
if ret['message'].index('already exists')
|
253
|
-
puts "
|
308
|
+
puts " -> Skip"
|
254
309
|
else
|
255
310
|
raise "Failed to create table. error=#{ret['message']}"
|
256
311
|
end
|
257
312
|
else
|
258
|
-
puts "
|
313
|
+
puts " -> OK"
|
259
314
|
end
|
260
|
-
else
|
261
|
-
puts "- Parsing table: #{mysql_table.table_name}"
|
262
315
|
end
|
263
316
|
|
264
317
|
# dump mysql_table for resume
|
@@ -273,19 +326,17 @@ module Flydata
|
|
273
326
|
end
|
274
327
|
ret = forwarder.emit(records)
|
275
328
|
tmp_num_inserted_record += 1
|
276
|
-
print '.'
|
277
329
|
ret
|
278
330
|
},
|
279
331
|
# checkpoint
|
280
332
|
Proc.new { |mysql_table, last_pos, binlog_pos, state, substate|
|
281
333
|
# flush if buffer records exist
|
282
334
|
if tmp_num_inserted_record > 0 && forwarder.buffer_record_count > 0
|
283
|
-
puts
|
284
335
|
forwarder.flush # send buffer data to the server before checkpoint
|
285
336
|
end
|
286
337
|
|
287
338
|
# show the current progress
|
288
|
-
puts "
|
339
|
+
puts " -> #{(last_pos.to_f/dump_file_size * 100).round(1)}% (#{last_pos}/#{dump_file_size}) completed..."
|
289
340
|
|
290
341
|
# save check point
|
291
342
|
table_name = mysql_table.nil? ? '' : mysql_table.table_name
|
@@ -294,37 +345,57 @@ module Flydata
|
|
294
345
|
)
|
295
346
|
forwarder.close
|
296
347
|
|
348
|
+
puts " -> Done"
|
349
|
+
|
297
350
|
if ENV['FLYDATA_BENCHMARK']
|
298
|
-
puts "Done!"
|
299
351
|
bench_end_time = Time.now
|
300
352
|
elapsed_time = bench_end_time.to_i - bench_start_time.to_i
|
301
353
|
puts "Elapsed:#{elapsed_time}sec start:#{bench_start_time} end:#{bench_end_time}"
|
302
354
|
return true
|
303
355
|
end
|
304
356
|
# wait until finish
|
305
|
-
|
306
|
-
sleep 10
|
307
|
-
until check
|
308
|
-
sleep 10
|
309
|
-
end
|
357
|
+
wait_for_server_data_processing
|
310
358
|
|
311
359
|
sync_fm.save_dump_pos(STATUS_COMPLETE, '', dump_file_size, binlog_pos)
|
312
360
|
tables = de['mysql_data_entry_preference']['tables'].split(',').join(' ')
|
313
361
|
sync_fm.save_table_binlog_pos(tables, binlog_pos)
|
314
362
|
end
|
315
363
|
|
364
|
+
ALL_DONE_MESSAGE_TEMPLATE = <<-EOM
|
365
|
+
|
366
|
+
Congratulations! FlyData has started synchronizing your database tables.
|
367
|
+
|
368
|
+
What's next?
|
369
|
+
|
370
|
+
- Check data on Redshift (%s)
|
371
|
+
- Check your FlyData usage on the FlyData Dashboard (%s)
|
372
|
+
- To manage the FlyData Agent, use the 'flydata' command (type 'flydata' for help)
|
373
|
+
- If you encounter an issue,
|
374
|
+
please check our documentation (https://www.flydata.com/docs/) or
|
375
|
+
contact our customer support team (support@flydata.com)
|
376
|
+
|
377
|
+
Thank you for using FlyData!
|
378
|
+
EOM
|
316
379
|
def complete
|
317
380
|
de = load_sync_info(retrieve_data_entries.first)
|
318
381
|
sync_fm = Flydata::FileUtil::SyncFileManager.new(de)
|
319
382
|
info = sync_fm.load_dump_pos
|
320
383
|
if info[:status] == STATUS_COMPLETE
|
384
|
+
puts "Starting FlyData Agent..."
|
321
385
|
if de['mysql_data_entry_preference']['initial_sync']
|
322
386
|
sync_fm.save_binlog(info[:binlog_pos])
|
323
387
|
end
|
324
388
|
sync_fm.move_table_binlog_files(de['mysql_data_entry_preference']['tables'].split(','))
|
325
389
|
sync_fm.reset_table_position_files(de['mysql_data_entry_preference']['tables'].split(','))
|
326
390
|
sync_fm.backup_dump_dir
|
327
|
-
Flydata::Command::Sender.new.start
|
391
|
+
Flydata::Command::Sender.new.start(quiet: true)
|
392
|
+
puts " -> Done"
|
393
|
+
|
394
|
+
data_port = flydata.data_port.get
|
395
|
+
dashboard_url = "#{flydata.flydata_api_host}/data_ports/#{data_port['id']}"
|
396
|
+
redshift_console_url = "#{flydata.flydata_api_host}/redshift_clusters/query/new"
|
397
|
+
last_message = ALL_DONE_MESSAGE_TEMPLATE % [redshift_console_url, dashboard_url]
|
398
|
+
puts last_message
|
328
399
|
else
|
329
400
|
raise "Initial sync status is not complete. Try running 'flydata sync'."
|
330
401
|
end
|
@@ -359,10 +430,7 @@ module Flydata
|
|
359
430
|
def flush_buffer_and_stop
|
360
431
|
sender = Flydata::Command::Sender.new
|
361
432
|
sender.flush_client_buffer
|
362
|
-
|
363
|
-
until check
|
364
|
-
sleep 10
|
365
|
-
end
|
433
|
+
wait_for_server_data_processing
|
366
434
|
sender.stop(quiet: true)
|
367
435
|
end
|
368
436
|
end
|
@@ -374,10 +442,10 @@ module Flydata
|
|
374
442
|
def self.create(forwarder_key, tag, servers, options = {})
|
375
443
|
case forwarder_key
|
376
444
|
when nil, "tcpforwarder"
|
377
|
-
puts "Creating TCP connection"
|
445
|
+
puts "Creating TCP connection" if FLYDATA_DEBUG
|
378
446
|
forward = TcpForwarder.new(tag, servers, options)
|
379
447
|
when "sslforwarder"
|
380
|
-
puts "Creating SSL connection"
|
448
|
+
puts "Creating SSL connection" if FLYDATA_DEBUG
|
381
449
|
forward = SslForwarder.new(tag, servers, options)
|
382
450
|
else
|
383
451
|
raise "Unsupported Forwarding type #{forwarder_key}"
|
@@ -432,7 +500,6 @@ module Flydata
|
|
432
500
|
#TODO retry logic
|
433
501
|
def send
|
434
502
|
if @buffer_size > 0
|
435
|
-
puts " -> Sending #{@buffer_record_count}records #{@buffer_size}byte"
|
436
503
|
else
|
437
504
|
return false
|
438
505
|
end
|
@@ -1139,12 +1206,12 @@ EOS
|
|
1139
1206
|
else
|
1140
1207
|
if chars.end_with?(')')
|
1141
1208
|
chars = chars[0..-2]
|
1142
|
-
@values << (chars == 'NULL' ? nil : chars)
|
1209
|
+
@values << (chars == 'NULL' ? nil : remove_leading_zeros(chars))
|
1143
1210
|
@values_set << @values
|
1144
1211
|
@values = []
|
1145
1212
|
cur_state = State::NEXT_VALUES
|
1146
1213
|
else
|
1147
|
-
@values << (chars == 'NULL' ? nil : chars)
|
1214
|
+
@values << (chars == 'NULL' ? nil : remove_leading_zeros(chars))
|
1148
1215
|
end
|
1149
1216
|
end
|
1150
1217
|
else
|
@@ -1154,10 +1221,10 @@ EOS
|
|
1154
1221
|
return @values_set
|
1155
1222
|
end
|
1156
1223
|
|
1157
|
-
ESCAPE_HASH_TABLE = {"\\\\" => "\\", "\\'" => "'", "\\n" => "\n",
|
1224
|
+
ESCAPE_HASH_TABLE = {"\\\\" => "\\", "\\'" => "'", "\\\"" => "\"", "\\n" => "\n", "\\r" => "\r"}
|
1158
1225
|
|
1159
1226
|
def replace_escape_char(original)
|
1160
|
-
original.gsub(/\\\\|\\'|\\n|\\r/, ESCAPE_HASH_TABLE)
|
1227
|
+
original.gsub(/\\\\|\\'|\\"|\\n|\\r/, ESCAPE_HASH_TABLE)
|
1161
1228
|
end
|
1162
1229
|
|
1163
1230
|
# This method assume that the last character is '(single quotation)
|
@@ -1175,6 +1242,14 @@ EOS
|
|
1175
1242
|
end
|
1176
1243
|
flag
|
1177
1244
|
end
|
1245
|
+
|
1246
|
+
def remove_leading_zeros(number_string)
|
1247
|
+
if number_string.start_with?('0')
|
1248
|
+
number_string.sub(/^0*([1-9][0-9]*(\.\d*)?|0(\.\d*)?)$/,'\1')
|
1249
|
+
else
|
1250
|
+
number_string
|
1251
|
+
end
|
1252
|
+
end
|
1178
1253
|
end
|
1179
1254
|
end
|
1180
1255
|
end
|
data/lib/flydata/helpers.rb
CHANGED
@@ -13,8 +13,14 @@ module Flydata
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def print_usage
|
16
|
-
puts
|
17
|
-
|
16
|
+
puts usage_text
|
17
|
+
end
|
18
|
+
|
19
|
+
def usage_text
|
20
|
+
text = ""
|
21
|
+
text += '-' * 64
|
22
|
+
text += "\n"
|
23
|
+
text += <<-EOM
|
18
24
|
Usage: flydata COMMAND
|
19
25
|
setup # setup initially
|
20
26
|
start # start flydata process
|
@@ -32,7 +38,10 @@ please setup flydata again by following commands.
|
|
32
38
|
You can check the logs of sender(flydata) process.
|
33
39
|
Log path: #{File.join(FLYDATA_HOME, 'flydata.log')}
|
34
40
|
EOM
|
35
|
-
|
41
|
+
text += "\n"
|
42
|
+
text += '-' * 64
|
43
|
+
text += "\n"
|
44
|
+
text
|
36
45
|
end
|
37
46
|
|
38
47
|
def to_command_class(class_name)
|
@@ -5,7 +5,7 @@ class MysqlTableDef
|
|
5
5
|
TYPE_MAP_M2F = {
|
6
6
|
'bigint' => 'int8',
|
7
7
|
'binary' => 'binary',
|
8
|
-
'blob' => 'varbinary',
|
8
|
+
'blob' => 'varbinary(65535)',
|
9
9
|
'bool' => 'int1',
|
10
10
|
'boolean' => 'int1',
|
11
11
|
'char' => 'varchar',
|
@@ -20,9 +20,9 @@ class MysqlTableDef
|
|
20
20
|
'float' => 'float4',
|
21
21
|
'int' => 'int4',
|
22
22
|
'integer' => 'int4',
|
23
|
-
'longblob' => 'varbinary',
|
23
|
+
'longblob' => 'varbinary(4294967295)',
|
24
24
|
'longtext' => 'text',
|
25
|
-
'mediumblob' => 'varbinary',
|
25
|
+
'mediumblob' => 'varbinary(16777215)',
|
26
26
|
'mediumint' => 'int3',
|
27
27
|
'mediumtext' => 'text',
|
28
28
|
'numeric' => 'numeric',
|
@@ -30,7 +30,7 @@ class MysqlTableDef
|
|
30
30
|
'text' => 'text',
|
31
31
|
'time' => 'time',
|
32
32
|
'timestamp' => 'datetime',
|
33
|
-
'tinyblob' => 'varbinary',
|
33
|
+
'tinyblob' => 'varbinary(255)',
|
34
34
|
'tinyint' => 'int1',
|
35
35
|
'tinytext' => 'text',
|
36
36
|
'varbinary' => 'varbinary',
|
@@ -113,6 +113,8 @@ class MysqlTableDef
|
|
113
113
|
# index creation. No action required.
|
114
114
|
elsif stripped_line.start_with?("CONSTRAINT")
|
115
115
|
# constraint definition. No acction required.
|
116
|
+
elsif stripped_line.start_with?("UNIQUE KEY")
|
117
|
+
# constraint definition. No acction required.
|
116
118
|
else
|
117
119
|
$stderr.puts "Unknown table definition. Skip. (#{line})"
|
118
120
|
end
|
@@ -174,6 +176,7 @@ class MysqlTableDef
|
|
174
176
|
|
175
177
|
cond = :options
|
176
178
|
when :options
|
179
|
+
column[:type] += ' unsigned' if line =~ /unsigned/i
|
177
180
|
column[:auto_increment] = true if line =~ /AUTO_INCREMENT/i
|
178
181
|
column[:not_null] = true if line =~ /NOT NULL/i
|
179
182
|
if /DEFAULT\s+((?:[^'\s]+\b)|(?:'(?:\\'|[^'])*'))/i.match(line)
|