Queue device updates and commit them using fewer database queries for better performance. Partial updates caused by faulty tasks will no longer be committed in order to avoid messing the device model.

This commit is contained in:
Zaid Abdulla committed 2014-07-24 01:49:04 +03:00
1 parent 4b244f43e0
commit db6eae2aee
3 files changed
+213 -142

No files matched your search

+29 -22
View File
@@ -111,13 +111,11 @@ inform = (currentRequest, cwmpRequest) ->
informHash = crypto.createHash('md5').update(JSON.stringify(parameterNames)).update(JSON.stringify(config.CUSTOM_COMMANDS)).digest('hex') informHash = crypto.createHash('md5').update(JSON.stringify(parameterNames)).update(JSON.stringify(config.CUSTOM_COMMANDS)).digest('hex')
actions = {parameterValues : cwmpRequest.methodRequest.parameterList, set : {}} deviceUpdates = {parameterValues : cwmpRequest.methodRequest.parameterList, informEvents : cwmpRequest.methodRequest.event}
actions.set._lastInform = now
actions.set._lastBoot = now if '1 BOOT' in cwmpRequest.methodRequest.event
# Delete all pending tasks on bootstrap (e.g. factory reset)
if '0 BOOTSTRAP' in cwmpRequest.methodRequest.event if '0 BOOTSTRAP' in cwmpRequest.methodRequest.event
actions.set._lastBootstrap = lastBootstrap = now lastBootstrap = now
# ignore all pending tasks
db.tasksCollection.remove({device : currentRequest.session.deviceId}, (err, removed) -> db.tasksCollection.remove({device : currentRequest.session.deviceId}, (err, removed) ->
throw err if err throw err if err
) )
@@ -126,17 +124,17 @@ inform = (currentRequest, cwmpRequest) ->
throw err if err throw err if err
updateAndRespond = () -> updateAndRespond = () ->
updateDevice(currentRequest.session.deviceId, actions, (err) -> updateDevice.commitUpdates(currentRequest.session.deviceId, deviceUpdates, false, (err) ->
throw err if err throw err if err
res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'InformResponse'},
cwmpVersion : cwmpRequest.cwmpVersion
})
res.headers['Set-Cookie'] = "session=#{currentRequest.sessionId}"
writeResponse(currentRequest, res)
db.redisClient.setex("session_#{currentRequest.sessionId}", currentRequest.session.sessionTimeout, JSON.stringify(currentRequest.session), (err) -> db.redisClient.setex("session_#{currentRequest.sessionId}", currentRequest.session.sessionTimeout, JSON.stringify(currentRequest.session), (err) ->
throw err if err throw err if err
res = soap.response({
id : cwmpRequest.id,
methodResponse : {type : 'InformResponse'},
cwmpVersion : cwmpRequest.cwmpVersion
})
res.headers['Set-Cookie'] = "session=#{currentRequest.sessionId}"
writeResponse(currentRequest, res)
) )
) )
@@ -189,8 +187,8 @@ inform = (currentRequest, cwmpRequest) ->
delete deviceCustomCommands[cmd] delete deviceCustomCommands[cmd]
for cmd of deviceCustomCommands for cmd of deviceCustomCommands
actions.deletedObjects ?= [] deviceUpdates.deletedObjects ?= []
actions.deletedObjects.push("_customCommands.#{cmd}") deviceUpdates.deletedObjects.push("_customCommands.#{cmd}")
# clear presets hash if parameters are potentially modified # clear presets hash if parameters are potentially modified
if _tasks.length > 0 if _tasks.length > 0
@@ -227,7 +225,7 @@ runTask = (currentRequest, task, methodResponse) ->
tasks.task(task, methodResponse, (err, status, methodRequest, deviceUpdates) -> tasks.task(task, methodResponse, (err, status, methodRequest, deviceUpdates) ->
throw err if err throw err if err
# TODO handle error # TODO handle error
updateDevice(currentRequest.session.deviceId, deviceUpdates, (err) -> updateDevice.queueUpdates(currentRequest.session.deviceId, deviceUpdates, (err) ->
throw err if err throw err if err
timeDiff = process.hrtime(timeDiff)[0] + 1 timeDiff = process.hrtime(timeDiff)[0] + 1
@@ -251,19 +249,25 @@ runTask = (currentRequest, task, methodResponse) ->
) )
if save if save
db.tasksCollection.update({_id : mongodb.ObjectID(String(task._id))}, {$set : {session : task.session}}, (err) -> updateDevice.commitUpdates(currentRequest.session.deviceId, deviceUpdates, true, (err) ->
throw err if err throw err if err
f() db.tasksCollection.update({_id : mongodb.ObjectID(String(task._id))}, {$set : {session : task.session}}, (err) ->
throw err if err
f()
)
) )
else else
f() f()
when tasks.STATUS_COMPLETED when tasks.STATUS_COMPLETED
util.log("#{currentRequest.session.deviceId}: Completed task #{task.name}(#{task._id})") util.log("#{currentRequest.session.deviceId}: Completed task #{task.name}(#{task._id})")
db.tasksCollection.remove({'_id' : mongodb.ObjectID(String(task._id))}, (err, removed) -> updateDevice.commitUpdates(currentRequest.session.deviceId, deviceUpdates, true, (err) ->
throw err if err throw err if err
db.redisClient.del(String(task._id), (err, res) -> db.tasksCollection.remove({'_id' : mongodb.ObjectID(String(task._id))}, (err, removed) ->
throw err if err throw err if err
nextTask(currentRequest) db.redisClient.del(String(task._id), (err, res) ->
throw err if err
nextTask(currentRequest)
)
) )
) )
when tasks.STATUS_FAULT when tasks.STATUS_FAULT
@@ -358,7 +362,10 @@ nextTask = (currentRequest) ->
) )
else else
util.log("#{currentRequest.session.deviceId}: Started task #{task.name}(#{task._id})") util.log("#{currentRequest.session.deviceId}: Started task #{task.name}(#{task._id})")
runTask(currentRequest, task, {}) updateDevice.clearUpdatesQueue(currentRequest.session.deviceId, (err) ->
throw err if err
runTask(currentRequest, task, {})
)
) )
+28 -25
View File
@@ -65,12 +65,16 @@ this.refreshObject = (task, methodResponse, callback) ->
if task.session.subtask.name is 'getParameterNames' if task.session.subtask.name is 'getParameterNames'
this.getParameterNames(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) => this.getParameterNames(task.session.subtask, methodResponse, (err, status, methodRequest, deviceUpdates) =>
if status & STATUS_COMPLETED task.session.parameterNames ?= []
parameterNames = [] if deviceUpdates?.parameterNames?
for p in deviceUpdates.parameterNames for p in deviceUpdates.parameterNames
if not common.endsWith(p[0], '.') if not common.endsWith(p[0], '.')
parameterNames.push(p[0]) task.session.parameterNames.push(p[0])
task.session.subtask = {name : 'getParameterValues', parameterNames : parameterNames}
if status & STATUS_COMPLETED
task.session.subtask = {name : 'getParameterValues', parameterNames : task.session.parameterNames}
delete task.session.parameterNames
this.getParameterValues(task.session.subtask, {}, (err, status, methodRequest) -> this.getParameterValues(task.session.subtask, {}, (err, status, methodRequest) ->
# ignore deviceUpdates returned by firt call to getParameterValues # ignore deviceUpdates returned by firt call to getParameterValues
callback(err, status, methodRequest, deviceUpdates) callback(err, status, methodRequest, deviceUpdates)
@@ -78,7 +82,7 @@ this.refreshObject = (task, methodResponse, callback) ->
else if status & STATUS_OK else if status & STATUS_OK
callback(err, STATUS_OK, methodRequest, deviceUpdates) callback(err, STATUS_OK, methodRequest, deviceUpdates)
else else
throw Error('Unexpected subtask status') throw new Error('Unexpected subtask status')
) )
else if task.session.subtask.name is 'getParameterValues' else if task.session.subtask.name is 'getParameterValues'
if methodResponse.faultcode? if methodResponse.faultcode?
@@ -103,15 +107,31 @@ this.getParameterNames = (task, methodResponse, callback) ->
return (param[...-1] + '.').split('.').length - 1 return (param[...-1] + '.').split('.').length - 1
if methodResponse.type is 'GetParameterNamesResponse' if methodResponse.type is 'GetParameterNamesResponse'
task.session.parameterNames = task.session.parameterNames.concat(methodResponse.parameterList)
path = task.session.queue.pop() path = task.session.queue.pop()
# If parameter depth higher than the threshold, nextLevel was set to false # If parameter depth higher than the threshold, nextLevel was set to false
if not task.nextLevel? and getParameterDepth(path) < config.GET_PARAMETER_NAMES_DEPTH_THRESHOLD if not task.nextLevel? and getParameterDepth(path) < config.GET_PARAMETER_NAMES_DEPTH_THRESHOLD
for p in methodResponse.parameterList for p in methodResponse.parameterList
task.session.queue.push(p[0]) if p[0][-1..] == '.' task.session.queue.push(p[0]) if p[0][-1..] == '.'
deviceUpdates = {parameterNames : methodResponse.parameterList}
# Make sure that for each parameter, all its parents are explicitly included
found = {}
found[path] = 0 if !!path
for p in deviceUpdates.parameterNames
param = p[0]
i = param.length
while (i = param.lastIndexOf('.', i-1)) > path.length
pp = param.slice(0, i + 1)
break if found[pp]?
found[pp] = 0
found[p[0]] = 1
for k, v of found
deviceUpdates.parameterNames.push([k]) if v == 0
else else
task.session = {queue : [task.parameterPath], parameterNames : []} task.session = {queue : [task.parameterPath]}
if task.session.queue.length > 0 if task.session.queue.length > 0
path = task.session.queue[-1..][0] path = task.session.queue[-1..][0]
@@ -120,25 +140,8 @@ this.getParameterNames = (task, methodResponse, callback) ->
parameterPath : path, parameterPath : path,
nextLevel : task.nextLevel ? getParameterDepth(path) < config.GET_PARAMETER_NAMES_DEPTH_THRESHOLD nextLevel : task.nextLevel ? getParameterDepth(path) < config.GET_PARAMETER_NAMES_DEPTH_THRESHOLD
} }
return callback(null, STATUS_OK, methodRequest) return callback(null, STATUS_OK, methodRequest, deviceUpdates)
else else
deviceUpdates = {parameterNames : task.session.parameterNames}
# Make sure that for each parameter, all its parents are explicitly included
found = {}
found[task.parameterPath] = 0 if !!task.parameterPath
for p in deviceUpdates.parameterNames
param = p[0]
i = param.length
while (i = param.lastIndexOf('.', i-1)) > task.parameterPath.length
pp = param.slice(0, i + 1)
break if found[pp]?
found[pp] = 0
found[p[0]] = 1
for k, v of found
deviceUpdates.parameterNames.push([k]) if v == 0
return callback(null, STATUS_COMPLETED, null, deviceUpdates) return callback(null, STATUS_COMPLETED, null, deviceUpdates)
+156 -95
View File
@@ -25,136 +25,197 @@ OBJECT_REGEX = /\.$/
INSTANCE_REGEX = /\.[\d]+\.$/ INSTANCE_REGEX = /\.[\d]+\.$/
updateDevice = (deviceId, actions, callback) -> queueUpdates = (deviceId, deviceUpdates, callback) ->
return callback?() if not actions? return callback() if not deviceUpdates? or Object.keys(deviceUpdates).length == 0
# TODO consider setting expiry
db.redisClient.rpush("#{deviceId}_updates", JSON.stringify(deviceUpdates), (err, res) ->
return callback(err)
)
clearUpdatesQueue = (deviceId, callback) ->
db.redisClient.del("#{deviceId}_updates", (err, res) ->
return callback(err)
)
commitUpdates = (deviceId, deviceUpdates, commitQueue, callback) ->
now = new Date(Date.now()) now = new Date(Date.now())
update = {'$set' : {}, '$unset' : {}} update = {'$set' : {}, '$unset' : {}}
if actions.parameterValues? # Used to find parameters that no longer exist in device
for p in actions.parameterValues presentPaths = null
origValue = p[1] projection = null
v = normalize(p[0], origValue)
path = if common.endsWith(p[0], '.') then p[0] else "#{p[0]}." # Keep track of alias changes to clear cache if needed
if v == origValue newAliases = null
update['$unset']["#{path}_orig"] = 1
else
update['$set']["#{path}_orig"] = origValue
update['$set']["#{path}_value"] = v
update['$set']["#{path}_timestamp"] = now
if p[2]?
update['$set']["#{path}_type"] = p[2]
if actions.deletedObjects? processBatch = (updatesBatch, cb) ->
for p in actions.deletedObjects if updatesBatch.informEvents?
update['$unset'][p] = 1 update['$set']['_lastInform'] = now
update['$set']['_lastBoot'] if '1 BOOT' in updatesBatch.informEvents
update['$set']['_lastBootstrap'] if '0 BOOTSTRAP' in updatesBatch.informEvents
if actions.instanceName? if updatesBatch.parameterValues?
for i in actions.instanceName for p in updatesBatch.parameterValues
update['$set']["#{i[0]}._name"] = i[1] origValue = p[1]
v = normalize(p[0], origValue)
if actions.parameterNames? path = if common.endsWith(p[0], '.') then p[0] else "#{p[0]}."
projection = {} # fetch only necessary parameters if v == origValue
presentPaths = {} # to be compared with to find no longer existing parameters update['$unset']["#{path}_orig"] = 1
for p in actions.parameterNames
if OBJECT_REGEX.test(p[0])
path = p[0].slice(0, -1)
if INSTANCE_REGEX.test(p[0])
update['$set']["#{path}._instance"] = true
else else
update['$set']["#{path}._object"] = true update['$set']["#{path}_orig"] = origValue
else update['$set']["#{path}_value"] = v
path = p[0] update['$set']["#{path}_timestamp"] = now
update['$set']["#{path}._writable"] = p[1] if p[1]? if p[2]?
update['$set']["#{path}._timestamp"] = now update['$set']["#{path}_type"] = p[2]
# Add parameter (sans any dot suffix) if has an alias if updatesBatch.deletedObjects?
if (a = parameters.getAlias(path))? for p in updatesBatch.deletedObjects
newAliases = {} if not newAliases? update['$unset'][p] = 1
newAliases[path] = a
projection[path] = 1 if updatesBatch.instanceName?
for i in updatesBatch.instanceName
update['$set']["#{i[0]}._name"] = i[1]
# Store this param path and all its parents for later look up if updatesBatch.parameterNames?
presentPaths[path] = true presentPaths ?= {}
i = p[0].indexOf('.') projection ?= {}
while i != -1
presentPaths[p[0][0...i]] = true
i = p[0].indexOf('.', i + 1)
mongoQuery.optimizeProjection(projection) for p in updatesBatch.parameterNames
if OBJECT_REGEX.test(p[0])
path = p[0].slice(0, -1)
if INSTANCE_REGEX.test(p[0])
update['$set']["#{path}._instance"] = true
else
update['$set']["#{path}._object"] = true
else
path = p[0]
update['$set']["#{path}._writable"] = p[1] if p[1]?
update['$set']["#{path}._timestamp"] = now
if actions.customCommands? # Add parameter (sans any dot suffix) if has an alias
for p in actions.customCommands if (a = parameters.getAlias(path))?
commandName = p[0] newAliases ?= {}
update['$set']["_customCommands.#{commandName}._value"] = p[1] newAliases[path] = a
update['$set']["_customCommands.#{commandName}._timestamp"] = now
if actions.set? projection[path] = 1
common.extend(update['$set'], actions.set)
f = () -> # Store this param path and all its parents for later look up
presentPaths[path] = true
i = p[0].indexOf('.')
while i != -1
presentPaths[p[0][0...i]] = true
i = p[0].indexOf('.', i + 1)
if updatesBatch.customCommands?
for p in updatesBatch.customCommands
commandName = p[0]
update['$set']["_customCommands.#{commandName}._value"] = p[1]
update['$set']["_customCommands.#{commandName}._timestamp"] = now
processBatches = (cb) ->
if not commitQueue?
processBatch(deviceUpdates) if deviceUpdates?
return cb()
db.redisClient.lpop("#{deviceId}_updates", (err, res) ->
callback(err) if err
if not res?
processBatch(deviceUpdates) if deviceUpdates?
return cb()
processBatch(JSON.parse(res))
return processBatches(cb)
)
commit = () ->
for k of update for k of update
delete update[k] if Object.keys(update[k]).length == 0 delete update[k] if Object.keys(update[k]).length == 0
if Object.keys(update).length if Object.keys(update).length
db.devicesCollection.update({'_id' : deviceId}, update, {}, (err, count) -> db.devicesCollection.update({'_id' : deviceId}, update, {}, (err, count) ->
throw err if err return callback(err) if err
# Clear aliases cache if there's a new aliased parameter that has not been included in aliases cache # Clear aliases cache if there's a new aliased parameter that has not been included in aliases cache
if newAliases? if newAliases?
newAliasFound = false
db.getAliases((aliases) -> db.getAliases((aliases) ->
for p, a of newAliases for p, a of newAliases
if not aliases[a]? or p not in aliases[a] if not aliases[a]? or p not in aliases[a]
db.redisClient.del('aliases', (err, res) -> newAliasFound = true
throw err if err
)
break break
return callback() if not newAliasFound
db.redisClient.del('aliases', (err, res) ->
return callback(err)
)
) )
callback?(err) else
return callback()
) )
else else
callback?() return callback()
if projection? processBatches(() ->
db.devicesCollection.findOne({'_id' : deviceId}, projection, (err, device) -> if projection?
throw err if err mongoQuery.optimizeProjection(projection)
recursive = (obj, prefix) -> # Fetch device from DB to discover and delete parameters that no longer exist
for k,v of obj # While we're fetching the object, we might as well use the information
p = prefix + k # to optimize our update query
if k[0] == '_' db.devicesCollection.findOne({'_id' : deviceId}, projection, (err, device) ->
# Remove set operations for unmodified values for optimization return callback(err) if err
if update['$set'][p] == v
delete update['$set'][p]
continue
# Remove unset operations for values that don't exist recursive = (obj, prefix) ->
delete update['$unset']["#{p}._orig"] if not v._orig? for k, v of obj
p = prefix + k
if presentPaths[p] # Mark unset operations we want to keep
recursive(v, "#{p}.") if update['$unset'][p]?
update['$unset'][p] = 2
continue
if k[0] == '_'
# Remove set operations for unmodified values for optimization
if update['$set'][p] == v
delete update['$set'][p]
continue
if presentPaths[p]
recursive(v, "#{p}.")
else
update['$unset'][p] = 1
# Scan the DB object and remove parameters that no longer exists in device
for path of projection
root = device
rootPath = ''
for p in path.split('.')
rootPath += "#{p}."
root = root[p]
break if not root?
if root
recursive(root, rootPath)
else else
update['$unset'][p] = 1 if not presentPaths[rootPath.slice(0, -1)]?
update['$unset'][rootPath.slice(0, -1)] = 1
# Scan the DB object and remove parameters that no longer exists in device # Remove unnecessary unset updates
for k, v of projection for k, v of update['$unset']
root = device if common.startsWith(k, path + '.') and v != 2
rootPath = '' delete update['$unset'][k]
for p in path.split('.')
rootPath += "#{p}."
root = root[p]
break if not root?
if root commit()
recursive(root, rootPath) )
else else
if not presentPaths[rootPath.slice(0, -1)] commit()
update['$unset'][rootPath.slice(0, -1)] = 1 )
f()
)
else
f()
module.exports = updateDevice
exports.queueUpdates = queueUpdates
exports.clearUpdatesQueue = clearUpdatesQueue
exports.commitUpdates = commitUpdates