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