orchestrator 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +158 -0
  3. data/README.md +13 -0
  4. data/Rakefile +7 -0
  5. data/app/controllers/orchestrator/api/dependencies_controller.rb +109 -0
  6. data/app/controllers/orchestrator/api/modules_controller.rb +183 -0
  7. data/app/controllers/orchestrator/api/systems_controller.rb +294 -0
  8. data/app/controllers/orchestrator/api/zones_controller.rb +62 -0
  9. data/app/controllers/orchestrator/api_controller.rb +13 -0
  10. data/app/controllers/orchestrator/base.rb +59 -0
  11. data/app/controllers/orchestrator/persistence_controller.rb +29 -0
  12. data/app/models/orchestrator/access_log.rb +35 -0
  13. data/app/models/orchestrator/control_system.rb +160 -0
  14. data/app/models/orchestrator/dependency.rb +87 -0
  15. data/app/models/orchestrator/mod/by_dependency/map.js +6 -0
  16. data/app/models/orchestrator/mod/by_module_type/map.js +6 -0
  17. data/app/models/orchestrator/module.rb +127 -0
  18. data/app/models/orchestrator/sys/by_modules/map.js +9 -0
  19. data/app/models/orchestrator/sys/by_zones/map.js +9 -0
  20. data/app/models/orchestrator/zone.rb +47 -0
  21. data/app/models/orchestrator/zone/all/map.js +6 -0
  22. data/config/routes.rb +43 -0
  23. data/lib/generators/module/USAGE +8 -0
  24. data/lib/generators/module/module_generator.rb +52 -0
  25. data/lib/orchestrator.rb +52 -0
  26. data/lib/orchestrator/control.rb +303 -0
  27. data/lib/orchestrator/core/mixin.rb +123 -0
  28. data/lib/orchestrator/core/module_manager.rb +258 -0
  29. data/lib/orchestrator/core/request_proxy.rb +109 -0
  30. data/lib/orchestrator/core/requests_proxy.rb +47 -0
  31. data/lib/orchestrator/core/schedule_proxy.rb +49 -0
  32. data/lib/orchestrator/core/system_proxy.rb +153 -0
  33. data/lib/orchestrator/datagram_server.rb +114 -0
  34. data/lib/orchestrator/dependency_manager.rb +131 -0
  35. data/lib/orchestrator/device/command_queue.rb +213 -0
  36. data/lib/orchestrator/device/manager.rb +83 -0
  37. data/lib/orchestrator/device/mixin.rb +35 -0
  38. data/lib/orchestrator/device/processor.rb +441 -0
  39. data/lib/orchestrator/device/transport_makebreak.rb +221 -0
  40. data/lib/orchestrator/device/transport_tcp.rb +139 -0
  41. data/lib/orchestrator/device/transport_udp.rb +89 -0
  42. data/lib/orchestrator/engine.rb +70 -0
  43. data/lib/orchestrator/errors.rb +23 -0
  44. data/lib/orchestrator/logger.rb +115 -0
  45. data/lib/orchestrator/logic/manager.rb +18 -0
  46. data/lib/orchestrator/logic/mixin.rb +11 -0
  47. data/lib/orchestrator/service/manager.rb +63 -0
  48. data/lib/orchestrator/service/mixin.rb +56 -0
  49. data/lib/orchestrator/service/transport_http.rb +55 -0
  50. data/lib/orchestrator/status.rb +229 -0
  51. data/lib/orchestrator/system.rb +108 -0
  52. data/lib/orchestrator/utilities/constants.rb +41 -0
  53. data/lib/orchestrator/utilities/transcoder.rb +57 -0
  54. data/lib/orchestrator/version.rb +3 -0
  55. data/lib/orchestrator/websocket_manager.rb +425 -0
  56. data/orchestrator.gemspec +35 -0
  57. data/spec/orchestrator/queue_spec.rb +200 -0
  58. 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