bunny-pub-sub 0.5.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: 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: []