2014-04-03 15:41:51 +00:00
|
|
|
###
|
|
|
|
|
|
|
|
|
|
Engine
|
|
|
|
|
==================
|
|
|
|
|
> The heart of the WebAPI ECA System. The engine loads action invoker modules
|
|
|
|
|
> corresponding to active rules actions and invokes them if an appropriate event
|
|
|
|
|
> is retrieved.
|
|
|
|
|
|
|
|
|
|
###
|
|
|
|
|
|
|
|
|
|
# **Loads Modules:**
|
|
|
|
|
|
|
|
|
|
# - [Persistence](persistence.html)
|
|
|
|
|
db = require './persistence'
|
|
|
|
|
# - [Dynamic Modules](dynamic-modules.html)
|
|
|
|
|
dynmod = require './dynamic-modules'
|
|
|
|
|
|
2014-04-03 21:35:02 +00:00
|
|
|
# - External Modules:
|
2014-04-05 17:02:03 +00:00
|
|
|
# [js-select](https://github.com/harthur/js-select)
|
2014-04-03 21:35:02 +00:00
|
|
|
jsonQuery = require 'js-select'
|
|
|
|
|
|
2014-04-04 17:53:42 +00:00
|
|
|
###
|
|
|
|
|
This is ging to have a structure like:
|
|
|
|
|
An object of users with their active rules and the required action modules
|
2014-04-05 01:09:40 +00:00
|
|
|
|
2014-04-16 15:42:56 +00:00
|
|
|
"user-1":
|
|
|
|
|
"rule-1":
|
|
|
|
|
"rule": oRule-1
|
|
|
|
|
"actions":
|
|
|
|
|
"action-1": oAction-1
|
|
|
|
|
"action-2": oAction-2
|
|
|
|
|
"rule-2":
|
|
|
|
|
"rule": oRule-2
|
|
|
|
|
"actions":
|
|
|
|
|
"action-1": oAction-1
|
|
|
|
|
"user-2":
|
|
|
|
|
"rule-3":
|
|
|
|
|
"rule": oRule-3
|
|
|
|
|
"actions":
|
|
|
|
|
"action-3": oAction-3
|
2014-04-04 17:53:42 +00:00
|
|
|
###
|
2014-04-15 12:25:26 +00:00
|
|
|
|
|
|
|
|
#TODO how often do we allow rules to be processed?
|
|
|
|
|
#it would make sense to implement a scheduler approach, which means to store the
|
|
|
|
|
#events in the DB and query for them if a rule is evaluated. Through this we would allow
|
|
|
|
|
#a CEP approach, rather than just ECA and could combine events/evaluate time constraints
|
2014-04-03 15:41:51 +00:00
|
|
|
listUserRules = {}
|
|
|
|
|
isRunning = false
|
|
|
|
|
|
|
|
|
|
###
|
|
|
|
|
Module call
|
|
|
|
|
-----------
|
|
|
|
|
Initializes the Engine and starts polling the event queue for new events.
|
|
|
|
|
|
|
|
|
|
@param {Object} args
|
|
|
|
|
###
|
|
|
|
|
exports = module.exports = ( args ) =>
|
2014-04-16 15:42:56 +00:00
|
|
|
if not isRunning
|
|
|
|
|
@log = args.logger
|
|
|
|
|
db args
|
|
|
|
|
dynmod args
|
|
|
|
|
setTimeout exports.startEngine, 10 # Very important, this forks a token for the poll task
|
|
|
|
|
module.exports
|
2014-04-03 15:41:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
###
|
2014-04-03 21:35:02 +00:00
|
|
|
This is a helper function for the unit tests so we can verify that action
|
|
|
|
|
modules are loaded correctly
|
2014-04-05 01:09:40 +00:00
|
|
|
|
2014-04-03 21:35:02 +00:00
|
|
|
@public getListUserRules ()
|
|
|
|
|
###
|
2014-04-05 01:09:40 +00:00
|
|
|
#TODO we should change this to functions returning true or false rather than returning
|
|
|
|
|
#the whole list
|
2014-04-03 21:35:02 +00:00
|
|
|
exports.getListUserRules = () ->
|
2014-04-16 15:42:56 +00:00
|
|
|
listUserRules
|
2014-04-03 21:35:02 +00:00
|
|
|
|
2014-04-05 17:02:03 +00:00
|
|
|
# We need this so we can shut it down after the module unit tests
|
|
|
|
|
exports.startEngine = () ->
|
2014-04-16 15:42:56 +00:00
|
|
|
if not isRunning
|
|
|
|
|
isRunning = true
|
|
|
|
|
pollQueue()
|
2014-04-03 15:41:51 +00:00
|
|
|
|
|
|
|
|
###
|
2014-04-03 21:35:02 +00:00
|
|
|
An event associated to rules happened and is captured here. Such events
|
|
|
|
|
are basically CRUD on rules.
|
2014-04-03 15:41:51 +00:00
|
|
|
|
2014-04-03 21:35:02 +00:00
|
|
|
@public internalEvent ( *evt* )
|
|
|
|
|
@param {Object} evt
|
|
|
|
|
###
|
2014-04-03 15:41:51 +00:00
|
|
|
exports.internalEvent = ( evt ) =>
|
2014-04-16 15:42:56 +00:00
|
|
|
if not listUserRules[evt.user] and evt.event isnt 'del'
|
|
|
|
|
listUserRules[evt.user] = {}
|
2014-04-03 15:41:51 +00:00
|
|
|
|
2014-04-16 15:42:56 +00:00
|
|
|
oUser = listUserRules[evt.user]
|
|
|
|
|
oRule = evt.rule
|
|
|
|
|
if evt.event is 'new' or ( evt.event is 'init' and not oUser[oRule.id] )
|
|
|
|
|
oUser[oRule.id] =
|
|
|
|
|
rule: oRule
|
|
|
|
|
actions: {}
|
|
|
|
|
updateActionModules oRule.id
|
2014-04-03 21:35:02 +00:00
|
|
|
|
2014-04-16 15:42:56 +00:00
|
|
|
if evt.event is 'del' and oUser
|
|
|
|
|
delete oUser[evt.ruleId]
|
2014-04-04 17:53:42 +00:00
|
|
|
|
2014-04-16 15:42:56 +00:00
|
|
|
# If a user is empty after all the updates above, we remove her from the list
|
|
|
|
|
if JSON.stringify( oUser ) is "{}"
|
|
|
|
|
delete listUserRules[evt.user]
|
2014-04-04 17:53:42 +00:00
|
|
|
|
2014-04-03 15:41:51 +00:00
|
|
|
|
2014-04-03 21:35:02 +00:00
|
|
|
|
|
|
|
|
###
|
|
|
|
|
As soon as changes were made to the rule set we need to ensure that the aprropriate action
|
|
|
|
|
invoker modules are loaded, updated or deleted.
|
|
|
|
|
|
2014-04-04 17:53:42 +00:00
|
|
|
@private updateActionModules ( *updatedRuleId* )
|
|
|
|
|
@param {Object} updatedRuleId
|
2014-04-03 21:35:02 +00:00
|
|
|
###
|
2014-04-16 15:42:56 +00:00
|
|
|
updateActionModules = ( updatedRuleId ) =>
|
|
|
|
|
|
|
|
|
|
# Remove all action invoker modules that are not required anymore
|
|
|
|
|
fRemoveNotRequired = ( oUser ) ->
|
|
|
|
|
|
|
|
|
|
# Check whether the action is still existing in the rule
|
|
|
|
|
fRequired = ( actionName ) ->
|
|
|
|
|
for action in oUser[updatedRuleId].rule.actions
|
|
|
|
|
# Since the event is in the format 'module -> function' we need to split the string
|
2014-04-21 12:42:26 +00:00
|
|
|
if (action.split ' -> ')[ 0 ] is actionName
|
2014-04-16 15:42:56 +00:00
|
|
|
return true
|
|
|
|
|
false
|
|
|
|
|
|
|
|
|
|
# Go thorugh all loaded action modules and check whether the action is still required
|
2014-04-21 12:42:26 +00:00
|
|
|
if oUser[updatedRuleId]
|
|
|
|
|
for action of oUser[updatedRuleId].rule.actions
|
|
|
|
|
delete oUser[updatedRuleId].actions[action] if not fRequired action
|
2014-04-16 15:42:56 +00:00
|
|
|
|
|
|
|
|
fRemoveNotRequired oUser for name, oUser of listUserRules
|
|
|
|
|
|
|
|
|
|
# Add action invoker modules that are not yet loaded
|
|
|
|
|
fAddRequired = ( userName, oUser ) =>
|
|
|
|
|
|
|
|
|
|
# Check whether the action is existing in a rule and load if not
|
|
|
|
|
fCheckRules = ( oMyRule ) =>
|
|
|
|
|
|
|
|
|
|
# Load the action invoker module if it was part of the updated rule or if it's new
|
|
|
|
|
fAddIfNewOrNotExisting = ( actionName ) =>
|
2014-04-21 12:42:26 +00:00
|
|
|
moduleName = (actionName.split ' -> ')[ 0 ]
|
2014-04-16 15:42:56 +00:00
|
|
|
if not oMyRule.actions[moduleName] or oMyRule.rule.id is updatedRuleId
|
|
|
|
|
db.actionInvokers.getModule moduleName, ( err, obj ) =>
|
|
|
|
|
if obj
|
|
|
|
|
# we compile the module and pass:
|
|
|
|
|
dynmod.compileString obj.data, # code
|
|
|
|
|
userName, # userId
|
|
|
|
|
oMyRule.rule.id, # ruleId
|
|
|
|
|
moduleName, # moduleId
|
|
|
|
|
obj.lang, # script language
|
|
|
|
|
db.actionInvokers, # the DB interface
|
|
|
|
|
( result ) =>
|
2014-04-16 21:38:41 +00:00
|
|
|
if result.answ.code is 200
|
|
|
|
|
@log.info "EN | Module '#{ moduleName }' successfully loaded for userName
|
|
|
|
|
'#{ userName }' in rule '#{ oMyRule.rule.id }'"
|
|
|
|
|
else
|
2014-04-16 15:42:56 +00:00
|
|
|
@log.error "EN | Compilation of code failed! #{ userName },
|
2014-04-16 21:38:41 +00:00
|
|
|
#{ oMyRule.rule.id }, #{ moduleName }: #{ result.answ.message }"
|
2014-04-19 20:35:05 +00:00
|
|
|
oMyRule.actions[moduleName] = result
|
2014-04-16 15:42:56 +00:00
|
|
|
else
|
2014-04-16 21:38:41 +00:00
|
|
|
@log.warn "EN | #{ moduleName } not found for #{ oMyRule.rule.id }!"
|
2014-04-16 15:42:56 +00:00
|
|
|
|
|
|
|
|
fAddIfNewOrNotExisting action for action in oMyRule.rule.actions
|
|
|
|
|
|
|
|
|
|
# Go thorugh all rules and check whether the action is still required
|
|
|
|
|
fCheckRules oRl for nmRl, oRl of oUser
|
|
|
|
|
|
|
|
|
|
# load all required modules for all users
|
|
|
|
|
fAddRequired userName, oUser for userName, oUser of listUserRules
|
|
|
|
|
|
|
|
|
|
numExecutingFunctions = 1
|
2014-04-03 15:41:51 +00:00
|
|
|
pollQueue = () ->
|
2014-04-16 15:42:56 +00:00
|
|
|
if isRunning
|
|
|
|
|
db.popEvent ( err, obj ) ->
|
|
|
|
|
if not err and obj
|
|
|
|
|
processEvent obj
|
|
|
|
|
setTimeout pollQueue, 20 * numExecutingFunctions #FIXME right way to adapt to load?
|
2014-04-03 15:41:51 +00:00
|
|
|
|
|
|
|
|
###
|
|
|
|
|
Checks whether all conditions of the rule are met by the event.
|
|
|
|
|
|
|
|
|
|
@private validConditions ( *evt, rule* )
|
|
|
|
|
@param {Object} evt
|
|
|
|
|
@param {Object} rule
|
|
|
|
|
###
|
|
|
|
|
validConditions = ( evt, rule ) ->
|
2014-04-16 15:42:56 +00:00
|
|
|
if rule.conditions.length is 0
|
|
|
|
|
return true
|
|
|
|
|
for prop in rule.conditions
|
|
|
|
|
return false if jsonQuery( evt, prop ).nodes().length is 0
|
|
|
|
|
return true
|
2014-04-03 15:41:51 +00:00
|
|
|
|
|
|
|
|
###
|
|
|
|
|
Handles retrieved events.
|
|
|
|
|
|
|
|
|
|
@private processEvent ( *evt* )
|
|
|
|
|
@param {Object} evt
|
|
|
|
|
###
|
2014-04-03 21:35:02 +00:00
|
|
|
processEvent = ( evt ) =>
|
2014-04-16 15:42:56 +00:00
|
|
|
fSearchAndInvokeAction = ( node, arrPath, funcName, evt, depth ) =>
|
|
|
|
|
if not node
|
2014-04-16 21:38:41 +00:00
|
|
|
@log.error "EN | Didn't find property in user rule list: " + arrPath.join( ', ' ) + " at depth " + depth
|
2014-04-16 15:42:56 +00:00
|
|
|
return
|
|
|
|
|
if depth is arrPath.length
|
|
|
|
|
try
|
|
|
|
|
numExecutingFunctions++
|
|
|
|
|
@log.info "EN | #{ funcName } executes..."
|
2014-04-19 20:35:05 +00:00
|
|
|
arrArgs = []
|
|
|
|
|
if node.funcArgs[ funcName ]
|
|
|
|
|
for oArg in node.funcArgs[ funcName ]
|
|
|
|
|
if oArg.jsselector
|
|
|
|
|
arrArgs.push jsonQuery( evt.payload, oArg.value ).nodes()[ 0 ]
|
|
|
|
|
else
|
|
|
|
|
arrArgs.push oArg.value
|
|
|
|
|
else
|
|
|
|
|
@log.warn "EN | Weird! arguments not loaded for function '#{ funcName }'!"
|
|
|
|
|
node.module[ funcName ].apply null, arrArgs
|
2014-04-16 15:42:56 +00:00
|
|
|
@log.info "EN | #{ funcName } finished execution"
|
|
|
|
|
catch err
|
|
|
|
|
@log.info "EN | ERROR IN ACTION INVOKER: " + err.message
|
|
|
|
|
node.logger err.message
|
|
|
|
|
if numExecutingFunctions-- % 100 is 0
|
|
|
|
|
@log.warn "EN | The system is producing too many tokens! Currently: #{ numExecutingFunctions }"
|
|
|
|
|
else
|
|
|
|
|
fSearchAndInvokeAction node[arrPath[depth]], arrPath, funcName, evt, depth + 1
|
|
|
|
|
|
|
|
|
|
@log.info 'EN | processing event: ' + evt.event + '(' + evt.eventid + ')'
|
|
|
|
|
for userName, oUser of listUserRules
|
|
|
|
|
for ruleName, oMyRule of oUser
|
|
|
|
|
if evt.event is oMyRule.rule.event and validConditions evt, oMyRule.rule
|
|
|
|
|
@log.info 'EN | EVENT FIRED: ' + evt.event + '(' + evt.eventid + ') for rule ' + ruleName
|
|
|
|
|
for action in oMyRule.rule.actions
|
|
|
|
|
arr = action.split ' -> '
|
|
|
|
|
fSearchAndInvokeAction listUserRules, [ userName, ruleName, 'actions', arr[0]], arr[1], evt, 0
|
2014-04-03 15:41:51 +00:00
|
|
|
|
|
|
|
|
exports.shutDown = () ->
|
2014-04-16 21:38:41 +00:00
|
|
|
isRunning = false
|
|
|
|
|
listUserRules = {}
|
|
|
|
|
|