twingly-amqp 4.2.1 → 5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 858c1c7b49f3bbd776a0aa0af9f070d1c40f31f9
4
- data.tar.gz: ee5ee54beff37137f74fe60d0d4e60dc0db4e893
2
+ SHA256:
3
+ metadata.gz: c61cf032af90e6b91d1c690ad02f89c6a46e3cc7879813e47f1ee89a1367609a
4
+ data.tar.gz: 6508f9911ef9dcb8c64854522915129878905e62620dbf022ec104e8a911354a
5
5
  SHA512:
6
- metadata.gz: ecc06c04e469767a9f9a521989f3ae8f85aec7b18da877f255bda9a12db90575e7d4b5f427624f6d5b7f3e7d94047a6e8ce30c007f14c82f0033341d00c2e12c
7
- data.tar.gz: dcf7197d39bdff607daaa44957bb817a4a4f983a697949723051376ac4345595356b0c2b48d25081ae05942948ed8b408df0ee9645e00c7354301ad87af15c18
6
+ metadata.gz: 0f66b4eda7c713585780fa20f1bae2948b3251f00929f16fd01273f3298a32fb921eaa334e5814f464eac85446bdc17be227e9f850293a0bb753131f061a6f37
7
+ data.tar.gz: 87c30a0c5e817ea29be19da7b99e3d156f4ba6ca303abd63ebb265827673c90ac3b64ca22bd9d3e5df355163fb29b888931e41d6637282460d598c2bff4114b4
data/README.md CHANGED
@@ -55,10 +55,11 @@ end
55
55
  ```ruby
56
56
  subscription = Twingly::AMQP::Subscription.new(
57
57
  queue_name: "crawler-urls",
58
- exchange_topic: "url-exchange", # Optional, uses the default exchange if omitted
59
- routing_key: "url.blog", # Optional, uses the default exchange if omitted
60
- consumer_threads: 4, # Optional
61
- prefetch: 20, # Optional
58
+ exchange_topic: "url-exchange", # Optional, uses the default exchange if omitted
59
+ routing_keys: %w(url.blog url.post), # Optional, uses the default exchange if omitted
60
+ consumer_threads: 4, # Optional
61
+ prefetch: 20, # Optional
62
+ max_length: 10_000, # Optional
62
63
  )
63
64
 
64
65
  subscription.on_exception { |exception| puts "Oh noes! #{exception.message}" }
@@ -80,6 +81,19 @@ subscription.each_message do |message| # An instance of Twingly::AMQP::Message
80
81
  end
81
82
  ```
82
83
 
84
+ #### Non-blocking subscription
85
+
86
+ ```ruby
87
+ subscription.each_message(blocking: false) do |message|
88
+ puts "Received #{message.payload.inspect}"
89
+ message.ack
90
+ end
91
+
92
+ # Do something
93
+
94
+ subscription.cancel! # Stops the subscriber
95
+ ```
96
+
83
97
  ### Publish to a queue on the default exchange
84
98
 
85
99
  ```ruby
@@ -131,9 +145,11 @@ pinger = Twingly::AMQP::Pinger.new(
131
145
 
132
146
  # Optional, options can also be given to #ping
133
147
  pinger.default_ping_options do |options|
134
- options.provider_name = "TestProvider"
135
- options.source_ip = "?.?.?.?"
136
- options.priority = 1
148
+ options.provider_name = "TestProvider"
149
+ options.source_ip = "?.?.?.?"
150
+ options.priority = 1
151
+ # Optional keys/values that will be included in each ping message
152
+ options.custom_options = { my_option: "This is my own option" }
137
153
  end
138
154
 
139
155
  urls = [
@@ -141,10 +157,12 @@ urls = [
141
157
  ]
142
158
 
143
159
  # Optional, is merged with the default options above
160
+ # options given to #ping takes precedence over the default options
144
161
  options = {
145
162
  provider_name: "a-provider-name",
146
163
  source_ip: "?.?.?.?",
147
164
  priority: 1,
165
+ custom_options: { my_other_option: "Another option" },
148
166
  }
149
167
 
150
168
  pinger.ping(urls, options) do |pinged_url|
@@ -197,10 +215,12 @@ bundle exec rake spec:unit
197
215
 
198
216
  To run static code analysis:
199
217
 
200
- bundle exec rubocop
218
+ gem install rubocop
219
+
220
+ rubocop
201
221
 
202
- # optionally on single file(s)
203
- bundle exec rubocop lib/twingly/amqp/*
222
+ # optionally on single file(s)
223
+ rubocop lib/twingly/amqp/*
204
224
 
205
225
  ## Release workflow
206
226
 
@@ -4,6 +4,7 @@ require "twingly/amqp/connection"
4
4
  require "twingly/amqp/subscription"
5
5
  require "twingly/amqp/ping_options"
6
6
  require "twingly/amqp/pinger"
7
+ require "twingly/amqp/publisher"
7
8
  require "twingly/amqp/default_exchange_publisher"
8
9
  require "twingly/amqp/topic_exchange_publisher"
9
10
  require "twingly/amqp/null_logger"
@@ -1,48 +1,17 @@
1
+ require "twingly/amqp/publisher"
1
2
  require "twingly/amqp/connection"
2
- require "json"
3
- require "ostruct"
4
3
 
5
4
  module Twingly
6
5
  module AMQP
7
6
  class DefaultExchangePublisher
7
+ include Publisher
8
+
8
9
  def initialize(queue_name:, connection: nil)
9
10
  options.routing_key = queue_name
10
11
 
11
12
  connection ||= Connection.instance
12
13
  @exchange = connection.create_channel.default_exchange
13
14
  end
14
-
15
- def publish(message)
16
- raise ArgumentError unless message.respond_to?(:to_h)
17
-
18
- payload = message.to_h.to_json
19
- opts = options.to_h
20
- @exchange.publish(payload, opts)
21
- end
22
-
23
- # only used by tests to avoid sleeping
24
- def publish_with_confirm(message)
25
- channel = @exchange.channel
26
- channel.confirm_select unless channel.using_publisher_confirmations?
27
-
28
- publish(message)
29
-
30
- @exchange.wait_for_confirms
31
- end
32
-
33
- def configure_publish_options
34
- yield options
35
- end
36
-
37
- private
38
-
39
- def options
40
- @options ||=
41
- OpenStruct.new(
42
- content_type: "application/json",
43
- persistent: true,
44
- )
45
- end
46
15
  end
47
16
  end
48
17
  end
@@ -3,25 +3,40 @@ module Twingly
3
3
  class PingOptions
4
4
  attr_accessor :provider_name, :source_ip, :priority
5
5
 
6
- def initialize(provider_name: nil, source_ip: nil, priority: nil)
7
- self.provider_name = provider_name
8
- self.source_ip = source_ip
9
- self.priority = priority
6
+ attr_reader :custom_options
7
+
8
+ def initialize(provider_name: nil, source_ip: nil, priority: nil,
9
+ custom_options: {})
10
+ self.provider_name = provider_name
11
+ self.source_ip = source_ip
12
+ self.priority = priority
13
+ self.custom_options = custom_options
10
14
 
11
15
  yield self if block_given?
12
16
  end
13
17
 
18
+ def custom_options=(options)
19
+ unless options.respond_to?(:to_h)
20
+ raise ArgumentError, "custom_options must respond to 'to_h'"
21
+ end
22
+
23
+ @custom_options = options.to_h
24
+ end
25
+
14
26
  def to_h
15
27
  {
16
- automatic_ping: false,
17
28
  provider_name: provider_name,
18
29
  source_ip: source_ip,
19
30
  priority: priority,
31
+ custom_options: custom_options,
20
32
  }
21
33
  end
22
34
 
23
35
  def validate
24
- missing_keys = to_h.select { |_, value| value.to_s.empty? }.keys
36
+ missing_keys = to_h.select do |key, value|
37
+ key != :custom_options && value.to_s.empty?
38
+ end.keys
39
+
25
40
  if missing_keys.any?
26
41
  raise ArgumentError, "Required options not set: #{missing_keys}"
27
42
  end
@@ -29,9 +44,10 @@ module Twingly
29
44
 
30
45
  def merge(other)
31
46
  PingOptions.new do |options|
32
- options.provider_name = other.provider_name || provider_name
33
- options.source_ip = other.source_ip || source_ip
34
- options.priority = other.priority || priority
47
+ options.provider_name = other.provider_name || provider_name
48
+ options.source_ip = other.source_ip || source_ip
49
+ options.priority = other.priority || priority
50
+ options.custom_options = custom_options.merge(other.custom_options)
35
51
  end
36
52
  end
37
53
  end
@@ -20,18 +20,17 @@ module Twingly
20
20
  end
21
21
 
22
22
  def ping(urls, options_hash = {})
23
- options = PingOptions.new(options_hash)
23
+ options = PingOptions.new(**options_hash)
24
24
  options = @default_ping_options.merge(options)
25
25
 
26
26
  options.validate
27
27
 
28
28
  Array(urls).each do |url|
29
- unless cached?(url)
30
- publish(url, options)
31
- cache!(url)
29
+ next if cached?(url)
30
+ publish(url, options)
31
+ cache!(url)
32
32
 
33
- yield url if block_given?
34
- end
33
+ yield url if block_given?
35
34
  end
36
35
  end
37
36
 
@@ -67,7 +66,7 @@ module Twingly
67
66
  end
68
67
 
69
68
  class NullCache
70
- def self.cached?(url)
69
+ def self.cached?(_url)
71
70
  false
72
71
  end
73
72
 
@@ -0,0 +1,47 @@
1
+ require "json"
2
+ require "ostruct"
3
+
4
+ module Twingly
5
+ module AMQP
6
+ module Publisher
7
+ def publish(message)
8
+ payload =
9
+ if message.kind_of?(Array)
10
+ message
11
+ elsif message.respond_to?(:to_h)
12
+ message.to_h
13
+ else
14
+ raise ArgumentError
15
+ end
16
+
17
+ json_payload = payload.to_json
18
+ opts = options.to_h
19
+ @exchange.publish(json_payload, opts)
20
+ end
21
+
22
+ # only used by tests to avoid sleeping
23
+ def publish_with_confirm(message)
24
+ channel = @exchange.channel
25
+ channel.confirm_select unless channel.using_publisher_confirmations?
26
+
27
+ publish(message)
28
+
29
+ @exchange.wait_for_confirms
30
+ end
31
+
32
+ def configure_publish_options
33
+ yield options
34
+ end
35
+
36
+ private
37
+
38
+ def options
39
+ @options ||=
40
+ OpenStruct.new(
41
+ content_type: "application/json",
42
+ persistent: true,
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -27,10 +27,6 @@ module Twingly
27
27
  options[:pass] ||= password_from_env
28
28
  options[:hosts] ||= hosts_from_env
29
29
 
30
- # Used as a workaround for
31
- # https://github.com/ruby-amqp/bunny/issues/446
32
- options[:hosts] = options.fetch(:hosts).dup
33
-
34
30
  default_connection_options.merge(options)
35
31
  end
36
32
 
@@ -4,46 +4,45 @@ require "twingly/amqp/message"
4
4
  module Twingly
5
5
  module AMQP
6
6
  class Subscription
7
- def initialize(queue_name:, exchange_topic: nil, routing_key: nil, consumer_threads: 4, prefetch: 20, connection: nil)
7
+ def initialize(queue_name:, exchange_topic: nil, routing_key: nil,
8
+ routing_keys: nil, consumer_threads: 1, prefetch: 20,
9
+ connection: nil, max_length: nil)
8
10
  @queue_name = queue_name
9
11
  @exchange_topic = exchange_topic
10
- @routing_key = routing_key
12
+ @routing_keys = Array(routing_keys || routing_key)
11
13
  @consumer_threads = consumer_threads
12
14
  @prefetch = prefetch
15
+ @max_length = max_length
16
+
17
+ if routing_key
18
+ warn "[DEPRECATION] `routing_key` is deprecated. "\
19
+ "Please use `routing_keys` instead."
20
+ end
13
21
 
14
22
  connection ||= Connection.instance
15
23
  @channel = create_channel(connection)
16
24
  @queue = @channel.queue(@queue_name, queue_options)
17
25
 
18
- if @exchange_topic && @routing_key
26
+ if @exchange_topic && @routing_keys.any?
19
27
  exchange = @channel.topic(@exchange_topic, durable: true)
20
- @queue.bind(exchange, routing_key: @routing_key)
28
+
29
+ @routing_keys.each do |routing_key|
30
+ @queue.bind(exchange, routing_key: routing_key)
31
+ end
21
32
  end
22
33
 
23
34
  @before_handle_message_callback = proc {}
24
35
  @on_exception_callback = proc {}
25
36
  end
26
37
 
27
- def each_message(&block)
28
- setup_traps
38
+ def each_message(blocking: true, &block)
39
+ consumer = create_consumer(&block)
29
40
 
30
- consumer = @queue.subscribe(subscribe_options) do |delivery_info, metadata, payload|
31
- @before_handle_message_callback.call(payload)
41
+ if blocking
42
+ sleep 0.01 until cancel?
32
43
 
33
- message = Message.new(
34
- delivery_info: delivery_info,
35
- metadata: metadata,
36
- payload: payload,
37
- channel: @channel,
38
- )
39
-
40
- block.call(message)
44
+ consumer.cancel
41
45
  end
42
-
43
- # The consumer isn't blocking, so we wait here
44
- sleep 0.5 until cancel?
45
-
46
- consumer.cancel
47
46
  end
48
47
 
49
48
  def before_handle_message(&block)
@@ -54,6 +53,14 @@ module Twingly
54
53
  @on_exception_callback = block
55
54
  end
56
55
 
56
+ def message_count
57
+ @queue.status.fetch(:message_count)
58
+ end
59
+
60
+ def raw_queue
61
+ @queue
62
+ end
63
+
57
64
  def cancel?
58
65
  @cancel
59
66
  end
@@ -64,6 +71,21 @@ module Twingly
64
71
 
65
72
  private
66
73
 
74
+ def create_consumer
75
+ @queue.subscribe(subscribe_options) do |delivery_info, metadata, payload|
76
+ @before_handle_message_callback.call(payload)
77
+
78
+ message = Message.new(
79
+ delivery_info: delivery_info,
80
+ metadata: metadata,
81
+ payload: payload,
82
+ channel: @channel,
83
+ )
84
+
85
+ yield message
86
+ end
87
+ end
88
+
67
89
  def create_channel(connection)
68
90
  channel = connection.create_channel(nil, @consumer_threads)
69
91
  channel.prefetch(@prefetch)
@@ -77,9 +99,16 @@ module Twingly
77
99
  def queue_options
78
100
  {
79
101
  durable: true,
102
+ arguments: queue_arguments,
80
103
  }
81
104
  end
82
105
 
106
+ def queue_arguments
107
+ {}.tap do |arguments|
108
+ arguments["x-max-length"] = @max_length if @max_length
109
+ end
110
+ end
111
+
83
112
  def subscribe_options
84
113
  {
85
114
  manual_ack: true,
@@ -91,18 +120,6 @@ module Twingly
91
120
  tag_name = Socket.gethostname
92
121
  @channel.generate_consumer_tag(tag_name)
93
122
  end
94
-
95
- def setup_traps
96
- [:INT, :TERM].each do |signal|
97
- Signal.trap(signal) do
98
- # Exit fast if we've already got a signal since before
99
- exit!(true) if cancel?
100
-
101
- # Set cancel flag, cancels consumers
102
- cancel!
103
- end
104
- end
105
- end
106
123
  end
107
124
  end
108
125
  end
@@ -1,48 +1,17 @@
1
+ require "twingly/amqp/publisher"
1
2
  require "twingly/amqp/connection"
2
- require "json"
3
- require "ostruct"
4
3
 
5
4
  module Twingly
6
5
  module AMQP
7
6
  class TopicExchangePublisher
7
+ include Publisher
8
+
8
9
  def initialize(exchange_name:, routing_key: nil, connection: nil, opts: {})
9
10
  options.routing_key = routing_key
10
11
 
11
12
  connection ||= Connection.instance
12
13
  @exchange = connection.create_channel.topic(exchange_name, opts)
13
14
  end
14
-
15
- def publish(message)
16
- raise ArgumentError unless message.respond_to?(:to_h)
17
-
18
- payload = message.to_h.to_json
19
- opts = options.to_h
20
- @exchange.publish(payload, opts)
21
- end
22
-
23
- # only used by tests to avoid sleeping
24
- def publish_with_confirm(message)
25
- channel = @exchange.channel
26
- channel.confirm_select unless channel.using_publisher_confirmations?
27
-
28
- publish(message)
29
-
30
- @exchange.wait_for_confirms
31
- end
32
-
33
- def configure_publish_options
34
- yield options
35
- end
36
-
37
- private
38
-
39
- def options
40
- @options ||=
41
- OpenStruct.new(
42
- content_type: "application/json",
43
- persistent: true,
44
- )
45
- end
46
15
  end
47
16
  end
48
17
  end
@@ -1,5 +1,5 @@
1
1
  module Twingly
2
2
  module Amqp
3
- VERSION = "4.2.1".freeze
3
+ VERSION = "5.0.1".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twingly-amqp
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.1
4
+ version: 5.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Twingly AB
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-17 00:00:00.000000000 Z
11
+ date: 2020-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -16,56 +16,48 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.2'
19
+ version: '2.6'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.6.2
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - "~>"
25
28
  - !ruby/object:Gem::Version
26
- version: '2.2'
27
- - !ruby/object:Gem::Dependency
28
- name: rspec
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
29
+ version: '2.6'
30
+ - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '3'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '3'
32
+ version: 2.6.2
41
33
  - !ruby/object:Gem::Dependency
42
34
  name: rake
43
35
  requirement: !ruby/object:Gem::Requirement
44
36
  requirements:
45
37
  - - "~>"
46
38
  - !ruby/object:Gem::Version
47
- version: '10.0'
39
+ version: '12'
48
40
  type: :development
49
41
  prerelease: false
50
42
  version_requirements: !ruby/object:Gem::Requirement
51
43
  requirements:
52
44
  - - "~>"
53
45
  - !ruby/object:Gem::Version
54
- version: '10.0'
46
+ version: '12'
55
47
  - !ruby/object:Gem::Dependency
56
- name: rubocop
48
+ name: rspec
57
49
  requirement: !ruby/object:Gem::Requirement
58
50
  requirements:
59
51
  - - "~>"
60
52
  - !ruby/object:Gem::Version
61
- version: '0'
53
+ version: '3'
62
54
  type: :development
63
55
  prerelease: false
64
56
  version_requirements: !ruby/object:Gem::Requirement
65
57
  requirements:
66
58
  - - "~>"
67
59
  - !ruby/object:Gem::Version
68
- version: '0'
60
+ version: '3'
69
61
  description: Publish and subscribe to messages via RabbitMQ
70
62
  email:
71
63
  - support@twingly.com
@@ -81,6 +73,7 @@ files:
81
73
  - lib/twingly/amqp/null_logger.rb
82
74
  - lib/twingly/amqp/ping_options.rb
83
75
  - lib/twingly/amqp/pinger.rb
76
+ - lib/twingly/amqp/publisher.rb
84
77
  - lib/twingly/amqp/session.rb
85
78
  - lib/twingly/amqp/subscription.rb
86
79
  - lib/twingly/amqp/topic_exchange_publisher.rb
@@ -88,7 +81,7 @@ files:
88
81
  homepage: https://github.com/twingly/twingly-amqp
89
82
  licenses: []
90
83
  metadata: {}
91
- post_install_message:
84
+ post_install_message:
92
85
  rdoc_options: []
93
86
  require_paths:
94
87
  - lib
@@ -103,9 +96,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
96
  - !ruby/object:Gem::Version
104
97
  version: '0'
105
98
  requirements: []
106
- rubyforge_project:
107
- rubygems_version: 2.4.5.1
108
- signing_key:
99
+ rubygems_version: 3.1.2
100
+ signing_key:
109
101
  specification_version: 4
110
102
  summary: Ruby library for talking to RabbitMQ
111
103
  test_files: []