ar-octopus 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +8 -8
  2. data/README.mkdn +80 -55
  3. data/lib/octopus.rb +20 -6
  4. data/lib/octopus/{rails3/abstract_adapter.rb → abstract_adapter.rb} +1 -4
  5. data/lib/octopus/association.rb +5 -99
  6. data/lib/octopus/association_shard_tracking.rb +105 -0
  7. data/lib/octopus/collection_association.rb +9 -0
  8. data/lib/octopus/collection_proxy.rb +14 -0
  9. data/lib/octopus/has_and_belongs_to_many_association.rb +2 -12
  10. data/lib/octopus/load_balancing.rb +3 -0
  11. data/lib/octopus/load_balancing/round_robin.rb +15 -0
  12. data/lib/octopus/{rails3/log_subscriber.rb → log_subscriber.rb} +0 -0
  13. data/lib/octopus/model.rb +74 -85
  14. data/lib/octopus/{rails3/persistence.rb → persistence.rb} +0 -0
  15. data/lib/octopus/proxy.rb +166 -29
  16. data/lib/octopus/relation_proxy.rb +39 -0
  17. data/lib/octopus/scope_proxy.rb +7 -10
  18. data/lib/octopus/shard_tracking.rb +45 -0
  19. data/lib/octopus/shard_tracking/attribute.rb +24 -0
  20. data/lib/octopus/shard_tracking/dynamic.rb +7 -0
  21. data/lib/octopus/singular_association.rb +7 -0
  22. data/lib/octopus/slave_group.rb +11 -0
  23. data/lib/octopus/version.rb +1 -1
  24. data/spec/config/shards.yml +53 -0
  25. data/spec/octopus/{association_spec.rb → association_shard_tracking_spec.rb} +1 -1
  26. data/spec/octopus/collection_proxy_spec.rb +15 -0
  27. data/spec/octopus/model_spec.rb +2 -2
  28. data/spec/octopus/octopus_spec.rb +34 -0
  29. data/spec/octopus/relation_proxy_spec.rb +77 -0
  30. data/spec/octopus/replicated_slave_grouped_spec.rb +64 -0
  31. data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
  32. data/spec/support/octopus_helper.rb +1 -0
  33. metadata +26 -9
  34. data/lib/octopus/association_collection.rb +0 -49
  35. data/lib/octopus/rails3/singular_association.rb +0 -34
@@ -0,0 +1,9 @@
1
+ module Octopus::CollectionAssociation
2
+ def self.included(base)
3
+ base.sharded_methods :reader, :writer, :ids_reader, :ids_writer, :create, :create!,
4
+ :build, :any?, :count, :empty?, :first, :include?, :last, :length,
5
+ :load_target, :many?, :reload, :size, :select, :uniq
6
+ end
7
+ end
8
+
9
+ ActiveRecord::Associations::CollectionAssociation.send(:include, Octopus::CollectionAssociation)
@@ -0,0 +1,14 @@
1
+ module Octopus::CollectionProxy
2
+ def self.included(base)
3
+ base.send(:include, Octopus::ShardTracking::Dynamic)
4
+ base.sharded_methods :any?, :build, :count, :create, :create!, :concat, :delete, :delete_all,
5
+ :destroy, :destroy_all, :empty?, :find, :first, :include?, :last, :length,
6
+ :many?, :pluck, :replace, :select, :size, :sum, :to_a, :uniq
7
+ end
8
+
9
+ def current_shard
10
+ @association.owner.current_shard
11
+ end
12
+ end
13
+
14
+ ActiveRecord::Associations::CollectionProxy.send(:include, Octopus::CollectionProxy)
@@ -1,17 +1,7 @@
1
1
  module Octopus::HasAndBelongsToManyAssociation
2
2
  def self.included(base)
3
- base.instance_eval do
4
- alias_method_chain :insert_record, :octopus
5
- end
6
- end
7
-
8
- def insert_record_with_octopus(record, force = true, validate = true)
9
- if should_wrap_the_connection?
10
- Octopus.using(@owner.current_shard) { insert_record_without_octopus(record, force, validate) }
11
- else
12
- insert_record_without_octopus(record, force, validate)
13
- end
3
+ base.sharded_methods :insert_record
14
4
  end
15
5
  end
16
6
 
17
- ActiveRecord::Associations::HasAndBelongsToManyAssociation.send(:include, Octopus::HasAndBelongsToManyAssociation)
7
+ ActiveRecord::Associations::HasAndBelongsToManyAssociation.send(:include, Octopus::HasAndBelongsToManyAssociation)
@@ -0,0 +1,3 @@
1
+ module Octopus::LoadBalancing
2
+
3
+ end
@@ -0,0 +1,15 @@
1
+ require 'octopus/load_balancing'
2
+
3
+ # The round-robin load balancing of slaves belonging to the same shard.
4
+ # It is a pool that contains slaves which queries are distributed to.
5
+ class Octopus::LoadBalancing::RoundRobin
6
+ def initialize(slaves_list)
7
+ @slaves_list = slaves_list
8
+ @slave_index = 0
9
+ end
10
+
11
+ # Returns the next available slave in the pool
12
+ def next
13
+ @slaves_list[@slave_index = (@slave_index + 1) % @slaves_list.length]
14
+ end
15
+ end
data/lib/octopus/model.rb CHANGED
@@ -2,10 +2,9 @@ require 'active_support/deprecation'
2
2
 
3
3
  module Octopus::Model
4
4
  def self.extended(base)
5
+ base.send(:include, Octopus::ShardTracking::Attribute)
5
6
  base.send(:include, InstanceMethods)
6
7
  base.extend(ClassMethods)
7
- base.hijack_connection()
8
- base.hijack_initializer()
9
8
  end
10
9
 
11
10
  module SharedMethods
@@ -28,82 +27,6 @@ module Octopus::Model
28
27
  self
29
28
  end
30
29
  end
31
-
32
- def hijack_initializer()
33
- attr_accessor :current_shard
34
- around_save :run_on_shard
35
-
36
- def set_current_shard
37
- return unless Octopus.enabled?
38
-
39
- if new_record? || self.class.connection_proxy.block
40
- self.current_shard = self.class.connection_proxy.current_shard
41
- else
42
- self.current_shard = self.class.connection_proxy.last_current_shard || self.class.connection_proxy.current_shard
43
- end
44
- end
45
-
46
- after_initialize :set_current_shard
47
- end
48
-
49
- def hijack_connection()
50
- def self.should_use_normal_connection?
51
- !Octopus.enabled? || self.custom_octopus_connection
52
- end
53
-
54
- def self.connection_proxy
55
- @@connection_proxy ||= Octopus::Proxy.new
56
- end
57
-
58
- def self.connection_with_octopus
59
- if should_use_normal_connection?
60
- connection_without_octopus
61
- else
62
- self.connection_proxy.current_model = self
63
- self.connection_proxy
64
- end
65
- end
66
-
67
- def self.connection_pool_with_octopus
68
- if should_use_normal_connection?
69
- connection_pool_without_octopus
70
- else
71
- connection_proxy.connection_pool
72
- end
73
- end
74
-
75
- def self.clear_active_connections_with_octopus!
76
- if should_use_normal_connection?
77
- clear_active_connections_without_octopus!
78
- else
79
- connection_proxy.clear_active_connections!
80
- end
81
- end
82
-
83
- def self.clear_all_connections_with_octopus!
84
- if should_use_normal_connection?
85
- clear_all_connections_without_octopus!
86
- else
87
- connection_proxy.clear_all_connections!
88
- end
89
- end
90
-
91
- def self.connected_with_octopus?
92
- if should_use_normal_connection?
93
- connected_without_octopus?
94
- else
95
- connection_proxy.connected?
96
- end
97
- end
98
-
99
- class << self
100
- alias_method_chain :connection, :octopus
101
- alias_method_chain :connection_pool, :octopus
102
- alias_method_chain :clear_all_connections!, :octopus
103
- alias_method_chain :clear_active_connections!, :octopus
104
- alias_method_chain :connected?, :octopus
105
- end
106
- end
107
30
  end
108
31
 
109
32
  module InstanceMethods
@@ -116,18 +39,20 @@ module Octopus::Model
116
39
  base.send(:alias_method_chain, :perform_validations, :octopus)
117
40
  end
118
41
 
119
- def should_set_current_shard?
120
- self.respond_to?(:current_shard) && !self.current_shard.nil?
121
- end
42
+ def set_current_shard
43
+ return unless Octopus.enabled?
122
44
 
123
- def run_on_shard(&block)
124
- if self.current_shard
125
- self.class.connection_proxy.run_queries_on_shard(self.current_shard, &block)
45
+ if new_record? || self.class.connection_proxy.block
46
+ self.current_shard = self.class.connection_proxy.current_shard
126
47
  else
127
- yield
48
+ self.current_shard = self.class.connection_proxy.last_current_shard || self.class.connection_proxy.current_shard
128
49
  end
129
50
  end
130
51
 
52
+ def should_set_current_shard?
53
+ self.respond_to?(:current_shard) && !self.current_shard.nil?
54
+ end
55
+
131
56
  def equality_with_octopus(comparison_object)
132
57
  equality_without_octopus(comparison_object) && comparison_object.current_shard == current_shard
133
58
  end
@@ -161,10 +86,19 @@ module Octopus::Model
161
86
  end
162
87
 
163
88
  def hijack_methods
89
+ around_save :run_on_shard
90
+ after_initialize :set_current_shard
91
+
164
92
  class << self
165
93
  attr_accessor :custom_octopus_connection
166
94
  attr_accessor :custom_octopus_table_name
167
95
 
96
+ alias_method_chain :connection, :octopus
97
+ alias_method_chain :connection_pool, :octopus
98
+ alias_method_chain :clear_all_connections!, :octopus
99
+ alias_method_chain :clear_active_connections!, :octopus
100
+ alias_method_chain :connected?, :octopus
101
+
168
102
  if Octopus.rails3?
169
103
  alias_method_chain(:set_table_name, :octopus)
170
104
  end
@@ -176,6 +110,61 @@ module Octopus::Model
176
110
  end
177
111
  end
178
112
 
113
+ def connection_proxy
114
+ cached = ActiveRecord::Base.class_variable_get :@@connection_proxy rescue nil
115
+ cached ||
116
+ begin
117
+ p = Octopus::Proxy.new
118
+ ActiveRecord::Base.class_variable_set :@@connection_proxy, p
119
+ p
120
+ end
121
+ end
122
+
123
+ def should_use_normal_connection?
124
+ !Octopus.enabled? || custom_octopus_connection
125
+ end
126
+
127
+ def connection_with_octopus
128
+ if should_use_normal_connection?
129
+ connection_without_octopus
130
+ else
131
+ connection_proxy.current_model = self
132
+ connection_proxy
133
+ end
134
+ end
135
+
136
+ def connection_pool_with_octopus
137
+ if should_use_normal_connection?
138
+ connection_pool_without_octopus
139
+ else
140
+ connection_proxy.connection_pool
141
+ end
142
+ end
143
+
144
+ def clear_active_connections_with_octopus!
145
+ if should_use_normal_connection?
146
+ clear_active_connections_without_octopus!
147
+ else
148
+ connection_proxy.clear_active_connections!
149
+ end
150
+ end
151
+
152
+ def clear_all_connections_with_octopus!
153
+ if should_use_normal_connection?
154
+ clear_all_connections_without_octopus!
155
+ else
156
+ connection_proxy.clear_all_connections!
157
+ end
158
+ end
159
+
160
+ def connected_with_octopus?
161
+ if should_use_normal_connection?
162
+ connected_without_octopus?
163
+ else
164
+ connection_proxy.connected?
165
+ end
166
+ end
167
+
179
168
  def set_table_name_with_octopus(value = nil, &block)
180
169
  self.custom_octopus_table_name = true
181
170
  set_table_name_without_octopus(value, &block)
data/lib/octopus/proxy.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require "set"
2
+ require 'octopus/slave_group'
3
+ require 'octopus/load_balancing/round_robin'
2
4
 
3
5
  class Octopus::Proxy
4
- attr_accessor :config
6
+ attr_accessor :config, :sharded
5
7
 
6
8
  def initialize(config = Octopus.config)
7
9
  initialize_shards(config)
@@ -10,18 +12,20 @@ class Octopus::Proxy
10
12
 
11
13
  def initialize_shards(config)
12
14
  @shards = HashWithIndifferentAccess.new
15
+ @shards_slave_groups = HashWithIndifferentAccess.new
16
+ @slave_groups = HashWithIndifferentAccess.new
13
17
  @groups = {}
14
18
  @adapters = Set.new
15
19
  @config = ActiveRecord::Base.connection_pool_without_octopus.connection.instance_variable_get(:@config)
16
20
 
17
21
  if !config.nil?
18
22
  @entire_sharded = config['entire_sharded']
19
- shards_config = config[Octopus.rails_env()]
23
+ @shards_config = config[Octopus.rails_env()]
20
24
  end
21
25
 
22
- shards_config ||= []
26
+ @shards_config ||= []
23
27
 
24
- shards_config.each do |key, value|
28
+ @shards_config.each do |key, value|
25
29
  if value.is_a?(String)
26
30
  value = resolve_string_connection(value).merge(:octopus_shard => key)
27
31
  initialize_adapter(value['adapter'])
@@ -30,6 +34,24 @@ class Octopus::Proxy
30
34
  value.merge!(:octopus_shard => key)
31
35
  initialize_adapter(value['adapter'])
32
36
  @shards[key.to_sym] = connection_pool_for(value, "#{value['adapter']}_connection")
37
+
38
+ slave_group_configs = value.select do |k,v|
39
+ structurally_slave_group? v
40
+ end
41
+
42
+ if slave_group_configs.present?
43
+ slave_groups = HashWithIndifferentAccess.new
44
+ slave_group_configs.each do |slave_group_name, slave_configs|
45
+ slaves = HashWithIndifferentAccess.new
46
+ slave_configs.each do |slave_name, slave_config|
47
+ @shards[slave_name.to_sym] = connection_pool_for(slave_config, "#{value['adapter']}_connection")
48
+ slaves[slave_name.to_sym] = slave_name.to_sym
49
+ end
50
+ slave_groups[slave_group_name.to_sym] = Octopus::SlaveGroup.new(slaves)
51
+ end
52
+ @shards_slave_groups[key.to_sym] = slave_groups
53
+ @sharded = true
54
+ end
33
55
  elsif value.is_a?(Hash)
34
56
  @groups[key.to_s] = []
35
57
 
@@ -42,6 +64,11 @@ class Octopus::Proxy
42
64
  @shards[k.to_sym] = connection_pool_for(config_with_octopus_shard, "#{v['adapter']}_connection")
43
65
  @groups[key.to_s] << k.to_sym
44
66
  end
67
+
68
+ if structurally_slave_group? value
69
+ slaves = Hash[@groups[key.to_s].map { |v| [v, v ] }]
70
+ @slave_groups[key.to_sym] = Octopus::SlaveGroup.new(slaves)
71
+ end
45
72
  end
46
73
  end
47
74
 
@@ -58,7 +85,7 @@ class Octopus::Proxy
58
85
 
59
86
  @slaves_list = @shards.keys.map {|sym| sym.to_s}.sort
60
87
  @slaves_list.delete('master')
61
- @slave_index = 0
88
+ @slaves_load_balancer = Octopus::LoadBalancing::RoundRobin.new(@slaves_list)
62
89
  end
63
90
 
64
91
  def current_model
@@ -74,8 +101,29 @@ class Octopus::Proxy
74
101
  end
75
102
 
76
103
  def current_shard=(shard_symbol)
104
+ self.current_slave_group = nil
77
105
  if shard_symbol.is_a?(Array)
78
106
  shard_symbol.each {|symbol| raise "Nonexistent Shard Name: #{symbol}" if @shards[symbol].nil? }
107
+ elsif shard_symbol.is_a?(Hash)
108
+ hash = shard_symbol
109
+ shard_symbol = hash[:shard]
110
+ slave_group_symbol = hash[:slave_group]
111
+
112
+ if shard_symbol.nil? && slave_group_symbol.nil?
113
+ raise "Neither shard or slave group must be specified"
114
+ end
115
+
116
+ if shard_symbol.present?
117
+ raise "Nonexistent Shard Name: #{shard_symbol}" if @shards[shard_symbol].nil?
118
+ end
119
+
120
+ if slave_group_symbol.present?
121
+ if (@shards_slave_groups.try(:[], shard_symbol).present? && @shards_slave_groups[shard_symbol][slave_group_symbol].nil?) ||
122
+ (@shards_slave_groups.try(:[], shard_symbol).nil? && @slave_groups[slave_group_symbol].nil?)
123
+ raise "Nonexistent Slave Group Name: #{slave_group_symbol} in shards config: #{@shards_config.inspect}"
124
+ end
125
+ self.current_slave_group = slave_group_symbol
126
+ end
79
127
  else
80
128
  raise "Nonexistent Shard Name: #{shard_symbol}" if @shards[shard_symbol].nil?
81
129
  end
@@ -96,6 +144,14 @@ class Octopus::Proxy
96
144
  Thread.current["octopus.current_group"] = group_symbol
97
145
  end
98
146
 
147
+ def current_slave_group
148
+ Thread.current["octopus.current_slave_group"]
149
+ end
150
+
151
+ def current_slave_group=(slave_group_symbol)
152
+ Thread.current["octopus.current_slave_group"] = slave_group_symbol
153
+ end
154
+
99
155
  def block
100
156
  Thread.current["octopus.block"]
101
157
  end
@@ -112,6 +168,10 @@ class Octopus::Proxy
112
168
  Thread.current["octopus.last_current_shard"] = last_current_shard
113
169
  end
114
170
 
171
+ def fully_replicated?
172
+ @fully_replicated || Thread.current["octopus.fully_replicated"]
173
+ end
174
+
115
175
  # Public: Whether or not a group exists with the given name converted to a
116
176
  # string.
117
177
  #
@@ -157,16 +217,10 @@ class Octopus::Proxy
157
217
  end
158
218
 
159
219
  def run_queries_on_shard(shard, &block)
160
- older_shard = self.current_shard
161
- last_block = self.block
162
-
163
- begin
164
- self.block = true
165
- self.current_shard = shard
166
- yield
167
- ensure
168
- self.block = last_block || false
169
- self.current_shard = older_shard
220
+ keeping_connection_proxy do
221
+ using_shard(shard) do
222
+ yield
223
+ end
170
224
  end
171
225
  end
172
226
 
@@ -176,7 +230,7 @@ class Octopus::Proxy
176
230
  end
177
231
  end
178
232
 
179
- def clean_proxy()
233
+ def clean_connection_proxy()
180
234
  self.current_shard = :master
181
235
  self.current_group = nil
182
236
  self.block = false
@@ -189,7 +243,8 @@ class Octopus::Proxy
189
243
  end
190
244
 
191
245
  def transaction(options = {}, &block)
192
- if @replicated && (current_model.replicated || @fully_replicated)
246
+ replicated = @replicated && (current_model.replicated || fully_replicated?)
247
+ if !sharded && replicated
193
248
  self.run_queries_on_shard(:master) do
194
249
  select_connection.transaction(options, &block)
195
250
  end
@@ -199,11 +254,15 @@ class Octopus::Proxy
199
254
  end
200
255
 
201
256
  def method_missing(method, *args, &block)
202
- if should_clean_connection?(method)
257
+ if should_clean_connection_proxy?(method)
203
258
  conn = select_connection()
204
259
  self.last_current_shard = self.current_shard
205
- clean_proxy()
260
+ clean_connection_proxy()
206
261
  conn.send(method, *args, &block)
262
+ elsif should_send_queries_to_shard_slave_group?(method)
263
+ send_queries_to_shard_slave_group(method, *args, &block)
264
+ elsif should_send_queries_to_slave_group?(method)
265
+ send_queries_to_slave_group(method, *args, &block)
207
266
  elsif should_send_queries_to_replicated_databases?(method)
208
267
  send_queries_to_selected_slave(method, *args, &block)
209
268
  else
@@ -244,6 +303,22 @@ class Octopus::Proxy
244
303
  @shards.any? { |k, v| v.connected? }
245
304
  end
246
305
 
306
+ def should_send_queries_to_shard_slave_group?(method)
307
+ should_use_slaves_for_method?(method) && @shards_slave_groups.try(:[], current_shard).try(:[], current_slave_group).present?
308
+ end
309
+
310
+ def send_queries_to_shard_slave_group(method, *args, &block)
311
+ send_queries_to_balancer(@shards_slave_groups[current_shard][current_slave_group], method, *args, &block)
312
+ end
313
+
314
+ def should_send_queries_to_slave_group?(method)
315
+ should_use_slaves_for_method?(method) && @slave_groups.try(:[], current_slave_group).present?
316
+ end
317
+
318
+ def send_queries_to_slave_group(method, *args, &block)
319
+ send_queries_to_balancer(@slave_groups[current_slave_group], method, *args, &block)
320
+ end
321
+
247
322
  protected
248
323
 
249
324
  def connection_pool_for(adapter, config)
@@ -274,27 +349,89 @@ class Octopus::Proxy
274
349
  resolver.spec.config.stringify_keys
275
350
  end
276
351
 
277
- def should_clean_connection?(method)
352
+ def should_clean_connection_proxy?(method)
278
353
  method.to_s =~ /insert|select|execute/ && !@replicated && !self.block
279
354
  end
280
355
 
356
+ # Try to use slaves if and only if `replicated: true` is specified in `shards.yml` and no slaves groups are defined
281
357
  def should_send_queries_to_replicated_databases?(method)
282
- @replicated && method.to_s =~ /select/ && !self.block
358
+ @replicated && method.to_s =~ /select/ && !self.block && !slaves_grouped?
283
359
  end
284
360
 
285
361
  def send_queries_to_selected_slave(method, *args, &block)
286
- old_shard = self.current_shard
362
+ if current_model.replicated || fully_replicated?
363
+ selected_slave = @slaves_load_balancer.next
364
+ else
365
+ selected_slave = :master
366
+ end
287
367
 
288
- begin
289
- if current_model.replicated || @fully_replicated
290
- self.current_shard = @slaves_list[@slave_index = (@slave_index + 1) % @slaves_list.length]
291
- else
292
- self.current_shard = :master
293
- end
368
+ send_queries_to_slave(selected_slave, method, *args, &block)
369
+ end
370
+
371
+ # We should use slaves if and only if its safe to do so.
372
+ #
373
+ # We can safely use slaves when:
374
+ # (1) `replicated: true` is specified in `shards.yml`
375
+ # (2) The current model is `replicated()`, or `fully_replicated: true` is specified in `shards.yml` which means that
376
+ # all the model is `replicated()`
377
+ # (3) It's a SELECT query
378
+ # while ensuring that we revert `current_shard` from the selected slave to the (shard's) master
379
+ # not to make queries other than SELECT leak to the slave.
380
+ def should_use_slaves_for_method?(method)
381
+ @replicated && (current_model.replicated || fully_replicated?) && method.to_s =~ /select/
382
+ end
383
+
384
+ def slaves_grouped?
385
+ @slave_groups.present?
386
+ end
294
387
 
388
+ # Temporarily switch `current_shard` to the next slave in a slave group and send queries to it
389
+ # while preserving `current_shard`
390
+ def send_queries_to_balancer(balancer, method, *args, &block)
391
+ send_queries_to_slave(balancer.next, method, *args, &block)
392
+ end
393
+
394
+ # Temporarily switch `current_shard` to the specified slave and send queries to it
395
+ # while preserving `current_shard`
396
+ def send_queries_to_slave(slave, method, *args, &block)
397
+ using_shard(slave) do
295
398
  select_connection.send(method, *args, &block)
399
+ end
400
+ end
401
+
402
+ # Temporarily block cleaning connection proxy and run the block
403
+ #
404
+ # @see Octopus::Proxy#should_clean_connection?
405
+ # @see Octopus::Proxy#clean_connection_proxy
406
+ def keeping_connection_proxy(&block)
407
+ last_block = self.block
408
+
409
+ begin
410
+ self.block = true
411
+ yield
412
+ ensure
413
+ self.block = last_block || false
414
+ end
415
+ end
416
+
417
+ # Temporarily switch `current_shard` and run the block
418
+ def using_shard(shard, &block)
419
+ older_shard = self.current_shard
420
+
421
+ begin
422
+ self.current_shard = shard
423
+ yield
296
424
  ensure
297
- self.current_shard = old_shard
425
+ self.current_shard = older_shard
298
426
  end
299
427
  end
428
+
429
+ def structurally_slave?(config)
430
+ config.is_a?(Hash) && config.key?("adapter")
431
+ end
432
+
433
+ def structurally_slave_group?(config)
434
+ config.is_a?(Hash) && config.values.any? {|v| structurally_slave? v }
435
+ end
436
+
300
437
  end