multi_dbs_load_balancer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +106 -0
- data/Rakefile +19 -0
- data/benchmark.rb +93 -0
- data/lib/multi_dbs_load_balancer/distribute_lock.rb +53 -0
- data/lib/multi_dbs_load_balancer/healthcheck.rb +36 -0
- data/lib/multi_dbs_load_balancer/load_balancer/algo.rb +64 -0
- data/lib/multi_dbs_load_balancer/load_balancer/errors.rb +3 -0
- data/lib/multi_dbs_load_balancer/load_balancer/hash.rb +37 -0
- data/lib/multi_dbs_load_balancer/load_balancer/least_connection.rb +114 -0
- data/lib/multi_dbs_load_balancer/load_balancer/least_response_time.rb +101 -0
- data/lib/multi_dbs_load_balancer/load_balancer/randomized.rb +19 -0
- data/lib/multi_dbs_load_balancer/load_balancer/round_robin.rb +52 -0
- data/lib/multi_dbs_load_balancer/load_balancer/weight_round_robin.rb +51 -0
- data/lib/multi_dbs_load_balancer/load_balancer.rb +53 -0
- data/lib/multi_dbs_load_balancer/min_heap.rb +90 -0
- data/lib/multi_dbs_load_balancer/redis_lua.rb +12 -0
- data/lib/multi_dbs_load_balancer/version.rb +5 -0
- data/lib/multi_dbs_load_balancer.rb +17 -0
- data/lib/rails/generators/multi_dbs_load_balancer/install_generator.rb +12 -0
- data/lib/rails/generators/multi_dbs_load_balancer/templates/initializer.rb +19 -0
- data/multi_dbs_load_balancer.gemspec +36 -0
- data/sig/multi_dbs_load_balancer.rbs +4 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 599c6e5a768320312bf00d07478354a250bd3f167b0dfc08071708864d6e0513
|
4
|
+
data.tar.gz: d0462ea28e57473be4aa5af57e9ca80359f2c3c0715ebffe02f60a400cd9f017
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 824e76ac8571b4499720c6aecc38b9987fb8550fd12e40610a8388bf907f85c572394a5bea86baec8433ce13c346cd4ff0b8fccad43af3d50c959106c4fb87c9
|
7
|
+
data.tar.gz: a86aef07d4fca8c20083193657cb4fa4e3f7727e69eb8cbfb0c5e7d47741873374aab566e3293994107cc398e3a13387ac3dc440242acccec0fdcf3dd2603bb8
|
data/.rspec
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 theforestvn88
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# MultiDbsLoadBalancer
|
2
|
+
|
3
|
+
Allow to setup load balancers sit on top of [rails multi-databases](https://guides.rubyonrails.org/active_record_multiple_databases.html).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem "multi_dbs_load_balancer"
|
9
|
+
|
10
|
+
$ bundle install
|
11
|
+
$ rails g multi_dbs_load_balancer:install
|
12
|
+
```
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Declaring load balancers
|
17
|
+
```ruby
|
18
|
+
# config/initializers/multi_dbs_load_balancer.rb
|
19
|
+
load_balancer.db_down_time = 120
|
20
|
+
load_balancer.redis_down_time = 120
|
21
|
+
load_balancer.init :rr_load_balancer,
|
22
|
+
[
|
23
|
+
{role: :reading1},
|
24
|
+
{role: :reading2},
|
25
|
+
{role: :reading3},
|
26
|
+
],
|
27
|
+
algorithm: :round_robin,
|
28
|
+
redis: Redis.new(...)
|
29
|
+
```
|
30
|
+
|
31
|
+
Now you could use them on controllers/services ...
|
32
|
+
```ruby
|
33
|
+
# products_controller.rb
|
34
|
+
def index
|
35
|
+
@products = ActiveRecord::Base.connected_through(:rr_load_balancer) { Product.all }
|
36
|
+
# alias methods: connected_by, connected_through_load_balancer
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
You could also create and use a Middleware to wrap load balancer base on the request, for example:
|
41
|
+
```ruby
|
42
|
+
class LoadBalancerMiddleware
|
43
|
+
def initialize(app)
|
44
|
+
@app = app
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(env)
|
48
|
+
request = ActionDispatch::Request.new(env)
|
49
|
+
if is_something?(request)
|
50
|
+
ActiveRecord::Base.connected_through(:rr_load_balancer) do
|
51
|
+
@app.call(env)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
@app.call(env)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private def is_something?(request)
|
59
|
+
# for example: check if reading request
|
60
|
+
request.get? || request.head?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Rails.application.config.app_middleware.use LoadBalancerMiddleware
|
65
|
+
```
|
66
|
+
|
67
|
+
## Notes
|
68
|
+
|
69
|
+
- Support algorithms: `round_robin`, `weight_round_robin`, `least_connection`, `least_response_time`, `hash`, `randomized`
|
70
|
+
|
71
|
+
- Distribute
|
72
|
+
|
73
|
+
If you launch multiple servers then you wish your load balancers will share states between servers,
|
74
|
+
there're 3 algorithms that will do that if you provide a redis server:
|
75
|
+
|
76
|
+
+ `round_robin` will share the current database
|
77
|
+
|
78
|
+
+ `least_connection` and `least_response_time` will share the sorted list of databases
|
79
|
+
|
80
|
+
Other algorithms are independent on each server, so you don't need to provide a redis server for them.
|
81
|
+
|
82
|
+
- Fail-over
|
83
|
+
|
84
|
+
All load balancers here are passive, they don't track database connections or redis connections.
|
85
|
+
|
86
|
+
Whenever it could not connect to a database, it mark that database have down for `db_down_time` seconds and ignore it on the next round,
|
87
|
+
and try to connect to the next available database.
|
88
|
+
|
89
|
+
After `db_down_time` seconds, the load balancer will try to connect this database again.
|
90
|
+
|
91
|
+
Whenever the redis-server has down (or you dont setup redis), distribute load balancers will process offline on each server until redis come back.
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
## Development
|
96
|
+
|
97
|
+
run test
|
98
|
+
```ruby
|
99
|
+
rake setup_db
|
100
|
+
rake spec
|
101
|
+
```
|
102
|
+
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rails_dbs_load_balancer.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
task :setup_db do
|
7
|
+
p "setup db ..."
|
8
|
+
`sqlite3 spec/dummy/db/primary.sqlite3 'CREATE TABLE IF NOT EXISTS developers (name VARCHAR (255))'`
|
9
|
+
`sqlite3 spec/dummy/db/primary_replica1.sqlite3 'CREATE TABLE IF NOT EXISTS developers (name VARCHAR (255))'`
|
10
|
+
`sqlite3 spec/dummy/db/primary_replica2.sqlite3 'CREATE TABLE IF NOT EXISTS developers (name VARCHAR (255))'`
|
11
|
+
`sqlite3 spec/dummy/db/primary_replica3.sqlite3 'CREATE TABLE IF NOT EXISTS developers (name VARCHAR (255))'`
|
12
|
+
`sqlite3 spec/dummy/db/primary_replica4.sqlite3 'CREATE TABLE IF NOT EXISTS developers (name VARCHAR (255))'`
|
13
|
+
`sqlite3 spec/dummy/db/primary_replica5.sqlite3 'CREATE TABLE IF NOT EXISTS developers (name VARCHAR (255))'`
|
14
|
+
`sqlite3 spec/dummy/db/primary_replica6.sqlite3 'CREATE TABLE IF NOT EXISTS developers (name VARCHAR (255))'`
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec::Core::RakeTask.new(:spec)
|
18
|
+
|
19
|
+
task default: [:setup_db, :spec]
|
data/benchmark.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'benchmark/ips'
|
2
|
+
require_relative "./lib/multi_dbs_load_balancer"
|
3
|
+
require_relative "./spec/dummy/models/developer"
|
4
|
+
|
5
|
+
Benchmark.ips do |bm|
|
6
|
+
bm.report("ActiveRecord#connected_to") do
|
7
|
+
threads = []
|
8
|
+
[:reading1, :reading2, :reading3, :reading4, :reading5, :reading6].each do |db|
|
9
|
+
10.times do |i|
|
10
|
+
threads << Thread.new do
|
11
|
+
ActiveRecord::Base.connected_to(role: db) do
|
12
|
+
result = Developer.all
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
threads.map(&:join)
|
18
|
+
end
|
19
|
+
|
20
|
+
bm.report("connect through round robin load balancer") do
|
21
|
+
threads = []
|
22
|
+
60.times do |i|
|
23
|
+
threads << Thread.new do
|
24
|
+
Developer.connected_through_load_balancer(:rr) do
|
25
|
+
result = Developer.all
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
threads.map(&:join)
|
30
|
+
end
|
31
|
+
|
32
|
+
bm.report("connect through least connection load balancer") do
|
33
|
+
threads = []
|
34
|
+
60.times do |i|
|
35
|
+
threads << Thread.new do
|
36
|
+
Developer.connected_through_load_balancer(:lc) do
|
37
|
+
result = Developer.all
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
threads.map(&:join)
|
42
|
+
end
|
43
|
+
|
44
|
+
bm.report("connect through least response time load balancer") do
|
45
|
+
threads = []
|
46
|
+
60.times do |i|
|
47
|
+
threads << Thread.new do
|
48
|
+
Developer.connected_through_load_balancer(:lrt) do
|
49
|
+
result = Developer.all
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
threads.map(&:join)
|
54
|
+
end
|
55
|
+
|
56
|
+
bm.report("connect through weight round robin load balancer") do
|
57
|
+
threads = []
|
58
|
+
60.times do |i|
|
59
|
+
threads << Thread.new do
|
60
|
+
Developer.connected_through_load_balancer(:wrr) do
|
61
|
+
result = Developer.all
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
threads.map(&:join)
|
66
|
+
end
|
67
|
+
|
68
|
+
bm.report("connect through hash load balancer") do
|
69
|
+
threads = []
|
70
|
+
60.times do |i|
|
71
|
+
threads << Thread.new do
|
72
|
+
Developer.connected_through_load_balancer(:hash, source: "197.168.1.1") do
|
73
|
+
result = Developer.all
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
threads.map(&:join)
|
78
|
+
end
|
79
|
+
|
80
|
+
bm.report("connect through randomized load balancer") do
|
81
|
+
threads = []
|
82
|
+
60.times do |i|
|
83
|
+
threads << Thread.new do
|
84
|
+
Developer.connected_through_load_balancer(:randomized) do
|
85
|
+
result = Developer.all
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
threads.map(&:join)
|
90
|
+
end
|
91
|
+
|
92
|
+
bm.compare!
|
93
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class DistributeLock
|
2
|
+
include RedisLua
|
3
|
+
attr_reader :redis
|
4
|
+
|
5
|
+
def initialize(redis)
|
6
|
+
@redis = redis
|
7
|
+
end
|
8
|
+
|
9
|
+
def synchronize(name, lock_time = 3600)
|
10
|
+
return yield if @redis.nil?
|
11
|
+
|
12
|
+
lock_name = "#{name}:lock"
|
13
|
+
time_lock = eval_lua_script(LOCK_SCRIPT, LOCK_SCRIPT_SHA1, [lock_name], [lock_time])
|
14
|
+
return if time_lock.nil?
|
15
|
+
|
16
|
+
begin
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
eval_lua_script(UNLOCK_SCRIPT, UNLOCK_SCRIPT_SHA1, [name], [time_lock.to_s])
|
20
|
+
end
|
21
|
+
rescue
|
22
|
+
yield
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
LOCK_SCRIPT = <<~LUA
|
28
|
+
local now = redis.call("time")[1]
|
29
|
+
local expire_time = now + ARGV[1]
|
30
|
+
local current_expire_time = redis.call("get", KEYS[1])
|
31
|
+
|
32
|
+
if current_expire_time and tonumber(now) <= tonumber(current_expire_time) then
|
33
|
+
return nil
|
34
|
+
else
|
35
|
+
local result = redis.call("setex", KEYS[1], ARGV[1] + 1, tostring(expire_time))
|
36
|
+
return expire_time
|
37
|
+
end
|
38
|
+
LUA
|
39
|
+
LOCK_SCRIPT_SHA1 = Digest::SHA1.hexdigest LOCK_SCRIPT
|
40
|
+
|
41
|
+
UNLOCK_SCRIPT = <<~LUA
|
42
|
+
local current_expire_time = redis.call("get", KEYS[1])
|
43
|
+
|
44
|
+
if current_expire_time == ARGV[1] then
|
45
|
+
local result = redis.call("del", KEYS[1])
|
46
|
+
return result ~= nil
|
47
|
+
else
|
48
|
+
return false
|
49
|
+
end
|
50
|
+
LUA
|
51
|
+
UNLOCK_SCRIPT_SHA1 = Digest::SHA1.hexdigest UNLOCK_SCRIPT
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Healthcheck
|
2
|
+
cattr_reader :db_down_times, :redis_down_times
|
3
|
+
|
4
|
+
def mark_redis_down
|
5
|
+
set_redis_down_time(Time.now.to_i + LoadBalancer.redis_down_time)
|
6
|
+
end
|
7
|
+
|
8
|
+
def redis_available?
|
9
|
+
@redis.present? && redis_down_time < Time.now.to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def mark_db_down(db_index)
|
13
|
+
db_down_times[db_index] = Time.now.to_i + LoadBalancer.db_down_time
|
14
|
+
end
|
15
|
+
|
16
|
+
def db_available?(db_index)
|
17
|
+
db_down_times[db_index] < Time.now.to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
private def db_down_times
|
21
|
+
@@db_down_times ||= {}
|
22
|
+
@@db_down_times["#{@key}:dt"] ||= Hash.new(0)
|
23
|
+
end
|
24
|
+
|
25
|
+
private def redis_down_times
|
26
|
+
@@redis_down_times ||= Hash.new(0)
|
27
|
+
end
|
28
|
+
|
29
|
+
private def redis_down_time
|
30
|
+
redis_down_times["#{@key}:dt"]
|
31
|
+
end
|
32
|
+
|
33
|
+
private def set_redis_down_time(t)
|
34
|
+
redis_down_times["#{@key}:dt"] = t
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module LoadBalancer
|
2
|
+
class Algo
|
3
|
+
include RedisLua
|
4
|
+
include Healthcheck
|
5
|
+
attr_reader :database_configs, :redis, :key
|
6
|
+
|
7
|
+
def initialize(database_configs, redis:, key:)
|
8
|
+
@database_configs = database_configs
|
9
|
+
@redis = redis
|
10
|
+
@key = key
|
11
|
+
end
|
12
|
+
|
13
|
+
def warm_up
|
14
|
+
end
|
15
|
+
|
16
|
+
def next_db(**options)
|
17
|
+
raise NotImplementedError, ""
|
18
|
+
end
|
19
|
+
|
20
|
+
def after_connected
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_executed
|
24
|
+
end
|
25
|
+
|
26
|
+
def connected_to_next_db(**options, &blk)
|
27
|
+
candidate_db, db_index = next_db(**options)
|
28
|
+
raise LoadBalancer::AllDatabasesHaveDown if candidate_db.nil?
|
29
|
+
|
30
|
+
if options.has_key?(:bases)
|
31
|
+
::ActiveRecord::Base.connected_to_many(*options[:bases], **candidate_db.slice(:shard, :role), prevent_writes: options[:prevent_writes]) do
|
32
|
+
after_connected
|
33
|
+
blk.call
|
34
|
+
end
|
35
|
+
else
|
36
|
+
::ActiveRecord::Base.connected_to(**candidate_db.slice(:shard, :role), prevent_writes: options[:prevent_writes]) do
|
37
|
+
after_connected
|
38
|
+
blk.call
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
42
|
+
mark_db_down(db_index)
|
43
|
+
@should_retry = true
|
44
|
+
ensure
|
45
|
+
after_executed
|
46
|
+
if @should_retry
|
47
|
+
@should_retry = false
|
48
|
+
connected_to_next_db(**options, &blk)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def fail_over(next_choices)
|
53
|
+
candidate = next_choices.find do |i|
|
54
|
+
db_available?(i)
|
55
|
+
end
|
56
|
+
|
57
|
+
if candidate
|
58
|
+
[@database_configs[candidate], candidate]
|
59
|
+
else
|
60
|
+
[nil, -1]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative "./algo"
|
2
|
+
|
3
|
+
module LoadBalancer
|
4
|
+
class Hash < Algo
|
5
|
+
def next_db(**options)
|
6
|
+
db_index = hash_to_index(**options)
|
7
|
+
return @database_configs[db_index], db_index if db_available?(db_index)
|
8
|
+
|
9
|
+
# fail over
|
10
|
+
next_dbs = (db_index+1...db_index+@database_configs.size).map { |i| i % @database_configs.size }
|
11
|
+
fail_over(next_dbs)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def hash_to_index(**options)
|
17
|
+
rand(0...@database_configs.size) if options[:source].nil?
|
18
|
+
|
19
|
+
h = options[:hash_func]&.respond_to?(:call) ? options[:hash_func].call(options[:source]) : hashcode(options[:source])
|
20
|
+
h % @database_configs.size
|
21
|
+
end
|
22
|
+
|
23
|
+
def hashcode(source)
|
24
|
+
# NOTE:
|
25
|
+
# From Ruby 2.0 initialize MurmurHash using a random seed value which is reinitialized each time you restart Ruby.
|
26
|
+
# So `source.hash` will not be deterministic across servers.
|
27
|
+
# Therefore, this method uses java hashcode algorithm (Apache Harmony) instead.
|
28
|
+
h = 0
|
29
|
+
multipler = 1
|
30
|
+
(source.size-1).downto(0) do |i|
|
31
|
+
h = source[i].ord * multipler
|
32
|
+
multipler = (multipler << 5) - multipler
|
33
|
+
end
|
34
|
+
h
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require_relative "../min_heap"
|
2
|
+
|
3
|
+
module LoadBalancer
|
4
|
+
class LeastConnection < Algo
|
5
|
+
cattr_reader :mutexes, default: {}
|
6
|
+
cattr_reader :leasts, default: {}
|
7
|
+
|
8
|
+
def warm_up
|
9
|
+
pq_mutex.synchronize do
|
10
|
+
begin
|
11
|
+
unless @redis.exists?(db_conns_pq_key)
|
12
|
+
@database_configs.size.times do |i|
|
13
|
+
@redis.zincrby(db_conns_pq_key, 1, i)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
rescue => e
|
17
|
+
# p e
|
18
|
+
ensure
|
19
|
+
return if @@leasts.has_key?(db_conns_pq_key)
|
20
|
+
|
21
|
+
@@leasts[db_conns_pq_key] = MinHeap.new
|
22
|
+
@database_configs.size.times do |i|
|
23
|
+
@@leasts[db_conns_pq_key].push([1, i])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def next_db(**options)
|
30
|
+
@least_db_index = top_least
|
31
|
+
return @database_configs[@least_db_index], @least_db_index if db_available?(@least_db_index)
|
32
|
+
|
33
|
+
# fail over
|
34
|
+
fail_over(next_dbs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_connected
|
38
|
+
increase
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_executed
|
42
|
+
decrease
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# CAS_TOP_SCRIPT = <<~LUA
|
48
|
+
# local top = unpack(redis.call("zrange", KEYS[1], 0, 0))
|
49
|
+
# redis.call("zincrby", KEYS[1], 1, top)
|
50
|
+
# return top
|
51
|
+
# LUA
|
52
|
+
# CAS_TOP_SCRIPT_SHA1 = ::Digest::SHA1.hexdigest CAS_TOP_SCRIPT
|
53
|
+
|
54
|
+
def db_conns_pq_key
|
55
|
+
"#{@key}:lc:pq"
|
56
|
+
end
|
57
|
+
|
58
|
+
def pq_mutex
|
59
|
+
@@mutexes[db_conns_pq_key] ||= Mutex.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def top_least
|
63
|
+
return local_least(:extract) unless redis_available?
|
64
|
+
# eval_lua_script(CAS_TOP_SCRIPT, CAS_TOP_SCRIPT_SHA1, [db_conns_pq_key], [])
|
65
|
+
@redis.zrange(db_conns_pq_key, 0, 0).first.to_i
|
66
|
+
rescue => error
|
67
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
68
|
+
local_least(:extract)
|
69
|
+
end
|
70
|
+
|
71
|
+
def local_least(action = :extract)
|
72
|
+
pq_mutex.synchronize do
|
73
|
+
case action
|
74
|
+
when :extract
|
75
|
+
count, x = @@leasts[db_conns_pq_key].peak
|
76
|
+
x
|
77
|
+
when :decrease
|
78
|
+
@@leasts[db_conns_pq_key].decrease(@least_db_index, 1) if @least_db_index
|
79
|
+
when :increase
|
80
|
+
@@leasts[db_conns_pq_key].increase(@least_db_index, 1) if @least_db_index
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def decrease
|
86
|
+
return unless @least_db_index
|
87
|
+
return local_least(:decrease) unless redis_available?
|
88
|
+
|
89
|
+
@redis.zincrby(db_conns_pq_key, -1, @least_db_index)
|
90
|
+
rescue => error
|
91
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
92
|
+
local_least(:decrease)
|
93
|
+
end
|
94
|
+
|
95
|
+
def increase
|
96
|
+
return unless @least_db_index
|
97
|
+
return local_least(:increase) unless redis_available?
|
98
|
+
|
99
|
+
@redis.zincrby(db_conns_pq_key, 1, @least_db_index)
|
100
|
+
rescue => error
|
101
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
102
|
+
local_least(:increase)
|
103
|
+
end
|
104
|
+
|
105
|
+
def next_dbs
|
106
|
+
return @@leasts[db_conns_pq_key].order unless redis_available?
|
107
|
+
|
108
|
+
@redis.zrange(db_conns_pq_key, 0, -1).map(&:to_i)
|
109
|
+
rescue => error
|
110
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
111
|
+
@@leasts[db_conns_pq_key].order
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative "../min_heap"
|
2
|
+
|
3
|
+
module LoadBalancer
|
4
|
+
class LeastResponseTime < Algo
|
5
|
+
cattr_reader :mutexes, default: {}
|
6
|
+
cattr_reader :response_times, default: {}
|
7
|
+
|
8
|
+
def warm_up
|
9
|
+
pq_mutex.synchronize do
|
10
|
+
begin
|
11
|
+
unless @redis.exists?(db_conns_pq_key)
|
12
|
+
@database_configs.size.times do |i|
|
13
|
+
@redis.zincrby(db_conns_pq_key, 0, i)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
rescue => error
|
17
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
18
|
+
ensure
|
19
|
+
return if @@response_times.has_key?(db_conns_pq_key)
|
20
|
+
|
21
|
+
@@response_times[db_conns_pq_key] = MinHeap.new
|
22
|
+
@database_configs.size.times do |i|
|
23
|
+
@@response_times[db_conns_pq_key].push([0, i])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def next_db(**options)
|
30
|
+
@least_db_index = top_least.to_i
|
31
|
+
return @database_configs[@least_db_index], @least_db_index if db_available?(@least_db_index)
|
32
|
+
|
33
|
+
# fail over
|
34
|
+
fail_over(next_dbs)
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_connected
|
38
|
+
@start_time = Time.now
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_executed
|
42
|
+
update_response_time(Time.now - @start_time) if @start_time
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
CAS_TOP_SCRIPT = <<~LUA
|
48
|
+
local top = unpack(redis.call("zrange", KEYS[1], 0, 0))
|
49
|
+
redis.call("zincrby", KEYS[1], 0.1, top)
|
50
|
+
return top
|
51
|
+
LUA
|
52
|
+
CAS_TOP_SCRIPT_SHA1 = ::Digest::SHA1.hexdigest CAS_TOP_SCRIPT
|
53
|
+
|
54
|
+
def db_conns_pq_key
|
55
|
+
"#{@key}:lrt:pq"
|
56
|
+
end
|
57
|
+
|
58
|
+
def pq_mutex
|
59
|
+
@@mutexes[db_conns_pq_key] ||= Mutex.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def top_least
|
63
|
+
return local_least unless redis_available?
|
64
|
+
|
65
|
+
eval_lua_script(CAS_TOP_SCRIPT, CAS_TOP_SCRIPT_SHA1, [db_conns_pq_key], [])
|
66
|
+
rescue => error
|
67
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
68
|
+
local_least
|
69
|
+
end
|
70
|
+
|
71
|
+
def local_least(action = :extract, **options)
|
72
|
+
pq_mutex.synchronize do
|
73
|
+
case action
|
74
|
+
when :extract
|
75
|
+
t, x = @@response_times[db_conns_pq_key].peak
|
76
|
+
x
|
77
|
+
when :update_response_time
|
78
|
+
@@response_times[db_conns_pq_key].replace(@least_db_index, options[:time]) if @least_db_index
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def update_response_time(t)
|
84
|
+
return local_least(:update_response_time, time: t) unless redis_available?
|
85
|
+
|
86
|
+
@redis.zadd(db_conns_pq_key, t, @least_db_index)
|
87
|
+
rescue => error
|
88
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
89
|
+
local_least(:update_response_time, time: t)
|
90
|
+
end
|
91
|
+
|
92
|
+
def next_dbs
|
93
|
+
return @@response_times[db_conns_pq_key].order unless redis_available?
|
94
|
+
|
95
|
+
@redis.zrange(db_conns_pq_key, 0, -1).map(&:to_i)
|
96
|
+
rescue => error
|
97
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
98
|
+
@@response_times[db_conns_pq_key].order
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "./algo"
|
2
|
+
|
3
|
+
module LoadBalancer
|
4
|
+
class Randomized < Algo
|
5
|
+
def next_db(**options)
|
6
|
+
r = random_index
|
7
|
+
return @database_configs[r], r if db_available?(r)
|
8
|
+
|
9
|
+
next_dbs = (r+1...r+@database_configs.size).map { |i| i % @database_configs.size }
|
10
|
+
fail_over(next_dbs)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def random_index
|
16
|
+
rand(0...@database_configs.size)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative "./algo"
|
2
|
+
|
3
|
+
module LoadBalancer
|
4
|
+
class RoundRobin < Algo
|
5
|
+
cattr_accessor :currents
|
6
|
+
|
7
|
+
def warm_up
|
8
|
+
@@currents ||= ::Hash.new(0)
|
9
|
+
end
|
10
|
+
|
11
|
+
def next_db(**options)
|
12
|
+
@current = cas_current
|
13
|
+
return @database_configs[@current], @current if db_available?(@current)
|
14
|
+
|
15
|
+
next_dbs = (@current+1...@current+@database_configs.size).map { |i| i % @database_configs.size }
|
16
|
+
fail_over(next_dbs)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
CAS_NEXT_SCRIPT = <<~LUA
|
22
|
+
local curr = redis.call("get", KEYS[1])
|
23
|
+
if curr then
|
24
|
+
local next = (tonumber(curr) + 1) % ARGV[1]
|
25
|
+
redis.call("set", KEYS[1], next)
|
26
|
+
return next
|
27
|
+
else
|
28
|
+
redis.call("set", KEYS[1], 0)
|
29
|
+
return 0
|
30
|
+
end
|
31
|
+
LUA
|
32
|
+
CAS_NEXT_SCRIPT_SHA1 = ::Digest::SHA1.hexdigest CAS_NEXT_SCRIPT
|
33
|
+
|
34
|
+
def current_cached_key
|
35
|
+
"#{@key}:rr:current"
|
36
|
+
end
|
37
|
+
|
38
|
+
def cas_current
|
39
|
+
return local_current unless redis_available?
|
40
|
+
eval_lua_script(CAS_NEXT_SCRIPT, CAS_NEXT_SCRIPT_SHA1, [current_cached_key], [@database_configs.size])
|
41
|
+
rescue => error
|
42
|
+
# in case of redis failed
|
43
|
+
mark_redis_down if error.is_a?(Redis::CannotConnectError)
|
44
|
+
# random local server current
|
45
|
+
@@currents[current_cached_key] = rand(0...@database_configs.size)
|
46
|
+
end
|
47
|
+
|
48
|
+
def local_current
|
49
|
+
@@currents[current_cached_key] = (@@currents[current_cached_key] + 1) % @database_configs.size
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative "./algo"
|
2
|
+
|
3
|
+
module LoadBalancer
|
4
|
+
class WeightRoundRobin < Algo
|
5
|
+
cattr_reader :mutexes, default: {}
|
6
|
+
cattr_reader :cum_weights, default: {}
|
7
|
+
cattr_reader :weight_sums, default: ::Hash.new(0)
|
8
|
+
|
9
|
+
def warm_up
|
10
|
+
(@@mutexes[weights_key] ||= Mutex.new).synchronize do
|
11
|
+
return if @@cum_weights.has_key?(weights_key) or @database_configs.empty?
|
12
|
+
|
13
|
+
@@cum_weights[weights_key] = [@database_configs[0][:weight]]
|
14
|
+
(1...@database_configs.size).each do |i|
|
15
|
+
@@weight_sums[weight_sum_key] += @database_configs[i][:weight]
|
16
|
+
@@cum_weights[weights_key][i] = @@cum_weights[weights_key][i-1] + @database_configs[i][:weight]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def next_db(**options)
|
22
|
+
db_index = pick_db_by_random_weight
|
23
|
+
return @database_configs[db_index], db_index if db_available?(db_index)
|
24
|
+
|
25
|
+
# fail over
|
26
|
+
next_dbs = (db_index+1...db_index+@database_configs.size).map { |i| i % @database_configs.size }
|
27
|
+
fail_over(next_dbs)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def weights_key
|
33
|
+
"#{@key}:weights"
|
34
|
+
end
|
35
|
+
|
36
|
+
def weight_sum_key
|
37
|
+
"#{@key}:weight_sum"
|
38
|
+
end
|
39
|
+
|
40
|
+
def random_weight
|
41
|
+
rand(@@weight_sums[weight_sum_key]-1)
|
42
|
+
end
|
43
|
+
|
44
|
+
def pick_db_by_random_weight
|
45
|
+
rw = random_weight
|
46
|
+
@@cum_weights[weights_key].find_index do |weight|
|
47
|
+
weight > rw
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require 'digest'
|
3
|
+
require_relative "./redis_lua"
|
4
|
+
require_relative "./healthcheck"
|
5
|
+
require_relative "./distribute_lock"
|
6
|
+
require_relative "./load_balancer/errors"
|
7
|
+
require_relative "./load_balancer/round_robin"
|
8
|
+
require_relative "./load_balancer/least_connection"
|
9
|
+
require_relative "./load_balancer/weight_round_robin"
|
10
|
+
require_relative "./load_balancer/hash"
|
11
|
+
require_relative "./load_balancer/randomized"
|
12
|
+
require_relative "./load_balancer/least_response_time"
|
13
|
+
|
14
|
+
module LoadBalancer
|
15
|
+
extend ::ActiveSupport::Concern
|
16
|
+
|
17
|
+
mattr_accessor :db_down_time, default: 120
|
18
|
+
mattr_accessor :redis_down_time, default: 120
|
19
|
+
mattr_reader :lb, default: {}
|
20
|
+
|
21
|
+
def init(name, db_configs, algorithm: :round_robin, redis: nil)
|
22
|
+
lb_algo_clazz = "LoadBalancer::#{algorithm.to_s.classify}".constantize
|
23
|
+
LoadBalancer.lb[name] = {
|
24
|
+
clazz: lb_algo_clazz,
|
25
|
+
db_configs: db_configs,
|
26
|
+
redis: redis,
|
27
|
+
key: name
|
28
|
+
}
|
29
|
+
|
30
|
+
DistributeLock.new(redis).synchronize(name) do
|
31
|
+
lb = lb_algo_clazz.new(db_configs, redis: redis, key: name)
|
32
|
+
lb.warm_up
|
33
|
+
end
|
34
|
+
end
|
35
|
+
module_function :init
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
def load_balancing(name, db_configs, algorithm: :round_robin, redis: nil)
|
39
|
+
LoadBalancer.init(name, db_configs, algorithm: algorithm, redis: redis)
|
40
|
+
end
|
41
|
+
|
42
|
+
def connected_through_load_balancer(name, **options, &blk)
|
43
|
+
raise ArgumentError, "not found #{name} load balancer" unless LoadBalancer.lb.has_key?(name)
|
44
|
+
|
45
|
+
configs = LoadBalancer.lb[name]
|
46
|
+
lb = configs[:clazz].new(configs[:db_configs], redis: configs[:redis], key: configs[:key])
|
47
|
+
lb.connected_to_next_db(**options, &blk)
|
48
|
+
end
|
49
|
+
alias_method :connected_through, :connected_through_load_balancer
|
50
|
+
alias_method :connected_by, :connected_through_load_balancer
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class MinHeap
|
2
|
+
def initialize(comparator = lambda { |x, y| x <=> y })
|
3
|
+
@comparator = comparator
|
4
|
+
@items = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def push(x)
|
8
|
+
@items.push(x)
|
9
|
+
swim_up(@items.size-1)
|
10
|
+
end
|
11
|
+
|
12
|
+
def pop
|
13
|
+
@items[0], @items[@items.size-1] = @items[@items.size-1], @items[0]
|
14
|
+
@items.pop.tap { sink_down(0) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def peak
|
18
|
+
@items[0]
|
19
|
+
end
|
20
|
+
|
21
|
+
def update(item_index)
|
22
|
+
return if item_index < 0 || item_index >= @items.size
|
23
|
+
|
24
|
+
swim_up(item_index)
|
25
|
+
sink_down(item_index)
|
26
|
+
end
|
27
|
+
|
28
|
+
def decrease(index, delta)
|
29
|
+
item_index = find_item_index(index)
|
30
|
+
@items[item_index][0] -= delta
|
31
|
+
update(item_index)
|
32
|
+
end
|
33
|
+
|
34
|
+
def increase(index, delta)
|
35
|
+
item_index = find_item_index(index)
|
36
|
+
@items[item_index][0] += delta
|
37
|
+
update(item_index)
|
38
|
+
end
|
39
|
+
|
40
|
+
def replace(index, val)
|
41
|
+
item_index = find_item_index(index)
|
42
|
+
@items[item_index][0] = val
|
43
|
+
update(item_index)
|
44
|
+
end
|
45
|
+
|
46
|
+
# def find_item(index)
|
47
|
+
# @items.find { |item| item.last == index }
|
48
|
+
# end
|
49
|
+
|
50
|
+
def find_item_index(index)
|
51
|
+
@items.find_index { |item| item.last == index }
|
52
|
+
end
|
53
|
+
|
54
|
+
def empty?
|
55
|
+
@items.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def order
|
59
|
+
@items.sort.map(&:last)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def parent(i)
|
65
|
+
((i-1)/2).floor
|
66
|
+
end
|
67
|
+
|
68
|
+
def left(i)
|
69
|
+
2*i + 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def swim_up(i)
|
73
|
+
pi = parent(i)
|
74
|
+
if pi >= 0 && @comparator.call(@items[i], @items[pi]) <= 0
|
75
|
+
@items[pi], @items[i] = @items[i], @items[pi]
|
76
|
+
swim_up(pi)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def sink_down(i)
|
81
|
+
return if (li = left(i)) >= @items.size
|
82
|
+
|
83
|
+
ri = li + 1
|
84
|
+
swap_i = (li == @items.size-1 || @comparator.call(@items[li], @items[ri]) <= 0) ? li : ri
|
85
|
+
if (@comparator.call(@items[swap_i], @items[i]) <= 0)
|
86
|
+
@items[swap_i], @items[i] = @items[i], @items[swap_i]
|
87
|
+
sink_down(swap_i)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module RedisLua
|
2
|
+
def eval_lua_script(script, sha1, *args, redis: @redis,**kwargs)
|
3
|
+
redis.evalsha sha1, *args, **kwargs
|
4
|
+
rescue ::Redis::CommandError => e
|
5
|
+
if e.to_s =~ /^NOSCRIPT/
|
6
|
+
redis.eval script, *args, **kwargs
|
7
|
+
else
|
8
|
+
raise
|
9
|
+
end
|
10
|
+
end
|
11
|
+
module_function :eval_lua_script
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "multi_dbs_load_balancer/version"
|
4
|
+
require_relative "multi_dbs_load_balancer/load_balancer"
|
5
|
+
|
6
|
+
module MultiDbsLoadBalancer
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
def setup
|
10
|
+
yield(LoadBalancer)
|
11
|
+
end
|
12
|
+
module_function :setup
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveSupport.on_load(:active_record) do
|
16
|
+
ActiveRecord::Base.send(:include, LoadBalancer)
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "rails/generators"
|
3
|
+
|
4
|
+
module MultiDbsLoadBalancer
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def create_initializer
|
9
|
+
copy_file "initializer.rb", "config/initializers/multi_dbs_load_balancer.rb"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
MultiDbsLoadBalancer.setup do |load_balancer|
|
2
|
+
## The interval time that any have-down(disconnected, error, ...) db will be ignored,
|
3
|
+
## after that, the load balancer will retry to pick this db if the algorithm point to this
|
4
|
+
# load_balancer.db_down_time = 120
|
5
|
+
|
6
|
+
## The interval time that all algorithms will process in local server if the redis disconnected,
|
7
|
+
## after that, they will retry to connect to redis again.
|
8
|
+
# load_balancer.redis_down_time = 120
|
9
|
+
|
10
|
+
# load_balancer.init :round_robin,
|
11
|
+
# [
|
12
|
+
# {role: :reading1},
|
13
|
+
# {role: :reading2},
|
14
|
+
# {role: :reading3},
|
15
|
+
# ],
|
16
|
+
# algorithm: :round_robin,
|
17
|
+
# redis: Redis.new(host: 'localhost')
|
18
|
+
#
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/multi_dbs_load_balancer/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "multi_dbs_load_balancer"
|
7
|
+
spec.version = MultiDbsLoadBalancer::VERSION
|
8
|
+
spec.authors = ["theforestvn88"]
|
9
|
+
spec.email = ["theforestvn88@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "rails multiple databases load balancer"
|
12
|
+
spec.description = "rails multiple databases load balancer"
|
13
|
+
spec.homepage = "https://github.com/theforestvn88/rails_dbs_load_balancer"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/theforestvn88/rails_dbs_load_balancer"
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/theforestvn88/rails_dbs_load_balancer"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_runtime_dependency 'activerecord', '>= 6.0'
|
34
|
+
spec.add_runtime_dependency 'activesupport', '>= 6.0'
|
35
|
+
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multi_dbs_load_balancer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- theforestvn88
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-03-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
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'
|
41
|
+
description: rails multiple databases load balancer
|
42
|
+
email:
|
43
|
+
- theforestvn88@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".rspec"
|
49
|
+
- LICENSE.txt
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- benchmark.rb
|
53
|
+
- lib/multi_dbs_load_balancer.rb
|
54
|
+
- lib/multi_dbs_load_balancer/distribute_lock.rb
|
55
|
+
- lib/multi_dbs_load_balancer/healthcheck.rb
|
56
|
+
- lib/multi_dbs_load_balancer/load_balancer.rb
|
57
|
+
- lib/multi_dbs_load_balancer/load_balancer/algo.rb
|
58
|
+
- lib/multi_dbs_load_balancer/load_balancer/errors.rb
|
59
|
+
- lib/multi_dbs_load_balancer/load_balancer/hash.rb
|
60
|
+
- lib/multi_dbs_load_balancer/load_balancer/least_connection.rb
|
61
|
+
- lib/multi_dbs_load_balancer/load_balancer/least_response_time.rb
|
62
|
+
- lib/multi_dbs_load_balancer/load_balancer/randomized.rb
|
63
|
+
- lib/multi_dbs_load_balancer/load_balancer/round_robin.rb
|
64
|
+
- lib/multi_dbs_load_balancer/load_balancer/weight_round_robin.rb
|
65
|
+
- lib/multi_dbs_load_balancer/min_heap.rb
|
66
|
+
- lib/multi_dbs_load_balancer/redis_lua.rb
|
67
|
+
- lib/multi_dbs_load_balancer/version.rb
|
68
|
+
- lib/rails/generators/multi_dbs_load_balancer/install_generator.rb
|
69
|
+
- lib/rails/generators/multi_dbs_load_balancer/templates/initializer.rb
|
70
|
+
- multi_dbs_load_balancer.gemspec
|
71
|
+
- sig/multi_dbs_load_balancer.rbs
|
72
|
+
homepage: https://github.com/theforestvn88/rails_dbs_load_balancer
|
73
|
+
licenses:
|
74
|
+
- MIT
|
75
|
+
metadata:
|
76
|
+
homepage_uri: https://github.com/theforestvn88/rails_dbs_load_balancer
|
77
|
+
source_code_uri: https://github.com/theforestvn88/rails_dbs_load_balancer
|
78
|
+
changelog_uri: https://github.com/theforestvn88/rails_dbs_load_balancer
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 2.6.0
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.5.4
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: rails multiple databases load balancer
|
98
|
+
test_files: []
|