rails_failover 0.2.0 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +6 -5
- data/Gemfile.lock +65 -2
- data/README.md +79 -10
- data/bin/console +2 -1
- data/bin/rspec +1 -1
- data/lib/rails_failover.rb +0 -2
- data/lib/rails_failover/active_record.rb +62 -0
- data/lib/rails_failover/active_record/handler.rb +140 -0
- data/lib/rails_failover/active_record/middleware.rb +103 -0
- data/lib/rails_failover/active_record/railtie.rb +58 -0
- data/lib/rails_failover/redis.rb +32 -15
- data/lib/rails_failover/redis/connector.rb +14 -9
- data/lib/rails_failover/redis/handler.rb +193 -0
- data/lib/rails_failover/version.rb +1 -1
- data/makefile +14 -24
- data/postgresql.mk +54 -0
- data/rails_failover.gemspec +4 -2
- data/redis.mk +28 -0
- metadata +29 -8
- data/lib/rails_failover/redis/failover_handler.rb +0 -109
data/makefile
CHANGED
@@ -1,31 +1,21 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
SOCKET_PATH := /tmp/redis.sock
|
4
|
-
DBFILENAME := master.rdb
|
5
|
-
replica_port := 6382
|
6
|
-
REPLICA_PID_PATH := /tmp/redis_replica.pid
|
7
|
-
REPLICA_SOCKET_PATH := /tmp/redis_replica.sock
|
8
|
-
REPLICA_DBFILENAME := replica.rdb
|
1
|
+
include postgresql.mk
|
2
|
+
include redis.mk
|
9
3
|
|
10
|
-
|
11
|
-
@make -s all
|
4
|
+
all: redis
|
12
5
|
|
13
|
-
|
6
|
+
active_record: teardown_dummy_rails_server setup_dummy_rails_server test_active_record
|
14
7
|
|
15
|
-
|
16
|
-
bundle exec rspec ${RSPEC_PATH}
|
8
|
+
test_active_record:
|
9
|
+
@ACTIVE_RECORD=1 bundle exec rspec --tag type:active_record ${RSPEC_PATH}
|
17
10
|
|
18
|
-
|
19
|
-
|
11
|
+
setup_dummy_rails_server:
|
12
|
+
@cd spec/support/dummy_app && bundle install --quiet --without test --without development && yarn install && RAILS_ENV=production $(BUNDLER_BIN) exec rails db:create db:migrate db:seed
|
20
13
|
|
21
|
-
|
22
|
-
@
|
14
|
+
start_dummy_rails_server:
|
15
|
+
@cd spec/support/dummy_app && BUNDLE_GEMFILE=Gemfile UNICORN_WORKERS=5 SECRET_KEY_BASE=somekey bundle exec unicorn -c config/unicorn.conf.rb -D -E production
|
23
16
|
|
24
|
-
|
25
|
-
@
|
17
|
+
stop_dummy_rails_server:
|
18
|
+
@kill -TERM $(shell cat spec/support/dummy_app/tmp/pids/unicorn.pid)
|
26
19
|
|
27
|
-
|
28
|
-
@
|
29
|
-
|
30
|
-
start_replica:
|
31
|
-
@redis-server --daemonize yes --pidfile ${REPLICA_PID_PATH} --port ${replica_port} --unixsocket ${REPLICA_SOCKET_PATH} --replicaof 127.0.0.1 ${PORT} --dbfilename ${REPLICA_DBFILENAME} --logfile /dev/null
|
20
|
+
teardown_dummy_rails_server:
|
21
|
+
@cd spec/support/dummy_app && DISABLE_DATABASE_ENVIRONMENT_CHECK=1 RAILS_ENV=production $(BUNDLER_BIN) exec rails db:drop
|
data/postgresql.mk
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
PG_BIN_DIR := $(shell pg_config --bindir)
|
2
|
+
PWD := $(shell pwd)
|
3
|
+
PG_PRIMARY_DIR := $(PWD)/tmp/primary
|
4
|
+
PG_PRIMARY_DATA_DIR := $(PG_PRIMARY_DIR)/data
|
5
|
+
PG_PRIMARY_RUN_DIR := $(PG_PRIMARY_DIR)/run
|
6
|
+
PG_REPLICA_DIR := $(PWD)/tmp/replica
|
7
|
+
PG_REPLICA_DATA_DIR := $(PG_REPLICA_DIR)/data
|
8
|
+
PG_REPLICA_RUN_DIR := $(PG_REPLICA_DIR)/run
|
9
|
+
PG_PRIMARY_PORT := 5434
|
10
|
+
PG_REPLICA_PORT := 5435
|
11
|
+
PG_REPLICATION_USER := replication
|
12
|
+
PG_REPLICATION_PASSWORD := password
|
13
|
+
PG_REPLICATION_SLOT_NAME := replication
|
14
|
+
|
15
|
+
setup_pg: init_primary start_pg_primary setup_primary init_replica stop_pg_primary
|
16
|
+
|
17
|
+
setup_primary:
|
18
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "CREATE USER $(PG_REPLICATION_USER) WITH REPLICATION ENCRYPTED PASSWORD '$(PG_REPLICATION_PASSWORD)';"
|
19
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "SELECT * FROM pg_create_physical_replication_slot('$(PG_REPLICATION_SLOT_NAME)');"
|
20
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "CREATE USER test;"
|
21
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "CREATE DATABASE test;"
|
22
|
+
|
23
|
+
start_pg: start_pg_primary start_pg_replica
|
24
|
+
|
25
|
+
stop_pg: stop_pg_replica stop_pg_primary
|
26
|
+
|
27
|
+
init_primary:
|
28
|
+
@mkdir -p $(PG_PRIMARY_DATA_DIR)
|
29
|
+
@mkdir -p $(PG_PRIMARY_RUN_DIR)
|
30
|
+
@$(PG_BIN_DIR)/initdb -E UTF8 -D $(PG_PRIMARY_DATA_DIR)
|
31
|
+
|
32
|
+
init_replica:
|
33
|
+
@mkdir -p $(PG_REPLICA_DATA_DIR)
|
34
|
+
@mkdir -p $(PG_REPLICA_RUN_DIR)
|
35
|
+
@PGPASSWORD=$(PG_REPLICATION_PASSWORD) $(PG_BIN_DIR)/pg_basebackup -D $(PG_REPLICA_DATA_DIR) -X stream -h $(PG_PRIMARY_RUN_DIR) -p $(PG_PRIMARY_PORT) -U $(PG_REPLICATION_USER) -w -R
|
36
|
+
@chmod 0700 $(PG_REPLICA_DATA_DIR)
|
37
|
+
|
38
|
+
start_pg_primary:
|
39
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_PRIMARY_DATA_DIR) -o "-p $(PG_PRIMARY_PORT)" -o "-k $(PG_PRIMARY_RUN_DIR)" start
|
40
|
+
|
41
|
+
start_pg_replica:
|
42
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_REPLICA_DATA_DIR) -o "-p $(PG_REPLICA_PORT)" -o "-k $(PG_REPLICA_RUN_DIR)" start
|
43
|
+
|
44
|
+
restart_pg_primary:
|
45
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_PRIMARY_DATA_DIR) -o "-p $(PG_PRIMARY_PORT)" -o "-k $(PG_PRIMARY_RUN_DIR)" restart
|
46
|
+
|
47
|
+
stop_pg_primary:
|
48
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_PRIMARY_DATA_DIR) -o "-p $(PG_PRIMARY_PORT)" -o "-k $(PG_PRIMARY_RUN_DIR)" stop
|
49
|
+
|
50
|
+
stop_pg_replica:
|
51
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_REPLICA_DATA_DIR) -o "-p $(PG_REPLICA_PORT)" -o "-k $(PG_REPLICA_RUN_DIR)" stop
|
52
|
+
|
53
|
+
cleanup_pg:
|
54
|
+
@rm -rf $(PG_PRIMARY_DIR) $(PG_REPLICA_DIR)
|
data/rails_failover.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["Alan Tan"]
|
9
9
|
spec.email = ["tgx@discourse.org"]
|
10
10
|
|
11
|
-
spec.summary = %q{Failover for
|
11
|
+
spec.summary = %q{Failover for ActiveRecord and Redis}
|
12
12
|
spec.license = "MIT"
|
13
13
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
14
14
|
|
@@ -21,5 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
|
24
|
+
["activerecord", "railties"].each do |gem_name|
|
25
|
+
spec.add_dependency gem_name, "~> 6.0"
|
26
|
+
end
|
25
27
|
end
|
data/redis.mk
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
REDIS_PORT := 6381
|
2
|
+
REDIS_PID_PATH := /tmp/redis.pid
|
3
|
+
REDIS_SOCKET_PATH := /tmp/redis.sock
|
4
|
+
REDIS_DBFILENAME := primary.rdb
|
5
|
+
REDIS_REPLICA_PORT := 6382
|
6
|
+
REDIS_REPLICA_PID_PATH := /tmp/redis_replica.pid
|
7
|
+
REDIS_REPLICA_SOCKET_PATH := /tmp/redis_replica.sock
|
8
|
+
REDIS_REPLICA_DBFILENAME := replica.rdb
|
9
|
+
|
10
|
+
redis: start_redis test_redis stop_redis
|
11
|
+
|
12
|
+
test_redis:
|
13
|
+
@REDIS=1 bundle exec rspec --tag type:redis ${RSPEC_PATH}
|
14
|
+
|
15
|
+
start_redis: start_redis_primary start_redis_replica
|
16
|
+
stop_redis: stop_redis_replica stop_redis_primary
|
17
|
+
|
18
|
+
stop_redis_primary:
|
19
|
+
@redis-cli -p ${REDIS_PORT} shutdown
|
20
|
+
|
21
|
+
start_redis_primary:
|
22
|
+
@redis-server --daemonize yes --pidfile ${REDIS_PID_PATH} --port ${REDIS_PORT} --unixsocket ${REDIS_SOCKET_PATH} --dbfilename ${REDIS_DBFILENAME} --logfile /dev/null
|
23
|
+
|
24
|
+
stop_redis_replica:
|
25
|
+
@redis-cli -p ${REDIS_REPLICA_PORT} shutdown
|
26
|
+
|
27
|
+
start_redis_replica:
|
28
|
+
@redis-server --daemonize yes --pidfile ${REDIS_REPLICA_PID_PATH} --port ${REDIS_REPLICA_PORT} --unixsocket ${REDIS_REPLICA_SOCKET_PATH} --slaveof 127.0.0.1 ${REDIS_PORT} --dbfilename ${REDIS_REPLICA_DBFILENAME} --logfile /dev/null
|
metadata
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_failover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2
|
4
|
+
version: 0.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alan Tan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: railties
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.0'
|
27
41
|
description:
|
28
42
|
email:
|
29
43
|
- tgx@discourse.org
|
@@ -35,6 +49,7 @@ files:
|
|
35
49
|
- ".rspec"
|
36
50
|
- ".rubocop.yml"
|
37
51
|
- ".travis.yml"
|
52
|
+
- CHANGELOG.md
|
38
53
|
- CODE_OF_CONDUCT.md
|
39
54
|
- Gemfile
|
40
55
|
- Gemfile.lock
|
@@ -45,13 +60,19 @@ files:
|
|
45
60
|
- bin/rspec
|
46
61
|
- bin/setup
|
47
62
|
- lib/rails_failover.rb
|
63
|
+
- lib/rails_failover/active_record.rb
|
64
|
+
- lib/rails_failover/active_record/handler.rb
|
65
|
+
- lib/rails_failover/active_record/middleware.rb
|
66
|
+
- lib/rails_failover/active_record/railtie.rb
|
48
67
|
- lib/rails_failover/redis.rb
|
49
68
|
- lib/rails_failover/redis/connector.rb
|
50
|
-
- lib/rails_failover/redis/
|
69
|
+
- lib/rails_failover/redis/handler.rb
|
51
70
|
- lib/rails_failover/version.rb
|
52
71
|
- lib/redis/patches/client.rb
|
53
72
|
- makefile
|
73
|
+
- postgresql.mk
|
54
74
|
- rails_failover.gemspec
|
75
|
+
- redis.mk
|
55
76
|
homepage:
|
56
77
|
licenses:
|
57
78
|
- MIT
|
@@ -71,8 +92,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
92
|
- !ruby/object:Gem::Version
|
72
93
|
version: '0'
|
73
94
|
requirements: []
|
74
|
-
rubygems_version: 3.
|
95
|
+
rubygems_version: 3.1.2
|
75
96
|
signing_key:
|
76
97
|
specification_version: 4
|
77
|
-
summary: Failover for
|
98
|
+
summary: Failover for ActiveRecord and Redis
|
78
99
|
test_files: []
|
@@ -1,109 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'monitor'
|
4
|
-
require 'singleton'
|
5
|
-
|
6
|
-
module RailsFailover
|
7
|
-
class Redis
|
8
|
-
class FailoverHandler
|
9
|
-
include Singleton
|
10
|
-
include MonitorMixin
|
11
|
-
|
12
|
-
MASTER_ROLE_STATUS = "role:master"
|
13
|
-
MASTER_LOADED_STATUS = "loading:0"
|
14
|
-
|
15
|
-
def initialize
|
16
|
-
@master = true
|
17
|
-
@clients = []
|
18
|
-
|
19
|
-
super() # Monitor#initialize
|
20
|
-
end
|
21
|
-
|
22
|
-
def verify_master(options)
|
23
|
-
mon_synchronize do
|
24
|
-
return if @thread&.alive?
|
25
|
-
|
26
|
-
self.master = false
|
27
|
-
disconnect_clients
|
28
|
-
RailsFailover::Redis.master_down_callbacks.each { |callback| callback.call }
|
29
|
-
|
30
|
-
@thread = Thread.new do
|
31
|
-
loop do
|
32
|
-
thread = Thread.new { initiate_fallback_to_master(options) }
|
33
|
-
thread.join
|
34
|
-
break if self.master
|
35
|
-
sleep (RailsFailover::Redis.verify_master_frequency_seconds + (Time.now.to_i % RailsFailover::Redis.verify_master_frequency_seconds))
|
36
|
-
ensure
|
37
|
-
thread.kill
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def initiate_fallback_to_master(options)
|
44
|
-
info = nil
|
45
|
-
|
46
|
-
begin
|
47
|
-
master_client = ::Redis::Client.new(options.dup)
|
48
|
-
log "#{log_prefix}: Checking connection to master server..."
|
49
|
-
info = master_client.call([:info])
|
50
|
-
rescue => e
|
51
|
-
log "#{log_prefix}: Connection to Master server failed with '#{e.message}'"
|
52
|
-
ensure
|
53
|
-
master_client&.disconnect
|
54
|
-
end
|
55
|
-
|
56
|
-
if info && info.include?(MASTER_LOADED_STATUS) && info.include?(MASTER_ROLE_STATUS)
|
57
|
-
self.master = true
|
58
|
-
log "#{log_prefix}: Master server is active, disconnecting clients from replica"
|
59
|
-
disconnect_clients
|
60
|
-
RailsFailover::Redis.master_up_callbacks.each { |callback| callback.call }
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def register_client(client)
|
65
|
-
mon_synchronize do
|
66
|
-
@clients << client
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def deregister_client(client)
|
71
|
-
mon_synchronize do
|
72
|
-
@clients.delete(client)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def clients
|
77
|
-
mon_synchronize { @clients }
|
78
|
-
end
|
79
|
-
|
80
|
-
def master
|
81
|
-
mon_synchronize { @master }
|
82
|
-
end
|
83
|
-
|
84
|
-
def master=(args)
|
85
|
-
mon_synchronize { @master = args }
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
def disconnect_clients
|
91
|
-
mon_synchronize do
|
92
|
-
@clients.dup.each do |c|
|
93
|
-
c.disconnect
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def log(message)
|
99
|
-
if logger = RailsFailover::Redis.logger
|
100
|
-
logger.warn(message)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def log_prefix
|
105
|
-
"#{self.class}"
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|