redis_dynamic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 046e2753ce2319d2b8c1ab4972d677eeaa92b7c2d2e8696a7224edf222d49b1c
4
+ data.tar.gz: fcccc0a2969bb430df31ddcad70e4e715cd8bdd59fbc2fd1158c4838fa6ebea8
5
+ SHA512:
6
+ metadata.gz: 0a4f85d887156bbd10ce067e5561cd213093f591b19ba1ecda9ddeb6853a8be6a124d0e597b9a10e631541866b353ab3fea4749c388b1f678224b0a1639fea92
7
+ data.tar.gz: 8e25c5daf8c85d0594ca7cf1b26d179f9e0cdb4c92bc9789f6d431cff97a583c9f1d52ee369f7695e35f21d49256c6e13f9cc7a0f9c13e234c14437cd42cf448
data/lib/redis_pool.rb ADDED
@@ -0,0 +1,88 @@
1
+ require 'redis'
2
+ require_relative './redis_pool/connection_queue'
3
+ require_relative './redis_pool/reaper'
4
+
5
+ class RedisPool
6
+ DEFAULT_REDIS_CONFIG = { host: 'localhost', port: 6379 }.freeze
7
+
8
+ attr_reader :max_size, :connection_timeout, :idle_timeout,
9
+ :reaping_frequency, :available
10
+
11
+ def initialize(max_size: 5, connection_timeout: 5, idle_timeout: 100, reaping_frequency: 300, redis_config: {})
12
+ @redis_config = DEFAULT_REDIS_CONFIG.merge(redis_config)
13
+
14
+ @max_size = max_size
15
+ @connection_timeout = connection_timeout
16
+ @idle_timeout = idle_timeout
17
+ @reaping_frequency = reaping_frequency
18
+
19
+ @available = ConnectionQueue.new(@max_size, &redis_creation_block)
20
+ @reaper = Reaper.new(self, @reaping_frequency, @idle_timeout)
21
+
22
+ @key = :"pool-#{@available.object_id}"
23
+ @key_count = :"pool-#{@available.object_id}-count"
24
+
25
+ @reaper.reap
26
+ end
27
+
28
+ def with(timeout = nil)
29
+ Thread.handle_interrupt(Exception => :never) do
30
+ conn = checkout(timeout)
31
+ begin
32
+ Thread.handle_interrupt(Exception => :immediate) do
33
+ yield conn.first
34
+ end
35
+ ensure
36
+ checkin
37
+ end
38
+ end
39
+ end
40
+ alias with_conn with
41
+ alias with_connection with
42
+
43
+ def checkout(timeout = nil)
44
+ if current_thread[@key]
45
+ current_thread[@key_count] += 1
46
+ current_thread[@key]
47
+ else
48
+ current_thread[@key_count] = 1
49
+ current_thread[@key] = @available.poll(timeout || @connection_timeout)
50
+ end
51
+ end
52
+
53
+ def checkin
54
+ raise 'no connections are checked out' unless current_thread[@key]
55
+
56
+ if current_thread[@key_count] == 1
57
+ @available.add current_thread[@key]
58
+ current_thread[@key] = nil
59
+ current_thread[@key_count] = nil
60
+ else
61
+ current_thread[@key_count] -= 1
62
+ end
63
+ end
64
+
65
+ def stats
66
+ conn_stats = @available.queue.map do |conn|
67
+ conn.last
68
+ end
69
+ pool_stats = {
70
+ available_to_create: @available.available_to_create,
71
+ total_available: @available.total_available,
72
+ connections_stats: conn_stats
73
+ }
74
+ end
75
+
76
+ private
77
+
78
+ def current_thread
79
+ Thread.current
80
+ end
81
+
82
+ def redis_creation_block
83
+ -> { Redis.new(@redis_config) }
84
+ end
85
+ end
86
+
87
+ require 'redis_pool/connection_queue'
88
+ require 'redis_pool/reaper'
@@ -0,0 +1,115 @@
1
+ require 'monitor'
2
+ require 'concurrent'
3
+
4
+ ##
5
+ # A thread-safe implementation of a connection queue.
6
+ # Supports adding, removing, and polling a connection
7
+ # synchronously, and doesn't create more than `max_size`
8
+ # elements.
9
+ # All connections are created lazily (only when needed).
10
+ #
11
+ class ConnectionQueue
12
+ attr_reader :max_size, :queue
13
+
14
+ def initialize(max_size = 0, &block)
15
+ @create_block = block
16
+ @created = 0
17
+ @queue = []
18
+ @max_size = max_size
19
+ @lock = Monitor.new
20
+ @lock_cond = @lock.new_cond
21
+ end
22
+
23
+ ##
24
+ # Adds (or returns) a connection to the available queue, synchronously.
25
+ #
26
+ def add(element)
27
+ synchronize do
28
+ @queue.push element
29
+ @lock_cond.signal
30
+ end
31
+ end
32
+ alias << add
33
+ alias push add
34
+
35
+ ##
36
+ # Fetches any available connection from the queue. If a connection
37
+ # is not available, waits for +timeout+ until a connection is
38
+ # available or raises a TimeoutError.
39
+ #
40
+ def poll(timeout = 5)
41
+ t0 = Concurrent.monotonic_time
42
+ elapsed = 0
43
+ synchronize do
44
+ loop do
45
+ return get_connection if connection_available?
46
+
47
+ connection = create_connection
48
+ return connection if connection
49
+
50
+ elapsed = Concurrent.monotonic_time - t0
51
+ raise TimeoutError, 'could not obtain connection' if elapsed >= timeout
52
+
53
+ @lock_cond.wait(timeout - elapsed)
54
+ end
55
+ end
56
+ end
57
+ alias pop poll
58
+
59
+ ##
60
+ # Removes an idle connection from the queue
61
+ # synchronously.
62
+ #
63
+ def delete(element)
64
+ synchronize do
65
+ @queue.delete element
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Returns the total available connections to be used. This
71
+ # takes into account the number of connections that can be
72
+ # created as well. So it is all connections that can be used
73
+ # AND created.
74
+ #
75
+ def total_available
76
+ @max_size - @created + @queue.length
77
+ end
78
+
79
+ ##
80
+ # Returns the number of available connections to create.
81
+ #
82
+ def available_to_create
83
+ @max_size - @created
84
+ end
85
+
86
+ private
87
+
88
+ def synchronize(&block)
89
+ @lock.synchronize(&block)
90
+ end
91
+
92
+ def connection_available?
93
+ !@queue.empty?
94
+ end
95
+
96
+ def get_connection
97
+ conn = @queue.pop
98
+ conn.last[:last_used_at] = Time.now.utc
99
+ conn
100
+ end
101
+
102
+ def create_connection
103
+ return unless @created < @max_size
104
+
105
+ conn = @create_block.call
106
+ # TODO: add more stats.
107
+ stats = {
108
+ id: @created,
109
+ alive_since: Time.now.utc,
110
+ last_used_at: Time.now.utc
111
+ }
112
+ @created += 1
113
+ [conn, stats]
114
+ end
115
+ end
@@ -0,0 +1,33 @@
1
+ ##
2
+ # A reaper class that initializes a thread running in the
3
+ # background, that kills all connections in `pool` that
4
+ # has been idle for more than `idle_timeout`.
5
+ #
6
+ class Reaper
7
+ attr_reader :frequency, :idle_timeout
8
+
9
+ def initialize(pool, frequency, idle_timeout)
10
+ @frequency = frequency
11
+ @idle_timeout = idle_timeout
12
+ @pool = pool
13
+ @lock = Mutex.new
14
+ end
15
+
16
+ def reap
17
+ Thread.new do
18
+ loop do
19
+ @pool.available.queue.each do |conn|
20
+ idle_since = conn.last[:last_used_at] - Time.now.utc
21
+
22
+ next unless idle_since >= @idle_timeout
23
+
24
+ @lock.synchronize do
25
+ @pool.available.delete conn
26
+ conn.first.disconnect!
27
+ end
28
+ end
29
+ sleep @frequency
30
+ end
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_dynamic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mohammed Amarnah
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-01-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: A simple dynamic-sized redis connection pool.
70
+ email: m.amarnah@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/redis_pool.rb
76
+ - lib/redis_pool/connection_queue.rb
77
+ - lib/redis_pool/reaper.rb
78
+ homepage: https://github.com/mohammedamarnah/redis-pool
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 2.2.0
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.1.2
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Redis Dynamic Pool
101
+ test_files: []