twingly-amqp 4.2.1 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []