multi_dbs_load_balancer 0.0.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.
- 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: []
|