db-charmer 1.6.14 → 1.6.15
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/.gitignore +2 -0
- data/VERSION +1 -1
- data/db-charmer.gemspec +37 -33
- data/lib/db_charmer/sharding/method/db_block_group_map.rb +261 -0
- data/lib/db_charmer/sharding/method/range.rb +4 -0
- metadata +10 -8
data/.gitignore
ADDED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.6.
|
1
|
+
1.6.15
|
data/db-charmer.gemspec
CHANGED
@@ -1,57 +1,61 @@
|
|
1
1
|
# Generated by jeweler
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{db-charmer}
|
8
|
-
s.version = "1.6.
|
8
|
+
s.version = "1.6.15"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Alexey Kovyrin"]
|
12
|
-
s.date = %q{2011-
|
12
|
+
s.date = %q{2011-02-28}
|
13
13
|
s.description = %q{ActiveRecord Connections Magic (slaves, multiple connections, etc)}
|
14
14
|
s.email = %q{alexey@kovyrin.net}
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
17
|
-
|
17
|
+
"README.rdoc"
|
18
18
|
]
|
19
19
|
s.files = [
|
20
|
-
"
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
20
|
+
".gitignore",
|
21
|
+
"CHANGES",
|
22
|
+
"LICENSE",
|
23
|
+
"Makefile",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"db-charmer.gemspec",
|
28
|
+
"init.rb",
|
29
|
+
"lib/db_charmer.rb",
|
30
|
+
"lib/db_charmer/abstract_adapter_extensions.rb",
|
31
|
+
"lib/db_charmer/active_record_extensions.rb",
|
32
|
+
"lib/db_charmer/association_preload.rb",
|
33
|
+
"lib/db_charmer/connection_factory.rb",
|
34
|
+
"lib/db_charmer/connection_proxy.rb",
|
35
|
+
"lib/db_charmer/connection_switch.rb",
|
36
|
+
"lib/db_charmer/core_extensions.rb",
|
37
|
+
"lib/db_charmer/db_magic.rb",
|
38
|
+
"lib/db_charmer/finder_overrides.rb",
|
39
|
+
"lib/db_charmer/multi_db_migrations.rb",
|
40
|
+
"lib/db_charmer/multi_db_proxy.rb",
|
41
|
+
"lib/db_charmer/scope_proxy.rb",
|
42
|
+
"lib/db_charmer/sharding.rb",
|
43
|
+
"lib/db_charmer/sharding/connection.rb",
|
44
|
+
"lib/db_charmer/sharding/method/db_block_group_map.rb",
|
45
|
+
"lib/db_charmer/sharding/method/db_block_map.rb",
|
46
|
+
"lib/db_charmer/sharding/method/hash_map.rb",
|
47
|
+
"lib/db_charmer/sharding/method/range.rb",
|
48
|
+
"lib/db_charmer/stub_connection.rb",
|
49
|
+
"lib/tasks/databases.rake"
|
48
50
|
]
|
49
51
|
s.homepage = %q{http://github.com/kovyrin/db-charmer}
|
52
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
50
53
|
s.require_paths = ["lib"]
|
51
|
-
s.rubygems_version = %q{1.
|
54
|
+
s.rubygems_version = %q{1.3.7}
|
52
55
|
s.summary = %q{ActiveRecord Connections Magic}
|
53
56
|
|
54
57
|
if s.respond_to? :specification_version then
|
58
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
55
59
|
s.specification_version = 3
|
56
60
|
|
57
61
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
@@ -0,0 +1,261 @@
|
|
1
|
+
# This is a more sophisticated sharding method based on a two layer database-backed
|
2
|
+
# blocks map that holds block-shard associations. Record blocks are mapped to tablegroups
|
3
|
+
# and groups are mapped to shards.
|
4
|
+
#
|
5
|
+
# It automatically creates new blocks for new keys and assigns them to existing groups.
|
6
|
+
# Warning: make sure to create at least one shard and one group before inserting any records.
|
7
|
+
#
|
8
|
+
module DbCharmer
|
9
|
+
module Sharding
|
10
|
+
module Method
|
11
|
+
class DbBlockGroupMap
|
12
|
+
# Shard connection info model
|
13
|
+
class Shard < ActiveRecord::Base
|
14
|
+
validates_presence_of :db_host
|
15
|
+
validates_presence_of :db_port
|
16
|
+
validates_presence_of :db_user
|
17
|
+
validates_presence_of :db_pass
|
18
|
+
validates_presence_of :db_name_prefix
|
19
|
+
|
20
|
+
has_many :groups, :class_name => 'DbCharmer::Sharding::Method::DbBlockGroupMap::Group'
|
21
|
+
end
|
22
|
+
|
23
|
+
# Table group info model
|
24
|
+
class Group < ActiveRecord::Base
|
25
|
+
validates_presence_of :shard_id
|
26
|
+
belongs_to :shard, :class_name => 'DbCharmer::Sharding::Method::DbBlockGroupMap::Shard'
|
27
|
+
end
|
28
|
+
|
29
|
+
#---------------------------------------------------------------------------------------------------------------
|
30
|
+
# Sharder name
|
31
|
+
attr_accessor :name
|
32
|
+
|
33
|
+
# Mapping db connection
|
34
|
+
attr_accessor :connection, :connection_name
|
35
|
+
|
36
|
+
# Mapping table name
|
37
|
+
attr_accessor :map_table
|
38
|
+
|
39
|
+
# Tablegroups table name
|
40
|
+
attr_accessor :groups_table
|
41
|
+
|
42
|
+
# Shards table name
|
43
|
+
attr_accessor :shards_table
|
44
|
+
|
45
|
+
# Sharding keys block size
|
46
|
+
attr_accessor :block_size
|
47
|
+
|
48
|
+
def initialize(config)
|
49
|
+
@name = config[:name] or raise(ArgumentError, "Missing required :name parameter!")
|
50
|
+
@connection = DbCharmer::ConnectionFactory.connect(config[:connection], true)
|
51
|
+
@block_size = (config[:block_size] || 10000).to_i
|
52
|
+
|
53
|
+
@map_table = config[:map_table] or raise(ArgumentError, "Missing required :map_table parameter!")
|
54
|
+
@groups_table = config[:groups_table] or raise(ArgumentError, "Missing required :groups_table parameter!")
|
55
|
+
@shards_table = config[:shards_table] or raise(ArgumentError, "Missing required :shards_table parameter!")
|
56
|
+
|
57
|
+
# Local caches
|
58
|
+
@shard_info_cache = {}
|
59
|
+
@group_info_cache = {}
|
60
|
+
|
61
|
+
@blocks_cache = Rails.cache
|
62
|
+
@blocks_cache_prefix = config[:blocks_cache_prefix] || "#{@name}_block:"
|
63
|
+
end
|
64
|
+
|
65
|
+
#---------------------------------------------------------------------------------------------------------------
|
66
|
+
def shard_for_key(key)
|
67
|
+
block = block_for_key(key)
|
68
|
+
|
69
|
+
# Auto-allocate new blocks
|
70
|
+
block ||= allocate_new_block_for_key(key)
|
71
|
+
raise ArgumentError, "Invalid key value, no shards found for this key and could not create a new block!" unless block
|
72
|
+
|
73
|
+
# Load shard
|
74
|
+
group_id = block['group_id'].to_i
|
75
|
+
shard_info = shard_info_by_group_id(group_id)
|
76
|
+
|
77
|
+
# Get config
|
78
|
+
shard_connection_config(shard_info, group_id)
|
79
|
+
end
|
80
|
+
|
81
|
+
#---------------------------------------------------------------------------------------------------------------
|
82
|
+
# Returns a block for a key
|
83
|
+
def block_for_key(key, cache = true)
|
84
|
+
# Cleanup the cache if asked to
|
85
|
+
key_range = [ block_start_for_key(key), block_end_for_key(key) ]
|
86
|
+
block_cache_key = "%d-%d" % key_range
|
87
|
+
|
88
|
+
if cache
|
89
|
+
cached_block = get_cached_block(block_cache_key)
|
90
|
+
return cached_block if cached_block
|
91
|
+
end
|
92
|
+
|
93
|
+
# Fetch cached value or load from db
|
94
|
+
block = begin
|
95
|
+
sql = "SELECT * FROM #{map_table} WHERE start_id = #{key_range.first} AND end_id = #{key_range.last} LIMIT 1"
|
96
|
+
connection.select_one(sql, 'Find a shard block')
|
97
|
+
end
|
98
|
+
|
99
|
+
set_cached_block(block_cache_key, block)
|
100
|
+
|
101
|
+
return block
|
102
|
+
end
|
103
|
+
|
104
|
+
#---------------------------------------------------------------------------------------------------------------
|
105
|
+
def get_cached_block(block_cache_key)
|
106
|
+
@blocks_cache.read("#{@blocks_cache_prefix}#{block_cache_key}")
|
107
|
+
end
|
108
|
+
|
109
|
+
def set_cached_block(block_cache_key, block)
|
110
|
+
@blocks_cache.write("#{@blocks_cache_prefix}#{block_cache_key}", block)
|
111
|
+
end
|
112
|
+
|
113
|
+
#---------------------------------------------------------------------------------------------------------------
|
114
|
+
# Load group info
|
115
|
+
def group_info_by_id(group_id, cache = true)
|
116
|
+
# Cleanup the cache if asked to
|
117
|
+
@group_info_cache[group_id] = nil unless cache
|
118
|
+
|
119
|
+
# Either load from cache or from db
|
120
|
+
@group_info_cache[group_id] ||= begin
|
121
|
+
prepare_shard_models
|
122
|
+
Group.find_by_id(group_id)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Load shard info
|
127
|
+
def shard_info_by_id(shard_id, cache = true)
|
128
|
+
# Cleanup the cache if asked to
|
129
|
+
@shard_info_cache[shard_id] = nil unless cache
|
130
|
+
|
131
|
+
# Either load from cache or from db
|
132
|
+
@shard_info_cache[shard_id] ||= begin
|
133
|
+
prepare_shard_models
|
134
|
+
Shard.find_by_id(shard_id)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Load shard info using mapping info for a group
|
139
|
+
def shard_info_by_group_id(group_id)
|
140
|
+
# Load group
|
141
|
+
group_info = group_info_by_id(group_id)
|
142
|
+
raise ArgumentError, "Invalid group_id: #{group_id}" unless group_info
|
143
|
+
|
144
|
+
shard_info = shard_info_by_id(group_info.shard_id)
|
145
|
+
raise ArgumentError, "Invalid shard_id: #{group_info.shard_id}" unless shard_info
|
146
|
+
|
147
|
+
return shard_info
|
148
|
+
end
|
149
|
+
|
150
|
+
#---------------------------------------------------------------------------------------------------------------
|
151
|
+
def allocate_new_block_for_key(key)
|
152
|
+
# Can't find any groups to use for blocks allocation!
|
153
|
+
return nil unless group = least_loaded_group
|
154
|
+
|
155
|
+
# Figure out block limits
|
156
|
+
start_id = block_start_for_key(key)
|
157
|
+
end_id = block_end_for_key(key)
|
158
|
+
|
159
|
+
# Try to insert a new mapping (ignore duplicate key errors)
|
160
|
+
sql = <<-SQL
|
161
|
+
INSERT IGNORE INTO #{map_table}
|
162
|
+
SET start_id = #{start_id},
|
163
|
+
end_id = #{end_id},
|
164
|
+
group_id = #{group.id},
|
165
|
+
block_size = #{block_size},
|
166
|
+
created_at = NOW(),
|
167
|
+
updated_at = NOW()
|
168
|
+
SQL
|
169
|
+
connection.execute(sql, "Allocate new block")
|
170
|
+
|
171
|
+
# Increment the blocks counter on the shard
|
172
|
+
Group.update_counters(group.id, :blocks_count => +1)
|
173
|
+
|
174
|
+
# Retry block search after creation
|
175
|
+
block_for_key(key)
|
176
|
+
end
|
177
|
+
|
178
|
+
def least_loaded_group
|
179
|
+
prepare_shard_models
|
180
|
+
|
181
|
+
# Select group
|
182
|
+
group = Group.first(:conditions => { :enabled => true, :open => true }, :order => 'blocks_count ASC')
|
183
|
+
raise "Can't find any tablegroups to use for blocks allocation!" unless group
|
184
|
+
return group
|
185
|
+
end
|
186
|
+
|
187
|
+
#---------------------------------------------------------------------------------------------------------------
|
188
|
+
def block_start_for_key(key)
|
189
|
+
block_size.to_i * (key.to_i / block_size.to_i)
|
190
|
+
end
|
191
|
+
|
192
|
+
def block_end_for_key(key)
|
193
|
+
block_size.to_i + block_start_for_key(key)
|
194
|
+
end
|
195
|
+
|
196
|
+
#---------------------------------------------------------------------------------------------------------------
|
197
|
+
# Create configuration (use mapping connection as a template)
|
198
|
+
def shard_connection_config(shard, group_id)
|
199
|
+
# Format connection name
|
200
|
+
shard_name = "db_charmer_db_block_group_map_#{name}_s%d_g%d" % [ shard.id, group_id]
|
201
|
+
|
202
|
+
# Here we get the mapping connection's configuration
|
203
|
+
# They do not expose configs so we hack in and get the instance var
|
204
|
+
# FIXME: Find a better way, maybe move config method to our ar extenstions
|
205
|
+
connection.instance_variable_get(:@config).clone.merge(
|
206
|
+
# Name for the connection factory
|
207
|
+
:name => shard_name,
|
208
|
+
# Connection params
|
209
|
+
:host => shard.db_host,
|
210
|
+
:port => shard.db_port,
|
211
|
+
:username => shard.db_user,
|
212
|
+
:password => shard.db_pass,
|
213
|
+
:database => group_database_name(shard, group_id)
|
214
|
+
)
|
215
|
+
end
|
216
|
+
|
217
|
+
def group_database_name(shard, group_id)
|
218
|
+
"%s_%05d" % [ shard.db_name_prefix, group_id ]
|
219
|
+
end
|
220
|
+
|
221
|
+
#---------------------------------------------------------------------------------------------------------------
|
222
|
+
def create_shard(params)
|
223
|
+
params = params.symbolize_keys
|
224
|
+
[ :db_host, :db_port, :db_user, :db_pass, :db_name_prefix ].each do |arg|
|
225
|
+
raise ArgumentError, "Missing required parameter: #{arg}" unless params[arg]
|
226
|
+
end
|
227
|
+
|
228
|
+
# Prepare model
|
229
|
+
prepare_shard_models
|
230
|
+
|
231
|
+
# Create the record
|
232
|
+
Shard.create! do |shard|
|
233
|
+
shard.db_host = params[:db_host]
|
234
|
+
shard.db_port = params[:db_port]
|
235
|
+
shard.db_user = params[:db_user]
|
236
|
+
shard.db_pass = params[:db_pass]
|
237
|
+
shard.db_name_prefix = params[:db_name_prefix]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def shard_connections
|
242
|
+
# Find all groups
|
243
|
+
prepare_shard_models
|
244
|
+
groups = Group.all(:conditions => { :enabled => true })
|
245
|
+
# Map them to shards
|
246
|
+
groups.map { |group| shard_connection_config(group.shard, group.id) }
|
247
|
+
end
|
248
|
+
|
249
|
+
# Prepare model for working with our shards table
|
250
|
+
def prepare_shard_models
|
251
|
+
Shard.set_table_name(shards_table)
|
252
|
+
Shard.switch_connection_to(connection)
|
253
|
+
|
254
|
+
Group.set_table_name(groups_table)
|
255
|
+
Group.switch_connection_to(connection)
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: db-charmer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 17
|
5
|
+
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 6
|
9
|
-
-
|
10
|
-
version: 1.6.
|
9
|
+
- 15
|
10
|
+
version: 1.6.15
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Alexey Kovyrin
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-02-28 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -57,6 +57,7 @@ extra_rdoc_files:
|
|
57
57
|
- LICENSE
|
58
58
|
- README.rdoc
|
59
59
|
files:
|
60
|
+
- .gitignore
|
60
61
|
- CHANGES
|
61
62
|
- LICENSE
|
62
63
|
- Makefile
|
@@ -80,6 +81,7 @@ files:
|
|
80
81
|
- lib/db_charmer/scope_proxy.rb
|
81
82
|
- lib/db_charmer/sharding.rb
|
82
83
|
- lib/db_charmer/sharding/connection.rb
|
84
|
+
- lib/db_charmer/sharding/method/db_block_group_map.rb
|
83
85
|
- lib/db_charmer/sharding/method/db_block_map.rb
|
84
86
|
- lib/db_charmer/sharding/method/hash_map.rb
|
85
87
|
- lib/db_charmer/sharding/method/range.rb
|
@@ -90,8 +92,8 @@ homepage: http://github.com/kovyrin/db-charmer
|
|
90
92
|
licenses: []
|
91
93
|
|
92
94
|
post_install_message:
|
93
|
-
rdoc_options:
|
94
|
-
|
95
|
+
rdoc_options:
|
96
|
+
- --charset=UTF-8
|
95
97
|
require_paths:
|
96
98
|
- lib
|
97
99
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -115,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
117
|
requirements: []
|
116
118
|
|
117
119
|
rubyforge_project:
|
118
|
-
rubygems_version: 1.
|
120
|
+
rubygems_version: 1.3.7
|
119
121
|
signing_key:
|
120
122
|
specification_version: 3
|
121
123
|
summary: ActiveRecord Connections Magic
|