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.
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