bunny-pub-sub 0.5.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: ebf2dd45611315008e984f3ba704b303493287641d4bee5d38093fe3b590c961
4
+ data.tar.gz: e27dd48bb74db55c094649a22a3e38a71e21a9753acbafbc46fa21f4b27d0999
5
+ SHA512:
6
+ metadata.gz: 4034d92342ebe378956c949041b9e73dce79751c2264d5f568ffc7f2aa9ffb0b81ca15d77c8380f04db971ada7d2cb3028df570bc2b14afff46dfeabf837f5c9
7
+ data.tar.gz: df35aea44be82b1300da5927c92b1e691b6f3a78da50717baf57eb713a8bb9fa695321d3f48023449f9d1008763257325fcc138748c736849443a8dc07173651
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ConfigError < StandardError; end
4
+
5
+ def valid_config?(config)
6
+ raise ConfigError, 'CONFIG must not be nil' if config.nil?
7
+
8
+ flag = true, error_msgs = []
9
+ if config[:RABBITMQ_HOSTNAME].nil? ||
10
+ config[:RABBITMQ_HOSTNAME]&.strip&.empty?
11
+ error_msgs << 'Must define config variable RABBITMQ_HOSTNAME'
12
+ flag = false
13
+ end
14
+ if config[:RABBITMQ_USERNAME].nil? ||
15
+ config[:RABBITMQ_USERNAME]&.strip&.empty?
16
+ error_msgs << 'Must define config variable RABBITMQ_USERNAME'
17
+ flag = false
18
+ end
19
+ if config[:RABBITMQ_PASSWORD].nil? ||
20
+ config[:RABBITMQ_PASSWORD]&.strip&.empty?
21
+ error_msgs << 'Must define config variable RABBITMQ_PASSWORD'
22
+ flag = false
23
+ end
24
+ if config[:EXCHANGE_NAME].nil? ||
25
+ config[:EXCHANGE_NAME]&.strip&.empty?
26
+ error_msgs << 'Must define config variable EXCHANGE_NAME'
27
+ flag = false
28
+ end
29
+ if config[:DURABLE_QUEUE_NAME].nil? ||
30
+ config[:DURABLE_QUEUE_NAME]&.strip&.empty?
31
+ error_msgs << 'Must define config variable DURABLE_QUEUE_NAME'
32
+ flag = false
33
+ end
34
+ raise ConfigError, error_msgs unless flag
35
+
36
+ flag
37
+ end
38
+
39
+ # Subscriber only checks BEGIN =================================================
40
+ # ==============================================================================
41
+ def valid_binding_keys?(language_environments, type='topic')
42
+ # TODO: Exchanges of type `topic` PROBABLY can't simply have multiple
43
+ # words like `direct` exchanges.
44
+ # Rather they must be a list of words, delimited by dots.
45
+ # VERIFY and if true, ensure that all the strings in
46
+ # language_environments adhere to this rule.
47
+ # language_environments must be something like "#.csharp",
48
+ # "#.splashkit.csharp", "#.python", etc.
49
+ case type
50
+ when 'topic'
51
+ language_environments.each do |language_environment|
52
+ if !language_environment.is_a?(String) ||
53
+ language_environment.strip.empty?
54
+ # TODO: Add regex check here.
55
+ return false
56
+ end
57
+ end
58
+ end
59
+ true
60
+ end
61
+
62
+ def binding_keys_to_array(config)
63
+ language_environments = []
64
+ unless config[:DEFAULT_BINDING_KEY]&.strip&.empty?
65
+ language_environments.push(config[:DEFAULT_BINDING_KEY])
66
+ end
67
+
68
+ if !config[:BINDING_KEYS].nil? && !config[:BINDING_KEYS].empty?
69
+ # Pushing array to another array:
70
+ # https://stackoverflow.com/questions/1801516/how-do-you-add-an-array-to-another-array-in-ruby-and-not-end-up-with-a-multi-dim
71
+ language_environments = [language_environments | config[:BINDING_KEYS].split(',')].flatten
72
+ end
73
+
74
+ return nil if language_environments.empty?
75
+
76
+ valid_binding_keys?(language_environments) ? language_environments : nil
77
+ end
78
+
79
+ def at_least_one_binding_key_exists?(config)
80
+ if (config[:BINDING_KEYS].nil? &&
81
+ config[:DEFAULT_BINDING_KEY].nil?) ||
82
+ (config[:BINDING_KEYS]&.strip&.empty? &&
83
+ config[:DEFAULT_BINDING_KEY]&.strip&.empty?)
84
+ puts 'Either BINDING_KEYS or '\
85
+ 'DEFAULT_BINDING_KEY must be defined and must not be empty'
86
+ false
87
+ else
88
+ true
89
+ end
90
+ end
91
+
92
+ # Subscriber only checks END ===================================================
93
+ # ==============================================================================
94
+
95
+ # Publisher only checks BEGIN ==================================================
96
+ # ==============================================================================
97
+ def routing_key_exists?(config)
98
+ if config[:ROUTING_KEY].nil? ||
99
+ config[:ROUTING_KEY]&.strip&.empty?
100
+ puts 'ROUTING_KEY must be defined and must not be empty'
101
+ false
102
+ else
103
+ true
104
+ end
105
+ end
106
+
107
+ # Publisher only checks END ====================================================
108
+ # ==============================================================================
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper/config_checks'
4
+ require 'bunny'
5
+ require 'json'
6
+
7
+ class Publisher
8
+ attr_reader :connection
9
+ attr_reader :exchange
10
+ attr_reader :channel
11
+ attr_reader :config
12
+
13
+ def initialize(config)
14
+ return unless valid_config? config
15
+ return unless routing_key_exists? config
16
+
17
+ @config = config
18
+
19
+ @connection = Bunny.new(
20
+ hostname: @config[:RABBITMQ_HOSTNAME],
21
+ username: @config[:RABBITMQ_USERNAME],
22
+ password: @config[:RABBITMQ_PASSWORD]
23
+ )
24
+ end
25
+
26
+ def start_connection
27
+ ServicesManager.instance.start_connection(@connection, 0)
28
+ end
29
+
30
+ def create_channel
31
+ @channel = @connection.create_channel
32
+ @channel.confirm_select if @publisher_confirms
33
+ end
34
+
35
+ def set_topic_exchange
36
+ @exchange = @channel.topic(@config[:EXCHANGE_NAME], durable: true)
37
+ end
38
+
39
+ def connect_publisher(publisher_confirms = true)
40
+ start_connection
41
+ @publisher_confirms = publisher_confirms
42
+ create_channel
43
+ set_topic_exchange
44
+ end
45
+
46
+ def default_routing_key=(routing_key)
47
+ if routing_key.nil? || routing_key&.strip&.empty?
48
+ puts 'routing_key must be defined and must not be empty'
49
+ return
50
+ end
51
+
52
+ @config[:ROUTING_KEY] = routing_key
53
+ end
54
+
55
+ def unset_default_routing_key
56
+ @config[:ROUTING_KEY] = nil
57
+ end
58
+
59
+ def publish_message(msg, routing_key=nil)
60
+ if (routing_key.nil? || routing_key&.strip&.empty?) &&
61
+ (@config[:ROUTING_KEY].nil? || @config[:ROUTING_KEY]&.strip&.empty?)
62
+ return 'No default routing key exists in config. You must specify one.'
63
+ end
64
+
65
+ @exchange.publish(msg.to_json, routing_key: routing_key || @config[:ROUTING_KEY], persistent: true)
66
+ return puts ' [x] Message sent!' unless @publisher_confirms
67
+
68
+ success = @channel.wait_for_confirms
69
+ return puts ' [x] Message sent!' if success
70
+
71
+ @channel.nacked_set.each do |n|
72
+ # Do something with the nacked message ID
73
+ # TODO: Add a yielding block to this
74
+ puts " [x] Message #{n} failed."
75
+ end
76
+ end
77
+
78
+ def close_channel
79
+ @channel.close
80
+ end
81
+
82
+ def close_connection
83
+ @connection.close
84
+ end
85
+
86
+ def disconnect_publisher
87
+ close_channel
88
+ close_connection
89
+ end
90
+ end
@@ -0,0 +1,219 @@
1
+ require_relative 'publisher.rb'
2
+ require_relative 'subscriber.rb'
3
+ require 'singleton'
4
+
5
+ class ServicesManager
6
+ include Singleton
7
+ attr_reader :clients
8
+
9
+ def initialize
10
+ @clients = {}
11
+ end
12
+
13
+ def register_client(name,
14
+ publisher_config = nil,
15
+ subscriber_config = nil,
16
+ action = nil,
17
+ results_publisher = nil)
18
+
19
+ return unless valid_name? name, true
20
+ unless @clients[name].nil?
21
+ return puts "Service with the name: #{name} already registered"
22
+ end
23
+
24
+ @clients[name] = RabbitServiceClient.new name
25
+ return @clients[name] if publisher_config.nil?
26
+
27
+ # Note: results_publisher CAN be nil.
28
+ @clients[name].create_publisher publisher_config
29
+ return @clients[name] if subscriber_config.nil?
30
+
31
+ if action.nil?
32
+ @clients[name].create_subscriber_without_action(
33
+ subscriber_config, results_publisher
34
+ )
35
+ return @clients[name]
36
+ end
37
+
38
+ @clients[name].create_and_start_subscriber(
39
+ subscriber_config, action, results_publisher
40
+ )
41
+
42
+ @clients[name]
43
+ end
44
+
45
+ def create_client_publisher(name, config)
46
+ return unless valid_name? name
47
+
48
+ @clients[name].create_publisher config
49
+ end
50
+
51
+ def remove_client_publisher(name)
52
+ return unless valid_name? name
53
+
54
+ @clients[name].remove_publisher
55
+ end
56
+
57
+ def create_and_start_client_subscriber(
58
+ name, subscriber_config, action, results_publisher
59
+ )
60
+ return unless valid_name? name
61
+
62
+ @clients[name].create_and_start_subscriber(
63
+ subscriber_config, action, results_publisher
64
+ )
65
+ end
66
+
67
+ def cancel_and_remove_client_subscriber(name)
68
+ return unless valid_name? name
69
+
70
+ @clients[name].cancel_and_remove_subscriber
71
+ end
72
+
73
+ def deregister_client(name)
74
+ return unless valid_name? name
75
+
76
+ @clients[name].remove_all
77
+ @clients[name] = nil
78
+ @clients.delete name
79
+ end
80
+
81
+ def start_connection(connection, repeat)
82
+ (1 + repeat).times do
83
+ begin
84
+ connection.start
85
+ return
86
+ rescue
87
+ puts 'Unable to start connection to rabbitmq -- delaying 10 seconds'
88
+ sleep(10) unless repeat == 0
89
+ end
90
+ end
91
+ raise "Unable to connect to rabbitmq"
92
+ end
93
+
94
+ private
95
+ def service_exists?(name)
96
+ return true unless @clients[name].nil?
97
+
98
+ puts "Service with the name: #{name} not found"
99
+ false
100
+ end
101
+
102
+ def name_is_symbol?(name)
103
+ return true if name.is_a? Symbol
104
+
105
+ puts "NAME: #{name} must be a symbol"
106
+ false
107
+ end
108
+
109
+ def valid_name?(name, new_service=false)
110
+ if name.nil?
111
+ puts "NAME must be a defined symbol and can't be empty"
112
+ return false
113
+ end
114
+ return false if !new_service && !service_exists?(name)
115
+ return false unless name_is_symbol? name
116
+
117
+ true
118
+ end
119
+
120
+ class RabbitServiceClient
121
+ attr_reader :subscriber
122
+ attr_reader :publisher
123
+ attr_reader :name
124
+
125
+ def initialize(name)
126
+ @name = name
127
+ end
128
+
129
+ def create_publisher(publisher_config)
130
+ publisher_created?
131
+ @publisher = Publisher.new publisher_config
132
+ end
133
+
134
+ def remove_publisher
135
+ return if @publisher.nil?
136
+
137
+ @publisher = nil
138
+ end
139
+
140
+ def action=(action)
141
+ valid_action? action
142
+ @action = action
143
+ end
144
+
145
+ def create_subscriber_without_action(subscriber_config,
146
+ results_publisher)
147
+
148
+ subscriber_created?
149
+
150
+ @subscriber_config = subscriber_config
151
+ @results_publisher = results_publisher
152
+ @subscriber = Subscriber.new subscriber_config, results_publisher
153
+ end
154
+
155
+ def create_and_start_subscriber(subscriber_config,
156
+ action,
157
+ results_publisher)
158
+
159
+ subscriber_created?
160
+
161
+ @subscriber_config = subscriber_config
162
+ @results_publisher = results_publisher
163
+
164
+ valid_action? action
165
+ @action = action
166
+
167
+ @subscriber = Subscriber.new subscriber_config, results_publisher
168
+ start_subscriber
169
+ end
170
+
171
+ def start_subscriber
172
+ @subscriber.start_subscriber(@action)
173
+ end
174
+
175
+ def cancel_and_remove_subscriber
176
+ return if @subscriber.nil?
177
+
178
+ @subscriber.cancel_subscriber
179
+ @subscriber = nil
180
+ end
181
+
182
+ def remove_all
183
+ remove_publisher
184
+ cancel_and_remove_subscriber
185
+ end
186
+
187
+ private
188
+ def subscriber_created?
189
+ return if @subscriber.nil?
190
+
191
+ raise 'A subscriber for this service client'\
192
+ ' has already been created and can\'t be'\
193
+ ' created again. Please create a new RabbitServiceClient.'
194
+ end
195
+
196
+ def publisher_created?
197
+ return if @publisher.nil?
198
+
199
+ raise 'A publisher for this service client'\
200
+ ' has already been created and can\'t be'\
201
+ ' created again. Please create a new RabbitServiceClient.'
202
+ end
203
+
204
+ def valid_action?(action)
205
+ unless @action.nil?
206
+ raise 'An action has already been set'\
207
+ ' for this subscriber. A service\'s'\
208
+ ' action can only be set once.'
209
+ end
210
+
211
+ return unless action.nil?
212
+
213
+ raise 'Subscriber action can\'t be set to nil.'\
214
+ ' Halting the program.'
215
+
216
+ # TODO: Check if action is of the right type.
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bunny'
4
+ require 'json'
5
+ require_relative 'helper/config_checks'
6
+ require_relative 'publisher'
7
+
8
+ class ErrorReporter < RuntimeError
9
+ attr_reader :status
10
+
11
+ def self.publisher=(publisher)
12
+ @@publisher = publisher
13
+ end
14
+
15
+ def self.publisher
16
+ @@publisher
17
+ end
18
+
19
+ def initialize(message, status = 400)
20
+ unless @@publisher.nil?
21
+ @@publisher.connect_publisher
22
+ @@publisher.publish_message message
23
+ @@publisher.disconnect_publisher
24
+ end
25
+ @status = status
26
+ super(message)
27
+ end
28
+ end
29
+
30
+ class Subscriber
31
+ attr_reader :cancel_ok
32
+ class ClientException < ErrorReporter
33
+ def initialize(message, status = 400)
34
+ puts "Client error: #{message}, #{status}"
35
+ super(message, status)
36
+ end
37
+ end
38
+
39
+ class ServerException < ErrorReporter
40
+ def initialize(message, status = 500)
41
+ puts "Server error: #{message}, #{status}"
42
+ super(message, status)
43
+ end
44
+ end
45
+
46
+ def client_error!(message, status, _headers = {}, _backtrace = [])
47
+ raise ClientException.new message, status
48
+ end
49
+
50
+ def server_error!(message, status, _headers = {}, _backtrace = [])
51
+ raise ServerException.new message, status
52
+ end
53
+
54
+ def initialize(subscriber_config, results_publisher)
55
+ return unless valid_config? subscriber_config
56
+ return unless at_least_one_binding_key_exists? subscriber_config
57
+
58
+ subscriber_config[:BINDING_KEYS] = binding_keys_to_array subscriber_config
59
+ return if subscriber_config[:BINDING_KEYS].nil?
60
+
61
+ ServerException.publisher = results_publisher
62
+ ClientException.publisher = results_publisher
63
+
64
+ @subscriber_config = subscriber_config
65
+ @results_publisher = results_publisher
66
+ end
67
+
68
+ def start_subscriber(callback)
69
+ @connection = Bunny.new(
70
+ hostname: @subscriber_config[:RABBITMQ_HOSTNAME],
71
+ username: @subscriber_config[:RABBITMQ_USERNAME],
72
+ password: @subscriber_config[:RABBITMQ_PASSWORD]
73
+ )
74
+ ServicesManager.instance.start_connection(@connection, 6)
75
+
76
+ @channel = @connection.create_channel
77
+ # With the created communication @channel, create/join an existing exchange
78
+ # of the TYPE 'topic' and named as 'assessment'
79
+ # Durable exchanges survive broker restart while transient exchanges do not.
80
+ topic_exchange = @channel.topic(@subscriber_config[:EXCHANGE_NAME], durable: true)
81
+
82
+ # Use this for making rabbitMQ not give a worker more than 1 jobs
83
+ # if it is already working on one.
84
+ # @channel.prefetch(1)
85
+
86
+ queue = @channel.queue(@subscriber_config[:DURABLE_QUEUE_NAME], durable: true)
87
+
88
+ @subscriber_config[:BINDING_KEYS].each do |language_environment|
89
+ queue.bind(topic_exchange, routing_key: language_environment)
90
+ end
91
+
92
+ begin
93
+ puts ' [*] Waiting for messages. To exit press CTRL+C'
94
+
95
+ @consumer = queue.subscribe(manual_ack: true, block: true) do |delivery_info, properties, params|
96
+ callback.call(self, @channel, @results_publisher, delivery_info, properties, params)
97
+ end
98
+ rescue Interrupt => _e
99
+ @channel.close
100
+ @connection.close
101
+
102
+ exit(0)
103
+ end
104
+ end
105
+
106
+ # TODO: Fix this. Probably doesn't work because
107
+ # @consumer has no value assigned to it.
108
+ def cancel_subscriber
109
+ @cancel_ok = @consumer.cancel
110
+ puts 'Consumer cancelled:'
111
+ puts @cancel_ok.inspect
112
+ rescue RuntimeError => e
113
+ puts e
114
+ ensure
115
+ @channel.close
116
+ @connection.close
117
+ end
118
+
119
+ # We can't privatize the exception classes because they are being
120
+ # used in rescue blocks.
121
+ # private_constant :ServerException
122
+ # private_constant :ClientException
123
+ end
124
+
125
+ def register_subscriber(subscriber_config,
126
+ action,
127
+ results_publisher)
128
+
129
+ subscriber_instance = Subscriber.new subscriber_config, results_publisher
130
+ subscriber_instance.start_subscriber(action)
131
+ subscriber_instance
132
+ rescue RuntimeError => e
133
+ puts e
134
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bunny-pub-sub
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Akash Agarwal
8
+ - Andrew Cain
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2021-11-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bunny
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.14'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.14'
28
+ description: Bunny publisher/subscriber client gemfor OnTrack and Overseer.
29
+ email:
30
+ - agarwal.akash333@gmail.com
31
+ - macite@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/bunny-pub-sub/helper/config_checks.rb
37
+ - lib/bunny-pub-sub/publisher.rb
38
+ - lib/bunny-pub-sub/services_manager.rb
39
+ - lib/bunny-pub-sub/subscriber.rb
40
+ homepage: https://github.com/doubtfire-lms/bunny-pub-sub
41
+ licenses:
42
+ - MIT
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.3.1
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.0.3
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: bunny-pub-sub
63
+ test_files: []