fake_servicebus 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +4 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.md +20 -0
- data/README.md +41 -0
- data/Rakefile +31 -0
- data/bin/fake_servicebus +65 -0
- data/fake_servicebus.gemspec +33 -0
- data/lib/fake_servicebus.rb +90 -0
- data/lib/fake_servicebus/actions/create_queue.rb +64 -0
- data/lib/fake_servicebus/actions/delete_message.rb +19 -0
- data/lib/fake_servicebus/actions/delete_queue.rb +18 -0
- data/lib/fake_servicebus/actions/get_queue.rb +20 -0
- data/lib/fake_servicebus/actions/list_queues.rb +28 -0
- data/lib/fake_servicebus/actions/receive_message.rb +44 -0
- data/lib/fake_servicebus/actions/renew_lock_message.rb +19 -0
- data/lib/fake_servicebus/actions/send_message.rb +22 -0
- data/lib/fake_servicebus/actions/unlock_message.rb +19 -0
- data/lib/fake_servicebus/api.rb +71 -0
- data/lib/fake_servicebus/catch_errors.rb +19 -0
- data/lib/fake_servicebus/collection_view.rb +20 -0
- data/lib/fake_servicebus/daemonize.rb +30 -0
- data/lib/fake_servicebus/databases/file.rb +129 -0
- data/lib/fake_servicebus/databases/memory.rb +30 -0
- data/lib/fake_servicebus/error_response.rb +55 -0
- data/lib/fake_servicebus/error_responses.yml +32 -0
- data/lib/fake_servicebus/message.rb +59 -0
- data/lib/fake_servicebus/queue.rb +201 -0
- data/lib/fake_servicebus/queue_factory.rb +16 -0
- data/lib/fake_servicebus/queues.rb +72 -0
- data/lib/fake_servicebus/responder.rb +41 -0
- data/lib/fake_servicebus/server.rb +19 -0
- data/lib/fake_servicebus/show_output.rb +21 -0
- data/lib/fake_servicebus/test_integration.rb +122 -0
- data/lib/fake_servicebus/version.rb +3 -0
- data/lib/fake_servicebus/web_interface.rb +74 -0
- data/spec/acceptance/message_actions_spec.rb +452 -0
- data/spec/acceptance/queue_actions_spec.rb +82 -0
- data/spec/integration_spec_helper.rb +23 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/unit/api_spec.rb +76 -0
- data/spec/unit/catch_errors_spec.rb +43 -0
- data/spec/unit/collection_view_spec.rb +41 -0
- data/spec/unit/error_response_spec.rb +65 -0
- data/spec/unit/message_spec.rb +76 -0
- data/spec/unit/queue_factory_spec.rb +13 -0
- data/spec/unit/queue_spec.rb +204 -0
- data/spec/unit/queues_spec.rb +102 -0
- data/spec/unit/responder_spec.rb +44 -0
- data/spec/unit/show_output_spec.rb +22 -0
- data/spec/unit/web_interface_spec.rb +15 -0
- metadata +266 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module FakeServiceBus
|
2
|
+
module Actions
|
3
|
+
class ListQueues
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@server = options.fetch(:server)
|
7
|
+
@queues = options.fetch(:queues)
|
8
|
+
@responder = options.fetch(:responder)
|
9
|
+
@request = options.fetch(:request)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(params)
|
13
|
+
found = @queues.list(params)
|
14
|
+
xml = Builder::XmlMarkup.new()
|
15
|
+
xml.tag! "feed", :xmlns=>"http://www.w3.org/2005/Atom" do
|
16
|
+
xml.title "Queues", :type=>"text"
|
17
|
+
xml.id "https://fake_servicebus/$Resources/Queues"
|
18
|
+
xml.updated Time.now.utc.iso8601
|
19
|
+
xml.link :rel=>"self", :href=>"fake_servicebus/$Resources/Queues"
|
20
|
+
found.each do |queue|
|
21
|
+
@responder.queue xml, queue
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module FakeServiceBus
|
2
|
+
module Actions
|
3
|
+
class ReceiveMessage
|
4
|
+
|
5
|
+
MAX_WAIT_TIME_SECONDS = 20
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@server = options.fetch(:server)
|
9
|
+
@queues = options.fetch(:queues)
|
10
|
+
@start_ts = Time.now.to_f
|
11
|
+
@satisfied = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(queue_name, params)
|
15
|
+
queue = @queues.get(queue_name)
|
16
|
+
message = queue.receive_message(params.merge(queues: @queues))
|
17
|
+
@satisfied = !message.nil? || expired?(queue, params)
|
18
|
+
if !message.nil?
|
19
|
+
[201,
|
20
|
+
{'location'=>message.location,
|
21
|
+
'BrokerProperties'=>{'SequenceNumber'=>message.sequence_number,
|
22
|
+
'LockToken'=>message.lock_token}.to_json},
|
23
|
+
message.body]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def satisfied?
|
28
|
+
@satisfied
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
def elapsed
|
33
|
+
Time.now.to_f - @start_ts
|
34
|
+
end
|
35
|
+
|
36
|
+
def expired?(queue, params)
|
37
|
+
wait_time_seconds = Integer params.fetch("timeout") { 0 }
|
38
|
+
wait_time_seconds <= 0 ||
|
39
|
+
elapsed >= wait_time_seconds ||
|
40
|
+
elapsed >= MAX_WAIT_TIME_SECONDS
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module FakeServiceBus
|
2
|
+
module Actions
|
3
|
+
class RenewLockMessage
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@server = options.fetch(:server)
|
7
|
+
@queues = options.fetch(:queues)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(queue_name, lock_token, params)
|
11
|
+
queue = @queues.get(queue_name)
|
12
|
+
|
13
|
+
queue.renew_lock_message(lock_token)
|
14
|
+
200
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FakeServiceBus
|
2
|
+
module Actions
|
3
|
+
class SendMessage
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@server = options.fetch(:server)
|
7
|
+
@queues = options.fetch(:queues)
|
8
|
+
@request = options.fetch(:request)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(queue_name, params)
|
12
|
+
queue = @queues.get(queue_name)
|
13
|
+
message = queue.send_message(params.merge(
|
14
|
+
{'queue_name'=>queue_name,
|
15
|
+
'body'=>@request.body.read})
|
16
|
+
)
|
17
|
+
201
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module FakeServiceBus
|
2
|
+
module Actions
|
3
|
+
class UnlockMessage
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@server = options.fetch(:server)
|
7
|
+
@queues = options.fetch(:queues)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(queue_name, lock_token, params)
|
11
|
+
queue = @queues.get(queue_name)
|
12
|
+
|
13
|
+
queue.unlock_message(lock_token)
|
14
|
+
200
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'fake_servicebus/actions/create_queue'
|
2
|
+
require 'fake_servicebus/actions/delete_queue'
|
3
|
+
require 'fake_servicebus/actions/list_queues'
|
4
|
+
require 'fake_servicebus/actions/get_queue'
|
5
|
+
require 'fake_servicebus/actions/send_message'
|
6
|
+
require 'fake_servicebus/actions/receive_message'
|
7
|
+
require 'fake_servicebus/actions/unlock_message'
|
8
|
+
require 'fake_servicebus/actions/renew_lock_message'
|
9
|
+
require 'fake_servicebus/actions/delete_message'
|
10
|
+
|
11
|
+
module FakeServiceBus
|
12
|
+
|
13
|
+
InvalidAction = Class.new(ArgumentError)
|
14
|
+
|
15
|
+
class API
|
16
|
+
|
17
|
+
attr_reader :queues, :options
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
@queues = options.fetch(:queues)
|
21
|
+
@options = options
|
22
|
+
@halt = false
|
23
|
+
@timer = Thread.new do
|
24
|
+
until @halt
|
25
|
+
queues.timeout_messages!
|
26
|
+
sleep(0.1)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(action, request, *args)
|
32
|
+
if FakeServiceBus::Actions.const_defined?(action)
|
33
|
+
action = FakeServiceBus::Actions.const_get(action).new(options.merge({:request => request}))
|
34
|
+
if action.respond_to?(:satisfied?)
|
35
|
+
result = nil
|
36
|
+
until @halt
|
37
|
+
result = attempt_once(action, *args)
|
38
|
+
break if action.satisfied?
|
39
|
+
sleep(0.1)
|
40
|
+
end
|
41
|
+
result
|
42
|
+
else
|
43
|
+
attempt_once(action, *args)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
fail InvalidAction, "Unknown (or not yet implemented) action: #{action}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def attempt_once(action, *args)
|
51
|
+
queues.transaction do
|
52
|
+
action.call(*args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Fake actions
|
57
|
+
|
58
|
+
def reset
|
59
|
+
queues.reset
|
60
|
+
end
|
61
|
+
|
62
|
+
def expire
|
63
|
+
queues.expire
|
64
|
+
end
|
65
|
+
|
66
|
+
def stop
|
67
|
+
@halt = true
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module FakeServiceBus
|
4
|
+
class CatchErrors
|
5
|
+
|
6
|
+
def initialize(app, options = {})
|
7
|
+
@app = app
|
8
|
+
@response = options.fetch(:response)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
@app.call(env)
|
13
|
+
rescue => error
|
14
|
+
response = @response.new(error)
|
15
|
+
[ response.status, {}, [ response.body ] ]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module FakeServiceBus
|
4
|
+
class CollectionView
|
5
|
+
include Enumerable
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@original, :[], :each, :empty?, :size, :length
|
8
|
+
|
9
|
+
UnmodifiableObjectError = Class.new(StandardError)
|
10
|
+
|
11
|
+
def initialize( original )
|
12
|
+
@original = original
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(key_or_index,value)
|
16
|
+
raise UnmodifiableObjectError.new("This is a collection view and can not be modified - #{key_or_index} => #{value}")
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module FakeServiceBus
|
2
|
+
class Daemonize
|
3
|
+
|
4
|
+
attr_reader :pid
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@pid = options.fetch(:pid) {
|
8
|
+
warn "No PID file specified while daemonizing!"
|
9
|
+
exit 1
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
Process.daemon(true, true)
|
15
|
+
|
16
|
+
if File.exist?(pid)
|
17
|
+
existing_pid = File.open(pid, 'r').read.chomp.to_i
|
18
|
+
running = Process.getpgid(existing_pid) rescue false
|
19
|
+
if running
|
20
|
+
warn "Error, Process #{existing_pid} already running"
|
21
|
+
exit 1
|
22
|
+
else
|
23
|
+
warn "Cleaning up stale pid at #{pid}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
File.open(pid, 'w') { |f| f.write(Process.pid) }
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "yaml/store"
|
2
|
+
|
3
|
+
module FakeServiceBus
|
4
|
+
class FileDatabase
|
5
|
+
|
6
|
+
attr_reader :filename
|
7
|
+
|
8
|
+
def initialize(filename)
|
9
|
+
@filename = filename
|
10
|
+
@queue_objects = {}
|
11
|
+
unless thread_safe_store?
|
12
|
+
# before ruby 2.4, YAML::Store cannot be declared thread safe
|
13
|
+
#
|
14
|
+
# without that declaration, attempting to have some thread B enter a
|
15
|
+
# store.transaction on the store while another thread A is in one
|
16
|
+
# already will raise an error unnecessarily.
|
17
|
+
#
|
18
|
+
# to prevent this, we'll use our own mutex around store.transaction,
|
19
|
+
# so only one thread can even _try_ to enter the transaction at a
|
20
|
+
# time.
|
21
|
+
@store_mutex = Mutex.new
|
22
|
+
@store_mutex_owner = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def load
|
27
|
+
transaction do
|
28
|
+
store["queues"] ||= {}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def transaction
|
33
|
+
if thread_safe_store? || store_mutex_owned?
|
34
|
+
# if we already own the store mutex, we can expect the next line to
|
35
|
+
# raise (appropriately) when we try to nest transactions in the store.
|
36
|
+
# but if we took the other branch, the # @store_mutex.synchronize call
|
37
|
+
# would self-deadlock before we could raise the error.
|
38
|
+
store.transaction do
|
39
|
+
yield
|
40
|
+
end
|
41
|
+
else
|
42
|
+
# we still need to use an inner store.transaction block because it does
|
43
|
+
# more than just lock synchronization. it's unfortunately inefficient,
|
44
|
+
# but this isn't a production-oriented library.
|
45
|
+
@store_mutex.synchronize do
|
46
|
+
begin
|
47
|
+
# allows us to answer `store_mutex_owned?` above
|
48
|
+
@store_mutex_owner = Thread.current
|
49
|
+
store.transaction do
|
50
|
+
yield
|
51
|
+
end
|
52
|
+
ensure
|
53
|
+
@store_mutex_owner = nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def reset
|
60
|
+
transaction do
|
61
|
+
store["queues"] = {}
|
62
|
+
end
|
63
|
+
@queue_objects = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
def []=(key, value)
|
67
|
+
storage[key] = value.to_yaml
|
68
|
+
end
|
69
|
+
|
70
|
+
def [](key)
|
71
|
+
value = storage[key]
|
72
|
+
if value
|
73
|
+
deserialize(key)
|
74
|
+
else
|
75
|
+
value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def each(&block)
|
80
|
+
storage.each do |key, value|
|
81
|
+
yield key, deserialize(key)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def select(&block)
|
86
|
+
new_list = storage.select do |key, value|
|
87
|
+
yield key, deserialize(key)
|
88
|
+
end
|
89
|
+
Hash[new_list.map { |key, value| [key, deserialize(key)] }]
|
90
|
+
end
|
91
|
+
|
92
|
+
def delete(key)
|
93
|
+
@queue_objects.delete(key)
|
94
|
+
storage.delete(key)
|
95
|
+
end
|
96
|
+
|
97
|
+
def values
|
98
|
+
storage.map { |key, value|
|
99
|
+
deserialize(key)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def deserialize(key)
|
106
|
+
@queue_objects[key] ||= Queue.new(storage[key].merge(name: key, message_factory: Message))
|
107
|
+
end
|
108
|
+
|
109
|
+
def storage
|
110
|
+
store["queues"]
|
111
|
+
end
|
112
|
+
|
113
|
+
def thread_safe_store?
|
114
|
+
Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.4")
|
115
|
+
end
|
116
|
+
|
117
|
+
def store_mutex_owned?
|
118
|
+
# this could be just "@store_mutex && @store_mutex.owned?" in ruby 2.x,
|
119
|
+
# but we still support 1.9.3 which doesn't have the "owned?" method
|
120
|
+
@store_mutex_owner == Thread.current
|
121
|
+
end
|
122
|
+
|
123
|
+
def store
|
124
|
+
@store ||= thread_safe_store? ?
|
125
|
+
YAML::Store.new(filename, true) :
|
126
|
+
YAML::Store.new(filename)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module FakeServiceBus
|
5
|
+
class MemoryDatabase
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@queues,
|
9
|
+
:[], :[]=, :delete, :each, :select, :values
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@semaphore = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def load
|
16
|
+
@queues = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def transaction
|
20
|
+
@semaphore.synchronize do
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset
|
26
|
+
@queues = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'builder'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module FakeServiceBus
|
6
|
+
class ErrorResponse
|
7
|
+
|
8
|
+
attr_reader :error
|
9
|
+
|
10
|
+
def initialize(error)
|
11
|
+
@error = error
|
12
|
+
end
|
13
|
+
|
14
|
+
def status
|
15
|
+
@status ||= statuses.fetch(code)
|
16
|
+
end
|
17
|
+
|
18
|
+
def body
|
19
|
+
xml = Builder::XmlMarkup.new()
|
20
|
+
xml.ErrorResponse do
|
21
|
+
xml.Error do
|
22
|
+
xml.Type type
|
23
|
+
xml.Code code
|
24
|
+
xml.Message error.to_s
|
25
|
+
xml.Detail
|
26
|
+
end
|
27
|
+
xml.RequestId SecureRandom.uuid
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def code
|
34
|
+
code = error.class.name.sub(/^FakeServiceBus::/, '')
|
35
|
+
if statuses.has_key?(code)
|
36
|
+
code
|
37
|
+
else
|
38
|
+
"InternalError"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def type
|
43
|
+
if status < 500
|
44
|
+
"Sender"
|
45
|
+
else
|
46
|
+
"Receiver"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def statuses
|
51
|
+
@statuses ||= YAML.load_file(File.expand_path('../error_responses.yml', __FILE__))
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|