fake_servicebus 0.0.2
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 +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
|