rails_failover 0.1.0 → 0.5.1

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.
@@ -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