orchestrator 1.0.1
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/LICENSE.md +158 -0
- data/README.md +13 -0
- data/Rakefile +7 -0
- data/app/controllers/orchestrator/api/dependencies_controller.rb +109 -0
- data/app/controllers/orchestrator/api/modules_controller.rb +183 -0
- data/app/controllers/orchestrator/api/systems_controller.rb +294 -0
- data/app/controllers/orchestrator/api/zones_controller.rb +62 -0
- data/app/controllers/orchestrator/api_controller.rb +13 -0
- data/app/controllers/orchestrator/base.rb +59 -0
- data/app/controllers/orchestrator/persistence_controller.rb +29 -0
- data/app/models/orchestrator/access_log.rb +35 -0
- data/app/models/orchestrator/control_system.rb +160 -0
- data/app/models/orchestrator/dependency.rb +87 -0
- data/app/models/orchestrator/mod/by_dependency/map.js +6 -0
- data/app/models/orchestrator/mod/by_module_type/map.js +6 -0
- data/app/models/orchestrator/module.rb +127 -0
- data/app/models/orchestrator/sys/by_modules/map.js +9 -0
- data/app/models/orchestrator/sys/by_zones/map.js +9 -0
- data/app/models/orchestrator/zone.rb +47 -0
- data/app/models/orchestrator/zone/all/map.js +6 -0
- data/config/routes.rb +43 -0
- data/lib/generators/module/USAGE +8 -0
- data/lib/generators/module/module_generator.rb +52 -0
- data/lib/orchestrator.rb +52 -0
- data/lib/orchestrator/control.rb +303 -0
- data/lib/orchestrator/core/mixin.rb +123 -0
- data/lib/orchestrator/core/module_manager.rb +258 -0
- data/lib/orchestrator/core/request_proxy.rb +109 -0
- data/lib/orchestrator/core/requests_proxy.rb +47 -0
- data/lib/orchestrator/core/schedule_proxy.rb +49 -0
- data/lib/orchestrator/core/system_proxy.rb +153 -0
- data/lib/orchestrator/datagram_server.rb +114 -0
- data/lib/orchestrator/dependency_manager.rb +131 -0
- data/lib/orchestrator/device/command_queue.rb +213 -0
- data/lib/orchestrator/device/manager.rb +83 -0
- data/lib/orchestrator/device/mixin.rb +35 -0
- data/lib/orchestrator/device/processor.rb +441 -0
- data/lib/orchestrator/device/transport_makebreak.rb +221 -0
- data/lib/orchestrator/device/transport_tcp.rb +139 -0
- data/lib/orchestrator/device/transport_udp.rb +89 -0
- data/lib/orchestrator/engine.rb +70 -0
- data/lib/orchestrator/errors.rb +23 -0
- data/lib/orchestrator/logger.rb +115 -0
- data/lib/orchestrator/logic/manager.rb +18 -0
- data/lib/orchestrator/logic/mixin.rb +11 -0
- data/lib/orchestrator/service/manager.rb +63 -0
- data/lib/orchestrator/service/mixin.rb +56 -0
- data/lib/orchestrator/service/transport_http.rb +55 -0
- data/lib/orchestrator/status.rb +229 -0
- data/lib/orchestrator/system.rb +108 -0
- data/lib/orchestrator/utilities/constants.rb +41 -0
- data/lib/orchestrator/utilities/transcoder.rb +57 -0
- data/lib/orchestrator/version.rb +3 -0
- data/lib/orchestrator/websocket_manager.rb +425 -0
- data/orchestrator.gemspec +35 -0
- data/spec/orchestrator/queue_spec.rb +200 -0
- metadata +271 -0
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'algorithms'
|
2
|
+
require 'bisect'
|
3
|
+
|
4
|
+
|
5
|
+
# Transport -> connection (make break etc)
|
6
|
+
# * attach connected, disconnected callbacks
|
7
|
+
# * udp, makebreak and tcp transports
|
8
|
+
# Manager + CommandProcessor + Transport
|
9
|
+
|
10
|
+
|
11
|
+
module Orchestrator
|
12
|
+
module Device
|
13
|
+
class CommandQueue
|
14
|
+
|
15
|
+
|
16
|
+
OFFLINE_MSG = Error::CommandCanceled.new 'command canceled as module went offline'
|
17
|
+
|
18
|
+
|
19
|
+
attr_accessor :waiting
|
20
|
+
attr_reader :state
|
21
|
+
attr_reader :pause
|
22
|
+
|
23
|
+
|
24
|
+
# init -> mod.load -> post_init
|
25
|
+
# So config can be set in on_load if desired
|
26
|
+
def initialize(loop, callback)
|
27
|
+
@loop = loop
|
28
|
+
@callback = callback
|
29
|
+
|
30
|
+
@named_commands = {
|
31
|
+
# name: [[priority list], command]
|
32
|
+
# where command may be nil
|
33
|
+
}
|
34
|
+
@comparison = method(:comparison)
|
35
|
+
@pending_commands = Containers::Heap.new(&@comparison)
|
36
|
+
|
37
|
+
@waiting = nil # Last command sent that was marked as waiting
|
38
|
+
@pause = 0
|
39
|
+
@state = :online # online / offline
|
40
|
+
@pause_shift = method(:pause_shift)
|
41
|
+
@move_forward = method(:move_forward)
|
42
|
+
end
|
43
|
+
|
44
|
+
def shift_next_tick
|
45
|
+
@pause += 1
|
46
|
+
@loop.next_tick @pause_shift
|
47
|
+
end
|
48
|
+
|
49
|
+
def shift
|
50
|
+
return if @pause > 0 # we are waiting for the next_tick?
|
51
|
+
|
52
|
+
@waiting = nil # Discard the current command
|
53
|
+
if length > 0
|
54
|
+
next_cmd = @pending_commands.pop
|
55
|
+
|
56
|
+
if next_cmd.is_a? Symbol # (named command)
|
57
|
+
result = @named_commands[next_cmd]
|
58
|
+
result[0].shift
|
59
|
+
cmd = result[1]
|
60
|
+
if cmd.nil?
|
61
|
+
shift_next_tick if length > 0
|
62
|
+
return # command already executed, this is a no-op
|
63
|
+
else
|
64
|
+
result[1] = nil
|
65
|
+
end
|
66
|
+
else
|
67
|
+
cmd = next_cmd
|
68
|
+
end
|
69
|
+
|
70
|
+
@waiting = cmd if cmd[:wait]
|
71
|
+
shift_promise = @callback.call cmd
|
72
|
+
|
73
|
+
if shift_promise.is_a? ::Libuv::Q::Promise
|
74
|
+
@pause += 1
|
75
|
+
shift_promise.finally do # NOTE:: This schedule may not be required...
|
76
|
+
@loop.schedule @move_forward
|
77
|
+
end
|
78
|
+
else
|
79
|
+
shift_next_tick if length > 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def push(command, priority)
|
85
|
+
if @state == :offline && command[:name].nil?
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
if command[:name]
|
90
|
+
name = command[:name].to_sym
|
91
|
+
|
92
|
+
current = @named_commands[name] ||= [[], nil]
|
93
|
+
|
94
|
+
# Chain the promises if the named command is already in the queue
|
95
|
+
cmd = current[1]
|
96
|
+
cmd[:defer].resolve(command[:defer].promise) if cmd
|
97
|
+
|
98
|
+
|
99
|
+
current[1] = command # replace the old command
|
100
|
+
priors = current[0]
|
101
|
+
|
102
|
+
# Only add commands of higher priority to the queue
|
103
|
+
if priors.empty? || priors[-1] < priority
|
104
|
+
priors << priority
|
105
|
+
queue_push(@pending_commands, name, priority)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
queue_push(@pending_commands, command, priority)
|
109
|
+
end
|
110
|
+
|
111
|
+
if @waiting.nil? && @state == :online
|
112
|
+
shift # This will trigger the callback
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def length
|
117
|
+
@pending_commands.size
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
# If offline we'll only maintain named command state and queue
|
122
|
+
def online
|
123
|
+
@state = :online
|
124
|
+
|
125
|
+
# next tick is important as it allows the module time to updated
|
126
|
+
# any named commands that it desires in the connected callback
|
127
|
+
shift_next_tick
|
128
|
+
end
|
129
|
+
|
130
|
+
def offline(clear = false)
|
131
|
+
@state = :offline
|
132
|
+
|
133
|
+
if clear
|
134
|
+
@waiting[:defer].reject(OFFLINE_MSG) if @waiting
|
135
|
+
cancel_all(OFFLINE_MSG)
|
136
|
+
@waiting = nil
|
137
|
+
else
|
138
|
+
# Keep named commands
|
139
|
+
new_queue = Containers::Heap.new(&@comparison)
|
140
|
+
|
141
|
+
while length > 0
|
142
|
+
cmd = @pending_commands.pop
|
143
|
+
if cmd.is_a? Symbol
|
144
|
+
res = @named_commands[cmd][0]
|
145
|
+
pri = res.shift
|
146
|
+
res << pri
|
147
|
+
queue_push(new_queue, cmd, pri)
|
148
|
+
else
|
149
|
+
cmd[:defer].reject(OFFLINE_MSG)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
@pending_commands = new_queue
|
153
|
+
|
154
|
+
# clear waiting if it is not a named command.
|
155
|
+
# The processor will re-queue it if retry on disconnect is set
|
156
|
+
if @waiting && @waiting[:name].nil?
|
157
|
+
@waiting = nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def cancel_all(msg)
|
163
|
+
while length > 0
|
164
|
+
cmd = @pending_commands.pop
|
165
|
+
if cmd.is_a? Symbol
|
166
|
+
res = @named_commands[cmd]
|
167
|
+
if res
|
168
|
+
res[1][:defer].reject(msg)
|
169
|
+
@named_commands.delete(cmd)
|
170
|
+
end
|
171
|
+
else
|
172
|
+
cmd[:defer].reject(msg)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
protected
|
179
|
+
|
180
|
+
|
181
|
+
# If we next_tick a shift then a push may be able to
|
182
|
+
# sneak in before that command is shifted.
|
183
|
+
# If the new push is a waiting command then the next
|
184
|
+
# tick shift will discard it which is undesirable
|
185
|
+
def pause_shift
|
186
|
+
@pause -= 1
|
187
|
+
shift
|
188
|
+
end
|
189
|
+
|
190
|
+
def move_forward
|
191
|
+
@pause -= 1
|
192
|
+
if !@waiting && length > 0
|
193
|
+
shift
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
# Queue related methods
|
199
|
+
def comparison(x, y)
|
200
|
+
if x[0] == y[0]
|
201
|
+
x[1] < y[1]
|
202
|
+
else
|
203
|
+
(x[0] <=> y[0]) == 1
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def queue_push(queue, obj, pri)
|
208
|
+
pri = [pri, Time.now.to_f]
|
209
|
+
queue.push(pri, obj)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Orchestrator
|
2
|
+
module Device
|
3
|
+
class Manager < ::Orchestrator::Core::ModuleManager
|
4
|
+
def initialize(*args)
|
5
|
+
super(*args)
|
6
|
+
|
7
|
+
# Do we want to start here?
|
8
|
+
# Should be ok.
|
9
|
+
@thread.next_tick method(:start)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :processor, :connection
|
13
|
+
|
14
|
+
def start
|
15
|
+
return true unless @processor.nil?
|
16
|
+
@processor = Processor.new(self)
|
17
|
+
|
18
|
+
super # Calls on load (allows setting of tls certs)
|
19
|
+
|
20
|
+
# Load UV-Rays abstraction here
|
21
|
+
@connection = if @settings.udp
|
22
|
+
UdpConnection.new(self, @processor)
|
23
|
+
elsif @settings.makebreak
|
24
|
+
::UV.connect(@settings.ip, @settings.port, MakebreakConnection, self, @processor, @settings.tls)
|
25
|
+
else
|
26
|
+
::UV.connect(@settings.ip, @settings.port, TcpConnection, self, @processor, @settings.tls)
|
27
|
+
end
|
28
|
+
|
29
|
+
@processor.transport = @connection
|
30
|
+
true # for REST API
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
super
|
35
|
+
@processor.terminate unless @processor.nil?
|
36
|
+
@processor = nil
|
37
|
+
@connection.terminate unless @connection.nil?
|
38
|
+
@connection = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def notify_connected
|
42
|
+
if @instance.respond_to? :connected, true
|
43
|
+
begin
|
44
|
+
@instance.__send__(:connected)
|
45
|
+
rescue => e
|
46
|
+
@logger.print_error(e, 'error in module connected callback')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
update_connected_status(true)
|
51
|
+
end
|
52
|
+
|
53
|
+
def notify_disconnected
|
54
|
+
if @instance.respond_to? :disconnected, true
|
55
|
+
begin
|
56
|
+
@instance.__send__(:disconnected)
|
57
|
+
rescue => e
|
58
|
+
@logger.print_error(e, 'error in module disconnected callback')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
update_connected_status(false)
|
63
|
+
end
|
64
|
+
|
65
|
+
def notify_received(data, resolve, command = nil)
|
66
|
+
begin
|
67
|
+
blk = command.nil? ? nil : command[:on_receive]
|
68
|
+
if blk.respond_to? :call
|
69
|
+
blk.call(data, resolve, command)
|
70
|
+
elsif @instance.respond_to? :received, true
|
71
|
+
@instance.__send__(:received, data, resolve, command)
|
72
|
+
else
|
73
|
+
@logger.warn('no received function provided')
|
74
|
+
:abort
|
75
|
+
end
|
76
|
+
rescue => e
|
77
|
+
@logger.print_error(e, 'error in received callback')
|
78
|
+
return :abort
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Orchestrator
|
2
|
+
module Device
|
3
|
+
module Mixin
|
4
|
+
include ::Orchestrator::Core::Mixin
|
5
|
+
|
6
|
+
def send(data, options = {}, &blk)
|
7
|
+
options[:data] = data
|
8
|
+
options[:defer] = @__config__.thread.defer
|
9
|
+
options[:on_receive] = blk if blk # on command success
|
10
|
+
@__config__.thread.schedule do
|
11
|
+
@__config__.processor.queue_command(options)
|
12
|
+
end
|
13
|
+
options[:defer].promise
|
14
|
+
end
|
15
|
+
|
16
|
+
def disconnect
|
17
|
+
@__config__.thread.schedule do
|
18
|
+
@__config__.connection.disconnect
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def config(options)
|
23
|
+
@__config__.thread.schedule do
|
24
|
+
@__config__.processor.config = options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def defaults(options)
|
29
|
+
@__config__.thread.schedule do
|
30
|
+
@__config__.processor.send_options(options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,441 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
|
4
|
+
# Transport -> connection (make break etc)
|
5
|
+
# * attach connected, disconnected callbacks
|
6
|
+
# * udp, makebreak and tcp transports
|
7
|
+
# Manager + CommandProcessor + Transport
|
8
|
+
|
9
|
+
|
10
|
+
module Orchestrator
|
11
|
+
module Device
|
12
|
+
class Processor
|
13
|
+
include ::Orchestrator::Transcoder
|
14
|
+
|
15
|
+
|
16
|
+
# Any command that waits:
|
17
|
+
# send('power on?').then( execute after command complete )
|
18
|
+
# Named commands mean the promise may never resolve
|
19
|
+
# Totally replaces emit as we don't care and makes cross module request super easy
|
20
|
+
# -- non-wait commands resolve after they have been written to the socket!!
|
21
|
+
|
22
|
+
|
23
|
+
SEND_DEFAULTS = {
|
24
|
+
wait: true, # wait for a response before continuing with sends
|
25
|
+
delay: 0, # make sure sends are separated by at least this (in milliseconds)
|
26
|
+
delay_on_receive: 0, # delay the next send by this (milliseconds) after a receive
|
27
|
+
max_waits: 3, # number of times we will ignore valid tokens before retry
|
28
|
+
retries: 2, # Retry attempts before we give up on the command
|
29
|
+
hex_string: false, # Does the input need conversion
|
30
|
+
timeout: 5000, # Time we will wait for a response
|
31
|
+
priority: 50, # Priority of a send
|
32
|
+
force_disconnect: false # Mainly for use with make and break
|
33
|
+
|
34
|
+
# Other options include:
|
35
|
+
# * emit callback to occur once command complete (may be discarded if a named command)
|
36
|
+
# * on_receive (alternative to received function)
|
37
|
+
}
|
38
|
+
|
39
|
+
CONFIG_DEFAULTS = {
|
40
|
+
tokenize: false, # If replaced with a callback can define custom tokenizers
|
41
|
+
size_limit: 524288, # 512kb buffer max
|
42
|
+
clear_queue_on_disconnect: false,
|
43
|
+
flush_buffer_on_disconnect: false,
|
44
|
+
priority_bonus: 20, # give commands bonus priority under certain conditions
|
45
|
+
update_status: true, # auto update connected status?
|
46
|
+
thrashing_threshold: 1500 # min milliseconds between connection retries
|
47
|
+
|
48
|
+
# Other options include:
|
49
|
+
# * inactivity_timeout (used with make and break)
|
50
|
+
# * delimiter (string or regex to match message end)
|
51
|
+
# * indicator (string or regex to match message start)
|
52
|
+
# * verbose (throw errors or silently recover)
|
53
|
+
# * wait_ready (wait for some signal before signaling connected)
|
54
|
+
# * encoding (BINARY) (force encoding on incoming data)
|
55
|
+
}
|
56
|
+
|
57
|
+
|
58
|
+
SUCCESS = Set.new([true, :success, :abort, nil, :ignore])
|
59
|
+
FAILURE = Set.new([false, :retry, :failed, :fail])
|
60
|
+
DUMMY_RESOLVER = proc {}
|
61
|
+
TERMINATE_MSG = Error::CommandCanceled.new 'command canceled due to module shutdown'
|
62
|
+
UNNAMED = 'unnamed'
|
63
|
+
|
64
|
+
|
65
|
+
attr_reader :config, :queue, :thread
|
66
|
+
attr_accessor :transport
|
67
|
+
|
68
|
+
# For statistics only
|
69
|
+
attr_reader :last_sent_at, :last_receive_at, :timeout
|
70
|
+
|
71
|
+
|
72
|
+
# init -> mod.load -> post_init
|
73
|
+
# So config can be set in on_load if desired
|
74
|
+
def initialize(man)
|
75
|
+
@man = man
|
76
|
+
|
77
|
+
@thread = @man.thread
|
78
|
+
@logger = @man.logger
|
79
|
+
@defaults = SEND_DEFAULTS.dup
|
80
|
+
@config = CONFIG_DEFAULTS.dup
|
81
|
+
|
82
|
+
@queue = CommandQueue.new(@thread, method(:send_next))
|
83
|
+
@responses = []
|
84
|
+
@wait = false
|
85
|
+
@connected = false
|
86
|
+
@checking = Mutex.new
|
87
|
+
@bonus = 0
|
88
|
+
|
89
|
+
@last_sent_at = 0
|
90
|
+
@last_receive_at = 0
|
91
|
+
|
92
|
+
|
93
|
+
# Used to indicate when we can start the next response processing
|
94
|
+
@head = ::Libuv::Q::ResolvedPromise.new(@thread, true)
|
95
|
+
@tail = ::Libuv::Q::ResolvedPromise.new(@thread, true)
|
96
|
+
|
97
|
+
# Method variables
|
98
|
+
@resolver = proc { |resp| @thread.schedule { resolve_callback(resp) } }
|
99
|
+
|
100
|
+
@resp_success = proc { |result| @thread.next_tick { resp_success(result) } }
|
101
|
+
@resp_failure = proc { |reason| @thread.next_tick { resp_failure(reason) } }
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Helper functions ------------------
|
106
|
+
def send_options(options)
|
107
|
+
@defaults.merge!(options)
|
108
|
+
end
|
109
|
+
|
110
|
+
def config=(options)
|
111
|
+
@config.merge!(options)
|
112
|
+
# use tokenize to signal a buffer update
|
113
|
+
new_buffer if options.include?(:tokenize)
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# Public interface
|
118
|
+
def queue_command(options)
|
119
|
+
# Make sure we are sending appropriately formatted data
|
120
|
+
raw = options[:data]
|
121
|
+
|
122
|
+
if raw.is_a?(Array)
|
123
|
+
options[:data] = array_to_str(raw)
|
124
|
+
elsif options[:hex_string] == true
|
125
|
+
options[:data] = hex_to_byte(raw)
|
126
|
+
end
|
127
|
+
|
128
|
+
data = options[:data]
|
129
|
+
options[:retries] = 0 if options[:wait] == false
|
130
|
+
|
131
|
+
if options[:name].is_a? String
|
132
|
+
options[:name] = options[:name].to_sym
|
133
|
+
end
|
134
|
+
|
135
|
+
# merge in the defaults
|
136
|
+
options = @defaults.merge(options)
|
137
|
+
|
138
|
+
@queue.push(options, options[:priority] + @bonus)
|
139
|
+
|
140
|
+
rescue => e
|
141
|
+
options[:defer].reject(e)
|
142
|
+
@logger.print_error(e, 'error queuing command')
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Callbacks -------------------------
|
147
|
+
def connected
|
148
|
+
@connected = true
|
149
|
+
new_buffer
|
150
|
+
@man.notify_connected
|
151
|
+
if @config[:update_status]
|
152
|
+
@man.trak(:connected, true)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def disconnected
|
157
|
+
@connected = false
|
158
|
+
@man.notify_disconnected
|
159
|
+
if @config[:update_status]
|
160
|
+
@man.trak(:connected, false)
|
161
|
+
end
|
162
|
+
if @buffer && @config[:flush_buffer_on_disconnect]
|
163
|
+
check_data(@buffer.flush)
|
164
|
+
end
|
165
|
+
@buffer = nil
|
166
|
+
|
167
|
+
if @queue.waiting
|
168
|
+
resp_failure(:disconnected)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def buffer(data)
|
173
|
+
@last_receive_at = @thread.now
|
174
|
+
|
175
|
+
if @buffer
|
176
|
+
@responses.concat @buffer.extract(data)
|
177
|
+
else
|
178
|
+
# tokenizing buffer above will enforce encoding
|
179
|
+
if @config[:encoding]
|
180
|
+
data.force_encoding(@config[:encoding])
|
181
|
+
end
|
182
|
+
@responses << data
|
183
|
+
end
|
184
|
+
|
185
|
+
# if we are waiting we don't want to process this data just yet
|
186
|
+
if !@wait
|
187
|
+
check_next
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def terminate
|
192
|
+
@thread.schedule method(:do_terminate)
|
193
|
+
end
|
194
|
+
|
195
|
+
def check_next
|
196
|
+
return if @checking.locked? || @responses.length <= 0
|
197
|
+
@checking.synchronize {
|
198
|
+
loop do
|
199
|
+
check_data(@responses.shift)
|
200
|
+
break if @wait || @responses.length == 0
|
201
|
+
end
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
protected
|
207
|
+
|
208
|
+
|
209
|
+
def new_buffer
|
210
|
+
tokenize = @config[:tokenize]
|
211
|
+
if tokenize
|
212
|
+
if tokenize.respond_to? :call
|
213
|
+
@buffer = tokenize.call
|
214
|
+
else
|
215
|
+
@buffer = ::UV::BufferedTokenizer.new(@config)
|
216
|
+
end
|
217
|
+
elsif @buffer
|
218
|
+
# remove the buffer if none
|
219
|
+
@buffer = nil
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def do_terminate
|
224
|
+
if @queue.waiting
|
225
|
+
@queue.waiting[:defer].reject(TERMINATE_MSG)
|
226
|
+
end
|
227
|
+
@queue.cancel_all(TERMINATE_MSG)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Check transport response data
|
231
|
+
def check_data(data)
|
232
|
+
resp = nil
|
233
|
+
|
234
|
+
# Provide commands with a bonus in this section
|
235
|
+
@bonus = @config[:priority_bonus]
|
236
|
+
|
237
|
+
begin
|
238
|
+
cmd = @queue.waiting
|
239
|
+
if cmd
|
240
|
+
@wait = true
|
241
|
+
@defer = @thread.defer
|
242
|
+
@defer.promise.then @resp_success, @resp_failure
|
243
|
+
|
244
|
+
# Disconnect before processing the response
|
245
|
+
transport.disconnect if cmd[:force_disconnect]
|
246
|
+
|
247
|
+
# Send response, early resolver and command
|
248
|
+
resp = @man.notify_received(data, @resolver, cmd)
|
249
|
+
else
|
250
|
+
resp = @man.notify_received(data, DUMMY_RESOLVER, nil)
|
251
|
+
# Don't need to trigger Queue next here as we are not waiting on anything
|
252
|
+
end
|
253
|
+
rescue => e
|
254
|
+
# NOTE:: This error should never be called
|
255
|
+
@logger.print_error(e, 'error processing response data')
|
256
|
+
@defer.reject :abort if @defer
|
257
|
+
ensure
|
258
|
+
@bonus = 0
|
259
|
+
end
|
260
|
+
|
261
|
+
# Check if response is a success or failure
|
262
|
+
resolve_callback(resp) unless resp == :async
|
263
|
+
end
|
264
|
+
|
265
|
+
def resolve_callback(resp)
|
266
|
+
if @defer
|
267
|
+
if FAILURE.include? resp
|
268
|
+
@defer.reject resp
|
269
|
+
else
|
270
|
+
@defer.resolve resp
|
271
|
+
end
|
272
|
+
@defer = nil
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def resp_failure(result_raw)
|
277
|
+
if @queue.waiting
|
278
|
+
begin
|
279
|
+
result = result_raw.is_a?(Fixnum) ? :timeout : result_raw
|
280
|
+
cmd = @queue.waiting
|
281
|
+
debug = "with #{result}: <#{cmd[:name] || UNNAMED}> "
|
282
|
+
if cmd[:data]
|
283
|
+
debug << "#{cmd[:data].inspect}"
|
284
|
+
else
|
285
|
+
debug << cmd[:path]
|
286
|
+
end
|
287
|
+
@logger.debug "command failed #{debug}"
|
288
|
+
|
289
|
+
if cmd[:retries] == 0
|
290
|
+
err = Error::CommandFailure.new "command aborted #{debug}"
|
291
|
+
cmd[:defer].reject(err)
|
292
|
+
@logger.warn err.message
|
293
|
+
else
|
294
|
+
cmd[:retries] -= 1
|
295
|
+
cmd[:wait_count] = 0 # reset our ignore count
|
296
|
+
@queue.push(cmd, cmd[:priority] + @config[:priority_bonus])
|
297
|
+
end
|
298
|
+
rescue => e
|
299
|
+
# Prevent the queue from ever pausing - this should never be called
|
300
|
+
@logger.print_error(e, 'error handling request failure')
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
clear_timers
|
305
|
+
|
306
|
+
@wait = false
|
307
|
+
@queue.waiting = nil
|
308
|
+
check_next # Process already received
|
309
|
+
@queue.shift if @connected # Then send a new command
|
310
|
+
end
|
311
|
+
|
312
|
+
# We only care about queued commands here
|
313
|
+
# Promises resolve on the next tick so processing
|
314
|
+
# is guaranteed to have completed
|
315
|
+
# Check for queue wait as we may have gone offline
|
316
|
+
def resp_success(result)
|
317
|
+
if @queue.waiting && result && result != :ignore
|
318
|
+
if result == :abort
|
319
|
+
cmd = @queue.waiting
|
320
|
+
err = Error::CommandFailure.new "module aborted command with #{result}: <#{cmd[:name] || UNNAMED}> #{(cmd[:data] || cmd[:path]).inspect}"
|
321
|
+
@queue.waiting[:defer].reject(err)
|
322
|
+
else
|
323
|
+
@queue.waiting[:defer].resolve(result)
|
324
|
+
call_emit @queue.waiting
|
325
|
+
end
|
326
|
+
|
327
|
+
clear_timers
|
328
|
+
|
329
|
+
@wait = false
|
330
|
+
@queue.waiting = nil
|
331
|
+
check_next # Process pending
|
332
|
+
@queue.shift # Send the next command
|
333
|
+
|
334
|
+
# Else it must have been a nil or :ignore
|
335
|
+
elsif @queue.waiting
|
336
|
+
cmd = @queue.waiting
|
337
|
+
cmd[:wait_count] ||= 0
|
338
|
+
cmd[:wait_count] += 1
|
339
|
+
if cmd[:wait_count] > cmd[:max_waits]
|
340
|
+
resp_failure(:max_waits_exceeded)
|
341
|
+
else
|
342
|
+
check_next
|
343
|
+
end
|
344
|
+
|
345
|
+
else # ensure consistent state (offline event may have occurred)
|
346
|
+
|
347
|
+
clear_timers
|
348
|
+
|
349
|
+
@wait = false
|
350
|
+
check_next
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
# If a callback was in place for the current
|
355
|
+
def call_emit(cmd)
|
356
|
+
callback = cmd[:emit]
|
357
|
+
if callback
|
358
|
+
@thread.next_tick do
|
359
|
+
begin
|
360
|
+
callback.call
|
361
|
+
rescue => e
|
362
|
+
@logger.print_error(e, 'error in emit callback')
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
|
369
|
+
# Callback for queued commands
|
370
|
+
def send_next(command)
|
371
|
+
# Check for any required delays between sends
|
372
|
+
if command[:delay] > 0
|
373
|
+
gap = @last_sent_at + command[:delay] - @thread.now
|
374
|
+
if gap > 0
|
375
|
+
defer = @thread.defer
|
376
|
+
sched = schedule.in(gap) do
|
377
|
+
defer.resolve(process_send(command))
|
378
|
+
end
|
379
|
+
# in case of shutdown we need to resolve this promise
|
380
|
+
sched.catch do
|
381
|
+
defer.reject(:shutdown)
|
382
|
+
end
|
383
|
+
defer.promise
|
384
|
+
else
|
385
|
+
process_send(command)
|
386
|
+
end
|
387
|
+
else
|
388
|
+
process_send(command)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def process_send(command)
|
393
|
+
# delay on receive
|
394
|
+
if command[:delay_on_receive] > 0
|
395
|
+
gap = @last_receive_at + command[:delay_on_receive] - @thread.now
|
396
|
+
|
397
|
+
if gap > 0
|
398
|
+
defer = @thread.defer
|
399
|
+
|
400
|
+
sched = schedule.in(gap) do
|
401
|
+
defer.resolve(process_send(command))
|
402
|
+
end
|
403
|
+
# in case of shutdown we need to resolve this promise
|
404
|
+
sched.catch do
|
405
|
+
defer.reject(:shutdown)
|
406
|
+
end
|
407
|
+
defer.promise
|
408
|
+
else
|
409
|
+
transport_send(command)
|
410
|
+
end
|
411
|
+
else
|
412
|
+
transport_send(command)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def transport_send(command)
|
417
|
+
@transport.transmit(command)
|
418
|
+
@last_sent_at = @thread.now
|
419
|
+
|
420
|
+
if @queue.waiting
|
421
|
+
# Set up timers for command timeout
|
422
|
+
@timeout = schedule.in(command[:timeout], @resp_failure)
|
423
|
+
else
|
424
|
+
# resole the send promise early as we are not waiting for the response
|
425
|
+
command[:defer].resolve(:no_wait)
|
426
|
+
call_emit command # the command has been sent
|
427
|
+
end
|
428
|
+
nil # ensure promise chain is not propagated
|
429
|
+
end
|
430
|
+
|
431
|
+
def clear_timers
|
432
|
+
@timeout.cancel if @timeout
|
433
|
+
@timeout = nil
|
434
|
+
end
|
435
|
+
|
436
|
+
def schedule
|
437
|
+
@schedule ||= @man.get_scheduler
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|