switchman 1.5.13 → 2.0.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.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/app/models/switchman/shard.rb +746 -11
  3. data/db/migrate/20130328212039_create_switchman_shards.rb +3 -1
  4. data/db/migrate/20130328224244_create_default_shard.rb +4 -2
  5. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +13 -0
  6. data/db/migrate/20180828183945_add_default_shard_index.rb +15 -0
  7. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +17 -0
  8. data/db/migrate/20190114212900_add_unique_name_indexes.rb +9 -0
  9. data/lib/switchman/action_controller/caching.rb +2 -0
  10. data/lib/switchman/active_record/abstract_adapter.rb +14 -4
  11. data/lib/switchman/active_record/association.rb +64 -37
  12. data/lib/switchman/active_record/attribute_methods.rb +16 -5
  13. data/lib/switchman/active_record/base.rb +67 -22
  14. data/lib/switchman/active_record/batches.rb +3 -1
  15. data/lib/switchman/active_record/calculations.rb +16 -21
  16. data/lib/switchman/active_record/connection_handler.rb +71 -79
  17. data/lib/switchman/active_record/connection_pool.rb +28 -23
  18. data/lib/switchman/active_record/finder_methods.rb +37 -28
  19. data/lib/switchman/active_record/log_subscriber.rb +14 -19
  20. data/lib/switchman/active_record/migration.rb +80 -0
  21. data/lib/switchman/active_record/model_schema.rb +3 -1
  22. data/lib/switchman/active_record/persistence.rb +9 -1
  23. data/lib/switchman/active_record/postgresql_adapter.rb +166 -126
  24. data/lib/switchman/active_record/predicate_builder.rb +2 -0
  25. data/lib/switchman/active_record/query_cache.rb +22 -87
  26. data/lib/switchman/active_record/query_methods.rb +123 -124
  27. data/lib/switchman/active_record/reflection.rb +37 -20
  28. data/lib/switchman/active_record/relation.rb +50 -29
  29. data/lib/switchman/active_record/spawn_methods.rb +2 -0
  30. data/lib/switchman/active_record/statement_cache.rb +44 -52
  31. data/lib/switchman/active_record/table_definition.rb +4 -2
  32. data/lib/switchman/active_record/type_caster.rb +2 -0
  33. data/lib/switchman/active_record/where_clause_factory.rb +5 -2
  34. data/lib/switchman/active_support/cache.rb +18 -0
  35. data/lib/switchman/arel.rb +8 -25
  36. data/lib/switchman/call_super.rb +19 -0
  37. data/lib/switchman/connection_pool_proxy.rb +70 -24
  38. data/lib/switchman/database_server.rb +72 -60
  39. data/lib/switchman/default_shard.rb +3 -0
  40. data/lib/switchman/engine.rb +45 -40
  41. data/lib/switchman/environment.rb +2 -0
  42. data/lib/switchman/errors.rb +3 -0
  43. data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
  44. data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
  45. data/lib/switchman/open4.rb +80 -0
  46. data/lib/switchman/r_spec_helper.rb +14 -8
  47. data/lib/switchman/rails.rb +2 -0
  48. data/lib/switchman/schema_cache.rb +17 -0
  49. data/lib/switchman/sharded_instrumenter.rb +4 -2
  50. data/lib/switchman/standard_error.rb +14 -0
  51. data/lib/switchman/test_helper.rb +6 -3
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +4 -2
  54. data/lib/tasks/switchman.rake +54 -68
  55. metadata +87 -40
  56. data/app/models/switchman/shard_internal.rb +0 -659
  57. data/lib/switchman/engine.rb~ +0 -203
@@ -1,659 +0,0 @@
1
- require 'switchman/database_server'
2
- require 'switchman/default_shard'
3
- require 'switchman/environment'
4
-
5
- module Switchman
6
- class Shard < ::ActiveRecord::Base
7
- # ten trillion possible ids per shard. yup.
8
- IDS_PER_SHARD = 10_000_000_000_000
9
-
10
- CATEGORIES =
11
- {
12
- # special cased to mean all other models
13
- :default => nil,
14
- # special cased to not allow activating a shard other than the default
15
- :unsharded => [Shard]
16
- }
17
- private_constant :CATEGORIES
18
- @shard_category = :unsharded
19
-
20
- if defined?(::ProtectedAttributes)
21
- attr_accessible :default, :name, :database_server
22
- end
23
-
24
- # only allow one default
25
- validates_uniqueness_of :default, :if => lambda { |s| s.default? }
26
-
27
- after_save :clear_cache
28
-
29
- scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
30
-
31
- class << self
32
- def categories
33
- CATEGORIES.keys
34
- end
35
-
36
- def default(reload_deprecated = false, reload: false, with_fallback: false)
37
- reload = reload_deprecated if reload_deprecated
38
- if !@default || reload
39
- # Have to create a dummy object so that several key methods still work
40
- # (it's easier to do this in one place here, and just assume that sharding
41
- # is up and running everywhere else). This includes for looking up the
42
- # default shard itself. This also needs to be a local so that this method
43
- # can be re-entrant
44
- default = DefaultShard.instance
45
-
46
- # if we already have a default shard in place, and the caller wants
47
- # to use it as a fallback, use that instead of the dummy instance
48
- if with_fallback && @default
49
- default = @default
50
- end
51
-
52
- # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
53
- @default ||= default
54
-
55
- # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
56
- @default = begin
57
- Shard.where(default: true).first || default
58
- rescue
59
- default
60
- end
61
-
62
- # rebuild current shard activations - it might have "another" default shard serialized there
63
- active_shards.replace(active_shards.map do |category, shard|
64
- shard = Shard.lookup((!shard || shard.default?) ? 'default' : shard.id)
65
- [category, shard]
66
- end.to_h)
67
-
68
- activate!(default: @default) if active_shards.empty?
69
- end
70
- @default
71
- end
72
-
73
- def current(category = :default)
74
- active_shards[category] || Shard.default
75
- end
76
-
77
- def activate(shards)
78
- old_shards = activate!(shards)
79
- yield
80
- ensure
81
- active_shards.merge!(old_shards) if old_shards
82
- end
83
-
84
- def activate!(shards)
85
- old_shards = nil
86
- currently_active_shards = active_shards
87
- shards.each do |category, shard|
88
- next if category == :unsharded
89
- unless currently_active_shards[category] == shard
90
- old_shards ||= {}
91
- old_shards[category] = currently_active_shards[category]
92
- currently_active_shards[category] = shard
93
- end
94
- end
95
- old_shards
96
- end
97
-
98
- def lookup(id)
99
- id_i = id.to_i
100
- return current if id_i == current.id || id == 'self'
101
- return default if id_i == default.id || id.nil? || id == 'default'
102
- id = id_i
103
- raise ArgumentError if id == 0
104
-
105
- unless cached_shards.has_key?(id)
106
- cached_shards[id] = Shard.default.activate do
107
- # can't simply cache the AR object since Shard has a custom serializer
108
- # that calls this method
109
- attributes = Switchman.cache.fetch(['shard', id].join('/')) do
110
- shard = find_by_id(id)
111
- if shard
112
- attributes = shard.attributes
113
- if ::Rails.version < '4.2'
114
- attributes.each_key do |key|
115
- attributes[key] = attributes[key].unserialize if attributes[key].is_a?(::ActiveRecord::AttributeMethods::Serialization::Attribute)
116
- end
117
- end
118
- attributes
119
- else
120
- :nil
121
- end
122
- end
123
- if attributes == :nil
124
- nil
125
- else
126
- shard = Shard.new
127
- attributes.each do |attr, value|
128
- shard.send(:"#{attr}=", value)
129
- end
130
- shard.instance_variable_set(:@new_record, false)
131
- # connection info doesn't exist in database.yml;
132
- # pretend the shard doesn't exist either
133
- shard = nil unless shard.database_server
134
- shard
135
- end
136
- end
137
- end
138
- cached_shards[id]
139
- end
140
-
141
- def clear_cache
142
- cached_shards.clear
143
- end
144
-
145
- # ==== Parameters
146
- #
147
- # * +shards+ - an array or relation of Shards to iterate over
148
- # * +categories+ - an array of categories to activate
149
- # * +options+ -
150
- # :parallel - true/false to execute in parallel, or a integer of how many
151
- # sub-processes per database server. Note that parallel
152
- # invocation currently uses forking, so should be used sparingly
153
- # because errors are not raised, and you cannot get results back
154
- # :exception - :ignore, :raise, :defer (wait until the end and raise the first
155
- # error), or a proc
156
- def with_each_shard(*args)
157
- raise ArgumentError, "wrong number of arguments (#{args.length} for 0...3)" if args.length > 3
158
-
159
- unless default.is_a?(Shard)
160
- return Array.wrap(yield)
161
- end
162
-
163
- options = args.extract_options!
164
- if args.length == 1
165
- if Array === args.first && args.first.first.is_a?(Symbol)
166
- categories = args.first
167
- else
168
- scope = args.first
169
- end
170
- else
171
- scope, categories = args
172
- end
173
-
174
- parallel = case options[:parallel]
175
- when true
176
- 1
177
- when false, nil
178
- 0
179
- else
180
- options[:parallel]
181
- end
182
- options.delete(:parallel)
183
-
184
- scope ||= Shard.all
185
- if ::ActiveRecord::Relation === scope && scope.order_values.empty?
186
- scope = scope.order("database_server_id IS NOT NULL, database_server_id, id")
187
- end
188
-
189
- if parallel > 0
190
- max_procs = determine_max_procs(options.delete(:max_procs), parallel)
191
- if ::ActiveRecord::Relation === scope
192
- # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
193
- database_servers = scope.reorder('database_server_id').select(:database_server_id).uniq.
194
- map(&:database_server).compact.uniq
195
- parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
196
-
197
- scopes = Hash[database_servers.map do |server|
198
- server_scope = server.shards.merge(scope)
199
- if parallel == 1
200
- subscopes = [server_scope]
201
- else
202
- subscopes = []
203
- total = server_scope.count
204
- ranges = []
205
- server_scope.find_ids_in_ranges(:batch_size => (total.to_f / parallel).ceil) do |min, max|
206
- ranges << [min, max]
207
- end
208
- # create a half-open range on the last one
209
- ranges.last[1] = nil
210
- ranges.each do |min, max|
211
- subscope = server_scope.where("id>=?", min)
212
- subscope = subscope.where("id<=?", max) if max
213
- subscopes << subscope
214
- end
215
- end
216
- [server, subscopes]
217
- end]
218
- else
219
- scopes = scope.group_by(&:database_server)
220
- if parallel > 1
221
- parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
222
- scopes = Hash[scopes.map do |(server, shards)|
223
- [server, shards.in_groups(parallel, false).compact]
224
- end]
225
- end
226
- end
227
-
228
- fd_to_name_map = {}
229
- out_fds = []
230
- err_fds = []
231
- pids = []
232
-
233
- wait_for_output = lambda do |out_fds, err_fds, fd_to_name_map|
234
- ready, _ = IO.select(out_fds + err_fds)
235
- ready.each do |fd|
236
- if fd.eof?
237
- fd.close
238
- out_fds.delete(fd)
239
- err_fds.delete(fd)
240
- next
241
- end
242
- line = fd.readline
243
- puts "#{fd_to_name_map[fd]}: #{line}"
244
- end
245
- end
246
-
247
- exception_pipe = IO.pipe
248
- scopes.each do |server, subscopes|
249
- if !(::ActiveRecord::Relation === subscopes.first) && subscopes.first.class != Array
250
- subscopes = [subscopes]
251
- end
252
- # only one process; don't bother forking
253
- if scopes.length == 1 && subscopes.length == 1
254
- exception_pipe.first.close
255
- exception_pipe.last.close
256
- return with_each_shard(subscopes.first, categories, options) { yield }
257
- end
258
-
259
- subscopes.each_with_index do |subscope, idx|
260
- if subscopes.length > 1
261
- name = "#{server.id} #{idx + 1}"
262
- else
263
- name = server.id
264
- end
265
-
266
- details = Open4.pfork4(lambda do
267
- begin
268
- ::ActiveRecord::Base.clear_all_connections!
269
- Switchman.config[:on_fork_proc].try(:call)
270
- $0 = [$0, ARGV, name].flatten.join(' ')
271
- with_each_shard(subscope, categories, options) { yield }
272
- rescue Exception => e
273
- exception_pipe.last.write(Marshal.dump(e))
274
- exception_pipe.last.flush
275
- exit 1
276
- end
277
- end)
278
- # don't care about writing to stdin
279
- details[1].close
280
- out_fds << details[2]
281
- err_fds << details[3]
282
- pids << details[0]
283
- fd_to_name_map[details[2]] = name
284
- fd_to_name_map[details[3]] = name
285
-
286
- while max_procs && pids.count >= max_procs
287
- while max_procs && out_fds.count >= max_procs
288
- # wait for output if we've hit the max_procs limit
289
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
290
- end
291
- pids.delete(Process.wait) # we've gotten all the output from one fd so wait for its child process to exit
292
- end
293
- end
294
- end
295
-
296
- exception_pipe.last.close
297
-
298
- while out_fds.any? || err_fds.any?
299
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
300
- end
301
- pids.each { |pid| Process.waitpid2(pid) }
302
-
303
- # I'm not sure why, but we have to do this
304
- ::ActiveRecord::Base.clear_all_connections!
305
- # check for an exception; we only re-raise the first one
306
- # (all the sub-processes shared the same pipe, so we only
307
- # have to check the one)
308
- begin
309
- exception = Marshal.load exception_pipe.first
310
- raise exception
311
- rescue EOFError
312
- # No exceptions
313
- ensure
314
- exception_pipe.first.close
315
- end
316
- return
317
- end
318
-
319
- categories ||= []
320
-
321
- previous_shard = nil
322
- close_connections_if_needed = lambda do |shard|
323
- # prune the prior connection unless it happened to be the same
324
- if previous_shard && shard != previous_shard &&
325
- (shard.database_server != previous_shard.database_server || !previous_shard.database_server.shareable?)
326
- previous_shard.activate do
327
- ::Shackles.activated_environments.each do |env|
328
- ::Shackles.activate(env) do
329
- if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.open_transactions == 0
330
- ::ActiveRecord::Base.connection_pool.current_pool.disconnect!
331
- end
332
- end
333
- end
334
- end
335
- end
336
- end
337
-
338
- result = []
339
- exception = nil
340
- scope.each do |shard|
341
- # shard references a database server that isn't configured in this environment
342
- next unless shard.database_server
343
- close_connections_if_needed.call(shard)
344
- shard.activate(*categories) do
345
- begin
346
- result.concat Array.wrap(yield)
347
- rescue
348
- case options[:exception]
349
- when :ignore
350
- when :defer
351
- exception ||= $!
352
- when Proc
353
- options[:exception].call
354
- when :raise
355
- raise
356
- else
357
- raise
358
- end
359
- end
360
- end
361
- previous_shard = shard
362
- end
363
- close_connections_if_needed.call(Shard.current)
364
- raise exception if exception
365
- result
366
- end
367
-
368
- def partition_by_shard(array, partition_proc = nil)
369
- shard_arrays = {}
370
- array.each do |object|
371
- partition_object = partition_proc ? partition_proc.call(object) : object
372
- case partition_object
373
- when Shard
374
- shard = partition_object
375
- when ::ActiveRecord::Base
376
- if partition_object.respond_to?(:associated_shards)
377
- partition_object.associated_shards.each do |a_shard|
378
- shard_arrays[a_shard] ||= []
379
- shard_arrays[a_shard] << object
380
- end
381
- next
382
- else
383
- shard = partition_object.shard
384
- end
385
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
386
- local_id, shard = Shard.local_id_for(partition_object)
387
- local_id ||= partition_object
388
- object = local_id if !partition_proc
389
- end
390
- shard ||= Shard.current
391
- shard_arrays[shard] ||= []
392
- shard_arrays[shard] << object
393
- end
394
- # TODO: use with_each_shard (or vice versa) to get
395
- # connection management and parallelism benefits
396
- shard_arrays.inject([]) do |results, (shard, objects)|
397
- results.concat shard.activate { Array.wrap(yield objects) }
398
- end
399
- end
400
-
401
- # converts an AR object, integral id, string id, or string short-global-id to a
402
- # integral id. nil if it can't be interpreted
403
- def integral_id_for(any_id)
404
- if ::Rails.version >= '4.2' && any_id.is_a?(::Arel::Nodes::Casted)
405
- any_id = any_id.val
406
- end
407
-
408
- case any_id
409
- when ::ActiveRecord::Base
410
- any_id.id
411
- when /^(\d+)~(\d+)$/
412
- local_id = $2.to_i
413
- # doesn't make sense to have a double-global id
414
- return nil if local_id > IDS_PER_SHARD
415
- $1.to_i * IDS_PER_SHARD + local_id
416
- when Integer, /^\d+$/
417
- any_id.to_i
418
- else
419
- nil
420
- end
421
- end
422
-
423
- # takes an id-ish, and returns a local id and the shard it's
424
- # local to. [nil, nil] if it can't be interpreted. [id, nil]
425
- # if it's already a local ID
426
- NIL_NIL_ID = [nil, nil].freeze
427
- def local_id_for(any_id)
428
- id = integral_id_for(any_id)
429
- return NIL_NIL_ID unless id
430
- if id < IDS_PER_SHARD
431
- [id, nil]
432
- elsif shard = lookup(id / IDS_PER_SHARD)
433
- [id % IDS_PER_SHARD, shard]
434
- else
435
- NIL_NIL_ID
436
- end
437
- end
438
-
439
- # takes an id-ish, and returns an integral id relative to
440
- # target_shard. returns any_id itself if it can't be interpreted
441
- def relative_id_for(any_id, source_shard, target_shard)
442
- local_id, shard = local_id_for(any_id)
443
- return any_id unless local_id
444
- shard ||= source_shard
445
- return local_id if shard == target_shard
446
- shard.global_id_for(local_id)
447
- end
448
-
449
- # takes an id-ish, and returns a shortened global
450
- # string id if global, and itself if local.
451
- # returns any_id itself if it can't be interpreted
452
- def short_id_for(any_id)
453
- local_id, shard = local_id_for(any_id)
454
- return any_id unless local_id
455
- return local_id unless shard
456
- "#{shard.id}~#{local_id}"
457
- end
458
-
459
- # takes an id-ish, and returns an integral global id.
460
- # returns nil if it can't be interpreted
461
- def global_id_for(any_id, source_shard = nil)
462
- id = integral_id_for(any_id)
463
- return any_id unless id
464
- if id >= IDS_PER_SHARD
465
- id
466
- else
467
- source_shard ||= Shard.current
468
- source_shard.global_id_for(id)
469
- end
470
- end
471
-
472
- def shard_for(any_id, source_shard = nil)
473
- return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
474
- _, shard = local_id_for(any_id)
475
- shard || source_shard || Shard.current
476
- end
477
-
478
- # given the provided option, determines whether we need to (and whether
479
- # it's possible) to determine a reasonable default.
480
- def determine_max_procs(max_procs_input, parallel_input=2)
481
- max_procs = nil
482
- if max_procs_input
483
- max_procs = max_procs_input.to_i
484
- max_procs = nil if max_procs == 0
485
- else
486
- return 1 if parallel_input.nil? || parallel_input < 1
487
- cpus = Environment.cpu_count
488
- if cpus && cpus > 0
489
- max_procs = cpus * parallel_input
490
- end
491
- end
492
-
493
- return max_procs
494
- end
495
-
496
- private
497
- # in-process caching
498
- def cached_shards
499
- @cached_shards ||= {}.compare_by_identity
500
- end
501
-
502
- def add_to_cache(shard)
503
- cached_shards[shard.id] = shard
504
- end
505
-
506
- def remove_from_cache(shard)
507
- cached_shards.delete(shard.id)
508
- end
509
-
510
- def active_shards
511
- Thread.current[:active_shards] ||= {}.compare_by_identity
512
- end
513
- end
514
-
515
- def name
516
- unless instance_variable_defined?(:@name)
517
- # protect against re-entrancy
518
- @name = nil
519
- @name = read_attribute(:name) || default_name
520
- end
521
- @name
522
- end
523
-
524
- def name=(name)
525
- write_attribute(:name, @name = name)
526
- remove_instance_variable(:@name) if name == nil
527
- end
528
-
529
- def database_server
530
- @database_server ||= DatabaseServer.find(self.database_server_id)
531
- end
532
-
533
- def database_server=(database_server)
534
- self.database_server_id = database_server.id
535
- @database_server = database_server
536
- end
537
-
538
- def primary?
539
- self == database_server.primary_shard
540
- end
541
-
542
- def description
543
- [database_server.id, name].compact.join(':')
544
- end
545
-
546
- # Shards are always on the default shard
547
- def shard
548
- Shard.default
549
- end
550
-
551
- def activate(*categories)
552
- shards = hashify_categories(categories)
553
- Shard.activate(shards) do
554
- yield
555
- end
556
- end
557
-
558
- # for use from console ONLY
559
- def activate!(*categories)
560
- shards = hashify_categories(categories)
561
- Shard.activate!(shards)
562
- nil
563
- end
564
-
565
- # custom serialization, since shard is self-referential
566
- def _dump(depth)
567
- self.id.to_s
568
- end
569
-
570
- def self._load(str)
571
- lookup(str.to_i)
572
- end
573
-
574
- def drop_database
575
- raise("Cannot drop the database of the default shard") if self.default?
576
- return unless read_attribute(:name)
577
-
578
- begin
579
- adapter = self.database_server.config[:adapter]
580
- sharding_config = Switchman.config || {}
581
- drop_statement = sharding_config[adapter].try(:[], :drop_statement)
582
- drop_statement ||= sharding_config[:drop_statement]
583
- if drop_statement
584
- drop_statement = Array(drop_statement).dup.
585
- map { |statement| statement.gsub('%{name}', self.name) }
586
- end
587
-
588
- case adapter
589
- when 'mysql', 'mysql2'
590
- self.activate do
591
- ::Shackles.activate(:deploy) do
592
- drop_statement ||= "DROP DATABASE #{self.name}"
593
- Array(drop_statement).each do |stmt|
594
- ::ActiveRecord::Base.connection.execute(stmt)
595
- end
596
- end
597
- end
598
- when 'postgresql'
599
- self.activate do
600
- ::Shackles.activate(:deploy) do
601
- # Shut up, Postgres!
602
- conn = ::ActiveRecord::Base.connection
603
- old_proc = conn.raw_connection.set_notice_processor {}
604
- begin
605
- drop_statement ||= "DROP SCHEMA #{self.name} CASCADE"
606
- Array(drop_statement).each do |stmt|
607
- ::ActiveRecord::Base.connection.execute(stmt)
608
- end
609
- ensure
610
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
611
- end
612
- end
613
- end
614
- when 'sqlite3'
615
- File.delete(self.name) unless self.name == ':memory:'
616
- end
617
- rescue
618
- logger.info "Drop failed: #{$!}"
619
- end
620
- end
621
-
622
- # takes an id local to this shard, and returns a global id
623
- def global_id_for(local_id)
624
- return nil unless local_id
625
- local_id + self.id * IDS_PER_SHARD
626
- end
627
-
628
- # skip global_id.hash
629
- def hash
630
- id.hash
631
- end
632
-
633
- def destroy
634
- raise("Cannot destroy the default shard") if self.default?
635
- super
636
- end
637
-
638
- private
639
-
640
- def clear_cache
641
- Shard.default.activate do
642
- Switchman.cache.delete(['shard', id].join('/'))
643
- end
644
- end
645
-
646
- def default_name
647
- database_server.shard_name(self)
648
- end
649
-
650
- def hashify_categories(categories)
651
- if categories.empty?
652
- { :default => self }
653
- else
654
- categories.inject({}) { |h, category| h[category] = self; h }
655
- end
656
- end
657
-
658
- end
659
- end