bi-frost 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +144 -0
- data/lib/bifrost.rb +18 -0
- data/lib/bifrost/entity.rb +16 -0
- data/lib/bifrost/exceptions/duplicate_subscriber_error.rb +6 -0
- data/lib/bifrost/exceptions/invalid_worker_definition_error.rb +6 -0
- data/lib/bifrost/exceptions/unsupported_lambda_error.rb +6 -0
- data/lib/bifrost/manager.rb +49 -0
- data/lib/bifrost/message.rb +46 -0
- data/lib/bifrost/subscriber.rb +13 -0
- data/lib/bifrost/topic.rb +75 -0
- data/lib/bifrost/version.rb +16 -0
- data/lib/bifrost/worker.rb +59 -0
- data/spec/bifrost/manager_spec.rb +13 -0
- data/spec/bifrost/message_spec.rb +40 -0
- data/spec/bifrost/subscriber_spec.rb +8 -0
- data/spec/bifrost/topic_spec.rb +89 -0
- data/spec/bifrost/version_spec.rb +7 -0
- data/spec/bifrost/worker_spec.rb +40 -0
- data/spec/spec_helper.rb +46 -0
- metadata +210 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 61a3243740e3d642efe0184d5c64d4ad11e72bff
|
4
|
+
data.tar.gz: 3834b3af2f9541dd7222a2ebf3e266f9430db2ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 764a14af9175bf5ffd9f25d8e1d84aa6fd31988db87da0cca2db0bc262715d669b66e591892b6a7e7f017899ad6e870813781ac25dd98860e7c519faee77ecfa
|
7
|
+
data.tar.gz: 1319f4d4eb92d423485943ecdbdd941f68c78b9360b584213c442f1f8f5ea735e22e1f044e0a8e40e02d37c1f160ada2df9b96e89685ffdb2b22354a8a76b0a7
|
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# Bifrost
|
2
|
+
|
3
|
+
Bifrost is a component which abstracts access to the Azure messaging bus. This is a lower level library which
|
4
|
+
can be used to commuicate with Azure via the Azure SDK.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'bi-frost'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
This component consists of 4 primary entities; Topic, Subscriber, Message and Listener.
|
21
|
+
|
22
|
+
Message's are what we deliver across various applications via an asynchronous pub/sub architecture. Messages are
|
23
|
+
posted to Topics. Topics are a collection of unique names which exist in a given Azure namespace. Each instance
|
24
|
+
of the Bifrost can only operate in a single Azure namespace. Topic names support alpha numeric characters only.
|
25
|
+
|
26
|
+
The architecture of this component is based on a fan out pub/sub model, i.e. a topic can have 1 or more subscribers,
|
27
|
+
each subscriber registers their interest in receiving messages posted to the topic. For more information on service
|
28
|
+
bus and how it works please refer to this [article](https://azure.microsoft.com/en-us/documentation/articles/service-bus-fundamentals-hybrid-solutions/#service-bus-fundamentals).
|
29
|
+
|
30
|
+
### Configuration
|
31
|
+
|
32
|
+
To begin using this gem we need to start by setting some environment variables. The gem in development and
|
33
|
+
test environments supports .env files (i.e. .env.test and .env.development etc). The environment variables that
|
34
|
+
require to be set prior to using the gem are; NAMESPACE, KEY_NAME and KEY_SECRET. The values for these variables
|
35
|
+
can be obtained from your Azure management portal. For more information on how to obtain these values for your
|
36
|
+
service bus please refer to this [article](https://azure.microsoft.com/en-us/documentation/articles/service-bus-authentication-and-authorization/).
|
37
|
+
|
38
|
+
### Examples
|
39
|
+
|
40
|
+
To define a topic to which messages can be delivered;
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
topic = Bifrost::Topic.new('topic_name')
|
44
|
+
topic.save
|
45
|
+
```
|
46
|
+
|
47
|
+
The `save` method returns a `true` or `false` to indicate if the topic was successfully saved. A topic may not save
|
48
|
+
if it already exists.
|
49
|
+
|
50
|
+
To add a subscriber to a particular topic;
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
topic = Bifrost::Topic.new('topic_name')
|
54
|
+
topic.save
|
55
|
+
subscriber = Bifrost::Subscriber.new('new_subscriber')
|
56
|
+
topic.add_subscriber(subscriber)
|
57
|
+
```
|
58
|
+
|
59
|
+
Each topic can only have a unique list of subscribers, a subscriber cannot be added to a topic more than once. When a subscriber is added to
|
60
|
+
a topic this function will return a true or false indicating success of the add.
|
61
|
+
|
62
|
+
To post a message to a topic;
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
topic = Bifrost::Topic.new('topic_name')
|
66
|
+
message = Bifrost::Message.new(content: 'some data')
|
67
|
+
message.post_to(topic)
|
68
|
+
```
|
69
|
+
|
70
|
+
This function returns a `true` or `false` indicating the success of the message delivery. This method is synchronous. Each message has an
|
71
|
+
identifier which gets sets upon successful delivery only.
|
72
|
+
|
73
|
+
A message can also be optionally published with a subject;
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
topic = Bifrost::Topic.new('topic_name')
|
77
|
+
message = Bifrost::Message.new(content: 'some data', 'message subject')
|
78
|
+
message.post_to(topic)
|
79
|
+
```
|
80
|
+
|
81
|
+
Subscribers in the Bifrost are [actors](http://http://doc.akka.io/docs/akka/2.4/general/actors.html), these actors run in
|
82
|
+
their own threads. At present the Bifrost does not support thread pools, this is something we are investigating and are
|
83
|
+
hoping to add at some point. In the Bifrost each actor is referred to as a `worker`. A worker is designed to receive
|
84
|
+
messages published to a particular topic with a specific subscriber in mind (refer to the fan-out comment earlier).
|
85
|
+
|
86
|
+
Workers are added to the Bifrost via the manager. The manager is what activates the workers in the Bifrost environment.
|
87
|
+
You can use multiple managers if you like at any given point in time to orchestrate the workers. These managers and workers
|
88
|
+
are only allowed to work in a single namespace at a time.
|
89
|
+
|
90
|
+
To setup a single worker to receive messages sent to a particular topic and subscriber;
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
manager = Bifrost::Manager.new
|
94
|
+
manager.add('topic_name', 'subscriber_name', proc { |m| puts "Received: message #{m}" })
|
95
|
+
manager.run
|
96
|
+
```
|
97
|
+
|
98
|
+
To setup multiple workers to receive messages;
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
manager = Bifrost::Manager.new
|
102
|
+
manager.add('topic_name', 'subscriber_name', proc { |m| puts "Received: message #{m}" })
|
103
|
+
manager.add('another_topic_name', 'another_subscriber_name', proc { |m| puts "Received: message #{m}" })
|
104
|
+
manager.run
|
105
|
+
```
|
106
|
+
|
107
|
+
Workers in the Bifrost are self healing, when an actor dies the manager receives a message alerting the manager
|
108
|
+
to the failure, the worker then restarts after the failure.
|
109
|
+
|
110
|
+
**We hope to introduce custom lambda functions in the future to support custom actions when a worker dies**
|
111
|
+
|
112
|
+
#Is it any good?
|
113
|
+
|
114
|
+
We are currently [dogfooding](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) this product in our production
|
115
|
+
environment. We will no doubt find issues and rectify this over time.
|
116
|
+
|
117
|
+
#Contributing
|
118
|
+
|
119
|
+
We'd love to have you involved. Please read our [contributing guide]() for information on how to get stuck in.
|
120
|
+
|
121
|
+
Contributors
|
122
|
+
|
123
|
+
This project is managed by the Filmpond team.
|
124
|
+
|
125
|
+
These individuals have come up with the ideas and written the code that made this possible:
|
126
|
+
|
127
|
+
1. Shirren Premaratne
|
128
|
+
2. Leslie Fung
|
129
|
+
|
130
|
+
#Licence
|
131
|
+
|
132
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the MIT License.
|
133
|
+
|
134
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MIT License for more details.
|
135
|
+
|
136
|
+
The MIT License (MIT)
|
137
|
+
|
138
|
+
Copyright (C) 2016 Filmpond Pty Ltd
|
139
|
+
|
140
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
141
|
+
|
142
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
143
|
+
|
144
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/bifrost.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bifrost/message'
|
2
|
+
require 'bifrost/topic'
|
3
|
+
require 'bifrost/subscriber'
|
4
|
+
require 'bifrost/manager'
|
5
|
+
require 'bifrost/worker'
|
6
|
+
|
7
|
+
# Bifrost is a pub/sub gem built on top of the Azure MessageBus system
|
8
|
+
module Bifrost
|
9
|
+
# Simple utlity that creates a topic and a single subscriber for the given
|
10
|
+
# topic. The topic is returned
|
11
|
+
def self.create_topic_with_subscriber(topic_name, sub_name)
|
12
|
+
topic = Bifrost::Topic.new(topic_name)
|
13
|
+
topic.save
|
14
|
+
subscriber = Bifrost::Subscriber.new(sub_name)
|
15
|
+
topic.add_subscriber(subscriber)
|
16
|
+
topic
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'Azure'
|
2
|
+
|
3
|
+
module Bifrost
|
4
|
+
# Types that require access to the message bus should inherit this type. We've chosen
|
5
|
+
# inheritance at this point, perhaps a mixin might be a better approach
|
6
|
+
class Entity
|
7
|
+
attr_reader :bus
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
Azure.sb_namespace = ENV['NAMESPACE']
|
11
|
+
host = "https://#{Azure.sb_namespace}.servicebus.windows.net"
|
12
|
+
signer = Azure::ServiceBus::Auth::SharedAccessSigner.new(ENV['KEY_NAME'], ENV['KEY_SECRET'])
|
13
|
+
@bus ||= Azure::ServiceBus::ServiceBusService.new(host, signer: signer)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'azure'
|
2
|
+
require 'celluloid/current'
|
3
|
+
require_relative 'exceptions/invalid_worker_definition_error'
|
4
|
+
|
5
|
+
module Bifrost
|
6
|
+
# This class is used to handle the setup and execution of multiple listeners
|
7
|
+
class Manager
|
8
|
+
include Celluloid
|
9
|
+
|
10
|
+
# The supervisor needs to be notified when a worker dies, it also needs to
|
11
|
+
# protect itself from harm
|
12
|
+
trap_exit :worker_died
|
13
|
+
|
14
|
+
# A supervised worker can be added to the current collection of supervised workers
|
15
|
+
# this also starts the actor
|
16
|
+
def add(topic_name, subscriber_name, proc)
|
17
|
+
if topic_name.nil? || subscriber_name.nil? || proc.nil?
|
18
|
+
raise InvalidWorkerDefinitionError, 'Invalid worker'
|
19
|
+
else
|
20
|
+
@supervisor = Worker.supervise(
|
21
|
+
as: Worker.supervisor_handle(topic_name, subscriber_name),
|
22
|
+
args: [topic_name, subscriber_name, proc]
|
23
|
+
)
|
24
|
+
# Link the worker to the supervisor so if the worker misbehaves the supervisor is alerted
|
25
|
+
# to this poor behaviour, the supervisor decides how to handle recovery
|
26
|
+
link(@supervisor.actors.last)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# When we run all the workers as actors in their own threads. This run also blocks to make sure
|
31
|
+
# the spawned threads remain operational indefinitely
|
32
|
+
def run
|
33
|
+
@supervisor.actors.each { |w| w.async.run }
|
34
|
+
# Put the supervisor thread to sleep indefinitely # Better way?
|
35
|
+
loop do
|
36
|
+
# TODO: Better way?
|
37
|
+
sleep(5)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# This callback function fires when an worker dies
|
44
|
+
def worker_died(worker, reason)
|
45
|
+
# TODO: Log this instead
|
46
|
+
puts "#{worker.inspect} has died: #{reason.class}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'azure'
|
2
|
+
require_relative 'entity'
|
3
|
+
|
4
|
+
module Bifrost
|
5
|
+
# A message is a letter that we send to a topic. It must contain a subject and body
|
6
|
+
# The receiver can process both fields on receipt of the message
|
7
|
+
class Message < Bifrost::Entity
|
8
|
+
attr_reader :subject, :body, :status, :message_id
|
9
|
+
|
10
|
+
# A message must have a valid subject and body. The service
|
11
|
+
# bus is initialised in the Entity class
|
12
|
+
def initialize(body, subject = nil)
|
13
|
+
@subject = subject || SecureRandom.base64
|
14
|
+
@body ||= body
|
15
|
+
@status ||= :undelivered
|
16
|
+
super()
|
17
|
+
end
|
18
|
+
|
19
|
+
# A message can be posted to a particular topic
|
20
|
+
def post_to(topic)
|
21
|
+
if topic.exists?
|
22
|
+
message = create_brokered_message
|
23
|
+
bus.send_topic_message(topic.name, message)
|
24
|
+
update_message_state_to_delivered(message)
|
25
|
+
true
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Create the brokered message
|
34
|
+
def create_brokered_message
|
35
|
+
message = Azure::ServiceBus::BrokeredMessage.new(subject, message: body)
|
36
|
+
message.correlation_id = SecureRandom.hex
|
37
|
+
message
|
38
|
+
end
|
39
|
+
|
40
|
+
# Update the status of the message post delivery
|
41
|
+
def update_message_state_to_delivered(message)
|
42
|
+
@status = :delivered
|
43
|
+
@message_id ||= message.correlation_id
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'azure'
|
2
|
+
|
3
|
+
module Bifrost
|
4
|
+
# A subscriber is a type that registers interest in receiving messages posted to certain topics
|
5
|
+
class Subscriber
|
6
|
+
attr_reader :name, :options
|
7
|
+
|
8
|
+
def initialize(name, options = {})
|
9
|
+
@name ||= name
|
10
|
+
@options ||= options
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'azure'
|
2
|
+
require_relative 'entity'
|
3
|
+
require_relative 'exceptions/duplicate_subscriber_error'
|
4
|
+
|
5
|
+
module Bifrost
|
6
|
+
# Topics are central to the pub/sub system in the Bifrost. All messages must be delivered
|
7
|
+
# to a topic. The topic is responsible for forwarding the message to registered subscribers
|
8
|
+
class Topic < Entity
|
9
|
+
attr_reader :name, :options
|
10
|
+
|
11
|
+
def initialize(name, options = {})
|
12
|
+
@name ||= name
|
13
|
+
@options ||= options
|
14
|
+
super()
|
15
|
+
end
|
16
|
+
|
17
|
+
# If the topic has been defined this method returns true, if not
|
18
|
+
# it returns false
|
19
|
+
def exists?
|
20
|
+
bus.list_topics.each do |topic|
|
21
|
+
return true if topic.name == name
|
22
|
+
end
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
# If a topic has not been defined we can save it, so it becomes defined
|
27
|
+
def save
|
28
|
+
if exists?
|
29
|
+
false
|
30
|
+
else
|
31
|
+
bus.create_topic(name)
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# If a topic is defined, we can remove the definition
|
37
|
+
def delete
|
38
|
+
if exists?
|
39
|
+
bus.delete_topic(name)
|
40
|
+
true
|
41
|
+
else
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# A new subscriber can be added to a topic
|
47
|
+
def add_subscriber(subscriber)
|
48
|
+
if exists?
|
49
|
+
begin
|
50
|
+
bus.create_subscription(name, subscriber.name)
|
51
|
+
rescue Azure::Core::Http::HTTPError => e
|
52
|
+
raise Bifrost::Exceptions::DuplicateSubscriberError, "Duplicate subscriber for topic #{name}" if e.status_code == 409
|
53
|
+
end
|
54
|
+
true
|
55
|
+
else
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# A topic subscriber can be removed from the topic if it exists
|
61
|
+
def remove_subscriber(subscriber)
|
62
|
+
if exists?
|
63
|
+
begin
|
64
|
+
bus.delete_subscription(name, subscriber.name)
|
65
|
+
rescue Azure::Core::Http::HTTPError => e
|
66
|
+
return false if e.status_code == 404
|
67
|
+
raise e
|
68
|
+
end
|
69
|
+
true
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Major.Minor.Patch version numbering
|
2
|
+
module Bifrost
|
3
|
+
# The major version of Sif, updated only for major changes that are
|
4
|
+
# likely to require modification to Filmpond apps.
|
5
|
+
MAJOR_VERSION = 0
|
6
|
+
|
7
|
+
# The minor version of Sif, updated for new feature releases.
|
8
|
+
MINOR_VERSION = 1
|
9
|
+
|
10
|
+
# The patch version of Sif, updated only for bug fixes from the last
|
11
|
+
# feature release.
|
12
|
+
PATCH_VERSION = 0
|
13
|
+
|
14
|
+
# The full version as a string.
|
15
|
+
VERSION = "#{MAJOR_VERSION}.#{MINOR_VERSION}.#{PATCH_VERSION}".freeze
|
16
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'azure'
|
2
|
+
require 'celluloid/current'
|
3
|
+
require_relative 'entity'
|
4
|
+
require_relative 'exceptions/unsupported_lambda_error'
|
5
|
+
|
6
|
+
module Bifrost
|
7
|
+
# This class is used to read messages from the subscriber, and process the messages one
|
8
|
+
# by one. This class is a worker/actor which focusses on processes a single topic/subscriber
|
9
|
+
# combination one at a time
|
10
|
+
class Worker < Bifrost::Entity
|
11
|
+
include Celluloid
|
12
|
+
|
13
|
+
attr_reader :topic_name, :subscriber_name, :callback
|
14
|
+
|
15
|
+
def initialize(topic_name, subscriber_name, callback)
|
16
|
+
raise Bifrost::Exceptions::UnsupportedLambdaError, 'not a proc' unless callback.respond_to?(:call)
|
17
|
+
@topic_name ||= topic_name
|
18
|
+
@subscriber_name ||= subscriber_name
|
19
|
+
@callback ||= callback
|
20
|
+
super()
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method starts the actor, which runs in an infinite loop. This means the worker should
|
24
|
+
# not terminate, but if it does, the supervisor will make sure it restarts
|
25
|
+
def run
|
26
|
+
loop do
|
27
|
+
read_message
|
28
|
+
sleep(ENV['QUEUE_DELAY'] || 10)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Workers have a friendly name which is a combination of the topic and subscriber name
|
33
|
+
# which in the operational environment should be unique
|
34
|
+
def to_s
|
35
|
+
Worker.supervisor_handle(topic_name, subscriber_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Utlity method to get the name of the worker as a symbol
|
39
|
+
def to_sym
|
40
|
+
to_s.to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
# A worker can tell you what it's friendly name will be, this is in order for supervision
|
44
|
+
def self.supervisor_handle(topic_name, subscriber_name)
|
45
|
+
"#{topic_name}-#{subscriber_name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Actual processing of the message
|
51
|
+
def read_message
|
52
|
+
message = bus.receive_subscription_message(topic_name, subscriber_name, timeout: ENV['TIMEOUT'] || 10)
|
53
|
+
if message
|
54
|
+
callback.call(message.properties['message'])
|
55
|
+
bus.delete_subscription_message(message)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bifrost'
|
3
|
+
|
4
|
+
describe Bifrost::Manager do
|
5
|
+
let(:cb ) { proc { |m| puts "Principle Received: message #{m}" } }
|
6
|
+
let(:worker) { Bifrost::Worker.new('topic', 'subscriber', proc) }
|
7
|
+
let(:manager) { Bifrost::Manager.new }
|
8
|
+
|
9
|
+
skip 'should be able to help a faulty actor heal' do
|
10
|
+
manager.add('topicX', 'blah', proc { |m| puts "Secondary Received: message #{m}" })
|
11
|
+
manager.run
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bifrost/message'
|
3
|
+
require 'bifrost/topic'
|
4
|
+
require 'bifrost/subscriber'
|
5
|
+
|
6
|
+
describe Bifrost::Message do
|
7
|
+
subject(:message) { Bifrost::Message.new({ content: 'some data' }, 'subscriber_name') }
|
8
|
+
|
9
|
+
it { is_expected.to respond_to(:subject) }
|
10
|
+
it { is_expected.to respond_to(:status) }
|
11
|
+
it { is_expected.to respond_to(:message_id) }
|
12
|
+
it { is_expected.to respond_to(:body) }
|
13
|
+
it { is_expected.to respond_to(:post_to) }
|
14
|
+
|
15
|
+
context 'which is initialized' do
|
16
|
+
it 'should be in an undelivered status' do
|
17
|
+
expect(message.status).to eq(:undelivered)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should be able to auto generate a subject' do
|
22
|
+
new_message = Bifrost::Message.new(content: 'some data')
|
23
|
+
expect(new_message).to_not be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should be postable to a valid topic' do
|
27
|
+
topic = Bifrost::Topic.new('valid-topic')
|
28
|
+
topic.save
|
29
|
+
topic.add_subscriber(Bifrost::Subscriber.new('new_subscriber'))
|
30
|
+
expect(message.post_to(topic)).to be_truthy
|
31
|
+
expect(message.status).to eq(:delivered)
|
32
|
+
expect(message.message_id).not_to be_nil
|
33
|
+
topic.delete
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should not be postable to an invalid topic' do
|
37
|
+
topic = Bifrost::Topic.new('invalid-topic') # A topic that is neither saved, nor with no subscribers is semantically invalid
|
38
|
+
expect(message.post_to(topic)).to be_falsey
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bifrost/topic'
|
3
|
+
require 'bifrost/subscriber'
|
4
|
+
|
5
|
+
describe Bifrost::Topic do
|
6
|
+
subject(:topic) { Bifrost::Topic.new('topic_name') }
|
7
|
+
|
8
|
+
it { is_expected.to respond_to(:name) }
|
9
|
+
it { is_expected.to respond_to(:exists?) }
|
10
|
+
it { is_expected.to respond_to(:save) }
|
11
|
+
it { is_expected.to respond_to(:delete) }
|
12
|
+
it { is_expected.to respond_to(:add_subscriber) }
|
13
|
+
it { is_expected.to respond_to(:remove_subscriber) }
|
14
|
+
|
15
|
+
it 'should not support blank topic names' do
|
16
|
+
invalid_topic = Bifrost::Topic.new(nil)
|
17
|
+
expect { invalid_topic.save }.to raise_error(TypeError)
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'for an undefined topic' do
|
21
|
+
it 'should return false for defined?' do
|
22
|
+
expect(topic.exists?).to be_falsey
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should persist the topic' do
|
26
|
+
new_topic = Bifrost::Topic.new('new_topic_name')
|
27
|
+
expect(new_topic.save).to be_truthy
|
28
|
+
expect(new_topic.delete).to be_truthy
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should return false for delete' do
|
32
|
+
expect(topic.delete).to be_falsey
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should not be able to add a new subscriber' do
|
36
|
+
subscriber = Bifrost::Subscriber.new('new_subscriber')
|
37
|
+
expect(topic.add_subscriber(subscriber)).to be_falsey
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should not be able to remove a subscriber' do
|
41
|
+
subscriber = Bifrost::Subscriber.new('new_subscriber')
|
42
|
+
expect(topic.remove_subscriber(subscriber)).to be_falsey
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'for a defined topic' do
|
47
|
+
let(:new_topic) { Bifrost::Topic.new('test_donotdelete') }
|
48
|
+
|
49
|
+
before(:all) do
|
50
|
+
tp = Bifrost::Topic.new('test_donotdelete')
|
51
|
+
tp.save unless tp.exists?
|
52
|
+
end
|
53
|
+
|
54
|
+
after(:all) do
|
55
|
+
tp = Bifrost::Topic.new('test_donotdelete')
|
56
|
+
tp.delete if tp.exists?
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should return true for defined?' do
|
60
|
+
expect(new_topic.exists?).to be_truthy
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should return false for save' do
|
64
|
+
expect(new_topic.save).to be_falsey
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should be able to add and remove a new subscriber' do
|
68
|
+
subscriber = Bifrost::Subscriber.new('new_subscriber')
|
69
|
+
expect(new_topic.add_subscriber(subscriber)).to be_truthy
|
70
|
+
expect(new_topic.remove_subscriber(subscriber)).to be_truthy
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should not be able to remove a non existent subscriber' do
|
74
|
+
subscriber = Bifrost::Subscriber.new('non_existent_subscriber')
|
75
|
+
expect(new_topic.remove_subscriber(subscriber)).to be_falsey
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should not be able to add a duplicate subscriber' do
|
79
|
+
subscriber = Bifrost::Subscriber.new('another_new_subscriber')
|
80
|
+
expect(new_topic.add_subscriber(subscriber)).to be_truthy
|
81
|
+
expect { new_topic.add_subscriber(subscriber) }.to raise_error(Bifrost::Exceptions::DuplicateSubscriberError)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should not accept names with spaces in them' do
|
86
|
+
invalid_topic_name = Bifrost::Topic.new('topic name')
|
87
|
+
expect { invalid_topic_name.save }.to raise_error(URI::InvalidURIError)
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bifrost/exceptions/unsupported_lambda_error'
|
3
|
+
require 'bifrost/message'
|
4
|
+
require 'bifrost/topic'
|
5
|
+
require 'bifrost/subscriber'
|
6
|
+
require 'bifrost/worker'
|
7
|
+
|
8
|
+
describe Bifrost::Worker do
|
9
|
+
let(:cb) { proc { |m| puts "Received: message #{m}" } }
|
10
|
+
subject(:worker) { Bifrost::Worker.new('topic', 'subscriber', cb) }
|
11
|
+
|
12
|
+
it { is_expected.to respond_to(:topic_name) }
|
13
|
+
it { is_expected.to respond_to(:subscriber_name) }
|
14
|
+
|
15
|
+
it 'should not except non procs in last argument' do
|
16
|
+
expect { Bifrost::Worker.new('topic', 'subscriber', 'x') }
|
17
|
+
.to raise_error(Bifrost::Exceptions::UnsupportedLambdaError)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should have a friendly string name' do
|
21
|
+
expect(worker.to_s).to eq("#{worker.topic_name}-#{worker.subscriber_name}")
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should have a friendly symbol name' do
|
25
|
+
expect(worker.to_sym).to eq(worker.to_s.to_sym)
|
26
|
+
end
|
27
|
+
|
28
|
+
skip 'should be able to listen for messages' do
|
29
|
+
topic = Bifrost::Topic.new('topic')
|
30
|
+
topic.save
|
31
|
+
subscriber = Bifrost::Subscriber.new('subscriber')
|
32
|
+
topic.add_subscriber(subscriber)
|
33
|
+
msg = Bifrost::Message.new([item1: { data: 2 }, item2: { more_data: 3 }])
|
34
|
+
msg.post_to(topic)
|
35
|
+
expect(msg.status).to eq(:delivered)
|
36
|
+
expect(msg.message_id).not_to be_nil
|
37
|
+
worker.async.run
|
38
|
+
sleep
|
39
|
+
end
|
40
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'azure'
|
2
|
+
require 'byebug'
|
3
|
+
require 'dotenv'
|
4
|
+
require 'simplecov'
|
5
|
+
|
6
|
+
SimpleCov.start do
|
7
|
+
add_filter '/spec/'
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
# Load environment variables for Azure
|
12
|
+
Dotenv.load('.env.test')
|
13
|
+
|
14
|
+
# All specs use this pre-defined namespace in Azure
|
15
|
+
Azure.sb_namespace = ENV['NAMESPACE']
|
16
|
+
|
17
|
+
# rspec-expectations config goes here. You can use an alternate
|
18
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
19
|
+
# assertions if you prefer.
|
20
|
+
config.expect_with :rspec do |expectations|
|
21
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
22
|
+
# and `failure_message` of custom matchers include text for helper methods
|
23
|
+
# defined using `chain`, e.g.:
|
24
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
25
|
+
# # => "be bigger than 2 and smaller than 4"
|
26
|
+
# ...rather than:
|
27
|
+
# # => "be bigger than 2"
|
28
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
29
|
+
end
|
30
|
+
|
31
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
32
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
33
|
+
config.mock_with :rspec do |mocks|
|
34
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
35
|
+
# a real object. This is generally recommended, and will default to
|
36
|
+
# `true` in RSpec 4.
|
37
|
+
mocks.verify_partial_doubles = true
|
38
|
+
end
|
39
|
+
|
40
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
41
|
+
# have no way to turn it off -- the option exists only for backwards
|
42
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
43
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
44
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
45
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bi-frost
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shirren Premaratne
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: azure
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.7.6
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.7.6
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: celluloid
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.17.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.17.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '9.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '9.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: dotenv
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.1'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: factory_girl_rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4.2'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-mocks
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec-rails
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: shoulda-matchers
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '2.4'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '2.4'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: simplecov
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.7'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.7'
|
153
|
+
description: Topic definitions should occur via the Bifrost. Subscriptions should
|
154
|
+
take place via the Bifrost. Therefore all messages need to transit through the Bifrost
|
155
|
+
email:
|
156
|
+
- shirren@filmpond.com
|
157
|
+
executables: []
|
158
|
+
extensions: []
|
159
|
+
extra_rdoc_files: []
|
160
|
+
files:
|
161
|
+
- README.md
|
162
|
+
- lib/bifrost.rb
|
163
|
+
- lib/bifrost/entity.rb
|
164
|
+
- lib/bifrost/exceptions/duplicate_subscriber_error.rb
|
165
|
+
- lib/bifrost/exceptions/invalid_worker_definition_error.rb
|
166
|
+
- lib/bifrost/exceptions/unsupported_lambda_error.rb
|
167
|
+
- lib/bifrost/manager.rb
|
168
|
+
- lib/bifrost/message.rb
|
169
|
+
- lib/bifrost/subscriber.rb
|
170
|
+
- lib/bifrost/topic.rb
|
171
|
+
- lib/bifrost/version.rb
|
172
|
+
- lib/bifrost/worker.rb
|
173
|
+
- spec/bifrost/manager_spec.rb
|
174
|
+
- spec/bifrost/message_spec.rb
|
175
|
+
- spec/bifrost/subscriber_spec.rb
|
176
|
+
- spec/bifrost/topic_spec.rb
|
177
|
+
- spec/bifrost/version_spec.rb
|
178
|
+
- spec/bifrost/worker_spec.rb
|
179
|
+
- spec/spec_helper.rb
|
180
|
+
homepage: https://github.com/filmpond/bifrost
|
181
|
+
licenses: []
|
182
|
+
metadata: {}
|
183
|
+
post_install_message:
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: '0'
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
197
|
+
requirements: []
|
198
|
+
rubyforge_project:
|
199
|
+
rubygems_version: 2.5.1
|
200
|
+
signing_key:
|
201
|
+
specification_version: 4
|
202
|
+
summary: Bifrost is a simple pub/sub messaging wrapper component.
|
203
|
+
test_files:
|
204
|
+
- spec/bifrost/manager_spec.rb
|
205
|
+
- spec/bifrost/message_spec.rb
|
206
|
+
- spec/bifrost/subscriber_spec.rb
|
207
|
+
- spec/bifrost/topic_spec.rb
|
208
|
+
- spec/bifrost/version_spec.rb
|
209
|
+
- spec/bifrost/worker_spec.rb
|
210
|
+
- spec/spec_helper.rb
|