sneakers 2.5.0 → 2.6.0

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
2
  SHA1:
3
- metadata.gz: 1e8f7e984a402f29c32f62b919a5ba0be9db25c1
4
- data.tar.gz: 9502698889301eacdfa4fa94d9b7ae575e407ca7
3
+ metadata.gz: c52a398aecf272a36fcb3431f7b242a0d36dc0d1
4
+ data.tar.gz: c5974b0f7823f4d1e91c99d88adc3bbe50a2e572
5
5
  SHA512:
6
- metadata.gz: 842cabff8a0f3c2375533061b32a665d9e81102e53fee32eceb96f14d3126c78499a29e80efb0f247b10ed4de9fdb52f8aea57e7ccf65a7641ac020a28ab804c
7
- data.tar.gz: 17e5b565b4162c7bbf4ddeffc8801309291c00684fe1c13914b0bae64bc81dce50772ce9874971cdabee462afe506a10b99f751636fc38d0ddc8e13080813ad9
6
+ metadata.gz: 85bc282b42260ebb6a08fdab01fc2396a30450f3cabb93bed27ccac801bc216e5e139613349cfb874a9c5c2a453234591759671de64c9f7654ddd5076384f746
7
+ data.tar.gz: 06fd1978121e6e59971152fd202e1cb2d5b13225d0f08f75583a5d021b0cfb8d52768102a007cef7d1dc191ed9264505a9e4157987054bc9631ee3eaaf749605
data/.travis.yml CHANGED
@@ -5,8 +5,10 @@ sudo: false
5
5
  cache: bundler
6
6
  language: ruby
7
7
  rvm:
8
- - ruby-head
9
- - 2.2.5
8
+ - ruby-head
9
+ - "2.4.1"
10
+ - "2.3.4"
11
+ - "2.2.7"
10
12
  matrix:
11
13
  include:
12
14
  - rvm: 2.2.5
@@ -25,8 +25,8 @@ class MetricsWorker
25
25
  logger.info "FOUND <#{title}>"
26
26
  ack!
27
27
  end
28
-
29
- add_transaction_tracer :work, name: 'MetricsWorker', params: 'args[0]'
28
+
29
+ add_transaction_tracer :work, name: 'MetricsWorker', params: 'args[0]', category: :task
30
30
 
31
31
  private
32
32
 
@@ -36,10 +36,5 @@ class MetricsWorker
36
36
  end
37
37
  end
38
38
 
39
-
40
39
  r = Sneakers::Runner.new([ MetricsWorker ])
41
40
  r.run
42
-
43
-
44
-
45
-
data/lib/sneakers.rb CHANGED
@@ -1,5 +1,5 @@
1
- require "sneakers/version"
2
- require 'thread/pool'
1
+ require 'sneakers/version'
2
+ require 'concurrent/executors'
3
3
  require 'bunny'
4
4
  require 'logger'
5
5
  require 'serverengine'
@@ -17,6 +17,7 @@ require 'sneakers/support/production_formatter'
17
17
  require 'sneakers/concerns/logging'
18
18
  require 'sneakers/concerns/metrics'
19
19
  require 'sneakers/handlers/oneshot'
20
+ require 'sneakers/content_type'
20
21
  require 'sneakers/worker'
21
22
  require 'sneakers/publisher'
22
23
 
@@ -0,0 +1,47 @@
1
+ module Sneakers
2
+ class ContentType
3
+ def self.register(content_type: nil, serializer: nil, deserializer: nil)
4
+ # This can be removed when support is dropped for ruby 2.0 and replaced
5
+ # by a keyword arg with no default value
6
+ fail ArgumentError, 'missing keyword: content_type' if content_type.nil?
7
+ fail ArgumentError, 'missing keyword: serializer' if serializer.nil?
8
+ fail ArgumentError, 'missing keyword: deserializer' if deserializer.nil?
9
+
10
+ fail ArgumentError, "#{content_type} serializer must be a proc" unless serializer.is_a? Proc
11
+ fail ArgumentError, "#{content_type} deserializer must be a proc" unless deserializer.is_a? Proc
12
+
13
+ fail ArgumentError, "#{content_type} serializer must accept one argument, the payload" unless serializer.arity == 1
14
+ fail ArgumentError, "#{content_type} deserializer must accept one argument, the payload" unless deserializer.arity == 1
15
+ @_types[content_type] = new(serializer, deserializer)
16
+ end
17
+
18
+ def self.serialize(payload, content_type)
19
+ return payload unless content_type
20
+ @_types[content_type].serializer.(payload)
21
+ end
22
+
23
+ def self.deserialize(payload, content_type)
24
+ return payload unless content_type
25
+ @_types[content_type].deserializer.(payload)
26
+ end
27
+
28
+ def self.reset!
29
+ @_types = Hash.new(
30
+ new(passthrough, passthrough)
31
+ )
32
+ end
33
+
34
+ def self.passthrough
35
+ ->(payload) { payload }
36
+ end
37
+
38
+ def initialize(serializer, deserializer)
39
+ @serializer = serializer
40
+ @deserializer = deserializer
41
+ end
42
+
43
+ attr_reader :serializer, :deserializer
44
+
45
+ reset!
46
+ end
47
+ end
@@ -135,7 +135,7 @@ module Sneakers
135
135
  "#{log_prefix} msg=failing, retry_count=#{num_attempts}, reason=#{reason}"
136
136
  end
137
137
  data = {
138
- error: reason,
138
+ error: reason.to_s,
139
139
  num_attempts: num_attempts,
140
140
  failed_at: Time.now.iso8601,
141
141
  payload: Base64.encode64(msg.to_s),
@@ -12,7 +12,7 @@ module Sneakers
12
12
  to_queue = options.delete(:to_queue)
13
13
  options[:routing_key] ||= to_queue
14
14
  Sneakers.logger.info {"publishing <#{msg}> to [#{options[:routing_key]}]"}
15
- @exchange.publish(msg, options)
15
+ @exchange.publish(ContentType.serialize(msg, options[:content_type]), options)
16
16
  end
17
17
 
18
18
 
@@ -34,8 +34,10 @@ module Sneakers
34
34
  end
35
35
 
36
36
  def create_bunny_connection
37
- Bunny.new(@opts[:amqp], :vhost => @opts[:vhost], :heartbeat => @opts[:heartbeat], :logger => Sneakers::logger)
37
+ Bunny.new(@opts[:amqp], :vhost => @opts[:vhost],
38
+ :heartbeat => @opts[:heartbeat],
39
+ :properties => @opts.fetch(:properties, {}),
40
+ :logger => Sneakers::logger)
38
41
  end
39
42
  end
40
43
  end
41
-
@@ -56,13 +56,26 @@ class Sneakers::Queue
56
56
  end
57
57
 
58
58
  def unsubscribe
59
- # XXX can we cancel bunny and channel too?
60
- @consumer.cancel if @consumer
61
- @consumer = nil
59
+ return unless @consumer
60
+
61
+ # TODO: should we simply close the channel here?
62
+ Sneakers.logger.info("Queue: will try to cancel consumer #{@consumer.inspect}")
63
+ cancel_ok = @consumer.cancel
64
+ if cancel_ok
65
+ Sneakers.logger.info "Queue: consumer #{cancel_ok.consumer_tag} cancelled"
66
+ @consumer = nil
67
+ else
68
+ Sneakers.logger.warn "Queue: could not cancel consumer #{@consumer.inspect}"
69
+ sleep(1)
70
+ unsubscribe
71
+ end
62
72
  end
63
73
 
64
74
  def create_bunny_connection
65
- Bunny.new(@opts[:amqp], :vhost => @opts[:vhost], :heartbeat => @opts[:heartbeat], :logger => Sneakers::logger)
75
+ Bunny.new(@opts[:amqp], :vhost => @opts[:vhost],
76
+ :heartbeat => @opts[:heartbeat],
77
+ :properties => @opts.fetch(:properties, {}),
78
+ :logger => Sneakers::logger)
66
79
  end
67
80
  private :create_bunny_connection
68
81
  end
@@ -8,6 +8,7 @@ module Sneakers
8
8
  unless File.exists?(worker_group_config_file)
9
9
  puts "No worker group file found."
10
10
  puts "Specify via ENV 'WORKER_GROUP_CONFIG' or by convention ./config/sneaker_worker_groups.yml"
11
+ Kernel.exit(1)
11
12
  end
12
13
  @pids = []
13
14
  @exec_string = "bundle exec rake sneakers:run"
@@ -1,3 +1,3 @@
1
1
  module Sneakers
2
- VERSION = "2.5.0"
2
+ VERSION = "2.6.0"
3
3
  end
@@ -19,8 +19,9 @@ module Sneakers
19
19
 
20
20
  @should_ack = opts[:ack]
21
21
  @timeout_after = opts[:timeout_job_after]
22
- @pool = pool || Thread.pool(opts[:threads]) # XXX config threads
22
+ @pool = pool || Concurrent::FixedThreadPool.new(opts[:threads])
23
23
  @call_with_params = respond_to?(:work_with_params)
24
+ @content_type = opts[:content_type]
24
25
 
25
26
  @queue = queue || Sneakers::Queue.new(
26
27
  queue_name,
@@ -39,13 +40,13 @@ module Sneakers
39
40
  to_queue = opts.delete(:to_queue)
40
41
  opts[:routing_key] ||= to_queue
41
42
  return unless opts[:routing_key]
42
- @queue.exchange.publish(msg, opts)
43
+ @queue.exchange.publish(Sneakers::ContentType.serialize(msg, opts[:content_type]), opts)
43
44
  end
44
45
 
45
46
  def do_work(delivery_info, metadata, msg, handler)
46
47
  worker_trace "Working off: #{msg.inspect}"
47
48
 
48
- @pool.process do
49
+ @pool.post do
49
50
  res = nil
50
51
  error = nil
51
52
 
@@ -53,10 +54,11 @@ module Sneakers
53
54
  metrics.increment("work.#{self.class.name}.started")
54
55
  Timeout.timeout(@timeout_after, WorkerTimeout) do
55
56
  metrics.timing("work.#{self.class.name}.time") do
57
+ deserialized_msg = ContentType.deserialize(msg, @content_type || metadata && metadata[:content_type])
56
58
  if @call_with_params
57
- res = work_with_params(msg, delivery_info, metadata)
59
+ res = work_with_params(deserialized_msg, delivery_info, metadata)
58
60
  else
59
- res = work(msg)
61
+ res = work(deserialized_msg)
60
62
  end
61
63
  end
62
64
  end
@@ -91,14 +93,15 @@ module Sneakers
91
93
  end
92
94
 
93
95
  metrics.increment("work.#{self.class.name}.ended")
94
- end #process
96
+ end #post
95
97
  end
96
98
 
97
99
  def stop
98
- worker_trace "Stopping worker: shutting down thread pool."
99
- @pool.shutdown
100
100
  worker_trace "Stopping worker: unsubscribing."
101
101
  @queue.unsubscribe
102
+ worker_trace "Stopping worker: shutting down thread pool."
103
+ @pool.shutdown
104
+ @pool.wait_for_termination
102
105
  worker_trace "Stopping worker: I'm gone."
103
106
  end
104
107
 
@@ -135,6 +138,7 @@ module Sneakers
135
138
 
136
139
  def enqueue(msg, opts={})
137
140
  opts[:routing_key] ||= @queue_opts[:routing_key]
141
+ opts[:content_type] ||= @queue_opts[:content_type]
138
142
  opts[:to_queue] ||= @queue_name
139
143
 
140
144
  publisher.publish(msg, opts)
@@ -21,7 +21,7 @@ module Sneakers
21
21
 
22
22
  # Allocate single thread pool if share_threads is set. This improves load balancing
23
23
  # when used with many workers.
24
- pool = config[:share_threads] ? Thread.pool(config[:threads]) : nil
24
+ pool = config[:share_threads] ? Concurrent::FixedThreadPool.new(config[:threads]) : nil
25
25
 
26
26
  worker_classes = config[:worker_classes]
27
27
 
@@ -29,7 +29,13 @@ module Sneakers
29
29
  worker_classes = worker_classes.call
30
30
  end
31
31
 
32
- @workers = worker_classes.map{|w| w.new(nil, pool) }
32
+ # if we don't provide a connection to a worker,
33
+ # the queue used in the worker will create a new one
34
+ # so if we want to have a shared bunny connection for the workers
35
+ # we must create it here
36
+ bunny_connection = create_connection_or_nil
37
+
38
+ @workers = worker_classes.map{|w| w.new(nil, pool, {connection: bunny_connection}) }
33
39
  # if more than one worker this should be per worker
34
40
  # accumulate clients and consumers as well
35
41
  @workers.each do |worker|
@@ -52,5 +58,13 @@ module Sneakers
52
58
  @stop_flag.set!
53
59
  end
54
60
 
61
+ def create_bunny_connection
62
+ Bunny.new(Sneakers::CONFIG[:amqp], :vhost => Sneakers::CONFIG[:vhost], :heartbeat => Sneakers::CONFIG[:heartbeat], :logger => Sneakers::logger)
63
+ end
64
+
65
+ def create_connection_or_nil
66
+ config[:share_bunny_connection] ? create_bunny_connection : nil
67
+ end
68
+ private :create_bunny_connection, :create_connection_or_nil
55
69
  end
56
70
  end
data/sneakers.gemspec CHANGED
@@ -11,6 +11,7 @@ Gem::Specification.new do |gem|
11
11
  gem.description = %q( Fast background processing framework for Ruby and RabbitMQ )
12
12
  gem.summary = %q( Fast background processing framework for Ruby and RabbitMQ )
13
13
  gem.homepage = 'http://sneakers.io'
14
+ gem.license = 'MIT'
14
15
  gem.required_ruby_version = Gem::Requirement.new(">= 2.0")
15
16
 
16
17
  gem.files = `git ls-files`.split($/).reject { |f| f == 'Gemfile.lock' }
@@ -18,8 +19,8 @@ Gem::Specification.new do |gem|
18
19
  gem.test_files = gem.files.grep(/^(test|spec|features)\//)
19
20
  gem.require_paths = ['lib']
20
21
  gem.add_dependency 'serverengine', '~> 1.5.11'
21
- gem.add_dependency 'bunny', '~> 2.6.4'
22
- gem.add_dependency 'thread', '~> 0.1.7'
22
+ gem.add_dependency 'bunny', '~> 2.7.0'
23
+ gem.add_dependency 'concurrent-ruby', '~> 1.0'
23
24
  gem.add_dependency 'thor'
24
25
 
25
26
  # for integration environment (see .travis.yml and integration_spec)
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+ require 'sneakers/content_type'
3
+ require 'base64'
4
+
5
+ describe Sneakers::ContentType do
6
+ after do
7
+ Sneakers::ContentType.reset!
8
+ end
9
+
10
+ describe '.deserialize' do
11
+ it 'uses the given deserializer' do
12
+ Sneakers::ContentType.register(
13
+ content_type: 'application/json',
14
+ serializer: ->(_) {},
15
+ deserializer: ->(payload) { JSON.parse(payload) },
16
+ )
17
+
18
+ Sneakers::ContentType.deserialize('{"foo":"bar"}', 'application/json').must_equal('foo' => 'bar')
19
+ end
20
+ end
21
+
22
+ describe '.serialize' do
23
+ it 'uses the given serializer' do
24
+ Sneakers::ContentType.register(
25
+ content_type: 'application/json',
26
+ serializer: ->(payload) { JSON.dump(payload) },
27
+ deserializer: ->(_) {},
28
+ )
29
+
30
+ Sneakers::ContentType.serialize({ 'foo' => 'bar' }, 'application/json').must_equal('{"foo":"bar"}')
31
+ end
32
+
33
+ it 'passes the payload through by default' do
34
+ payload = "just some text"
35
+ Sneakers::ContentType.serialize(payload, 'unknown/type').must_equal(payload)
36
+ Sneakers::ContentType.deserialize(payload, 'unknown/type').must_equal(payload)
37
+ Sneakers::ContentType.serialize(payload, nil).must_equal(payload)
38
+ Sneakers::ContentType.deserialize(payload, nil).must_equal(payload)
39
+ end
40
+
41
+ it 'passes the payload through if type not found' do
42
+ Sneakers::ContentType.register(content_type: 'found/type', serializer: ->(_) {}, deserializer: ->(_) {})
43
+ payload = "just some text"
44
+
45
+ Sneakers::ContentType.serialize(payload, 'unknown/type').must_equal(payload)
46
+ Sneakers::ContentType.deserialize(payload, 'unknown/type').must_equal(payload)
47
+ end
48
+ end
49
+
50
+ describe '.register' do
51
+ it 'provides a mechnism to register a given type' do
52
+ Sneakers::ContentType.register(
53
+ content_type: 'text/base64',
54
+ serializer: ->(payload) { Base64.encode64(payload) },
55
+ deserializer: ->(payload) { Base64.decode64(payload) },
56
+ )
57
+
58
+ ct = Sneakers::ContentType
59
+ ct.deserialize(ct.serialize('hello world', 'text/base64'), 'text/base64').must_equal('hello world')
60
+ end
61
+
62
+ it 'requires a content type' do
63
+ proc { Sneakers::ContentType.register(serializer: -> { }, deserializer: -> { }) }.must_raise ArgumentError
64
+ end
65
+
66
+ it 'expects serializer and deserializer to be present' do
67
+ proc { Sneakers::ContentType.register(content_type: 'foo', deserializer: -> { }) }.must_raise ArgumentError
68
+ proc { Sneakers::ContentType.register(content_type: 'foo', serializer: -> { }) }.must_raise ArgumentError
69
+ end
70
+
71
+ it 'expects serializer and deserializer to be a proc' do
72
+ proc { Sneakers::ContentType.register(content_type: 'foo', serializer: 'not a proc', deserializer: ->(_) { }) }.must_raise ArgumentError
73
+ proc { Sneakers::ContentType.register(content_type: 'foo', serializer: ->(_) {}, deserializer: 'not a proc' ) }.must_raise ArgumentError
74
+ end
75
+
76
+ it 'expects serializer and deserializer to have the correct arity' do
77
+ proc { Sneakers::ContentType.register(content_type: 'foo', serializer: ->(_,_) {}, deserializer: ->(_) {}) }.must_raise ArgumentError
78
+ proc { Sneakers::ContentType.register(content_type: 'foo', serializer: ->(_) {}, deserializer: ->() {} ) }.must_raise ArgumentError
79
+ end
80
+ end
81
+ end
@@ -65,7 +65,7 @@ describe "integration" do
65
65
  return
66
66
  end
67
67
  end
68
-
68
+
69
69
  integration_log "failed test. killing off workers."
70
70
  Process.kill("TERM", pid)
71
71
  sleep 1
@@ -85,6 +85,44 @@ describe "integration" do
85
85
  pid
86
86
  end
87
87
 
88
+ def any_consumers
89
+ rmq_addr = compose_or_localhost("rabbitmq")
90
+ result = false
91
+ begin
92
+ admin = RabbitMQ::HTTP::Client.new("http://#{rmq_addr}:15672/", username: "guest", password: "guest")
93
+ qs = admin.list_queues
94
+ qs.each do |q|
95
+ if q.name.start_with? 'integration_'
96
+ puts "We see #{q.consumers} consumers on #{q.name}"
97
+ return true if q.consumers > 0
98
+ end
99
+ end
100
+ return false
101
+ rescue
102
+ puts "Rabbitmq admin seems to not exist? you better be running this on Travis or Docker. proceeding.\n#{$!}"
103
+ end
104
+ end
105
+
106
+ it 'should be possible to terminate when queue is full' do
107
+ job_count = 40000
108
+
109
+ pid = start_worker(IntegrationWorker)
110
+ Process.kill("TERM", pid)
111
+
112
+ integration_log "publishing #{job_count} messages..."
113
+ p = Sneakers::Publisher.new
114
+ job_count.times do |i|
115
+ p.publish("m #{i}", to_queue: IntegrationWorker.queue_name)
116
+ end
117
+
118
+ pid = start_worker(IntegrationWorker)
119
+ any_consumers.must_equal true
120
+ integration_log "Killing #{pid} now!"
121
+ Process.kill("TERM", pid)
122
+ sleep(2)
123
+ any_consumers.must_equal false
124
+ end
125
+
88
126
  it 'should pull down 100 jobs from a real queue' do
89
127
  job_count = 100
90
128
 
@@ -95,7 +133,7 @@ describe "integration" do
95
133
  job_count.times do |i|
96
134
  p.publish("m #{i}", to_queue: IntegrationWorker.queue_name)
97
135
  end
98
-
136
+
99
137
  assert_all_accounted_for(
100
138
  queue: IntegrationWorker.queue_name,
101
139
  pid: pid,
@@ -103,8 +141,8 @@ describe "integration" do
103
141
  jobs: job_count,
104
142
  )
105
143
  end
106
-
144
+
107
145
  end
108
146
  end
109
-
147
+
110
148
 
@@ -1,5 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'sneakers'
3
+ require 'serverengine'
3
4
 
4
5
  describe Sneakers::Publisher do
5
6
  let :pub_vars do
@@ -75,6 +76,7 @@ describe Sneakers::Publisher do
75
76
  exchange: 'another_exchange',
76
77
  exchange_options: { :type => :topic, :arguments => { 'x-arg' => 'value' } },
77
78
  log: logger,
79
+ properties: { key: "value" },
78
80
  durable: false)
79
81
 
80
82
  channel = Object.new
@@ -86,7 +88,7 @@ describe Sneakers::Publisher do
86
88
  mock(bunny).start
87
89
  mock(bunny).create_channel { channel }
88
90
 
89
- mock(Bunny).new('amqp://someuser:somepassword@somehost:5672', heartbeat: 1, vhost: '/', logger: logger) { bunny }
91
+ mock(Bunny).new('amqp://someuser:somepassword@somehost:5672', heartbeat: 1, vhost: '/', logger: logger, properties: { key: "value" }) { bunny }
90
92
 
91
93
  p = Sneakers::Publisher.new
92
94
 
@@ -122,5 +124,24 @@ describe Sneakers::Publisher do
122
124
  p.publish('test msg', my_vars)
123
125
  p.instance_variable_get(:@bunny).must_equal existing_session
124
126
  end
127
+
128
+ it 'should publish using the content type serializer' do
129
+ Sneakers::ContentType.register(
130
+ content_type: 'application/json',
131
+ serializer: ->(payload) { JSON.dump(payload) },
132
+ deserializer: ->(_) {},
133
+ )
134
+
135
+ xchg = Object.new
136
+ mock(xchg).publish('{"foo":"bar"}', routing_key: 'downloads', content_type: 'application/json')
137
+
138
+ p = Sneakers::Publisher.new
139
+ p.instance_variable_set(:@exchange, xchg)
140
+
141
+ mock(p).ensure_connection! {}
142
+ p.publish({ 'foo' => 'bar' }, to_queue: 'downloads', content_type: 'application/json')
143
+
144
+ Sneakers::ContentType.reset!
145
+ end
125
146
  end
126
147
  end
@@ -36,12 +36,7 @@ class HandlerTestWorker
36
36
  end
37
37
  end
38
38
 
39
- class TestPool
40
- def process(*args,&block)
41
- block.call
42
- end
43
- end
44
-
39
+ TestPool ||= Concurrent::ImmediateExecutor
45
40
 
46
41
  describe 'Handlers' do
47
42
  let(:channel) { Object.new }
@@ -1,6 +1,7 @@
1
1
  require 'spec_helper'
2
2
  require 'sneakers'
3
3
  require 'timeout'
4
+ require 'serverengine'
4
5
 
5
6
  class DummyWorker
6
7
  include Sneakers::Worker
@@ -75,7 +76,16 @@ class PublishingWorker
75
76
  end
76
77
  end
77
78
 
79
+ class JSONPublishingWorker
80
+ include Sneakers::Worker
81
+ from_queue 'defaults',
82
+ :ack => false,
83
+ :exchange => 'foochange'
78
84
 
85
+ def work(msg)
86
+ publish msg, :to_queue => 'target', :content_type => 'application/json'
87
+ end
88
+ end
79
89
 
80
90
  class LoggingWorker
81
91
  include Sneakers::Worker
@@ -87,6 +97,15 @@ class LoggingWorker
87
97
  end
88
98
  end
89
99
 
100
+ class JSONWorker
101
+ include Sneakers::Worker
102
+ from_queue 'defaults',
103
+ :ack => false,
104
+ :content_type => 'application/json'
105
+
106
+ def work(msg)
107
+ end
108
+ end
90
109
 
91
110
  class MetricsWorker
92
111
  include Sneakers::Worker
@@ -123,11 +142,7 @@ class WithDeprecatedExchangeOptionsWorker
123
142
  end
124
143
  end
125
144
 
126
- class TestPool
127
- def process(*args,&block)
128
- block.call
129
- end
130
- end
145
+ TestPool ||= Concurrent::ImmediateExecutor
131
146
 
132
147
  def with_test_queuefactory(ctx, ack=true, msg=nil, nowork=false)
133
148
  qf = Object.new
@@ -164,7 +179,8 @@ describe Sneakers::Worker do
164
179
  mock(Sneakers::Publisher).new(DummyWorker.queue_opts) do
165
180
  mock(Object.new).publish(message, {
166
181
  :routing_key => 'test.routing.key',
167
- :to_queue => 'downloads'
182
+ :to_queue => 'downloads',
183
+ :content_type => nil,
168
184
  })
169
185
  end
170
186
 
@@ -330,6 +346,38 @@ describe Sneakers::Worker do
330
346
  w.do_work(nil, nil, "msg", nil)
331
347
  end
332
348
 
349
+ describe 'content type based deserialization' do
350
+ before do
351
+ Sneakers::ContentType.register(
352
+ content_type: 'application/json',
353
+ serializer: ->(_) {},
354
+ deserializer: ->(payload) { JSON.parse(payload) },
355
+ )
356
+ end
357
+
358
+ after do
359
+ Sneakers::ContentType.reset!
360
+ end
361
+
362
+ it 'should use the registered deserializer if the content type is in the metadata' do
363
+ w = DummyWorker.new(@queue, TestPool.new)
364
+ mock(w).work({'foo' => 'bar'}).once
365
+ w.do_work(nil, { content_type: 'application/json' }, '{"foo":"bar"}', nil)
366
+ end
367
+
368
+ it 'should use the registered deserializer if the content type is in the queue options' do
369
+ w = JSONWorker.new(@queue, TestPool.new)
370
+ mock(w).work({'foo' => 'bar'}).once
371
+ w.do_work(nil, {}, '{"foo":"bar"}', nil)
372
+ end
373
+
374
+ it 'should use the deserializer from the queue options even if the metadata has a different content type' do
375
+ w = JSONWorker.new(@queue, TestPool.new)
376
+ mock(w).work({'foo' => 'bar'}).once
377
+ w.do_work(nil, { content_type: 'not/real' }, '{"foo":"bar"}', nil)
378
+ end
379
+ end
380
+
333
381
  it "should catch runtime exceptions from a bad work" do
334
382
  w = AcksWorker.new(@queue, TestPool.new)
335
383
  mock(w).work("msg").once{ raise "foo" }
@@ -432,6 +480,27 @@ describe Sneakers::Worker do
432
480
  mock(@exchange).publish('msg', :routing_key => 'target', :expiration => 1).once
433
481
  w.publish 'msg', :to_queue => 'target', :expiration => 1
434
482
  end
483
+
484
+ describe 'content_type based serialization' do
485
+ before do
486
+ Sneakers::ContentType.register(
487
+ content_type: 'application/json',
488
+ serializer: ->(payload) { JSON.dump(payload) },
489
+ deserializer: ->(_) {},
490
+ )
491
+ end
492
+
493
+ after do
494
+ Sneakers::ContentType.reset!
495
+ end
496
+
497
+ it 'should be able to publish a message from working context' do
498
+ w = JSONPublishingWorker.new(@queue, TestPool.new)
499
+ mock(@exchange).publish('{"foo":"bar"}', :routing_key => 'target', :content_type => 'application/json').once
500
+ w.do_work(nil, {}, {'foo' => 'bar'}, nil)
501
+ end
502
+ end
503
+
435
504
  end
436
505
 
437
506
 
data/spec/spec_helper.rb CHANGED
@@ -11,7 +11,7 @@ require 'rr'
11
11
 
12
12
  def compose_or_localhost(key)
13
13
  Resolv::DNS.new.getaddress(key)
14
- rescue
14
+ rescue
15
15
  "localhost"
16
16
  end
17
17
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sneakers
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dotan Nahum
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-01 00:00:00.000000000 Z
11
+ date: 2017-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: serverengine
@@ -30,28 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.6.4
33
+ version: 2.7.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.6.4
40
+ version: 2.7.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: thread
42
+ name: concurrent-ruby
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.1.7
47
+ version: '1.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.1.7
54
+ version: '1.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: thor
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -267,6 +267,7 @@ files:
267
267
  - lib/sneakers/concerns/logging.rb
268
268
  - lib/sneakers/concerns/metrics.rb
269
269
  - lib/sneakers/configuration.rb
270
+ - lib/sneakers/content_type.rb
270
271
  - lib/sneakers/error_reporter.rb
271
272
  - lib/sneakers/errors.rb
272
273
  - lib/sneakers/handlers/maxretry.rb
@@ -294,6 +295,7 @@ files:
294
295
  - spec/sneakers/concerns/logging_spec.rb
295
296
  - spec/sneakers/concerns/metrics_spec.rb
296
297
  - spec/sneakers/configuration_spec.rb
298
+ - spec/sneakers/content_type_spec.rb
297
299
  - spec/sneakers/integration_spec.rb
298
300
  - spec/sneakers/publisher_spec.rb
299
301
  - spec/sneakers/queue_spec.rb
@@ -304,7 +306,8 @@ files:
304
306
  - spec/sneakers/worker_spec.rb
305
307
  - spec/spec_helper.rb
306
308
  homepage: http://sneakers.io
307
- licenses: []
309
+ licenses:
310
+ - MIT
308
311
  metadata: {}
309
312
  post_install_message:
310
313
  rdoc_options: []
@@ -322,7 +325,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
322
325
  version: '0'
323
326
  requirements: []
324
327
  rubyforge_project:
325
- rubygems_version: 2.4.5
328
+ rubygems_version: 2.6.11
326
329
  signing_key:
327
330
  specification_version: 4
328
331
  summary: Fast background processing framework for Ruby and RabbitMQ
@@ -333,6 +336,7 @@ test_files:
333
336
  - spec/sneakers/concerns/logging_spec.rb
334
337
  - spec/sneakers/concerns/metrics_spec.rb
335
338
  - spec/sneakers/configuration_spec.rb
339
+ - spec/sneakers/content_type_spec.rb
336
340
  - spec/sneakers/integration_spec.rb
337
341
  - spec/sneakers/publisher_spec.rb
338
342
  - spec/sneakers/queue_spec.rb