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,123 @@
1
+ module Orchestrator
2
+ module Core
3
+ SCHEDULE_ACCESS_DENIED = 'schedule unavailable in a task'.freeze
4
+
5
+ module Mixin
6
+
7
+ # Returns a wrapper around a shared instance of ::UV::Scheduler
8
+ #
9
+ # @return [::Orchestrator::Core::ScheduleProxy]
10
+ def schedule
11
+ raise SCHEDULE_ACCESS_DENIED unless @__config__.thread.reactor_thread?
12
+ @__config__.get_scheduler
13
+ end
14
+
15
+ # Looks up a system based on its name and returns a proxy to that system via a promise
16
+ #
17
+ # @param name [String] the name of the system being accessed
18
+ # @return [::Libuv::Q::Promise] Returns a single promise
19
+ def systems(name)
20
+ task do
21
+ @__config__.get_system(name)
22
+ end
23
+ end
24
+
25
+ # Performs a long running task on a thread pool in parallel.
26
+ #
27
+ # @param callback [Proc] the work to be processed on the thread pool
28
+ # @return [::Libuv::Q::Promise] Returns a single promise
29
+ def task(callback = nil, &block)
30
+ thread = @__config__.thread
31
+ defer = thread.defer
32
+ thread.schedule do
33
+ defer.resolve(thread.work(callback, &block))
34
+ end
35
+ defer.promise
36
+ end
37
+
38
+ # Thread safe status access
39
+ def [](name)
40
+ @__config__.status[name.to_sym]
41
+ end
42
+
43
+ # thread safe status settings
44
+ def []=(status, value)
45
+ @__config__.trak(status.to_sym, value)
46
+
47
+ # Check level to speed processing
48
+ if @__config__.logger.level == 0
49
+ @__config__.logger.debug "Status updated: #{status} = #{value}"
50
+ end
51
+ end
52
+
53
+ # thread safe status subscription
54
+ def subscribe(status, callback = nil, &block)
55
+ callback ||= block
56
+ raise 'callback required' unless callback.respond_to? :call
57
+
58
+ thread = @__config__.thread
59
+ defer = thread.defer
60
+ thread.schedule do
61
+ defer.resolve(@__config__.subscribe(status, callback))
62
+ end
63
+ defer.promise
64
+ end
65
+
66
+ # thread safe unsubscribe
67
+ def unsubscribe(sub)
68
+ @__config__.thread.schedule do
69
+ @__config__.unsubscribe(sub)
70
+ end
71
+ end
72
+
73
+ def logger
74
+ @__config__.logger
75
+ end
76
+
77
+ def setting(name)
78
+ set = name.to_sym
79
+ @__config__.setting(set)
80
+ end
81
+
82
+ # Updates a setting that will effect the local module only
83
+ #
84
+ # @param name [String|Symbol] the setting name
85
+ # @param value [String|Symbol|Numeric|Array|Hash] the setting value
86
+ # @return [::Libuv::Q::Promise] Promise that will resolve once the setting is persisted
87
+ def define_setting(name, value)
88
+ set = name.to_sym
89
+ @__config__.define_setting(set, value)
90
+ end
91
+
92
+ def wake_device(mac, ip = '<broadcast>')
93
+ @__config__.thread.schedule do
94
+ @__config__.thread.wake_device(mac, ip)
95
+ end
96
+ end
97
+
98
+ # Outputs any statistics collected on the module
99
+ def __STATS__
100
+ stats = {}
101
+ if @__config__.respond_to? :processor
102
+ stats[:queue_size] = @__config__.processor.queue.length
103
+ stats[:queue_waiting] = !@__config__.processor.queue.waiting.nil?
104
+ stats[:queue_pause] = @__config__.processor.queue.pause
105
+ stats[:queue_state] = @__config__.processor.queue.state
106
+
107
+ stats[:last_send] = @__config__.processor.last_sent_at
108
+ stats[:last_receive] = @__config__.processor.last_receive_at
109
+ if @__config__.processor.timeout
110
+ stats[:timeout_created] = @__config__.processor.timeout.created
111
+ stats[:timeout_triggered] = @__config__.processor.timeout.trigger_count
112
+ stats[:timeout_scheduled] = @__config__.processor.timeout.next_scheduled
113
+ end
114
+ end
115
+
116
+ stats[:time_now] = @__config__.thread.now
117
+ stats[:schedules] = schedule.schedules.to_a
118
+
119
+ logger.debug JSON.generate(stats)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,258 @@
1
+ module Orchestrator
2
+ module Core
3
+ class ModuleManager
4
+ def initialize(thread, klass, settings)
5
+ @thread = thread # Libuv Loop
6
+ @settings = settings # Database model
7
+ @klass = klass
8
+
9
+ # Bit of a hack - should make testing pretty easy though
10
+ @status = ::ThreadSafe::Cache.new
11
+ @stattrak = @thread.observer
12
+ @logger = ::Orchestrator::Logger.new(@thread, @settings)
13
+
14
+ @updating = Mutex.new
15
+ end
16
+
17
+
18
+ attr_reader :thread, :settings, :instance
19
+ attr_reader :status, :stattrak, :logger
20
+
21
+
22
+ # Should always be called on the module thread
23
+ def stop
24
+ return if @instance.nil?
25
+ begin
26
+ if @instance.respond_to? :on_unload, true
27
+ @instance.__send__(:on_unload)
28
+ end
29
+ rescue => e
30
+ @logger.print_error(e, 'error in module unload callback')
31
+ ensure
32
+ # Clean up
33
+ @instance = nil
34
+ @scheduler.clear if @scheduler
35
+ if @subsciptions
36
+ unsub = @stattrak.method(:unsubscribe)
37
+ @subsciptions.each &unsub
38
+ @subsciptions = nil
39
+ end
40
+ update_running_status(false)
41
+ end
42
+ end
43
+
44
+ def start
45
+ return true unless @instance.nil?
46
+ config = self
47
+ @instance = @klass.new
48
+ @instance.instance_eval { @__config__ = config }
49
+ if @instance.respond_to? :on_load, true
50
+ begin
51
+ @instance.__send__(:on_load)
52
+ rescue => e
53
+ @logger.print_error(e, 'error in module load callback')
54
+ end
55
+ end
56
+ update_running_status(true)
57
+ true # for REST API
58
+ rescue => e
59
+ @logger.print_error(e, 'module failed to start')
60
+ false
61
+ end
62
+
63
+ def reloaded(mod)
64
+ @thread.schedule do
65
+ # pass in any updated settings
66
+ @settings = mod
67
+
68
+ if @instance.respond_to? :on_update, true
69
+ begin
70
+ @instance.__send__(:on_update)
71
+ rescue => e
72
+ @logger.print_error(e, 'error in module update callback')
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def get_scheduler
79
+ @scheduler ||= ::Orchestrator::Core::ScheduleProxy.new(@thread)
80
+ end
81
+
82
+ # This is called from Core::Mixin on the thread pool as the DB query will be blocking
83
+ # NOTE:: Couchbase does support non-blocking gets although I think this is simpler
84
+ #
85
+ # @return [::Orchestrator::Core::SystemProxy]
86
+ # @raise [Couchbase::Error::NotFound] if unable to find the system in the DB
87
+ def get_system(name)
88
+ id = ::Orchestrator::ControlSystem.bucket.get("sysname-#{name}")
89
+ ::Orchestrator::Core::SystemProxy.new(@thread, id.to_sym, self)
90
+ end
91
+
92
+ # Called from Core::Mixin - thread safe
93
+ def trak(name, value)
94
+ if @status[name] != value
95
+ @status[name] = value
96
+
97
+ # Allows status to be updated in workers
98
+ # For the most part this will run straight away
99
+ @thread.schedule do
100
+ @stattrak.update(@settings.id.to_sym, name, value)
101
+ end
102
+ end
103
+ end
104
+
105
+ # Subscribe to status updates from status in the same module
106
+ # Called from Core::Mixin always on the module thread
107
+ def subscribe(status, callback)
108
+ sub = @stattrak.subscribe({
109
+ on_thread: @thread,
110
+ callback: callback,
111
+ status: status.to_sym,
112
+ mod_id: @settings.id.to_sym,
113
+ mod: self
114
+ })
115
+ add_subscription sub
116
+ sub
117
+ end
118
+
119
+ # Called from Core::Mixin always on the module thread
120
+ def unsubscribe(sub)
121
+ if sub.is_a? ::Libuv::Q::Promise
122
+ # Promise recursion?
123
+ sub.then method(:unsubscribe)
124
+ else
125
+ @subsciptions.delete sub
126
+ @stattrak.unsubscribe(sub)
127
+ end
128
+ end
129
+
130
+ # Called from subscribe and SystemProxy.subscribe always on the module thread
131
+ def add_subscription(sub)
132
+ if sub.is_a? ::Libuv::Q::Promise
133
+ # Promise recursion?
134
+ sub.then method(:add_subscription)
135
+ else
136
+ @subsciptions ||= Set.new
137
+ @subsciptions.add sub
138
+ end
139
+ end
140
+
141
+ # Called from Core::Mixin on any thread
142
+ # For Logics: instance -> system -> zones -> dependency
143
+ # For Device: instance -> dependency
144
+ def setting(name)
145
+ res = @settings.settings[name]
146
+ if res.nil?
147
+ if !@settings.control_system_id.nil?
148
+ sys = System.get(@settings.control_system_id)
149
+ res = sys.settings[name]
150
+
151
+ # Check if zones have the setting
152
+ if res.nil?
153
+ sys.zones.each do |zone|
154
+ res = zone.settings[name]
155
+ return res unless res.nil?
156
+ end
157
+
158
+ # Fallback to the dependency
159
+ res = @settings.dependency.settings[name]
160
+ end
161
+ else
162
+ # Fallback to the dependency
163
+ res = @settings.dependency.settings[name]
164
+ end
165
+ end
166
+ res
167
+ end
168
+
169
+ # Called from Core::Mixin on any thread
170
+ #
171
+ # Settings updates are done on the thread pool
172
+ # We have to replace the structure as other threads may be
173
+ # reading from the old structure and the settings hash is not
174
+ # thread safe
175
+ def define_setting(name, value)
176
+ defer = thread.defer
177
+ thread.schedule do
178
+ defer.resolve(thread.work(proc {
179
+ mod = Orchestrator::Module.find(@settings.id)
180
+ mod.settings[name] = value
181
+ mod.save!
182
+ mod
183
+ }))
184
+ end
185
+ defer.promise.then do |db_model|
186
+ @settings = db_model
187
+ value # Don't leak direct access to the database model
188
+ end
189
+ end
190
+
191
+
192
+ # override the default inspect method
193
+ # This provides relevant information and won't blow the stack on an error
194
+ def inspect
195
+ "#<#{self.class}:0x#{self.__id__.to_s(16)} @thread=#{@thread.inspect} running=#{!@instance.nil?} managing=#{@klass.to_s} id=#{@settings.id}>"
196
+ end
197
+
198
+
199
+ protected
200
+
201
+
202
+ def update_connected_status(connected)
203
+ id = settings.id
204
+
205
+ # Access the database in a non-blocking fashion
206
+ thread.work(proc {
207
+ @updating.synchronize {
208
+ model = ::Orchestrator::Module.find_by_id id
209
+
210
+ if model && model.connected != connected
211
+ model.connected = connected
212
+ model.save!
213
+ model
214
+ else
215
+ nil
216
+ end
217
+ }
218
+ }).then(proc { |model|
219
+ # Update the model if it was updated
220
+ if model
221
+ @settings = model
222
+ end
223
+ }, proc { |e|
224
+ # report any errors updating the model
225
+ @logger.print_error(e, 'error updating connected state in database model')
226
+ })
227
+ end
228
+
229
+ def update_running_status(running)
230
+ id = settings.id
231
+
232
+ # Access the database in a non-blocking fashion
233
+ thread.work(proc {
234
+ @updating.synchronize {
235
+ model = ::Orchestrator::Module.find_by_id id
236
+
237
+ if model && model.running != running
238
+ model.running = running
239
+ model.connected = false if !running
240
+ model.save!
241
+ model
242
+ else
243
+ nil
244
+ end
245
+ }
246
+ }).then(proc { |model|
247
+ # Update the model if it was updated
248
+ if model
249
+ @settings = model
250
+ end
251
+ }, proc { |e|
252
+ # report any errors updating the model
253
+ @logger.print_error(e, 'error updating running state in database model')
254
+ })
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,109 @@
1
+ module Orchestrator
2
+ module Core
3
+ PROTECTED = ::ThreadSafe::Cache.new
4
+ PROTECTED[:unsubscribe] = true
5
+ PROTECTED[:subscribe] = true
6
+ PROTECTED[:schedule] = true
7
+ PROTECTED[:systems] = true
8
+ #PROTECTED[:setting] = true # settings might be useful
9
+ PROTECTED[:system] = true
10
+ PROTECTED[:logger] = true
11
+ PROTECTED[:task] = true
12
+ PROTECTED[:wake_device] = true
13
+
14
+ # Object functions
15
+ PROTECTED[:__send__] = true
16
+ PROTECTED[:public_send] = true
17
+ PROTECTED[:taint] = true
18
+ PROTECTED[:untaint] = true
19
+ PROTECTED[:trust] = true
20
+ PROTECTED[:untrust] = true
21
+ PROTECTED[:freeze] = true
22
+
23
+ # Callbacks
24
+ PROTECTED[:on_load] = true
25
+ PROTECTED[:on_unload] = true
26
+ PROTECTED[:on_update] = true
27
+ PROTECTED[:connected] = true
28
+ PROTECTED[:disconnected] = true
29
+ PROTECTED[:received] = true
30
+
31
+ # Device module
32
+ PROTECTED[:send] = true
33
+ PROTECTED[:defaults] = true
34
+ PROTECTED[:disconnect] = true
35
+ PROTECTED[:config] = true
36
+
37
+ # Service module
38
+ PROTECTED[:get] = true
39
+ PROTECTED[:put] = true
40
+ PROTECTED[:post] = true
41
+ PROTECTED[:delete] = true
42
+ PROTECTED[:request] = true
43
+ PROTECTED[:clear_cookies] = true
44
+ PROTECTED[:use_middleware] = true
45
+
46
+
47
+ class RequestProxy
48
+ def initialize(thread, mod)
49
+ @mod = mod
50
+ @thread = thread
51
+ end
52
+
53
+ # Simplify access to status variables as they are thread safe
54
+ def [](name)
55
+ @mod.instance[name]
56
+ end
57
+
58
+ def []=(status, value)
59
+ @mod.instance[status] = value
60
+ end
61
+
62
+ # Returns true if there is no object to proxy
63
+ #
64
+ # @return [true|false]
65
+ def nil?
66
+ @mod.nil?
67
+ end
68
+
69
+ # Returns true if the module responds to the given method
70
+ #
71
+ # @return [true|false]
72
+ def respond_to?(symbol, include_all = false)
73
+ if @mod
74
+ @mod.instance.respond_to?(symbol, include_all)
75
+ else
76
+ false
77
+ end
78
+ end
79
+
80
+ # All other method calls are wrapped in a promise
81
+ def method_missing(name, *args, &block)
82
+ defer = @thread.defer
83
+
84
+ if @mod.nil?
85
+ err = Error::ModuleUnavailable.new "method '#{name}' request failed as the module is not available at this time"
86
+ defer.reject(err)
87
+ # TODO:: debug log here
88
+ elsif ::Orchestrator::Core::PROTECTED[name]
89
+ err = Error::ProtectedMethod.new "attempt to access module '#{@mod.settings.id}' protected method '#{name}'"
90
+ defer.reject(err)
91
+ @mod.logger.warn(err.message)
92
+ else
93
+ @mod.thread.schedule do
94
+ begin
95
+ defer.resolve(
96
+ @mod.instance.public_send(name, *args, &block)
97
+ )
98
+ rescue => e
99
+ @mod.logger.print_error(e)
100
+ defer.reject(e)
101
+ end
102
+ end
103
+ end
104
+
105
+ defer.promise
106
+ end
107
+ end
108
+ end
109
+ end