myreplicator 0.0.16 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/app/assets/stylesheets/myreplicator/application.css +53 -10
  2. data/app/controllers/myreplicator/exports_controller.rb +17 -1
  3. data/app/controllers/myreplicator/home_controller.rb +4 -1
  4. data/app/helpers/myreplicator/application_helper.rb +20 -0
  5. data/app/models/myreplicator/export.rb +108 -23
  6. data/app/models/myreplicator/log.rb +90 -0
  7. data/app/views/myreplicator/exports/_form.html.erb +33 -9
  8. data/app/views/myreplicator/home/_home_menu.erb +6 -2
  9. data/app/views/myreplicator/home/errors.html.erb +15 -5
  10. data/app/views/myreplicator/home/index.html.erb +20 -43
  11. data/db/migrate/20121025191622_create_myreplicator_exports.rb +0 -6
  12. data/db/migrate/20121212003652_create_myreplicator_logs.rb +21 -0
  13. data/lib/exporter/export_metadata.rb +8 -1
  14. data/lib/exporter/mysql_exporter.rb +32 -0
  15. data/lib/exporter/sql_commands.rb +85 -4
  16. data/lib/loader/loader.rb +48 -7
  17. data/lib/myreplicator.rb +16 -4
  18. data/lib/myreplicator/version.rb +1 -1
  19. data/lib/transporter/parallelizer.rb +8 -0
  20. data/lib/transporter/transporter.rb +75 -11
  21. data/test/dummy/config/myreplicator.yml +0 -2
  22. data/test/dummy/config/myreplicator.yml~ +2 -0
  23. data/test/dummy/db/migrate/{20121108204327_create_myreplicator_exports.myreplicator.rb~ → 20121213003552_create_myreplicator_exports.myreplicator.rb} +3 -1
  24. data/test/dummy/db/migrate/20121213003553_create_myreplicator_logs.myreplicator.rb +22 -0
  25. data/test/dummy/db/schema.rb +24 -7
  26. data/test/dummy/log/development.log +1083 -0
  27. data/test/dummy/tmp/cache/assets/CD5/B90/sprockets%2Fc999d13a6a21113981c0d820e8043bdf +0 -0
  28. data/test/dummy/tmp/cache/assets/D8B/B60/sprockets%2Faa32227c440a378ccd21218eefeb80bf +0 -0
  29. data/test/dummy/tmp/cache/assets/DF8/5D0/sprockets%2Fb815ed34d61cfed96222daa3bfd1d84d +0 -0
  30. data/test/dummy/tmp/myreplicator/okl_test_batchy_batches_1355432256.tsv.gz +0 -0
  31. data/test/unit/myreplicator/log_test.rb +9 -0
  32. metadata +14 -8
  33. data/test/dummy/db/migrate/20121115194022_create_myreplicator_exports.myreplicator.rb +0 -36
@@ -3,7 +3,6 @@
3
3
  export_to = ["destination_db","s3","both"]
4
4
  export_type = ["incremental","fulldump"]
5
5
  %>
6
-
7
6
  <%= form_for(@export) do |f| %>
8
7
  <% if @export.errors.any? %>
9
8
  <div id="error_explanation">
@@ -18,11 +17,11 @@ export_type = ["incremental","fulldump"]
18
17
  <% end %>
19
18
  <div class="form-section">
20
19
  <label>Source Schema</label>
21
- <%= f.text_field :source_schema %>
20
+ <%= f.select :source_schema, @dbs %>
22
21
  <label>Destination Schema</label>
23
- <%= f.text_field :destination_schema %>
22
+ <%= f.select :destination_schema, @dbs %>
24
23
  <label>Table Name</label>
25
- <%= f.text_field :table_name %>
24
+ <%= f.select :table_name, [] %>
26
25
  <label>Incremental Column</label>
27
26
  <%= f.text_field :incremental_column %>
28
27
  <label>Maximum Incremental Value</label>
@@ -93,12 +92,29 @@ export_type = ["incremental","fulldump"]
93
92
  <%= javascript_include_tag "myreplicator/chosen.jquery.min" %>
94
93
  <%= javascript_include_tag "myreplicator/cronwtf.min" %>
95
94
  <script>
95
+ var dbs = {
96
+ <% @tables.each do |key,values| %>
97
+ "<%= key %>":[
98
+ <% values.each do |table| %>
99
+ "<%= table %>",
100
+ <% end %>
101
+ ],
102
+ <% end %>
103
+ }
96
104
  $(function(){
97
105
  CronUI.translate();
98
106
  $(".chosen").chosen();
107
+ $("#export_destination_schema,#export_table_name").chosen();
99
108
  $(".cron").chosen().change(function(){CronUI.translate()});
109
+ $("#export_source_schema").chosen().change(function(){
110
+ exportSchemaSelect($(this).val())
111
+ });
112
+ exportSchemaSelect($("#export_source_schema").val());
113
+ <%- if @edit -%>
114
+ editInit();
115
+ <%- end -%>
116
+ })
100
117
 
101
- <%- if @edit -%>
102
118
  function editInit(){
103
119
  var cron = "<%= @export.cron %>";
104
120
  var displays = ["#min","#hour","#day","#month","#dow"];
@@ -108,10 +124,18 @@ function editInit(){
108
124
  $(displays[i]).val(cron_vals[i]).trigger("liszt:updated");
109
125
  }
110
126
  CronUI.translate();
127
+ $("#export_table_name").val("<%= @export.table_name %>").trigger("liszt:updated");
111
128
  }
112
- editInit();
113
- <%- end -%>
114
129
 
115
-
116
- })
130
+ function exportSchemaSelect(val){
131
+ var target = $("#export_table_name");
132
+ var tables = dbs[val];
133
+ var l = tables.length;
134
+ var options = "";
135
+ for(i=0;i<l;i++){
136
+ options += "<option value='"+tables[i]+"'>"+tables[i]+"</option>";
137
+ }
138
+ target.val('').find("option").remove().trigger("liszt:updated");
139
+ target.append(options).trigger("liszt:updated");
140
+ }
117
141
  </script>
@@ -1,5 +1,9 @@
1
+ <%
2
+ err = err_count
3
+ run = run_count
4
+ %>
1
5
  <div class="home-menu">
2
6
  <span>lists:</span>
3
- <a href="<%= root_url %>" class="<% if @option == 'overview' %>on<% end %>">overview</a>
4
- <a href="<%= errors_path %>" class="<% if @option == 'errors' %>on<% end %>">errors</a>
7
+ <a href="<%= root_url %>" class="<% if @option == 'overview' %>on<% end %> overview">overview <% if run > 0 %><span><%= run %></span><% end %></a>
8
+ <a href="<%= errors_path %>" class="<% if @option == 'errors' %>on<% end %>">errors <% if err > 0 %><span><%= err %></span><% end %></a>
5
9
  </div>
@@ -1,10 +1,20 @@
1
1
  <%= render :partial => 'home_menu' %>
2
2
  <% @exports.each do |ex| %>
3
3
  <div class="error-display">
4
- <span><em>table name:</em> <%= ex.table_name %></span>
5
- <span><em>source schema:</em><%= ex.source_schema %></span>
6
- <span><em>updated at:</em><%= ex.updated_at.strftime("%Y-%m-%d %H:%M") %></span>
7
- <span><em>error:</em></span>
8
- <pre><%= ex.error %></pre>
4
+ <span><em>table name:</em> <%= ex.table_name %></span>
5
+ <span><em>source schema:</em><%= ex.source_schema %></span>
6
+ <span><em>updated at:</em><%= ex.updated_at.strftime("%Y-%m-%d %H:%M") %></span>
7
+ <span><em>error:</em></span>
8
+ <pre><%= ex.error %></pre>
9
+ </div>
10
+ <% end %>
11
+ <% @logs.each do |log| %>
12
+ <div class="error-display">
13
+ <span><em>name:</em> <%= log.name %></span>
14
+ <span><em>file:</em> <%= log.file %></span>
15
+ <span><em>type:</em> <%= log.job_type %></span>
16
+ <span><em>started:</em> <%= log.started_at.strftime("%Y-%m-%d %H:%M:%S") %></span>
17
+ <span><em>error:</em></span>
18
+ <pre><%= log.error %></pre>
9
19
  </div>
10
20
  <% end %>
@@ -1,48 +1,25 @@
1
1
  <%= render :partial => 'home_menu' %>
2
- <% states = @exports.group_by(&:state) %>
3
2
  <div id="overview-wrapper">
4
- <div id="simple-wrapper">
5
- <table id="states-list">
6
- <thead>
7
- <tr>
8
- <th>State</th>
9
- <th class="centered">Count</th>
10
- </tr>
11
- </thead>
12
- <tbody>
13
- <% states.each do |state, exports| %>
14
- <tr>
15
- <td><a href="#<%= state.downcase %>_state" class="state-toggle"><%= state %></a></td>
16
- <td class="c"><%= exports.size %></td>
17
- </tr>
18
- <% end %>
19
- </tbody>
20
- </table>
21
- <p class="hint">***click a state to view export list</p>
22
- </div>
23
- <ul id="states-tables">
24
- <% states.each do |state, exports| %>
25
- <li id="<%= state.downcase %>_state">
26
- <h4><%= state %> Exports</h4>
27
- <table class="data-grid">
28
- <thead>
29
- <tr>
30
- <th>Table</th>
31
- <th>Schema</th>
32
- <th>Updated</th>
33
- </tr>
34
- </thead>
35
- <tbody>
36
- <% exports.sort{|a,b| b.updated_at<=>a.updated_at}.each do |ex| %>
37
- <td><%= ex.table_name %></td>
38
- <td><%= ex.source_schema %></td>
39
- <td><%= ex.updated_at.strftime("%Y-%m-%d %H:%M") %></td>
40
- <% end %>
41
- </tbody>
42
- </table>
43
- </li>
44
- <% end %>
45
- </ul>
3
+ <h6>Running tasks</h6>
4
+ <table class="overview">
5
+ <% @logs.each do |log| %>
6
+ <tr>
7
+ <td>
8
+ <span class="name"><%= log.name %></span>
9
+ <span class="file"><%= log.file %></span>
10
+ </td>
11
+ <td><%= log.state %></td>
12
+ <td><%= log.job_type %></td>
13
+ <td><%= log.started_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
14
+ <td><%= chronos(@now.to_i - log.started_at.to_i) %></td>
15
+ </tr>
16
+ <% end %>
17
+ </table>
18
+
19
+
20
+
21
+
22
+
46
23
  </div>
47
24
  <script>
48
25
  jQuery(function(){
@@ -15,14 +15,8 @@ class CreateMyreplicatorExports < ActiveRecord::Migration
15
15
  t.text :error
16
16
  t.boolean :active, :default => true
17
17
  t.integer :exporter_pid
18
- t.integer :transporter_pid
19
- t.integer :loader_pid
20
18
  t.datetime :export_started_at, :default => nil
21
19
  t.datetime :export_finished_at, :default => nil
22
- t.datetime :load_started_at, :default => nil
23
- t.datetime :load_finished_at, :default => nil
24
- t.datetime :transfer_started_at, :default => nil
25
- t.datetime :transfer_finished_at, :default => nil
26
20
  t.timestamps
27
21
  end
28
22
  add_index :myreplicator_exports, [:source_schema, :destination_schema, :table_name], :unique => true, :name => "unique_index"
@@ -0,0 +1,21 @@
1
+ class CreateMyreplicatorLogs < ActiveRecord::Migration
2
+ def change
3
+ create_table :myreplicator_logs do |t|
4
+ t.integer :pid
5
+ t.string :job_type
6
+ t.string :name
7
+ t.string :file
8
+ t.string :state
9
+ t.string :thread_state
10
+ t.string :hostname
11
+ t.string :export_id
12
+ t.text :error
13
+ t.text :backtrace
14
+ t.string :guid
15
+ t.datetime :started_at, :default => nil
16
+ t.datetime :finished_at, :default => nil
17
+
18
+ t.timestamps
19
+ end
20
+ end
21
+ end
@@ -32,6 +32,7 @@ module Myreplicator
32
32
  end
33
33
  end
34
34
 
35
+ # BOB : This only handles gzipped files, is that what you want?
35
36
  def filename
36
37
  name = filepath.split("/").last
37
38
  name = zipped ? "#{name}.gz" : name
@@ -44,7 +45,7 @@ module Myreplicator
44
45
 
45
46
  ##
46
47
  # Keeps track of the state of the export
47
- # Store itself in a JSON file on exit
48
+ # Stores itself in a JSON file on exit
48
49
  ##
49
50
  def self.record *args
50
51
  options = args.extract_options!
@@ -160,6 +161,11 @@ module Myreplicator
160
161
  return path
161
162
  end
162
163
 
164
+ ##
165
+ # Writes Json to file using echo
166
+ # file is written to remote server via SSH
167
+ # Echo is used for writing the file
168
+ ##
163
169
  def store!
164
170
  cmd = "echo \"#{self.to_json.gsub("\"","\\\\\"")}\" > #{@filepath}.json"
165
171
  puts cmd
@@ -175,6 +181,7 @@ module Myreplicator
175
181
 
176
182
  def set_attributes options
177
183
  options.symbolize_keys!
184
+
178
185
  @export_time = options[:export_time] if options[:export_time]
179
186
  @table = options[:table] if options[:table]
180
187
  @database = options[:database] if options[:database]
@@ -31,9 +31,12 @@ module Myreplicator
31
31
  metadata.state = "export_completed"
32
32
  wrapup metadata
33
33
  end
34
+
34
35
  elsif !is_running?
35
36
  # local max value for incremental export
37
+
36
38
  max_value = incremental_export(metadata)
39
+ #max_value = incremental_export_into_outfile(metadata)
37
40
 
38
41
  metadata.incremental_val = max_value # store max val in metadata
39
42
 
@@ -128,9 +131,38 @@ module Myreplicator
128
131
  puts "Exporting..."
129
132
  result = execute_export(cmd, metadata)
130
133
  check_result(result, 0)
134
+
135
+ return max_value
131
136
  end
132
137
 
133
138
 
139
+ ##
140
+ # Exports table incrementally, similar to incremental_export method
141
+ # Dumps file in tmp directory specified in myreplicator.yml
142
+ # Note that directory needs 777 permissions for mysql to be able to export the file
143
+ # Uses ;~; as the delimiter and new line for lines
144
+ ##
145
+
146
+ def incremental_export_into_outfile metadata
147
+ max_value = @export_obj.max_value
148
+ @export_obj.update_max_val if @export_obj.max_incremental_value.blank?
149
+
150
+ cmd = SqlCommands.mysql_export_outfile(:db => @export_obj.source_schema,
151
+ :table => @export_obj.table_name,
152
+ :filepath => filepath,
153
+ :incremental_col => @export_obj.incremental_column,
154
+ :incremental_col_type => @export_obj.incremental_column_type,
155
+ :incremental_val => @export_obj.max_incremental_value)
156
+
157
+ metadata.export_type = "incremental_outfile"
158
+ update_export(:state => "exporting", :export_started_at => Time.now, :exporter_pid => Process.pid)
159
+ puts "Exporting..."
160
+ result = execute_export(cmd, metadata)
161
+ check_result(result, 0)
162
+
163
+ return max_value
164
+ end
165
+
134
166
  ##
135
167
  # Completes an export process
136
168
  # Zips files, updates states etc
@@ -34,14 +34,25 @@ module Myreplicator
34
34
  return cmd
35
35
  end
36
36
 
37
+ ##
38
+ # Db configs for active record connection
39
+ ##
40
+
37
41
  def self.db_configs db
38
42
  ActiveRecord::Base.configurations[db]
39
43
  end
40
44
 
45
+ ##
46
+ # Configs needed for SSH connection to source server
47
+ ##
48
+
41
49
  def self.ssh_configs db
42
50
  Myreplicator.configs[db]
43
51
  end
44
52
 
53
+ ##
54
+ # Default dump flags
55
+ ##
45
56
  def self.dump_flags
46
57
  {"add-locks" => true,
47
58
  "compact" => false,
@@ -55,6 +66,10 @@ module Myreplicator
55
66
  }
56
67
  end
57
68
 
69
+ ##
70
+ # Mysql exports using -e flag
71
+ ##
72
+
58
73
  def self.mysql_export *args
59
74
  options = args.extract_options!
60
75
  options.reverse_merge! :flags => []
@@ -83,6 +98,69 @@ module Myreplicator
83
98
  return cmd
84
99
  end
85
100
 
101
+ ##
102
+ # Mysql export data into outfile option
103
+ # Provided for tables that need special delimiters
104
+ ##
105
+
106
+ def self.get_outfile_sql options
107
+ sql = "SELECT * INTO OUTFILE '#{options[:filepath]}' "
108
+
109
+ sql += " FIELDS TERMINATED BY ';~;' OPTIONALLY ENCLOSED BY '\\\"' LINES TERMINATED BY '\\n'"
110
+
111
+ sql += "FROM #{options[:db]}.#{options[:table]} "
112
+
113
+ if options[:incremental_col] && options[:incremental_val]
114
+ if options[:incremental_col_type] == "datetime"
115
+ sql += "WHERE #{options[:incremental_col]} >= '#{options[:incremental_val]}'"
116
+ else
117
+ sql += "WHERE #{options[:incremental_col]} >= #{options[:incremental_val]}"
118
+ end
119
+ end
120
+
121
+ return sql
122
+ end
123
+
124
+ ##
125
+ # Export using outfile
126
+ # ;~; delimited
127
+ # terminated by newline
128
+ # Location of the output file needs to have 777 perms
129
+ ##
130
+ def self.mysql_export_outfile *args
131
+ options = args.extract_options!
132
+ options.reverse_merge! :flags => []
133
+ db = options[:db]
134
+
135
+ # Database host when ssh'ed into the db server
136
+ db_host = ssh_configs(db)["ssh_db_host"].nil? ? "127.0.0.1" : ssh_configs(db)["ssh_db_host"]
137
+
138
+ flags = ""
139
+
140
+ self.mysql_flags.each_pair do |flag, value|
141
+ if options[:flags].include? flag
142
+ flags += " --#{flag} "
143
+ elsif value
144
+ flags += " --#{flag} "
145
+ end
146
+ end
147
+
148
+ # BOB : You always build the host,password,username part of the command
149
+ # Seems like this should be in a function somewhere
150
+ cmd = Myreplicator.mysql
151
+ cmd += "#{flags} -u#{db_configs(db)["username"]} -p#{db_configs(db)["password"]} "
152
+ cmd += "-h#{db_host} " if db_configs(db)["host"].blank?
153
+ cmd += db_configs(db)["port"].blank? ? "-P3306 " : "-P#{db_configs(db)["port"]} "
154
+ cmd += "--execute=\"#{get_outfile_sql(options)}\" "
155
+
156
+ puts cmd
157
+
158
+ return cmd
159
+ end
160
+
161
+ ##
162
+ # Default flags for mysql export
163
+ ##
86
164
  def self.mysql_flags
87
165
  {"column-names" => false,
88
166
  "quick" => true,
@@ -90,6 +168,9 @@ module Myreplicator
90
168
  }
91
169
  end
92
170
 
171
+ ##
172
+ # Builds SQL needed for incremental exports
173
+ ##
93
174
  def self.export_sql *args
94
175
  options = args.extract_options!
95
176
  sql = "SELECT * FROM #{options[:db]}.#{options[:table]} "
@@ -105,6 +186,10 @@ module Myreplicator
105
186
  return sql
106
187
  end
107
188
 
189
+ ##
190
+ # Gets the Maximum value for the incremental
191
+ # column of the export job
192
+ ##
108
193
  def self.max_value_sql *args
109
194
  options = args.extract_options!
110
195
  sql = ""
@@ -118,9 +203,5 @@ module Myreplicator
118
203
  return sql
119
204
  end
120
205
 
121
- def self.mysql_export_outfile
122
-
123
- end
124
-
125
206
  end
126
207
  end
@@ -3,6 +3,8 @@ require "exporter"
3
3
  module Myreplicator
4
4
  class Loader
5
5
 
6
+ @queue = :myreplicator_load # Provided for Resque
7
+
6
8
  def initialize *args
7
9
  options = args.extract_options!
8
10
  end
@@ -11,10 +13,22 @@ module Myreplicator
11
13
  @tmp_dir ||= File.join(Myreplicator.app_root,"tmp", "myreplicator")
12
14
  end
13
15
 
14
- def load
16
+ ##
17
+ # Main method provided for resque
18
+ ##
19
+ def self.perform
20
+ load # Kick off the load process
21
+ end
22
+
23
+ ##
24
+ # Kicks off all initial loads first and then all incrementals
25
+ # Looks at metadata files stored locally
26
+ ##
27
+ def self.load
15
28
  initials = []
16
29
  incrementals = []
17
30
 
31
+ # Read all metadata files
18
32
  metadata_files.each do |metadata|
19
33
  if metadata.export_type == "initial"
20
34
  initials << metadata
@@ -22,16 +36,32 @@ module Myreplicator
22
36
  incrementals << metadata
23
37
  end
24
38
  end
25
-
39
+
40
+ # Load all new tables
26
41
  initials.each do |metadata|
27
42
  puts metadata.table
28
- initial_load metadata
43
+
44
+ Log.run(:job_type => "loader",
45
+ :name => "initial_import",
46
+ :file => metadata.filename,
47
+ :export_id => metadata.export_id) do |log|
48
+
49
+ initial_load metadata
50
+ end
51
+
29
52
  cleanup metadata
30
53
  end
31
54
 
55
+ # Load all incremental files
32
56
  incrementals.each do |metadata|
33
57
  puts metadata.table
34
- incremental_load metadata
58
+ Log.run(:job_type => "loader",
59
+ :name => "incremental_import",
60
+ :file => metadata.filename,
61
+ :export_id => metadata.export_id) do |log|
62
+
63
+ incremental_load metadata
64
+ end
35
65
  cleanup metadata
36
66
  end
37
67
  end
@@ -47,12 +77,14 @@ module Myreplicator
47
77
  cmd = ImportSql.initial_load(:db => exp.destination_schema,
48
78
  :filepath => metadata.destination_filepath(tmp_dir))
49
79
  puts cmd
80
+
50
81
  result = `#{cmd}` # execute
51
82
  unless result.nil?
52
83
  if result.size > 0
53
84
  raise Exceptions::LoaderError.new("Initial Load #{metadata.filename} Failed!\n#{result}")
54
85
  end
55
86
  end
87
+
56
88
  end
57
89
 
58
90
  ##
@@ -63,12 +95,21 @@ module Myreplicator
63
95
  exp = Export.find(metadata.export_id)
64
96
  unzip(metadata.filename)
65
97
  metadata.zipped = false
98
+
99
+ options = {:table_name => exp.table_name, :db => exp.destination_schema,
100
+ :filepath => metadata.destination_filepath(tmp_dir)}
101
+
102
+ if metadata.export_type == "incremental_outfile"
103
+ options[:fields_terminated_by] = ";~;"
104
+ options[:lines_terminated_by] = "\\n"
105
+ end
106
+
107
+ cmd = ImportSql.load_data_infile(options)
66
108
 
67
- cmd = ImportSql.load_data_infile(:table_name => exp.table_name,
68
- :db => exp.destination_schema,
69
- :filepath => metadata.destination_filepath(tmp_dir))
70
109
  puts cmd
110
+
71
111
  result = `#{cmd}` # execute
112
+
72
113
  unless result.nil?
73
114
  if result.size > 0
74
115
  raise Exceptions::LoaderError.new("Incremental Load #{metadata.filename} Failed!\n#{result}")