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.
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+
2
+ module Hsdq
3
+ VERSION = "0.7.0"
4
+ def hsdq_version
5
+ VERSION
6
+ end
7
+ end
@@ -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
+