propono 0.11.1 → 1.0.0.rc1

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
2
  SHA1:
3
- metadata.gz: 8193ab42cb462b70c436b83459abb2df0203f6be
4
- data.tar.gz: 225ec5fc62711fee56fe6520ba4bd745fb8a5aca
3
+ metadata.gz: 86ff2b93684d9aa043023ff05f81ccebcc6e108c
4
+ data.tar.gz: 7da5b3c1461cb3602e56dff51bcd97343e776262
5
5
  SHA512:
6
- metadata.gz: 75c925d5b89a45630c70e9eb35e558db179eb77caf8a6c243dd9ce827da1e390e825a8205f5df9563e5ab5ab1b510f2d31fea1a0683188cdfdc64e85d102e51e
7
- data.tar.gz: 7ccad01b84956d6a6a3dc22475fc26b0a839f7de0a1a866c655d06da75d300116d2f48c658f3821dc4d804441058c07e79ab3fbe5467abe90738951fc20aeaa6
6
+ metadata.gz: 38bd2565e6e0a0b9eb328d329b8d890ed13def10331792b5dcae86eafbc8535143faf7734a3607f45290ace56e1fc72d8e7559ed4d894348bb66aec12636758b
7
+ data.tar.gz: f2f63928f0f4b565636abf7504706b08dfead8cbe0d71b75946289824ff79992bae05bb526ff5d3e977da3735fec7577a7952d3ab5fb7391aa131d3885f4e49a
data/CHANGELOG.md CHANGED
@@ -1,47 +1,39 @@
1
- # 0.11.1 / 2013-12-09
1
+ # 1.0.0.rc1 / 2013-12-15
2
+ * [FEATURE] Improve transactional handling of messages.
3
+ * [FEATURE] Add failed/corrupt queues.
2
4
 
5
+ # 0.11.1 / 2013-12-09
3
6
  * [BUGFIX] Re raise 403 forbidden excetion instead of continuing.
4
7
 
5
8
  # 0.11.0 / 2013-12-03
6
-
7
9
  * [FEATURE] Add support for IAM profiles for AWS auth
8
10
 
9
11
  # 0.10.0 / 2013-12-03
10
-
11
12
  * [FEATURE] Add queue_suffix config variable
12
13
 
13
14
  # 0.9.1 / Unreleased
14
-
15
15
  * [FEATURE] Propono will raise exceptions if the message processing fails
16
16
 
17
17
  # 0.9.0 / Unreleased
18
-
19
18
  * [FEATURE] Add message ids that track throughout Propono
20
19
 
21
20
  # 0.8.2 / 2013-11-01
22
-
23
21
  * [BUGFIX] Replace thread library with standard ruby threads to fix Unicorn problems.
24
22
 
25
23
  # 0.8.1 / 2013-11-01
26
-
27
24
  * [FEATURE] Log all messages published from Propono.
28
25
 
29
26
  # 0.8.0 / 2013-11-01
30
-
31
27
  * [FEATURE] SNS publish now delegates to a thread pool. The SNS response can be accessed via a future.
32
28
 
33
29
  # 0.7.0 / 2013-10-23
34
-
35
30
  * [FEATURE] Add TCP publish and listen methods.
36
31
 
37
32
  # 0.6.3 / 2013-10-20
38
-
39
33
  * [FEATURE] Catch all StandardError exceptions for UDP publishes.
40
34
 
41
35
  # 0.6.2 / 2013-10-20
42
-
43
36
  * [BUGFIX] Fixed integration tests that sometimes failed due to shared UDP ports or slow SQS subscriptions.
44
37
 
45
38
  # 0.6.1 / 2013-10-20
46
-
47
39
  * [BUGFIX] Added `require 'json'` to udp_listener.rb
data/lib/propono.rb CHANGED
@@ -15,6 +15,7 @@ require "propono/components/queue"
15
15
  require "propono/components/topic"
16
16
  require "propono/components/post_subscription"
17
17
  require "propono/components/queue_subscription"
18
+ require "propono/components/sqs_message"
18
19
 
19
20
  require "propono/services/publisher"
20
21
  require "propono/services/queue_creator"
@@ -4,17 +4,18 @@ module Propono
4
4
  include Sns
5
5
  include Sqs
6
6
 
7
- attr_reader :topic_arn, :queue
7
+ attr_reader :topic_arn, :queue_name, :queue
8
8
 
9
- def self.create(topic_id)
10
- new(topic_id).tap do |subscription|
9
+ def self.create(topic_id, options = {})
10
+ new(topic_id, options).tap do |subscription|
11
11
  subscription.create
12
12
  end
13
13
  end
14
14
 
15
- def initialize(topic_id)
15
+ def initialize(topic_id, options = {})
16
16
  @topic_id = topic_id
17
17
  @suffixed_topic_id = "#{topic_id}#{Propono.config.queue_suffix}"
18
+ @queue_name = "#{Propono.config.application_name.gsub(" ", "_")}-#{@suffixed_topic_id}#{options[:queue_name_suffix]}"
18
19
  end
19
20
 
20
21
  def create
@@ -25,10 +26,6 @@ module Propono
25
26
  sqs.set_queue_attributes(@queue.url, "Policy", generate_policy)
26
27
  end
27
28
 
28
- def queue_name
29
- @queue_name ||= "#{Propono.config.application_name.gsub(" ", "_")}-#{@suffixed_topic_id}"
30
- end
31
-
32
29
  private
33
30
 
34
31
  def generate_policy
@@ -5,9 +5,7 @@ module Propono
5
5
  private
6
6
 
7
7
  def sns
8
- @sns ||= Fog::AWS::SNS.new(
9
- Propono.aws_options
10
- )
8
+ @sns ||= Fog::AWS::SNS.new(Propono.aws_options)
11
9
  end
12
10
  end
13
11
  end
@@ -5,9 +5,7 @@ module Propono
5
5
  private
6
6
 
7
7
  def sqs
8
- @sqs ||= Fog::AWS::SQS.new(
9
- Propono.aws_options
10
- )
8
+ @sqs ||= Fog::AWS::SQS.new(Propono.aws_options)
11
9
  end
12
10
  end
13
11
  end
@@ -0,0 +1,20 @@
1
+ module Propono
2
+ class SqsMessage
3
+ include Sqs
4
+
5
+ attr_reader :context, :message, :raw_message, :receipt_handle
6
+ def initialize(raw_message)
7
+ body = JSON.parse(raw_message["Body"])["Message"]
8
+ body = JSON.parse(body)
9
+
10
+ @raw_message = raw_message
11
+ @context = body.symbolize_keys
12
+ @message = context.delete(:message)
13
+ @receipt_handle = raw_message["receipt_handle"]
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(SqsMessage) && other.receipt_handle == @receipt_handle
18
+ end
19
+ end
20
+ end
@@ -1,6 +1,5 @@
1
1
  module Propono
2
2
  class QueueListener
3
-
4
3
  include Sqs
5
4
 
6
5
  def self.listen(topic_id, &message_processor)
@@ -29,10 +28,10 @@ module Propono
29
28
  if messages.empty?
30
29
  false
31
30
  else
32
- messages.each { |msg| process_sqs_message(msg) }
31
+ messages.each { |msg| process_raw_message(msg) }
33
32
  end
34
33
  rescue Excon::Errors::Forbidden
35
- Propono.config.logger.error "Forbidden error caught and re raised. #{queue_url}"
34
+ Propono.config.logger.error "Forbidden error caught and re-raised. #{queue_url}"
36
35
  Propono.config.logger.error $!
37
36
  raise $!
38
37
  rescue
@@ -40,22 +39,44 @@ module Propono
40
39
  Propono.config.logger.error $!
41
40
  end
42
41
 
43
- def process_sqs_message(sqs_message)
44
- body = JSON.parse(sqs_message["Body"])["Message"]
45
-
46
- # Legacy syntax is covered in the rescue statement
47
- # This begin/rescue dance and the rescue block will be removed in v1.
42
+ # The calls to delete_message are deliberately duplicated so
43
+ # as to ensure the message is only deleted if the preceeding line
44
+ # has completed succesfully. We do *not* want to ensure that the
45
+ # message is deleted regardless of what happens in this method.
46
+ def process_raw_message(raw_sqs_message)
48
47
  begin
49
- body = JSON.parse(body)
50
- context = body.symbolize_keys
51
- message = context.delete(:message)
52
- Propono.config.logger.info "Propono [#{context[:id]}]: Received from sqs."
53
- @message_processor.call(message, context)
54
- rescue JSON::ParserError, TypeError
55
- Propono.config.logger.info("Sending and recieving messages without ids is deprecated")
56
- @message_processor.call(body)
48
+ sqs_message = SqsMessage.new(raw_sqs_message)
49
+ Propono.config.logger.info "Propono [#{sqs_message.context[:id]}]: Received from sqs."
50
+
51
+ begin
52
+ process_message(sqs_message)
53
+ delete_message(raw_sqs_message)
54
+ rescue
55
+ move_to_failed_queue(sqs_message)
56
+ delete_message(raw_sqs_message)
57
+ end
58
+ rescue
59
+ move_to_corrupt_queue(raw_sqs_message)
60
+ delete_message(raw_sqs_message)
57
61
  end
58
- sqs.delete_message(queue_url, sqs_message['ReceiptHandle'])
62
+ end
63
+
64
+ def process_message(sqs_message)
65
+ @message_processor.call(sqs_message.message, sqs_message.context)
66
+ end
67
+
68
+ def move_to_corrupt_queue(raw_sqs_message)
69
+ QueueSubscription.create(@topic_id, queue_name_suffix: "-corrupt")
70
+ Propono.publish("#{@topic_id}-corrupt", raw_sqs_message)
71
+ end
72
+
73
+ def move_to_failed_queue(sqs_message)
74
+ QueueSubscription.create(@topic_id, queue_name_suffix: "-failed")
75
+ Propono.publish("#{@topic_id}-failed", sqs_message.message, id: sqs_message.context[:id])
76
+ end
77
+
78
+ def delete_message(raw_sqs_message)
79
+ sqs.delete_message(queue_url, raw_sqs_message['ReceiptHandle'])
59
80
  end
60
81
 
61
82
  def queue_url
@@ -1,3 +1,3 @@
1
1
  module Propono
2
- VERSION = "0.11.1"
2
+ VERSION = "1.0.0.rc1"
3
3
  end
@@ -16,9 +16,9 @@ module Propono
16
16
  end
17
17
  end
18
18
 
19
- # Wait a max of 30secs before failing the test
19
+ # Wait a max of 20secs before failing the test
20
20
  def wait_for_thread(thread)
21
- 300.times do |x|
21
+ 200.times do |x|
22
22
  return true unless thread.alive?
23
23
  sleep(0.1)
24
24
  end
@@ -4,8 +4,9 @@ module Propono
4
4
  class SnsToSqsTest < IntegrationTest
5
5
  def test_the_message_gets_there
6
6
  topic = "test-topic"
7
- text = "This is my message"
7
+ text = "This is my message #{DateTime.now} #{rand()}"
8
8
  flunks = []
9
+ message_received = false
9
10
 
10
11
  Propono.subscribe_by_queue(topic)
11
12
 
@@ -14,7 +15,7 @@ module Propono
14
15
  Propono.listen_to_queue(topic) do |message, context|
15
16
  flunks << "Wrong message" unless message == text
16
17
  flunks << "Wrong id" unless context[:id] =~ Regexp.new("[a-z0-9]{6}")
17
- break
18
+ message_received = true
18
19
  end
19
20
  rescue => e
20
21
  flunks << e.message
@@ -23,7 +24,13 @@ module Propono
23
24
  end
24
25
  end
25
26
 
26
- sleep(2) # Make sure the listener has started
27
+ Thread.new do
28
+ sleep(1) while !message_received
29
+ sleep(5) # Make sure all the message deletion clear up in the thread has happened
30
+ thread.terminate
31
+ end
32
+
33
+ sleep(1) # Make sure the listener has started
27
34
 
28
35
  Propono.publish(topic, text)
29
36
  flunks << "Test Timeout" unless wait_for_thread(thread)
@@ -4,8 +4,9 @@ module Propono
4
4
  class UdpToSqsTest < IntegrationTest
5
5
  def test_the_message_gets_there
6
6
  topic = "test-topic"
7
- message = "This is my message"
7
+ message = "This is my message #{DateTime.now} #{rand()}"
8
8
  flunks = []
9
+ message_received = false
9
10
 
10
11
  Propono.config.tcp_host = "localhost"
11
12
  Propono.config.tcp_port = 20009
@@ -17,6 +18,7 @@ module Propono
17
18
  Propono.listen_to_queue(topic) do |sqs_message|
18
19
  flunks << "Wrong message" unless message == sqs_message
19
20
  sqs_thread.terminate
21
+ message_received = true
20
22
  end
21
23
  rescue => e
22
24
  flunks << e.message
@@ -25,13 +27,19 @@ module Propono
25
27
  end
26
28
  end
27
29
 
30
+ Thread.new do
31
+ sleep(1) while !message_received
32
+ sleep(5) # Make sure all the message deletion clear up in the thread has happened
33
+ sqs_thread.terminate
34
+ end
35
+
28
36
  tcp_thread = Thread.new do
29
37
  Propono.listen_to_tcp do |tcp_topic, tcp_message|
30
38
  Propono.publish(tcp_topic, tcp_message)
31
39
  tcp_thread.terminate
32
40
  end
33
41
  end
34
- sleep(2) # Make sure the listener has started
42
+ sleep(1) # Make sure the listener has started
35
43
 
36
44
  Propono.publish(topic, message, protocol: :tcp)
37
45
  flunks << "Test Timeout" unless wait_for_thread(tcp_thread) && wait_for_thread(sqs_thread)
@@ -4,8 +4,9 @@ module Propono
4
4
  class UdpProxyTest < IntegrationTest
5
5
  def test_the_message_gets_there
6
6
  topic = "test-topic"
7
- text = "This is my message"
7
+ text = "This is my message #{DateTime.now} #{rand()}"
8
8
  flunks = []
9
+ message_received = false
9
10
 
10
11
  Propono.config.udp_port = 20001
11
12
 
@@ -16,7 +17,7 @@ module Propono
16
17
  Propono.listen_to_queue(topic) do |message, context|
17
18
  flunks << "Wrong message" unless text == message
18
19
  flunks << "Wrong id" unless context[:id] =~ Regexp.new("[a-z0-9]{6}-[a-z0-9]{6}")
19
- break
20
+ message_received = true
20
21
  end
21
22
  rescue => e
22
23
  flunks << e.message
@@ -25,11 +26,17 @@ module Propono
25
26
  end
26
27
  end
27
28
 
29
+ Thread.new do
30
+ sleep(1) while !message_received
31
+ sleep(5) # Make sure all the message deletion clear up in the thread has happened
32
+ sqs_thread.terminate
33
+ end
34
+
28
35
  udp_thread = Thread.new do
29
36
  Propono.proxy_udp
30
37
  end
31
38
 
32
- sleep(2) # Make sure the proxy has started
39
+ sleep(1) # Make sure the proxy has started
33
40
 
34
41
  Propono::Publisher.publish(topic, text, protocol: :udp)
35
42
  flunks << "Test timeout" unless wait_for_thread(sqs_thread)
@@ -4,8 +4,9 @@ module Propono
4
4
  class UdpToSqsTest < IntegrationTest
5
5
  def test_the_message_gets_there
6
6
  topic = "test-topic"
7
- message = "This is my message"
7
+ message = "This is my message #{DateTime.now} #{rand()}"
8
8
  flunks = []
9
+ message_received = false
9
10
 
10
11
  Propono.config.udp_port = 20002
11
12
 
@@ -16,6 +17,7 @@ module Propono
16
17
  Propono.listen_to_queue(topic) do |sqs_message|
17
18
  assert_equal message, sqs_message
18
19
  sqs_thread.terminate
20
+ message_received = true
19
21
  end
20
22
  rescue => e
21
23
  flunks << e.message
@@ -24,6 +26,12 @@ module Propono
24
26
  end
25
27
  end
26
28
 
29
+ Thread.new do
30
+ sleep(1) while !message_received
31
+ sleep(5) # Make sure all the message deletion clear up in the thread has happened
32
+ sqs_thread.terminate
33
+ end
34
+
27
35
  udp_thread = Thread.new do
28
36
  Propono.listen_to_udp do |udp_topic, udp_message|
29
37
  Propono.publish(udp_topic, udp_message)
@@ -31,7 +39,7 @@ module Propono
31
39
  end
32
40
  end
33
41
 
34
- sleep(2) # Make sure the listener has started
42
+ sleep(1) # Make sure the listener has started
35
43
 
36
44
  Propono.publish(topic, message, protocol: :udp)
37
45
  flunks << "Test Timeout" unless wait_for_thread(udp_thread) && wait_for_thread(sqs_thread)
@@ -57,14 +57,14 @@ module Propono
57
57
  end
58
58
 
59
59
  def test_read_messages_calls_process_message_for_each_msg
60
- @listener.expects(:process_sqs_message).with(@sqs_message1)
61
- @listener.expects(:process_sqs_message).with(@sqs_message2)
60
+ @listener.expects(:process_raw_message).with(@sqs_message1)
61
+ @listener.expects(:process_raw_message).with(@sqs_message2)
62
62
  @listener.send(:read_messages)
63
63
  end
64
64
 
65
65
  def test_read_messages_does_not_call_process_messages_if_there_are_none
66
66
  @sqs_response.stubs(body: {"Message" => []})
67
- @listener.expects(:process_sqs_message).never
67
+ @listener.expects(:process_message).never
68
68
  @listener.send(:read_messages)
69
69
  end
70
70
 
@@ -79,7 +79,7 @@ module Propono
79
79
  def test_forbidden_error_is_logged_and_re_raised
80
80
  @listener.stubs(queue_url: "http://example.com")
81
81
  @sqs.stubs(:receive_message).raises(Excon::Errors::Forbidden.new(nil, nil, nil))
82
- Propono.config.logger.expects(:error).with("Forbidden error caught and re raised. http://example.com")
82
+ Propono.config.logger.expects(:error).with("Forbidden error caught and re-raised. http://example.com")
83
83
  Propono.config.logger.expects(:error).with() {|x| x.is_a?(Excon::Errors::Forbidden)}
84
84
  assert_raises Excon::Errors::Forbidden do
85
85
  @listener.send(:read_messages)
@@ -123,50 +123,47 @@ module Propono
123
123
  @listener.send(:read_messages)
124
124
  end
125
125
 
126
- def test_messages_are_not_deleted_if_there_is_an_exception
126
+ def test_messages_are_deleted_if_there_is_an_exception_processing
127
+ queue_url = "test-queue-url"
128
+
129
+ @sqs.expects(:delete_message).with(queue_url, @receipt_handle1)
130
+ @sqs.expects(:delete_message).with(queue_url, @receipt_handle2)
131
+
127
132
  @listener = QueueListener.new(@topic_id) { raise StandardError.new("Test Error") }
133
+ @listener.stubs(queue_url: queue_url)
128
134
  @listener.stubs(sqs: @sqs)
129
- @sqs.expects(:delete_message).never
130
135
  @listener.send(:read_messages)
131
136
  end
132
- end
133
- class QueueListenerLegacySyntaxTest < Minitest::Test
134
-
135
- def setup
136
- super
137
- @topic_id = "some-topic"
138
-
139
- @receipt_handle1 = "test-receipt-handle1"
140
- @receipt_handle2 = "test-receipt-handle2"
141
- @message1 = {'cat' => "Foobar 123"}
142
- @message2 = "qwertyuiop"
143
- @sqs_message1 = { "ReceiptHandle" => @receipt_handle1, "Body" => {"Message" => @message1}.to_json}
144
- @sqs_message2 = { "ReceiptHandle" => @receipt_handle2, "Body" => {"Message" => @message2}.to_json}
145
- @messages = { "Message" => [ @sqs_message1, @sqs_message2 ] }
146
- @sqs_response = mock().tap{|m|m.stubs(body: @messages)}
147
- @sqs = mock()
148
- @sqs.stubs(receive_message: @sqs_response)
149
- @sqs.stubs(:delete_message)
150
137
 
151
- @listener = QueueListener.new(@topic_id) {}
138
+ def test_messages_are_moved_to_failed_queue_if_there_is_an_exception
139
+ @listener = QueueListener.new(@topic_id) { raise StandardError.new("Test Error") }
140
+ @listener.expects(:move_to_failed_queue).with(SqsMessage.new(@sqs_message1))
141
+ @listener.expects(:move_to_failed_queue).with(SqsMessage.new(@sqs_message2))
152
142
  @listener.stubs(sqs: @sqs)
143
+ @listener.send(:read_messages)
153
144
  end
154
145
 
155
- def test_old_syntax_has_deprecation_warning
156
- Propono.config.logger.expects(:info).with("Sending and recieving messages without ids is deprecated")
157
- @listener.stubs(sqs: @sqs)
146
+ def test_messages_are_moved_to_corrupt_queue_if_there_is_an_parsing_exception
147
+ sqs_message1 = "foobar"
148
+ sqs_message2 = "barfoo"
149
+ @messages["Message"][0] = sqs_message1
150
+ @messages["Message"][1] = sqs_message2
151
+
152
+ @listener.expects(:move_to_corrupt_queue).with(sqs_message1)
153
+ @listener.expects(:move_to_corrupt_queue).with(sqs_message2)
158
154
  @listener.send(:read_messages)
159
155
  end
160
156
 
161
- def test_each_message_processor_is_yielded
162
- messages_yielded = []
163
- @listener = QueueListener.new(@topic_id) { |m| messages_yielded.push(m) }
164
- @listener.stubs(sqs: @sqs)
165
- @listener.send(:read_messages)
157
+ def test_move_to_failed_queue
158
+ QueueSubscription.expects(:create).with(@topic_id, queue_name_suffix: "-failed")
159
+ Propono.expects(:publish).with("#{@topic_id}-failed", @message1, id: @message1_id)
160
+ @listener.send(:move_to_failed_queue, SqsMessage.new(@sqs_message1))
161
+ end
166
162
 
167
- assert_equal messages_yielded.size, 2
168
- assert messages_yielded.include?(@message1)
169
- assert messages_yielded.include?(@message2)
163
+ def test_move_to_corrupt_queue
164
+ QueueSubscription.expects(:create).with(@topic_id, queue_name_suffix: "-corrupt")
165
+ Propono.expects(:publish).with("#{@topic_id}-corrupt", @sqs_message1)
166
+ @listener.send(:move_to_corrupt_queue, @sqs_message1)
170
167
  end
171
168
  end
172
169
  end
@@ -5,7 +5,7 @@ module Propono
5
5
 
6
6
  def test_subscribe_by_queue_calls_queue_subscriber
7
7
  subscriber = QueueSubscription.new("topic")
8
- QueueSubscription.expects(:new).with("topic").returns(subscriber)
8
+ QueueSubscription.expects(:new).with("topic", {}).returns(subscriber)
9
9
  QueueSubscription.any_instance.expects(:create)
10
10
  Subscriber.subscribe_by_queue("topic")
11
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: propono
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 1.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - MalcyL
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-12-09 00:00:00.000000000 Z
12
+ date: 2013-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fog
@@ -118,6 +118,7 @@ files:
118
118
  - lib/propono/components/queue_subscription.rb
119
119
  - lib/propono/components/sns.rb
120
120
  - lib/propono/components/sqs.rb
121
+ - lib/propono/components/sqs_message.rb
121
122
  - lib/propono/components/topic.rb
122
123
  - lib/propono/configuration.rb
123
124
  - lib/propono/helpers/hash.rb
@@ -172,9 +173,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
172
173
  version: '0'
173
174
  required_rubygems_version: !ruby/object:Gem::Requirement
174
175
  requirements:
175
- - - '>='
176
+ - - '>'
176
177
  - !ruby/object:Gem::Version
177
- version: '0'
178
+ version: 1.3.1
178
179
  requirements: []
179
180
  rubyforge_project:
180
181
  rubygems_version: 2.0.3