ocelot 0.0.0
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/lib/ocelot.rb +90 -0
- data/lib/ocelot/cli.rb +9 -0
- data/lib/ocelot/database.rb +41 -0
- data/lib/ocelot/dsl.rb +146 -0
- data/lib/ocelot/graph.rb +14 -0
- data/lib/ocelot/processor.rb +182 -0
- data/lib/ocelot/rules.rb +53 -0
- metadata +63 -0
data/lib/ocelot.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module Ocelot
|
2
|
+
class Base
|
3
|
+
|
4
|
+
def connection
|
5
|
+
Ocelot.connection
|
6
|
+
end
|
7
|
+
|
8
|
+
def in?(*args)
|
9
|
+
Ocelot.in?(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove_callbacks(*args)
|
13
|
+
Ocelot.remove_callbacks(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger(*args)
|
17
|
+
Ocelot.logger(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def obj_to_s(*args)
|
21
|
+
Ocelot.obj_to_s(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def safely(*args, &b)
|
25
|
+
Ocelot.safely(*args) do |*bargs|
|
26
|
+
b.call(*bargs)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'ocelot/graph'
|
32
|
+
require 'ocelot/dsl'
|
33
|
+
require 'ocelot/rules'
|
34
|
+
require 'ocelot/database'
|
35
|
+
require 'ocelot/processor'
|
36
|
+
|
37
|
+
class << self
|
38
|
+
attr_accessor :connection
|
39
|
+
attr_accessor :logger
|
40
|
+
|
41
|
+
def obj_to_s(obj, assoc=nil)
|
42
|
+
assoc = assoc.nil? ? "" : ".#{assoc.name}"
|
43
|
+
"#{obj.class}(#{obj.id})#{assoc}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def safely(obj, assoc=nil, val=nil)
|
47
|
+
begin
|
48
|
+
yield
|
49
|
+
rescue => e
|
50
|
+
target = obj.is_a?(Class) ? "#{obj}" : "#{obj_to_s(obj)}"
|
51
|
+
|
52
|
+
if assoc.nil?
|
53
|
+
logger.error "Could not read or write #{target}: #{e.message}"
|
54
|
+
elsif val.nil?
|
55
|
+
logger.error "Could not read data for #{target}.#{assoc}: #{e.message}"
|
56
|
+
else
|
57
|
+
logger.error "Could not write #{target}.#{assoc} adding #{val.class}: #{e.message}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# this should really be a class-id pair store instead of the entire object,
|
63
|
+
# /maybe/
|
64
|
+
def in?(set, node)
|
65
|
+
result = set.include? node
|
66
|
+
set << node
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove_callbacks(obj)
|
71
|
+
if obj.class.respond_to? :delete_observers
|
72
|
+
obj.class.delete_observers
|
73
|
+
end
|
74
|
+
|
75
|
+
class << obj
|
76
|
+
def valid?
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ActiveRecord::Callbacks::CALLBACKS.each do |cb|
|
82
|
+
class << obj
|
83
|
+
def run_callbacks(*goaway)
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/ocelot/cli.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
4
|
+
require 'active_record/connection_adapters/abstract/connection_pool'
|
5
|
+
|
6
|
+
module Ocelot
|
7
|
+
class Connection
|
8
|
+
attr_accessor :source
|
9
|
+
attr_accessor :target
|
10
|
+
attr_accessor :use_source
|
11
|
+
|
12
|
+
def initialize(source, target)
|
13
|
+
self.source = ActiveRecord::Base.mysql2_connection(source)
|
14
|
+
self.target = ActiveRecord::Base.mysql2_connection(target)
|
15
|
+
self.use_source = true
|
16
|
+
|
17
|
+
# ar doesn't like oop much, monkeypatch lots o stuff
|
18
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.class_variable_set(:@@conn, self)
|
19
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.class_eval do
|
20
|
+
def retrieve_connection(klass)
|
21
|
+
@@conn.current_connection
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
|
26
|
+
attr_reader :last_query
|
27
|
+
|
28
|
+
def log_with_last_query(sql, name, &block)
|
29
|
+
@last_query = [sql, name]
|
30
|
+
log_without_last_query(sql, name, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method_chain :log, :last_query
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_connection
|
38
|
+
self.use_source ? source : target
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/ocelot/dsl.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'ocelot'
|
2
|
+
require 'ocelot/rules'
|
3
|
+
|
4
|
+
module Ocelot
|
5
|
+
class Dsl
|
6
|
+
attr_accessor :rules
|
7
|
+
attr_accessor :filters
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@rules = {}
|
11
|
+
@filters = {}
|
12
|
+
@seeds = lambda { [] }
|
13
|
+
@logger = Logger.new(STDERR)
|
14
|
+
end
|
15
|
+
|
16
|
+
def source(source=nil)
|
17
|
+
@source = source unless source.nil?
|
18
|
+
@source
|
19
|
+
end
|
20
|
+
|
21
|
+
def seeds(&seeds)
|
22
|
+
@seeds = seeds unless seeds.nil?
|
23
|
+
@seeds
|
24
|
+
end
|
25
|
+
|
26
|
+
def target(target=nil)
|
27
|
+
@target = target unless target.nil?
|
28
|
+
@target
|
29
|
+
end
|
30
|
+
|
31
|
+
def classes(classes=nil)
|
32
|
+
@classes = classes.split(/\s+/) unless classes.nil?
|
33
|
+
@classes
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger
|
37
|
+
@logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_level(level=nil)
|
41
|
+
@logger.level = eval "Logger::#{level}" unless level.nil?
|
42
|
+
level
|
43
|
+
end
|
44
|
+
|
45
|
+
def init(&p)
|
46
|
+
@init = p unless p.nil?
|
47
|
+
@init
|
48
|
+
end
|
49
|
+
|
50
|
+
def rule(clazz, &p)
|
51
|
+
@rules[clazz] = ScriptRule.new(p)
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter(clazz, &p)
|
55
|
+
@filters[clazz] = ScriptFilter.new(p)
|
56
|
+
end
|
57
|
+
|
58
|
+
def go
|
59
|
+
Ocelot::logger = logger
|
60
|
+
Ocelot::connection = Ocelot::Connection.new(source, target)
|
61
|
+
|
62
|
+
unless init.nil?
|
63
|
+
Ocelot.connection.use_source = false
|
64
|
+
init.call
|
65
|
+
Ocelot.connection.use_source = true
|
66
|
+
end
|
67
|
+
|
68
|
+
copier = Ocelot::Processor.new
|
69
|
+
copier.classes = classes.collect { |c| eval c }
|
70
|
+
copier.seeds = seeds.call.flatten
|
71
|
+
copier.extra_rules = rules
|
72
|
+
copier.extra_filters = filters
|
73
|
+
|
74
|
+
if true
|
75
|
+
copier.go
|
76
|
+
else
|
77
|
+
copier_thread = Thread.new(copier) do
|
78
|
+
copier.go
|
79
|
+
end
|
80
|
+
copier_thread.run
|
81
|
+
copier_thread.set_trace_func proc { |*args| copier.watcher.trace(*args) }
|
82
|
+
last_warning = nil
|
83
|
+
|
84
|
+
# pole status
|
85
|
+
while !copier_thread.status.nil? and copier_thread.status != false
|
86
|
+
copier_thread.join(30)
|
87
|
+
warning = copier.watcher.ping
|
88
|
+
unless warning.nil? or last_warning == warning
|
89
|
+
logger.warn warning
|
90
|
+
last_warning = warning
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class ScriptFilter < Ocelot::Rules::Filter
|
98
|
+
alias :old_load :load
|
99
|
+
def initialize(p)
|
100
|
+
@p = p
|
101
|
+
end
|
102
|
+
|
103
|
+
def load(obj, collection, name)
|
104
|
+
@obj, @collection, @name = obj, collection, name
|
105
|
+
|
106
|
+
@result = nil
|
107
|
+
instance_exec(obj, collection, name, &@p)
|
108
|
+
@result || default
|
109
|
+
end
|
110
|
+
|
111
|
+
def nothing
|
112
|
+
@result = []
|
113
|
+
end
|
114
|
+
|
115
|
+
def result(val)
|
116
|
+
@result = val
|
117
|
+
end
|
118
|
+
|
119
|
+
def default
|
120
|
+
@result = old_load @obj, @collection, @name
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class ScriptRule < Ocelot::Rules::DefaultRule
|
125
|
+
alias :old_save :save
|
126
|
+
|
127
|
+
def initialize(p)
|
128
|
+
@p = p
|
129
|
+
end
|
130
|
+
|
131
|
+
def inspect
|
132
|
+
@inspect = true
|
133
|
+
end
|
134
|
+
|
135
|
+
def persist(obj)
|
136
|
+
old_save obj
|
137
|
+
end
|
138
|
+
|
139
|
+
def save(obj, *args)
|
140
|
+
@inspect = false
|
141
|
+
args = [obj, *args]
|
142
|
+
instance_exec(*args, &@p)
|
143
|
+
return @inspect
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/ocelot/graph.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
module Ocelot
|
2
|
+
class Watcher
|
3
|
+
attr_accessor :op
|
4
|
+
attr_accessor :association
|
5
|
+
attr_accessor :sql
|
6
|
+
attr_accessor :line
|
7
|
+
attr_accessor :rule
|
8
|
+
attr_reader :last
|
9
|
+
attr_reader :obj
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@last = Time.now
|
13
|
+
@mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def obj=(obj)
|
17
|
+
@obj = Ocelot::obj_to_s(obj)
|
18
|
+
end
|
19
|
+
|
20
|
+
def ping
|
21
|
+
sync do |w|
|
22
|
+
"Might be stuck processing #{w.obj}#{w.association.nil? ? "" : "."}#{w.association} at #{w.line} using #{w.rule}, may be exec'ing #{ActiveRecord::Base.connection.last_query}" if Time.now - w.last > 20
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def sync
|
27
|
+
@mutex.synchronize do
|
28
|
+
yield self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def trace(event, file, line, id, binding, clazz)
|
33
|
+
if clazz == Ocelot::Processor
|
34
|
+
@last = Time.now
|
35
|
+
@line = line
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Processor < Base
|
41
|
+
attr_accessor :seeds
|
42
|
+
attr_accessor :classes
|
43
|
+
attr_accessor :extra_rules
|
44
|
+
attr_accessor :extra_filters
|
45
|
+
attr_reader :watcher
|
46
|
+
attr_reader :saved
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@extra_rules = {}
|
50
|
+
@extra_filters = {}
|
51
|
+
@seeds = []
|
52
|
+
@unrecognised = Set.new
|
53
|
+
@watcher = Watcher.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_filter(params)
|
57
|
+
obj = params.delete :obj
|
58
|
+
collection = params.delete :collection
|
59
|
+
name = params.delete :name
|
60
|
+
|
61
|
+
result = nil
|
62
|
+
filter = @filters[obj.class]
|
63
|
+
|
64
|
+
if !filter.nil?
|
65
|
+
watcher.sync do |w|
|
66
|
+
w.obj = obj
|
67
|
+
w.association = name
|
68
|
+
w.rule = filter
|
69
|
+
end
|
70
|
+
|
71
|
+
logger.info "Filtering #{obj_to_s(obj)}.#{name} via #{filter.class}"
|
72
|
+
result = filter.load(obj, collection, name)
|
73
|
+
elsif !@unrecognised.include? obj.class
|
74
|
+
logger.error "Unrecognised type #{obj.class}"
|
75
|
+
@unrecognised << obj.class
|
76
|
+
end
|
77
|
+
result || []
|
78
|
+
end
|
79
|
+
|
80
|
+
def process_rule(params)
|
81
|
+
obj = params.delete :obj
|
82
|
+
src = params.delete :src
|
83
|
+
target = params.delete :target
|
84
|
+
calller = params.delete :caller
|
85
|
+
|
86
|
+
rule = @rules[obj.class]
|
87
|
+
|
88
|
+
watcher.sync do |w|
|
89
|
+
w.obj = obj
|
90
|
+
w.association = nil
|
91
|
+
w.rule = rule
|
92
|
+
end
|
93
|
+
|
94
|
+
if !rule.nil?
|
95
|
+
logger.info "Saving #{obj_to_s(obj)} thru #{rule.class} via #{calller}"
|
96
|
+
return rule.save(obj, src, target)
|
97
|
+
elsif !@unrecognised.include? obj.class
|
98
|
+
logger.warn "Unrecognised type #{obj.class}"
|
99
|
+
@unrecognised << obj.class
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def go
|
104
|
+
default_rule = Rules::DefaultRule.new
|
105
|
+
default_filter = Rules::Filter.new
|
106
|
+
|
107
|
+
@rules = Hash.new
|
108
|
+
@filters = Hash.new
|
109
|
+
@saved = Set.new
|
110
|
+
@inspected = Set.new
|
111
|
+
|
112
|
+
array_rule = ArrayRule.new
|
113
|
+
|
114
|
+
@classes.each { |c| @rules[c] = default_rule; @filters[c] = default_filter }
|
115
|
+
@rules.merge! @extra_rules
|
116
|
+
@filters.merge! @extra_filters
|
117
|
+
seeds = @seeds.collect { |o| [o, "script seed"] }
|
118
|
+
|
119
|
+
Graph::Traverser.run(seeds) do |objs, queue|
|
120
|
+
obj, calller = *objs
|
121
|
+
|
122
|
+
unless in? @saved, obj
|
123
|
+
@inspected << obj unless process_rule(:obj => obj, :caller => calller)
|
124
|
+
end
|
125
|
+
|
126
|
+
unless in? @inspected, obj
|
127
|
+
obj.class.reflect_on_all_associations.each do |assoc|
|
128
|
+
safely obj do
|
129
|
+
connection.use_source = true
|
130
|
+
value = obj.send(assoc.name)
|
131
|
+
|
132
|
+
unless value.nil?
|
133
|
+
|
134
|
+
# simple has_one (A->B handling), save B and conditionally inspect
|
135
|
+
if !assoc.collection?
|
136
|
+
unless in? @saved, value
|
137
|
+
process_rule(:obj => value, :caller => calller) and (queue << [value, obj_to_s(obj, assoc)])
|
138
|
+
end
|
139
|
+
# more complicated has many (A-B* and A-mapobj-B*), saving and inspecting each child
|
140
|
+
# monkeypatching the array_rule must yield B and whether to inspect it
|
141
|
+
# be very very very careful, as it handles saving objects whie yielding results
|
142
|
+
else
|
143
|
+
array_rule.save(obj, value, assoc, self) do |child, inspect|
|
144
|
+
@saved << child
|
145
|
+
if inspect
|
146
|
+
queue << [child, obj_to_s(obj, assoc)]
|
147
|
+
else
|
148
|
+
@inspected << child
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class ArrayRule < Rules::Rule
|
160
|
+
include Ocelot
|
161
|
+
|
162
|
+
def save(obj, array, assoc, c)
|
163
|
+
safely obj, assoc.name do
|
164
|
+
c.process_filter(:obj => obj, :collection => array, :name => assoc.name).each do |val|
|
165
|
+
unless in? c.saved, val
|
166
|
+
use_source_db!
|
167
|
+
inspect = c.process_rule(:obj => val, :caller => obj_to_s(obj))
|
168
|
+
yield val, inspect
|
169
|
+
end
|
170
|
+
|
171
|
+
use_target_db!
|
172
|
+
if assoc.is_a? ActiveRecord::Reflection::ThroughReflection
|
173
|
+
safely assoc.through_reflection.klass, assoc.name, val do
|
174
|
+
c.process_rule :obj => assoc.through_reflection.klass.new, :src => obj, :target => val, :caller => obj_to_s(obj,assoc)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/lib/ocelot/rules.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Ocelot
|
2
|
+
module Rules
|
3
|
+
class Filter < Ocelot::Base
|
4
|
+
def filter(value)
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def load(obj, collection, name)
|
9
|
+
result = []
|
10
|
+
collection.each do |val|
|
11
|
+
safely(obj, name) do
|
12
|
+
result << val if filter val
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
class Rule < Base
|
20
|
+
def relationships(obj)
|
21
|
+
obj.class.reflect_on_all_associations.collect { |assoc| assoc.name }
|
22
|
+
end
|
23
|
+
|
24
|
+
def use_target_db!
|
25
|
+
Ocelot::connection.use_source = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def use_source_db!
|
29
|
+
Ocelot::connection.use_source = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def save_to_db(obj, *extra)
|
33
|
+
begin
|
34
|
+
use_target_db!
|
35
|
+
obj.save(false)
|
36
|
+
ensure
|
37
|
+
use_source_db!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class DefaultRule < Rule
|
43
|
+
def save(obj, *extra)
|
44
|
+
id = obj.id
|
45
|
+
obj = obj.clone
|
46
|
+
obj.id = id
|
47
|
+
remove_callbacks(obj)
|
48
|
+
save_to_db(obj)
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ocelot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- spencer p
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-04-17 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Extreme alpha
|
18
|
+
email:
|
19
|
+
- exussum@gmail.com
|
20
|
+
executables: []
|
21
|
+
|
22
|
+
extensions: []
|
23
|
+
|
24
|
+
extra_rdoc_files: []
|
25
|
+
|
26
|
+
files:
|
27
|
+
- lib/ocelot/cli.rb
|
28
|
+
- lib/ocelot/database.rb
|
29
|
+
- lib/ocelot/dsl.rb
|
30
|
+
- lib/ocelot/graph.rb
|
31
|
+
- lib/ocelot/processor.rb
|
32
|
+
- lib/ocelot/rules.rb
|
33
|
+
- lib/ocelot.rb
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: ""
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.6.2
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: DBA like tool to copy a slice of data from one db to another
|
62
|
+
test_files: []
|
63
|
+
|