redis_dynamic 0.1.0

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 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: []