rails_failover 0.1.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsFailover
4
- VERSION = "0.1.0"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+
5
+ # See https://github.com/redis/redis-rb/pull/908
6
+ class Redis::Client
7
+ def disconnect
8
+ if connected?
9
+ result = connection.disconnect
10
+ @connector.on_disconnect(self)
11
+ result
12
+ end
13
+ end
14
+ end
15
+
16
+ class Redis::Client::Connector
17
+ def on_disconnect(client)
18
+ end
19
+ end
data/makefile CHANGED
@@ -1,31 +1,21 @@
1
- PORT := 6381
2
- PID_PATH := /tmp/redis.pid
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
- default:
11
- @make -s all
4
+ all: redis
12
5
 
13
- all: start test stop
6
+ active_record: teardown_dummy_rails_server setup_dummy_rails_server test_active_record
14
7
 
15
- test:
16
- bundle exec rspec
8
+ test_active_record:
9
+ @ACTIVE_RECORD=1 bundle exec rspec --tag type:active_record ${RSPEC_PATH}
17
10
 
18
- start: start_master start_replica
19
- stop: stop_replica stop_master
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
- stop_master:
22
- @redis-cli -p ${PORT} shutdown
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
- start_master:
25
- @redis-server --daemonize yes --pidfile ${PID_PATH} --port ${PORT} --unixsocket ${SOCKET_PATH} --dbfilename ${DBFILENAME} --logfile /dev/null
17
+ stop_dummy_rails_server:
18
+ @kill -TERM $(shell cat spec/support/dummy_app/tmp/pids/unicorn.pid)
26
19
 
27
- stop_replica:
28
- @redis-cli -p ${replica_port} shutdown
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
@@ -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)
@@ -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 PostgreSQL and Redis}
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
- spec.add_dependency('redis', '~> 4')
24
+ ["activerecord", "railties"].each do |gem_name|
25
+ spec.add_dependency gem_name, "~> 6.0"
26
+ end
25
27
  end
@@ -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.1.0
4
+ version: 0.5.1
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-05-20 00:00:00.000000000 Z
11
+ date: 2020-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: redis
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '4'
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: '4'
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
@@ -42,14 +56,22 @@ files:
42
56
  - README.md
43
57
  - Rakefile
44
58
  - bin/console
59
+ - bin/rspec
45
60
  - bin/setup
46
61
  - lib/rails_failover.rb
62
+ - lib/rails_failover/active_record.rb
63
+ - lib/rails_failover/active_record/handler.rb
64
+ - lib/rails_failover/active_record/middleware.rb
65
+ - lib/rails_failover/active_record/railtie.rb
47
66
  - lib/rails_failover/redis.rb
48
67
  - lib/rails_failover/redis/connector.rb
49
- - lib/rails_failover/redis/fallback_handler.rb
68
+ - lib/rails_failover/redis/handler.rb
50
69
  - lib/rails_failover/version.rb
70
+ - lib/redis/patches/client.rb
51
71
  - makefile
72
+ - postgresql.mk
52
73
  - rails_failover.gemspec
74
+ - redis.mk
53
75
  homepage:
54
76
  licenses:
55
77
  - MIT
@@ -69,8 +91,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
91
  - !ruby/object:Gem::Version
70
92
  version: '0'
71
93
  requirements: []
72
- rubygems_version: 3.0.3
94
+ rubygems_version: 3.1.2
73
95
  signing_key:
74
96
  specification_version: 4
75
- summary: Failover for PostgreSQL and Redis
97
+ summary: Failover for ActiveRecord and Redis
76
98
  test_files: []
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'monitor'
3
- require 'singleton'
4
-
5
- module RailsFailover
6
- class Redis
7
- class FallbackHandler
8
- include Singleton
9
- include MonitorMixin
10
-
11
- MASTER_ROLE_STATUS = "role:master"
12
- MASTER_LOADED_STATUS = "loading:0"
13
-
14
- def initialize
15
- @master = true
16
- @clients = []
17
-
18
- super() # Monitor#initialize
19
- end
20
-
21
- def verify_master(options)
22
- mon_synchronize do
23
- return if @thread&.alive?
24
-
25
- RailsFailover::Redis.master_down_callbacks.each { |callback| callback.call }
26
-
27
- @thread = Thread.new do
28
- loop do
29
- thread = Thread.new { initiate_fallback_to_master(options) }
30
- thread.join
31
- break if mon_synchronize { @master }
32
- sleep (RailsFailover::Redis.verify_master_frequency_seconds + (Time.now.to_i % RailsFailover::Redis.verify_master_frequency_seconds))
33
- ensure
34
- thread.kill
35
- end
36
- end
37
- end
38
- end
39
-
40
- def initiate_fallback_to_master(options)
41
- info = nil
42
-
43
- begin
44
- master_client = ::Redis::Client.new(options.dup)
45
- log "#{log_prefix}: Checking connection to master server..."
46
- info = master_client.call([:info])
47
- rescue => e
48
- log "#{log_prefix}: Connection to Master server failed with '#{e.message}'"
49
- ensure
50
- master_client&.disconnect
51
- end
52
-
53
- if info && info.include?(MASTER_LOADED_STATUS) && info.include?(MASTER_ROLE_STATUS)
54
- self.master = true
55
- log "#{log_prefix}: Master server is active, disconnecting clients from replica"
56
- disconnect_clients
57
- RailsFailover::Redis.master_up_callbacks.each { |callback| callback.call }
58
- end
59
- end
60
-
61
- def register_client(client)
62
- mon_synchronize do
63
- @clients << client
64
- end
65
- end
66
-
67
- def master
68
- mon_synchronize { @master }
69
- end
70
-
71
- def master=(args)
72
- mon_synchronize { @master = args }
73
- end
74
-
75
- private
76
-
77
- def disconnect_clients
78
- mon_synchronize do
79
- @clients.each(&:disconnect)
80
- @clients.clear
81
- end
82
- end
83
-
84
- def log(message)
85
- if logger = RailsFailover::Redis.logger
86
- logger.warn(message)
87
- end
88
- end
89
-
90
- def log_prefix
91
- "#{self.class}"
92
- end
93
- end
94
- end
95
- end