hsdq 0.7.0
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 +25 -0
- data/.travis.yml +3 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +26 -0
- data/Rakefile +1 -0
- data/bin/console +15 -0
- data/bin/setup +7 -0
- data/config/hsdq_your_class.yml,example +63 -0
- data/hsdq.gemspec +32 -0
- data/lib/hsdq.rb +38 -0
- data/lib/hsdq/admin.rb +60 -0
- data/lib/hsdq/connectors.rb +40 -0
- data/lib/hsdq/listener.rb +102 -0
- data/lib/hsdq/receiver.rb +269 -0
- data/lib/hsdq/sender.rb +124 -0
- data/lib/hsdq/session.rb +59 -0
- data/lib/hsdq/setting.rb +130 -0
- data/lib/hsdq/shared.rb +42 -0
- data/lib/hsdq/thread_store.rb +49 -0
- data/lib/hsdq/threadpool.rb +58 -0
- data/lib/hsdq/utilities.rb +20 -0
- data/lib/hsdq/version.rb +7 -0
- data/readme.md +178 -0
- metadata +210 -0
data/lib/hsdq/setting.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
|
2
|
+
module Hsdq
|
3
|
+
# This module provide the original setting for the hsdq class as well as some utility methods
|
4
|
+
module Setting
|
5
|
+
|
6
|
+
# Cached hash of the options thus avoiding to pass options all over the place.
|
7
|
+
# Initial state read the options from the config file if any provided and merge in it the opts
|
8
|
+
# parameter
|
9
|
+
# @param [Hash] opts The Options to be added/merged into the options from the config file
|
10
|
+
# @return [Hash] of the options
|
11
|
+
def hsdq_opts(opts={})
|
12
|
+
@hsdq_opts ||= initial_setup opts
|
13
|
+
end
|
14
|
+
|
15
|
+
# allow to add opions before listening
|
16
|
+
# @params [Hash] opts the options to add
|
17
|
+
def hsdq_add_options(opts)
|
18
|
+
if @hsdq_opts
|
19
|
+
@hsdq_opts.merge!(opts)
|
20
|
+
else
|
21
|
+
hsdq_opts(opts)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initial_setup(opts)
|
26
|
+
options = read_opts.merge opts
|
27
|
+
set_abort_on_exception(options)
|
28
|
+
options
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Hash] the default options
|
32
|
+
def default_opts
|
33
|
+
@default_opts ||= {
|
34
|
+
threaded: false,
|
35
|
+
timeout: 10
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def cx_opts
|
40
|
+
@cx_options ||= {
|
41
|
+
message: {
|
42
|
+
host: '127.0.0.1',
|
43
|
+
port: 6379,
|
44
|
+
db: 2
|
45
|
+
},
|
46
|
+
admin: {
|
47
|
+
host: '127.0.0.1',
|
48
|
+
port: 6379,
|
49
|
+
db: 2
|
50
|
+
},
|
51
|
+
session: {
|
52
|
+
host: '127.0.0.1',
|
53
|
+
port: 6379,
|
54
|
+
db: 2
|
55
|
+
}
|
56
|
+
}.merge hsdq_opts[:redis] || {}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Read the config file
|
60
|
+
# @param [String] file_path
|
61
|
+
# @return [Hash] options from defult and config
|
62
|
+
def read_opts(file_path=nil)
|
63
|
+
begin
|
64
|
+
default_opts.merge!(
|
65
|
+
deep_symbolize(YAML.load_file(file_path || config_file_path))[environment.to_sym])
|
66
|
+
rescue Errno::ENOENT => e
|
67
|
+
p "[warning] config file not read, using default options"
|
68
|
+
default_opts
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# cached value for the environment based on Rails.env or command line parameter (scripts do not have Rails.env)
|
73
|
+
# @param [String] environment the environment to be force set or nil or nothing
|
74
|
+
# @return [String] The environment string
|
75
|
+
# @default 'development'
|
76
|
+
def environment(environment=nil)
|
77
|
+
@environment ||= environment_from_app(environment) || 'development'
|
78
|
+
end
|
79
|
+
|
80
|
+
def environment_from_app(environment)
|
81
|
+
environment || (defined?(Rails) ? Rails.env : nil) || (RAILS_ENV if defined? RAILS_ENV)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param [String] name the HsdqClassName
|
85
|
+
# @return [String] the channel based on the class name
|
86
|
+
def channel(name=nil)
|
87
|
+
@channel ||= name || snakify(hsdq_get_name.gsub(/^hsdq/i, ""))
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [String] class name
|
91
|
+
def hsdq_get_name
|
92
|
+
self.respond_to?(:name) ? self.name : self.class.name
|
93
|
+
end
|
94
|
+
|
95
|
+
# Force the channel to be set to any name
|
96
|
+
# @param [String] name
|
97
|
+
# @return [String] the new channel name
|
98
|
+
def channel=(name)
|
99
|
+
@channel = name
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [String] The name for the config file (cached)
|
103
|
+
def config_filename(filename=nil)
|
104
|
+
@config_filename ||= filename || "#{snakify(hsdq_get_name.gsub(/^hsdq/i, ""))}.yml"
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param [String] path or nil or nothing. Force the path of a value is passed (cahed)
|
108
|
+
# @return [String] the path for the config folder, default to config relative the the actual path for a script
|
109
|
+
def config_path(path=nil)
|
110
|
+
@config_file_path ||= path || "#{(defined?(Rails) ? Rails.root : '.')}/config/"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Cached path to the config file
|
114
|
+
# @param [String] config_file_path if passed force the value
|
115
|
+
# @return [String] The value for the path to the config file
|
116
|
+
def config_file_path(config_file_path=nil)
|
117
|
+
@config_file_path ||= config_file_path || File.join(config_path, config_filename)
|
118
|
+
end
|
119
|
+
|
120
|
+
# sets abort_on_exception for debugging based on environment or parameter
|
121
|
+
#
|
122
|
+
# @param [Hash] options If options[exception] true,
|
123
|
+
# the main thread will break if a child thread break which is what we want in development/test
|
124
|
+
# but we do not want that for production
|
125
|
+
def set_abort_on_exception(options)
|
126
|
+
options[:exceptions] ? Thread.abort_on_exception = true : Thread.abort_on_exception = false
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
data/lib/hsdq/shared.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
module Hsdq
|
3
|
+
# The methods in this module are shared by different modules
|
4
|
+
module Shared
|
5
|
+
|
6
|
+
def placeholder
|
7
|
+
"This is a placeholder, you must implement this method in your hsdq class"
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid_type?(type)
|
11
|
+
[:request, :ack, :callback, :feedback, :error].include? type.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
# Build the namespaced key for the main hash storing the message history (collection of 'burst')
|
15
|
+
# @param [Hash] message_or_spark a burst or a spark
|
16
|
+
# @return [String] the unique key for the main redis hash
|
17
|
+
# @return nil if message_or_spark or the uid is nil
|
18
|
+
def hsdq_key(message_or_spark)
|
19
|
+
return unless message_or_spark
|
20
|
+
"hsdq_h_#{message_or_spark[:uid]}" if message_or_spark[:uid]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Build the namespaced key for the spark and burst unique shared uid
|
24
|
+
# @param [Hash] spark
|
25
|
+
# @return [String] the unique namespaced key
|
26
|
+
# @return nil if spark or the spark_uid is nil
|
27
|
+
def burst_key(spark)
|
28
|
+
return unless spark
|
29
|
+
"#{spark[:type]}_#{spark[:spark_uid]}" if spark[:type] && spark[:spark_uid]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Build the namespaced key for the main session hash
|
33
|
+
# @param [string] session_id the unique uid for the session
|
34
|
+
# @return [String] the unique namespaced key
|
35
|
+
# @return nil if session_id
|
36
|
+
def session_key(session_id)
|
37
|
+
return unless session_id
|
38
|
+
"hsdq_s_#{session_id}"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Hsdq
|
2
|
+
module ThreadStore
|
3
|
+
|
4
|
+
# Specialized proxy for set_get the key is the method name
|
5
|
+
# @see #set_get
|
6
|
+
def context(data=nil)
|
7
|
+
set_get __method__, data
|
8
|
+
end
|
9
|
+
|
10
|
+
# Specialized proxy for set_get the key is the method name
|
11
|
+
# @see #set_get
|
12
|
+
def context_params(data=nil)
|
13
|
+
set_get __method__, data
|
14
|
+
end
|
15
|
+
|
16
|
+
# Specialized proxy for set_get the key is the method name
|
17
|
+
# @see #set_get
|
18
|
+
def current_uid(data=nil)
|
19
|
+
set_get __method__, data
|
20
|
+
end
|
21
|
+
|
22
|
+
# Specialized proxy for set_get the key is the method name
|
23
|
+
# @see #set_get
|
24
|
+
def previous_sender(data=nil)
|
25
|
+
set_get __method__, data
|
26
|
+
end
|
27
|
+
|
28
|
+
# Specialized proxy for set_get the key is the method name
|
29
|
+
# @see #set_get
|
30
|
+
def sent_to(data=nil)
|
31
|
+
set_get __method__, data
|
32
|
+
end
|
33
|
+
|
34
|
+
# Specialized proxy for set_get the key is the method name
|
35
|
+
# @see #set_get
|
36
|
+
def reply_to(data=nil)
|
37
|
+
set_get __method__, data
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the value stored into the corresponding thread.current key only if no data is passed
|
41
|
+
# @param [any value] data save data into the corresponding Thread.current key if data is passed
|
42
|
+
# @return [Stored value]
|
43
|
+
def set_get(key, data=nil)
|
44
|
+
Thread.current[key] = data if data
|
45
|
+
Thread.current[key]
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hsdq
|
2
|
+
# This module is used to manage the threads
|
3
|
+
module Threadpool
|
4
|
+
|
5
|
+
# The maximum number of threads allowed to run at the same time. This is setter and a cached getter.
|
6
|
+
# @param [Integer or nil] max_count The max count to be set if passed if nil or no param, do not change the the value.
|
7
|
+
# @return [Integer] the max allowed number of threads
|
8
|
+
def max_thread_count(max_count=nil)
|
9
|
+
@max_thread_count = max_count if max_count
|
10
|
+
@max_thread_count ||= hsdq_opts[:max_thread_count] || 10
|
11
|
+
end
|
12
|
+
|
13
|
+
# Set paused flag
|
14
|
+
# @param [Boolean] paused
|
15
|
+
def paused(paused)
|
16
|
+
@paused = paused
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Boolean] true is paused
|
20
|
+
def paused?
|
21
|
+
@paused = false if @paused.nil?
|
22
|
+
@paused
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Boolean] true if below the max number of allowed thread
|
26
|
+
def allow_new_threads?
|
27
|
+
hsdq_threads_count < max_thread_count && !paused?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Cached ThreadGroup instance holding the threads for a given hsdq class
|
31
|
+
# @return [ThreadGroup]
|
32
|
+
def hsdq_threads
|
33
|
+
@hsdq_threads ||= ThreadGroup.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Add a thread to the thread group
|
37
|
+
# @param [Thread]
|
38
|
+
# @return [Threadgroup] the hdsq thread goup
|
39
|
+
def hsdq_threads_add(thread)
|
40
|
+
hsdq_threads.add thread
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return the number of thread in the thread group
|
44
|
+
def hsdq_threads_count
|
45
|
+
hsdq_threads.list.size
|
46
|
+
end
|
47
|
+
|
48
|
+
# Start a new thread and add it to the thread group
|
49
|
+
# @param [Proc] the thread staring block
|
50
|
+
# @return [TheadGroup] the hdsq thread goup
|
51
|
+
def hsdq_start_thread(ignition)
|
52
|
+
t = Thread.new(&ignition)
|
53
|
+
p "New thread: #{t}"
|
54
|
+
hsdq_threads_add t
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Hsdq
|
2
|
+
module Utilities
|
3
|
+
|
4
|
+
# utility method to symbolize the keys of a hash
|
5
|
+
#
|
6
|
+
# @param [Hash, #a_hash] the hash to be converted
|
7
|
+
# @return [Hash] with all keys as symbol
|
8
|
+
def deep_symbolize(a_hash)
|
9
|
+
JSON.parse(JSON[a_hash], symbolize_names: true)
|
10
|
+
end
|
11
|
+
|
12
|
+
# utility method (equivalent to Rails underscore)
|
13
|
+
# @param [String, #string] the string to be transformed ie: class/constant name
|
14
|
+
# @return [String] underscored string all in lowercase
|
15
|
+
def snakify(string)
|
16
|
+
string.split(/(?=[A-Z])/).map(&:downcase).join('_')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/lib/hsdq/version.rb
ADDED
data/readme.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# Hsdq
|
2
|
+
|
3
|
+
|
4
|
+
###High Speed Distributed Queue:
|
5
|
+
#####A messaging layer allowing distributed applications/scripts to communicate easily and exchange data at high speed.
|
6
|
+
**Also allow to offload deferred tasks to outside applications/scripts.**
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
* Connect with ease applications and/or scripts together.
|
11
|
+
* Easy to scale horizontally by adding or removing new application on line.
|
12
|
+
* Easy to balance the load, no round-robbing, the first listening application listening will get the message, then the next one will get the next message.
|
13
|
+
* hot swap, hot deployment. If the app message API is compatible with the previous API version, just deploy the new version and stop the old one.
|
14
|
+
* No need to have the target application on line to send a message, they will be stored in the bus and the target app will begin processing as soon as back online. (HTTP API need to be online to get the request).
|
15
|
+
|
16
|
+
## Dependencies
|
17
|
+
|
18
|
+
* HSDQ rely on Redis for the transport layer
|
19
|
+
|
20
|
+
## Recommended
|
21
|
+
|
22
|
+
* Ruby 2.3.1, Minimum ruby 2
|
23
|
+
* Jruby 9.1.2 Minimum 9.0.3
|
24
|
+
|
25
|
+
## How to use HSDQ
|
26
|
+
|
27
|
+
####Step 0: Getting a bit accustomed
|
28
|
+
|
29
|
+
Play with some quick and dirty scripts to get familiar, check the examples folder.
|
30
|
+
|
31
|
+
####Step 1: create the connected class
|
32
|
+
|
33
|
+
- Create a class in your application or script (for a Rails app in lib or accustomed in models)
|
34
|
+
- Prefix this class name with `Hsdq`, ie: `HsdqMyClass`
|
35
|
+
- Extend Hsdq into your class
|
36
|
+
|
37
|
+
The listening channel will then be created based on your class name ie: `my_class`
|
38
|
+
|
39
|
+
####Step 2: to get the events
|
40
|
+
|
41
|
+
Into your class override the 5 following methods:
|
42
|
+
|
43
|
+
| Method | Comment |
|
44
|
+
|:-------------- |:--------------------------------------------------------------------------------|
|
45
|
+
|`hsdq_request` |Called when a request is received |
|
46
|
+
|`hsdq_ack` |Called when an acknowledgment your request has been received by the receiving app|
|
47
|
+
|`hsdq_callback` |Called when the other other app respond to your request |
|
48
|
+
|`hsdq_feedback` |Called when the other app is sending intermediate feedback |
|
49
|
+
|`hsdq_error` |Called when an error occur |
|
50
|
+
|
51
|
+
You must implement these class methods into your class. They will receive the event.
|
52
|
+
All methods are receiving 2 parameters: A Hash for the message and a Hash or nil for the context.
|
53
|
+
|
54
|
+
```Ruby
|
55
|
+
def self.hsdq_request(message, context)
|
56
|
+
self.new.hsdq_request message, context
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.hsdq_request(message, context)
|
60
|
+
# Start your processing here
|
61
|
+
end
|
62
|
+
```
|
63
|
+
**Important:** Unless options[:threaded] is set to false, each event received are totally decoupled and independent. So you need to keep track of the context if needed.
|
64
|
+
|
65
|
+
####Step 3: sending messages
|
66
|
+
|
67
|
+
Four methods are used to send and respond to messages.
|
68
|
+
They are implemented internally in Hsdq and need a Hash as parameter.
|
69
|
+
|
70
|
+
| Method | Comment |
|
71
|
+
|:-----------------------------|:------------------------------------------|
|
72
|
+
|`hsdq_send_request(message)` | To send a request |
|
73
|
+
|`hsdq_send_calback(message)` | To send the final response to a request |
|
74
|
+
|`hsdq_send_feedback(message)` | To send intermediate data |
|
75
|
+
|`hsdq_send_error(message)` | To send an error message in case of error |
|
76
|
+
|
77
|
+
####Step 4:
|
78
|
+
Set the authorized topics and tasks in your class:
|
79
|
+
`hsdq_authorized_topics :beer, :wine, :cheese, :dishes`
|
80
|
+
`hsdq_authorized_tasks :drink, :eat, :clean`
|
81
|
+
Note you can in simple cases like in scripts only use topic or task but it is recommended to use both.
|
82
|
+
|
83
|
+
####Step 5:
|
84
|
+
Setup the hsdq config file:
|
85
|
+
|
86
|
+
In the config folder create a file name `hsdq_my_class.yml` There ia a sample file in hsdq config folder.
|
87
|
+
|
88
|
+
- Each class you connect to HSDQ must have it's specific yml file. This allow you to shard the bus for heavy
|
89
|
+
traffic application.
|
90
|
+
- Each subsection can point to a different Redis instance/host/db.
|
91
|
+
For heavy traffic sites, you can shard messages and have for example a unique host for admin.
|
92
|
+
- Session can be split to different host if different applications do not share the sessions data
|
93
|
+
|
94
|
+
```
|
95
|
+
development:
|
96
|
+
redis:
|
97
|
+
message:
|
98
|
+
host: 127.0.0.1
|
99
|
+
port: 6379
|
100
|
+
db: 0
|
101
|
+
admin:
|
102
|
+
host: 127.0.0.1
|
103
|
+
port: 6379
|
104
|
+
db: 0
|
105
|
+
session:
|
106
|
+
host: 127.0.0.1
|
107
|
+
port: 6379
|
108
|
+
db: 0
|
109
|
+
```
|
110
|
+
|
111
|
+
## Message specifications
|
112
|
+
|
113
|
+
#####Five type of message are running into the bus:
|
114
|
+
|
115
|
+
| | Type | Description |
|
116
|
+
|:---|:--------|:-----------------------------------|
|
117
|
+
| 1 | Request | Initial message sent |
|
118
|
+
| 2 | Ack | Acknowledgment of reception |
|
119
|
+
| 3 | Feedback| Intermediate response (progress) |
|
120
|
+
| 4 | Callback| Final response with the data |
|
121
|
+
| 5 | Error | Final response when an error occur |
|
122
|
+
|
123
|
+
#####A message event is composed of 2 parts:
|
124
|
+
|
125
|
+
1 - The spark:
|
126
|
+
|
127
|
+
- An ephemeral tiny part that include the minimum but sufficient information pointing to the 2nd part, the message itself.
|
128
|
+
- The spark is what the listener will be getting from the list queue and this will ignite the process.
|
129
|
+
- The spark is pushed to a redis list and popped by the listener. Once popped it will not be available in the redis layer anymore.
|
130
|
+
|
131
|
+
2 - The Burst:
|
132
|
+
|
133
|
+
- An event in the life of the message, the burst is stored as one value in a Redis hash and can be retrieved using the spark data.
|
134
|
+
|
135
|
+
#####The complete message
|
136
|
+
|
137
|
+
- It will contain all the operations executed during the life cycle of the message.
|
138
|
+
- It is stored into a hash and each element of this hash is one message event.
|
139
|
+
- The message element is stored in a Redis Hash with a default expiration of 72 hours (can be adjusted)
|
140
|
+
|
141
|
+
A message is composed of multiple events. At minimum there are 3 events:
|
142
|
+
|
143
|
+
1. Request
|
144
|
+
2. Ack
|
145
|
+
3. Callback or Error
|
146
|
+
|
147
|
+
Multiple feedback can be sent before the callback/error and multiple events can be present in the case of chained events.
|
148
|
+
|
149
|
+
#####Structure of a message:
|
150
|
+
**The burst:**
|
151
|
+
|
152
|
+
| Key | | Type | Description |
|
153
|
+
| :--------------|:-----:| :----- | :--------------------------------------------------------------- |
|
154
|
+
| sent_to | M | String | Name channel you publish |
|
155
|
+
| topic | O | Symbol | Topic to be processed (ex: :beer, :wine, :cheese etc...). |
|
156
|
+
| task | O | Symbol | Task to be processed (ex: :drink, eat, clean ,etc...). |
|
157
|
+
| params | M | Hash | parameters matching the receiver message API (can be empty hash) |
|
158
|
+
| data | C | Hash | Mandatory in the responses as this is the response |
|
159
|
+
| type | I | Symbol | Type of the message ie: :request, :callback, etc |
|
160
|
+
| sender | I | String | the listening channel of the sender. Used to reply |
|
161
|
+
| uid | I | UUID | Unique identifier for the message container (redis hash) |
|
162
|
+
| spark_uid | I | UUID | Unique identifier for the 'spark' and the 'burst' |
|
163
|
+
| tstamp | I | UTC | Timestamp for the event UTC |
|
164
|
+
| context | I | Hash | Data from the previous request. keys: :reply_to, spark_uid |
|
165
|
+
| previous_sender| I | String | The previous sender of a request. used in chained queries |
|
166
|
+
| hsdq_session | O | String | Key to load session data related to the context |
|
167
|
+
|
168
|
+
`M` Mandatory, `O` Optional (but recommended for pre-filtering), `I` Hsdq internal
|
169
|
+
|
170
|
+
- Topic and task values must be present respectively in the topics or task white lists when used.
|
171
|
+
- Redis return symbols as strings in the responses
|
172
|
+
|
173
|
+
**The spark:**
|
174
|
+
|
175
|
+
The spark has the same structure and values as the burst except it do not carry the payload.
|
176
|
+
The params and data as well as eventual custom keys keys which can contain heavy payloads are not included.
|
177
|
+
|
178
|
+
|