Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate 'slave' in favor of 'replica' #286

Merged
merged 1 commit into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
All notable changes to this project will be documented in this file.

## Unreleased
- Deprecated the term `slave` in favor of `replica` [#286](https://github.com/instacart/makara/pull/286) Matt Larraz
- Drop support for Ruby < 2.5 and ActiveRecord < 5.2 [#281](https://github.com/instacart/makara/pull/281) Matt Larraz

## v0.5.0 - 2021-01-08
Expand Down
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Code Climate](https://codeclimate.com/repos/526886a7f3ea00679b00cae6/badges/7905f7a000492a1078f7/gpa.png)](https://codeclimate.com/repos/526886a7f3ea00679b00cae6/feed)


Makara is generic master/slave proxy. It handles the heavy lifting of managing, choosing, blacklisting, and cycling through connections. It comes with an ActiveRecord database adapter implementation.
Makara is generic master/replica proxy. It handles the heavy lifting of managing, choosing, blacklisting, and cycling through connections. It comes with an ActiveRecord database adapter implementation.

## Installation

Expand Down Expand Up @@ -36,7 +36,7 @@ Next, you need to decide which methods are proxied and which methods should be s
send_to_all :connect, :reconnect, :disconnect, :clear_cache
```

Assuming you don't need to split requests between a master and a slave, you're done. If you do need to, implement the `needs_master?` method:
Assuming you don't need to split requests between a master and a replica, you're done. If you do need to, implement the `needs_master?` method:

```ruby
# within MyAwesomeSqlProxy
Expand All @@ -63,7 +63,7 @@ To handle persistence of context across requests in a Rack app, makara provides

When `sticky:true`, once a query as been sent to master, all queries for the rest of the request will also be sent to master. In addition, the cookie described above will be set client side with an expiration defined by time at end of original request + `master_ttl`. As long as the cookie is valid, all requests will send queries to master.

When `sticky:false`, only queries that need to go to master will go there. Subsequent read queries in the same request will go to slaves.
When `sticky:false`, only queries that need to go to master will go there. Subsequent read queries in the same request will go to replicas.

#### Releasing stuck connections (clearing context)

Expand Down Expand Up @@ -116,13 +116,13 @@ Makara::Logging::Logger.logger = ::Logger.new(STDOUT)

## ActiveRecord Database Adapter

So you've found yourself with an ActiveRecord-based project which is starting to get some traffic and you realize 95% of you DB load is from reads. Well you've come to the right spot. Makara is a great solution to break up that load not only between master and slave but potentially multiple masters and/or multiple slaves.
So you've found yourself with an ActiveRecord-based project which is starting to get some traffic and you realize 95% of you DB load is from reads. Well you've come to the right spot. Makara is a great solution to break up that load not only between master and replica but potentially multiple masters and/or multiple replicas.

By creating a makara database adapter which simply acts as a proxy we avoid any major complexity surrounding specific database implementations. The makara adapter doesn't care if the underlying connection is mysql, postgresql, etc it simply cares about the sql string being executed.

### What goes where?

In general: Any `SELECT` statements will execute against your slave(s), anything else will go to master.
In general: Any `SELECT` statements will execute against your replica(s), anything else will go to master.

There are some edge cases:
* `SET` operations will be sent to all connections
Expand All @@ -132,7 +132,7 @@ There are some edge cases:

### Errors / blacklisting

Whenever a node fails an operation due to a connection issue, it is blacklisted for the amount of time specified in your database.yml. After that amount of time has passed, the connection will begin receiving queries again. If all slave nodes are blacklisted, the master connection will begin receiving read queries as if it were a slave. Once all nodes are blacklisted the error is raised to the application and all nodes are whitelisted.
Whenever a node fails an operation due to a connection issue, it is blacklisted for the amount of time specified in your database.yml. After that amount of time has passed, the connection will begin receiving queries again. If all replica nodes are blacklisted, the master connection will begin receiving read queries as if it were a replica. Once all nodes are blacklisted the error is raised to the application and all nodes are whitelisted.

### Database.yml

Expand All @@ -156,14 +156,14 @@ production:
sticky: true

# list your connections with the override values (they're merged into the top-level config)
# be sure to provide the role if master, role is assumed to be a slave if not provided
# be sure to provide the role if master, role is assumed to be a replica if not provided
connections:
- role: master
host: master.sql.host
- role: slave
host: slave1.sql.host
- role: slave
host: slave2.sql.host
- role: replica
host: replica1.sql.host
- role: replica
host: replica2.sql.host
```

Let's break this down a little bit. At the top level of your config you have the standard `adapter` choice. Currently the available adapters are listed in [lib/active_record/connection_adapters/](lib/active_record/connection_adapters/). They are in the form of `#{db_type}_makara` where db_type is mysql, postgresql, etc.
Expand All @@ -177,7 +177,7 @@ The makara subconfig sets up the proxy with a few of its own options, then provi
* sticky - if a node should be stuck to once it's used during a specific context
* master_ttl - how long the master context is persisted. generally, this needs to be longer than any replication lag
* master_strategy - use a different strategy for picking the "current" master node: `failover` will try to keep the same one until it is blacklisted. The default is `round_robin` which will cycle through available ones.
* slave_strategy - use a different strategy for picking the "current" slave node: `failover` will try to keep the same one until it is blacklisted. The default is `round_robin` which will cycle through available ones.
* replica_strategy - use a different strategy for picking the "current" replica node: `failover` will try to keep the same one until it is blacklisted. The default is `round_robin` which will cycle through available ones.
* connection_error_matchers - array of custom error matchers you want to be handled gracefully by Makara (as in, errors matching these regexes will result in blacklisting the connection as opposed to raising directly).

Connection definitions contain any extra node-specific configurations. If the node should behave as a master you must provide `role: master`. Any previous configurations can be overridden within a specific node's config. Nodes can also contain weights if you'd like to balance usage based on hardware specifications. Optionally, you can provide a name attribute which will be used in sql logging.
Expand All @@ -188,16 +188,16 @@ connections:
host: mymaster.sql.host
blacklist_duration: 0

# implicit role: slave
- host: mybigslave.sql.host
# implicit role: replica
- host: mybigreplica.sql.host
weight: 8
name: Big Slave
- host: mysmallslave.sql.host
name: Big Replica
- host: mysmallreplica.sql.host
weight: 2
name: Small Slave
name: Small Replica
```

In the previous config the "Big Slave" would receive ~80% of traffic.
In the previous config the "Big Replica" would receive ~80% of traffic.

#### DATABASE_URL

Expand All @@ -217,8 +217,8 @@ connections:
- role: master
blacklist_duration: 0
url: <%= ENV['DATABASE_URL_MASTER'] %>
- role: slave
url: <%= ENV['DATABASE_URL_SLAVE'] %>
- role: replica
url: <%= ENV['DATABASE_URL_REPLICA'] %>
```

**Important**: *Do NOT use `ENV['DATABASE_URL']`*, as it inteferes with the the database configuration
Expand Down Expand Up @@ -271,7 +271,7 @@ You can provide strings or regexes. In the case of strings, if they start with
On occasion your app may deal with a situation where makara is not present during a write but a read should use master. In the generic proxy details above you are encouraged to use `stick_to_master!` to accomplish this. Here's an example:

```ruby
# some third party creates a resource in your db, slave replication may not have completed yet
# some third party creates a resource in your db, replication may not have completed yet
# ...
# then your app is told to read the resource.
def handle_request_after_third_party_record_creation
Expand Down
19 changes: 11 additions & 8 deletions lib/active_record/connection_adapters/makara_abstract_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,31 +114,34 @@ def custom_error_message?(connection, message)


SQL_MASTER_MATCHERS = [/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i, /\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i].map(&:freeze).freeze
SQL_SLAVE_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].map(&:freeze).freeze
SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].map(&:freeze).freeze
SQL_ALL_MATCHERS = [/\A\s*set\s/i].map(&:freeze).freeze
SQL_SKIP_STICKINESS_MATCHERS = [/\A\s*show\s([\w]+\s)?(field|table|database|schema|view|index)(es|s)?/i, /\A\s*(set|describe|explain|pragma)\s/i].map(&:freeze).freeze

SQL_SLAVE_MATCHERS = SQL_REPLICA_MATCHERS
deprecate_constant :SQL_SLAVE_MATCHERS

def sql_master_matchers
SQL_MASTER_MATCHERS
end

def sql_replica_matchers
SQL_REPLICA_MATCHERS
end

def sql_slave_matchers
SQL_SLAVE_MATCHERS
warn "sql_slave_matchers is deprecated. Use sql_replica_matchers"
sql_replica_matchers
end


def sql_all_matchers
SQL_ALL_MATCHERS
end


def sql_skip_stickiness_matchers
SQL_SKIP_STICKINESS_MATCHERS
end


def initialize(config)
@error_handler = ::ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter::ErrorHandler.new
@control = ActiveRecordPoolControl.new(self)
Expand All @@ -154,8 +157,8 @@ def appropriate_connection(method_name, args, &block)

handling_an_all_execution(method_name) do
hijacked do
# slave pool must run first.
@slave_pool.send_to_all(nil, &block) # just yields to each con
# replica pool must run first.
@replica_pool.send_to_all(nil, &block) # just yields to each con
@master_pool.send_to_all(nil, &block) # just yields to each con
end
end
Expand Down Expand Up @@ -187,7 +190,7 @@ def needed_by_all?(method_name, args)
def needs_master?(method_name, args)
sql = coerce_query_to_sql_string(args.first)
return true if sql_master_matchers.any?{|m| sql =~ m }
return false if sql_slave_matchers.any?{|m| sql =~ m }
return false if sql_replica_matchers.any?{|m| sql =~ m }
true
end

Expand Down
1 change: 0 additions & 1 deletion lib/makara.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
require 'makara/version'
require 'makara/railtie' if defined?(Rails)
module Makara

autoload :Cache, 'makara/cache'
autoload :ConfigParser, 'makara/config_parser'
autoload :ConnectionWrapper, 'makara/connection_wrapper'
Expand Down
27 changes: 23 additions & 4 deletions lib/makara/config_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
# blacklist_duration: 20
# connections:
# - role: 'master'
# - role: 'slave'
# - role: 'slave'
# name: 'slave2'
# - role: 'slave' # Deprecated in favor of 'replica'
# - role: 'replica'
# name: 'replica2'

module Makara
class ConfigParser
Expand Down Expand Up @@ -145,6 +145,9 @@ def initialize(config)
@config = config.symbolize_keys
@makara_config = DEFAULTS.merge(@config[:makara] || {})
@makara_config = @makara_config.symbolize_keys

deprecate_keys(:slave_strategy, :slave_shard_aware, :slave_default_shard)

@id = sanitize_id(@makara_config[:id])
end

Expand All @@ -164,12 +167,17 @@ def master_configs
end


def slave_configs
def replica_configs
all_configs
.reject { |config| config[:role] == 'master' }
.map { |config| config.except(:role) }
end

def slave_configs
warn "#slave_configs is deprecated. Switch to #replica_configs"
replica_configs
end


protected

Expand Down Expand Up @@ -208,5 +216,16 @@ def sanitize_id(id)
end
end
end

def deprecate_keys(*keys)
keys.each do |key|
next unless @makara_config[key]

new_key = key.to_s.gsub("slave", "replica").to_sym
warn "Config key #{key} is deprecated, use #{new_key} instead"

@makara_config[new_key] = @makara_config[key]
end
end
end
end
2 changes: 1 addition & 1 deletion lib/makara/logging/subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def sql(event)
# uses the adapter's connection proxy to modify the name of the event
# the name of the used connection will be prepended to the sql log
###
### [Master|Slave] User Load (1.3ms) SELECT * FROM `users`;
### [Master|Replica] User Load (1.3ms) SELECT * FROM `users`;
###
def current_wrapper_name(event)
connection_object_id = event.payload[:connection_id]
Expand Down
39 changes: 19 additions & 20 deletions lib/makara/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/string/inflections'

# The entry point of Makara. It contains a master and slave pool which are chosen based on the invocation
# The entry point of Makara. It contains a master and replica pool which are chosen based on the invocation
# being proxied. Makara::Proxy implementations should declare which methods they are hijacking via the
# `hijack_method` class method.
# While debugging this class use prepend debug calls with Kernel. (Kernel.byebug for example)
Expand Down Expand Up @@ -159,16 +159,16 @@ def disconnect!


def send_to_all(method_name, *args)
# slave pool must run first to allow for slave-->master failover without running operations on master twice.
# replica pool must run first to allow for replica --> master failover without running operations on master twice.
handling_an_all_execution(method_name) do
@slave_pool.send_to_all method_name, *args
@replica_pool.send_to_all method_name, *args
@master_pool.send_to_all method_name, *args
end
end

def any_connection
if @master_pool.disabled
@slave_pool.provide do |con|
@replica_pool.provide do |con|
yield con
end
else
Expand All @@ -179,7 +179,7 @@ def any_connection
rescue ::Makara::Errors::AllConnectionsBlacklisted, ::Makara::Errors::NoConnectionsAvailable
begin
@master_pool.disabled = true
@slave_pool.provide do |con|
@replica_pool.provide do |con|
yield con
end
ensure
Expand All @@ -201,17 +201,16 @@ def appropriate_connection(method_name, args)
end


# master or slave
# master or replica
def appropriate_pool(method_name, args)

# for testing purposes
pool = _appropriate_pool(method_name, args)
yield pool

rescue ::Makara::Errors::AllConnectionsBlacklisted, ::Makara::Errors::NoConnectionsAvailable => e
if pool == @master_pool
@master_pool.connections.each(&:_makara_whitelist!)
@slave_pool.connections.each(&:_makara_whitelist!)
@replica_pool.connections.each(&:_makara_whitelist!)
Kernel.raise e
else
@master_pool.blacklist_errors << e
Expand All @@ -232,17 +231,17 @@ def _appropriate_pool(method_name, args)
# stickiness is still valid
@master_pool

# all slaves are down (or empty)
elsif @slave_pool.completely_blacklisted?
# all replicas are down (or empty)
elsif @replica_pool.completely_blacklisted?
stick_to_master(method_name, args)
@master_pool

elsif in_transaction?
@master_pool

# yay! use a slave
# yay! use a replica
else
@slave_pool
@replica_pool
end
end

Expand Down Expand Up @@ -290,7 +289,7 @@ def sticky?
@sticky && !@skip_sticking
end

# use the config parser to generate a master and slave pool
# use the config parser to generate a master and replica pool
def instantiate_connections
@master_pool = Makara::Pool.new('master', self)
@config_parser.master_configs.each do |master_config|
Expand All @@ -299,10 +298,10 @@ def instantiate_connections
end
end

@slave_pool = Makara::Pool.new('slave', self)
@config_parser.slave_configs.each do |slave_config|
@slave_pool.add slave_config do
graceful_connection_for(slave_config)
@replica_pool = Makara::Pool.new('replica', self)
@config_parser.replica_configs.each do |replica_config|
@replica_pool.add replica_config do
graceful_connection_for(replica_config)
end
end
end
Expand All @@ -311,13 +310,13 @@ def handling_an_all_execution(method_name)
yield
rescue ::Makara::Errors::NoConnectionsAvailable => e
if e.role == 'master'
# this means slave connections are good.
# this means replica connections are good.
return
end
@slave_pool.disabled = true
@replica_pool.disabled = true
yield
ensure
@slave_pool.disabled = false
@replica_pool.disabled = false
end


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
it "determines that \"#{sql}\" #{should_send_to_all_connections ? 'should' : 'should not'} be sent to all underlying connections" do
proxy = klass.new(config(1,1))
proxy.master_pool.connections.each{|con| expect(con).to receive(:execute).with(sql).once}
proxy.slave_pool.connections.each do |con|
proxy.replica_pool.connections.each do |con|
if should_send_to_all_connections
expect(con).to receive(:execute).with(sql).once
else
Expand Down
Loading