flydata 0.1.8 → 0.1.9
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.
- 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)
|