Skip to content

Commit

Permalink
Allow cache node names
Browse files Browse the repository at this point in the history
Consistent hashing relies on using a fixed name for a specific node.

Allow the cluster config to pass in the names to use.
  • Loading branch information
djmb committed Aug 14, 2023
1 parent 536e7d0 commit dbf22f6
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 34 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,30 @@ Rails.application.configure do
end
```

### Named shard destinations

By default, the node key used for sharding is the name of the database in `database.yml`.

It is possible to add names for the shards in the cluster config. This will allow you to shuffle or remove shards without breaking consistent hashing.

```ruby
Rails.application.configure do
config.solid_cache.connects_to = {
shards: {
cache_primary_shard1: { writing: :cache_primary_shard1 },
cache_primary_shard2: { writing: :cache_primary_shard2 },
cache_secondary_shard1: { writing: :cache_secondary_shard1 },
cache_secondary_shard2: { writing: :cache_secondary_shard2 },
}
}

primary_cluster = { shards: { cache_primary_shard1: :node1, cache_primary_shard2: :node2 } }
secondary_cluster = { shards: { cache_primary_shard1: :node3, cache_primary_shard2: :node4 } }
config.cache_store = [ :solid_cache_store, clusters: [ primary_cluster, secondary_cluster ] ]
end
```


### Enabling encryption

Add this to an initializer:
Expand Down
8 changes: 0 additions & 8 deletions lib/solid_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ def self.shard_config(shard)
all_shards_config && all_shards_config[shard]
end

def self.shard_destinations
@shard_databases ||= each_shard.map.to_h do
config = Record.connection_db_config
destination = [ config.try(:host), config.try(:port), config.try(:database) ].compact.join("-")
[ Record.current_shard, destination ]
end
end

def self.each_shard
return to_enum(:each_shard) unless block_given?

Expand Down
54 changes: 41 additions & 13 deletions lib/solid_cache/cluster/connection_handling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,27 @@ module ConnectionHandling

def initialize(options = {})
super(options)
@shard_options = options.delete(:shards)
@async_writes = options.delete(:async_writes)
@shard_options = options.delete(:shards)

if [Hash, Array, NilClass].none? { |klass| @shard_options.is_a? klass }
raise ArgumentError, "`shards` is a `#{shards.class.name}`, it should be one of Array, Hash or nil"
end

# Done lazily as the cache maybe created before ActionRecord initialization
@shards_initialized = false
end

def shards
# Load lazily as the cache maybe created before the SolidCache connections are initialized
@shards ||= @shard_options || SolidCache.all_shard_keys || [nil]
initialize_shards unless shards_initialized?

@shards
end

def destination_shards
@destination_shards ||= shards.compact.to_h { |shard| [ SolidCache.shard_destinations[shard], shard ] }
def nodes
initialize_shards unless shards_initialized?

@nodes
end

def writing_all_shards
Expand Down Expand Up @@ -59,6 +69,29 @@ def reading_shard(normalized_key:)
end

private
attr_reader :consistent_hash

def shards_initialized?
@shards_initialized
end

def initialize_shards
case @shard_options
when Array, NilClass
@shards = @shard_options || SolidCache.all_shard_keys || []
@nodes = @shards.to_h { |shard| [ shard, shard ] }
when Hash
@shards = @shard_options.keys
@nodes = @shard_options.inverse
end

if @shards.count > 1
@consistent_hash = MaglevHash.new(@nodes.keys)
end

@shards_initialized = true
end

def with_shard(shard)
if shard
Record.connected_to(shard: shard) { yield }
Expand All @@ -82,15 +115,10 @@ def in_shards(list)
end

def shard_for_normalized_key(normalized_key)
return shards.first if shards.count == 1

destination = consistent_hash.node(normalized_key)
destination_shards[destination]
end
return shards.first if shards.count <= 1

def consistent_hash
return nil if shards.count == 1
@consistent_hash ||= MaglevHash.new(destination_shards.keys)
node = consistent_hash.node(normalized_key)
nodes[node]
end

def async_if_required
Expand Down
13 changes: 0 additions & 13 deletions test/unit/solid_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,6 @@ class SolidCacheTest < ActiveSupport::TestCase
shards = SolidCache.each_shard.map { SolidCache::Record.current_shard }
assert_equal [ :default, :default2, :primary_shard_one, :primary_shard_two, :secondary_shard_one, :secondary_shard_two ], shards
end

test "shard_databases" do
configs = SolidCache::Record.configurations
shard_names = [:default, :default2, :primary_shard_one, :primary_shard_two, :secondary_shard_one, :secondary_shard_two]
expected = shard_names.to_h do |shard_name|
database_name = SolidCache.connects_to.dig(:shards, shard_name, :writing).to_s
config = configs.configs_for(env_name: Rails.env, name: database_name)
destination = [ config.try(:host), config.try(:port), config.try(:database) ].compact.join("-")
[ shard_name, destination ]
end

assert_equal expected, SolidCache.shard_destinations
end
end

class SolidCacheFailsafeTest < ActiveSupport::TestCase
Expand Down

0 comments on commit dbf22f6

Please sign in to comment.