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