Merge branch 'feature/task-queue'

Conflicts:
	app/views/play/level/tome/spell.coffee
This commit is contained in:
Michael Schmatz 2014-02-11 15:58:38 -08:00
commit 1a2fa68473
23 changed files with 849 additions and 24 deletions

View file

@ -21,6 +21,7 @@ module.exports = class LevelBus extends Bus
'thang-code-ran': 'onCodeRan'
'level-show-victory': 'onVictory'
'tome:spell-changed': 'onSpellChanged'
'tome:spell-created': 'onSpellCreated'
constructor: ->
super(arguments...)
@ -91,12 +92,26 @@ module.exports = class LevelBus extends Bus
code = @session.get('code')
code ?= {}
parts = e.spell.spellKey.split('/')
code[parts[0]] ?= {}
code[parts[0]][parts[1]] = e.spell.getSource()
@changedSessionProperties.code = true
@session.set({'code': code})
@saveSession()
onSpellCreated: (e) ->
return unless @onPoint()
spellTeam = e.spell.team
@teamSpellMap[spellTeam] ?= []
unless e.spell.spellKey in @teamSpellMap[spellTeam]
@teamSpellMap[spellTeam].push e.spell.spellKey
@changedSessionProperties.teamSpells = true
@session.set({'teamSpells': @teamSpellMap})
@saveSession()
onScriptStateChanged: (e) ->
return unless @onPoint()
@fireScriptsRef?.update(e)
@ -220,4 +235,12 @@ module.exports = class LevelBus extends Bus
destroy: ->
super()
@session.off 'change:multiplayer', @onMultiplayerChanged, @
@session.off 'change:multiplayer', @onMultiplayerChanged, @
setTeamSpellMap: (spellMap) ->
@teamSpellMap = spellMap
console.log @teamSpellMap
@changedSessionProperties.teamSpells = true
@session.set({'teamSpells': @teamSpellMap})
@saveSession()

View file

@ -20,6 +20,9 @@ module.exports = class CocoRouter extends Backbone.Router
'db/*path': 'routeToServer'
'file/*path': 'routeToServer'
'play/level/:levelID/leaderboard/:teamID/:startRank/:endRank': 'getPaginatedLevelRank'
'play/level/:levelID/player/:playerID': 'getPlayerLevelInfo'
# most go through here
'*name': 'general'
@ -27,6 +30,13 @@ module.exports = class CocoRouter extends Backbone.Router
general: (name) ->
@openRoute(name)
getPaginatedLevelRank: (levelID,teamID,startRank,endRank) ->
return
getPlayerLevelInfo: (levelID,playerID) ->
return
editorModelView: (modelName, slugOrId, subview) ->
modulePrefix = "views/editor/#{modelName}/"
suffix = subview or (if slugOrId then 'edit' else 'home')

View file

@ -135,7 +135,6 @@ module.exports = class GoalManager extends CocoClass
status: null # should eventually be either 'success', 'failure', or 'incomplete'
keyFrame: 0 # when it became a 'success' or 'failure'
}
@initGoalState(state, [goal.killThangs, goal.saveThangs], 'killed')
@initGoalState(state, [goal.getToLocations?.who, goal.keepFromLocations?.who], 'arrived')
@initGoalState(state, [goal.leaveOffSides?.who, goal.keepFromLeavingOffSides?.who], 'left')

View file

@ -25,4 +25,6 @@ block content
div.homepage_button
a#beginner-campaign(href="/play/level/rescue-mission")
canvas(width="125", height="150")
button(data-i18n="home.play").btn.btn-warning.btn-lg.highlight Play
button(data-i18n="home.play").btn.btn-warning.btn-lg.highlight Play
if me.isAdmin()
button.btn.btn-warning.btn-lg.highlight#simulate-button SIMULATE

View file

@ -2,6 +2,10 @@ View = require 'views/kinds/RootView'
template = require 'templates/home'
WizardSprite = require 'lib/surface/WizardSprite'
ThangType = require 'models/ThangType'
LevelLoader = require 'lib/LevelLoader'
God = require 'lib/God'
GoalManager = require 'lib/world/GoalManager'
module.exports = class HomeView extends View
id: 'home-view'
@ -10,6 +14,7 @@ module.exports = class HomeView extends View
events:
'mouseover #beginner-campaign': 'onMouseOverButton'
'mouseout #beginner-campaign': 'onMouseOutButton'
'click #simulate-button': 'onSimulateButtonClick'
getRenderData: ->
c = super()
@ -97,4 +102,91 @@ module.exports = class HomeView extends View
destroy: ->
super()
@wizardSprite?.destroy()
@wizardSprite?.destroy()
onSimulateButtonClick: (e) =>
$.get "/queue/scoring", (data) =>
levelName = data.sessions[0].levelID
#TODO: Refactor. So much refactor.
world = {}
god = new God()
levelLoader = new LevelLoader(levelName, @supermodel, data.sessions[0].sessionID)
levelLoader.once 'loaded-all', =>
world = levelLoader.world
level = levelLoader.level
levelLoader.destroy()
god.level = level.serialize @supermodel
god.worldClassMap = world.classMap
god.goalManager = new GoalManager(world)
#move goals in here
goalsToAdd = god.goalManager.world.scripts[0].noteChain[0].goals.add
god.goalManager.goals = goalsToAdd
god.goalManager.goalStates =
"destroy-humans":
keyFrame: 0
killed:
"Human Base": false
status: "incomplete"
"destroy-ogres":
keyFrame:0
killed:
"Ogre Base": false
status: "incomplete"
god.spells = @filterProgrammableComponents level.attributes.thangs, @generateSpellToSourceMap data.sessions
god.createWorld()
Backbone.Mediator.subscribe 'god:new-world-created', @onWorldCreated, @
onWorldCreated: (data) ->
console.log "GOAL STATES"
console.log data
filterProgrammableComponents: (thangs, spellToSourceMap) =>
spells = {}
for thang in thangs
isTemplate = false
for component in thang.components
if component.config? and _.has component.config,'programmableMethods'
for methodName, method of component.config.programmableMethods
if typeof method is 'string'
isTemplate = true
break
pathComponents = [thang.id,methodName]
pathComponents[0] = _.string.slugify pathComponents[0]
spellKey = pathComponents.join '/'
spells[spellKey] ?= {}
spells[spellKey].thangs ?= {}
spells[spellKey].name = methodName
thangID = _.string.slugify thang.id
spells[spellKey].thangs[thang.id] ?= {}
spells[spellKey].thangs[thang.id].aether = @createAether methodName, method
if spellToSourceMap[thangID]? then source = spellToSourceMap[thangID][methodName] else source = ""
spells[spellKey].thangs[thang.id].aether.transpile source
if isTemplate
break
spells
createAether : (methodName, method) ->
aetherOptions =
functionName: methodName
protectAPI: false
includeFlow: false
return new Aether aetherOptions
generateSpellToSourceMap: (sessions) ->
spellKeyToSourceMap = {}
spellSources = {}
for session in sessions
teamSpells = session.teamSpells[session.team]
_.merge spellSources, _.pick(session.code, teamSpells)
#merge common ones, this overwrites until the last session
commonSpells = session.teamSpells["common"]
if commonSpells?
_.merge spellSources, _.pick(session.code, commonSpells)
spellSources

View file

@ -26,12 +26,16 @@ module.exports = class Spell
@view.render() # Get it ready and code loaded in advance
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel
@tabView.render()
@team = @permissions.readwrite[0] ? "common"
Backbone.Mediator.publish 'tome:spell-created', spell: @
destroy: ->
@view.destroy()
@tabView.destroy()
@thangs = null
addThang: (thang) ->
if @thangs[thang.id]
@thangs[thang.id].thang = thang

View file

@ -62,6 +62,7 @@ module.exports = class TomeView extends View
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel
@thangList = @insertSubView new ThangListView spells: @spells, thangs: @options.thangs, supermodel: @supermodel
@castButton = @insertSubView new CastButtonView spells: @spells
@teamSpellMap = @generateTeamSpellMap(@spells)
else
@cast()
console.warn "Warning: There are no Programmable Thangs in this level, which makes it unplayable."
@ -74,6 +75,21 @@ module.exports = class TomeView extends View
@thangList.adjustThangs @spells, thangs
@spellList.adjustSpells @spells
generateTeamSpellMap: (spellObject) ->
teamSpellMap = {}
for spellName, spell of spellObject
teamName = spell.team
teamSpellMap[teamName] ?= []
spellNameElements = spellName.split '/'
thangName = spellNameElements[0]
spellName = spellNameElements[1]
teamSpellMap[teamName].push thangName if thangName not in teamSpellMap[teamName]
return teamSpellMap
createSpells: (programmableThangs, world) ->
pathPrefixComponents = ['play', 'level', @options.levelID, @options.session.id, 'code']
@spells ?= {}

View file

@ -360,6 +360,7 @@ module.exports = class PlayLevelView extends View
register: ->
@bus = LevelBus.get(@levelID, @session.id)
@bus.setSession(@session)
@bus.setTeamSpellMap @tome.teamSpellMap
@bus.connect() if @session.get('multiplayer')
onSessionWillSave: (e) ->
@ -391,4 +392,4 @@ module.exports = class PlayLevelView extends View
@bus?.destroy()
#@instance.save() unless @instance.loading
console.profileEnd?() if PROFILE_ME
@session.off 'change:multiplayer', @onMultiplayerChanged, @
@session.off 'change:multiplayer', @onMultiplayerChanged, @

View file

@ -59,7 +59,9 @@
"express-useragent": "~0.0.9",
"gridfs-stream": "0.4.x",
"stream-buffers": "0.2.x",
"sendwithus": "2.0.x"
"sendwithus": "2.0.x",
"aws-sdk":"~2.0.0",
"bayesian-battle":"0.0.x"
},
"devDependencies": {
"jade": "0.33.x",

View file

@ -25,14 +25,3 @@ createAndConfigureApp = ->
serverSetup.setupRoutes app
app

View file

@ -7,18 +7,22 @@ testing = '--unittest' in process.argv
module.exports.connect = () ->
address = module.exports.generateMongoConnectionString()
winston.info "Connecting to Mongo with connection string #{address}"
mongoose.connect address
mongoose.connection.once 'open', -> Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
module.exports.generateMongoConnectionString = ->
if config.mongo.mongoose_replica_string
address = config.mongo.mongoose_replica_string
winston.info "Connecting to replica set: #{address}"
else
dbName = config.mongo.db
dbName += '_unittest' if testing
address = config.mongo.host + ":" + config.mongo.port
if config.mongo.username and config.mongo.password
address = config.mongo.username + ":" + config.mongo.password + "@" + address
# address = config.mongo.username + "@" + address # if connecting to production server
address = "mongodb://#{address}/#{dbName}"
winston.info "Connecting to standalone server #{address}"
mongoose.connect address
mongoose.connection.once 'open', ->
Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
return address

View file

@ -33,3 +33,11 @@ module.exports.badInput = (res, message='Unprocessable Entity. Bad Input.') ->
module.exports.serverError = (res, message='Internal Server Error') ->
res.send 500, message
res.end()
module.exports.gatewayTimeoutError = (res, message="Gateway timeout") ->
res.send 504, message
res.end()
module.exports.clientTimeout = (res, message="The server did not recieve the client response in a timely manner") ->
res.send 408, message
res.end()

View file

@ -33,4 +33,5 @@ module.exports.routes =
'routes/languages'
'routes/mail'
'routes/sprites'
'routes/queue'
]

283
server/commons/queue.coffee Normal file
View file

@ -0,0 +1,283 @@
config = require '../../server_config'
log = require 'winston'
mongoose = require 'mongoose'
async = require 'async'
aws = require 'aws-sdk'
db = require './database'
mongoose = require 'mongoose'
events = require 'events'
crypto = require 'crypto'
module.exports.queueClient = undefined
defaultMessageVisibilityTimeoutInSeconds = 20
defaultMessageReceiptTimeout = 10
module.exports.initializeQueueClient = (cb) ->
module.exports.queueClient = generateQueueClient() unless queueClient?
cb?()
generateQueueClient = ->
#if config.queue.accessKeyId
if false #TODO: Change this in production
queueClient = new SQSQueueClient()
else
queueClient = new MongoQueueClient()
class SQSQueueClient
registerQueue: (queueName, options, callback) ->
queueCreationOptions =
QueueName: queueName
@sqs.createQueue queueCreationOptions, (err,data) =>
@_logAndThrowFatalException "There was an error creating a new SQS queue, reason: #{JSON.stringify err}" if err?
newQueue = new SQSQueue queueName, data.QueueUrl, @sqs
callback? err, newQueue
constructor: ->
@_configure()
@sqs = @_generateSQSInstance()
_configure: ->
aws.config.update
accessKeyId: config.queue.accessKeyId
secretAccessKey: config.queue.secretAccessKey
region: config.queue.region
_generateSQSInstance: -> new aws.SQS()
_logAndThrowFatalException: (errorMessage) ->
log.error errorMessage
throw new Error errorMessage
class SQSQueue extends events.EventEmitter
constructor: (@queueName, @queueUrl, @sqs) ->
subscribe: (eventName, callback) -> @on eventName, callback
unsubscribe: (eventName, callback) -> @removeListener eventName, callback
receiveMessage: (callback) ->
queueReceiveOptions =
QueueUrl: @queueUrl
WaitTimeSeconds: defaultMessageReceiptTimeout
@sqs.receiveMessage queueReceiveOptions, (err, data) =>
if err?
@emit 'error',err,originalData
else
originalData = data
data = new SQSMessage originalData, this
@emit 'message',err,data
callback? err,data
deleteMessage: (receiptHandle, callback) ->
queueDeletionOptions =
QueueUrl: @queueUrl
ReceiptHandle: receiptHandle
@sqs.deleteMessage queueDeletionOptions, (err, data) =>
if err? then @emit 'error',err,data else @emit 'message',err,data
callback? err,data
changeMessageVisibilityTimeout: (secondsFromNow, receiptHandle, callback) ->
messageVisibilityTimeoutOptions =
QueueUrl: @queueUrl
ReceiptHandle: receiptHandle
VisibilityTimeout: secondsFromNow
@sqs.changeMessageVisibility messageVisibilityTimeoutOptions, (err, data) =>
if err? then @emit 'error',err,data else @emit 'edited',err,data
callback? err,data
sendMessage: (messageBody, delaySeconds, callback) ->
queueSendingOptions =
QueueUrl: @queueUrl
MessageBody: messageBody
DelaySeconds: delaySeconds
@sqs.sendMessage queueSendingOptions, (err, data) =>
if err? then @emit 'error',err,data else @emit 'sent',err, data
callback? err,data
listenForever: => async.forever (asyncCallback) => @receiveMessage (err, data) -> asyncCallback(null)
class SQSMessage
constructor: (@originalMessage, @parentQueue) ->
isEmpty: -> not @originalMessage.Messages?[0]?
getBody: -> @originalMessage.Messages[0].Body
getID: -> @originalMessage.Messages[0].MessageId
removeFromQueue: (callback) -> @parentQueue.deleteMessage @getReceiptHandle(), callback
requeue: (callback) -> @parentQueue.changeMessageVisibilityTimeout 0, @getReceiptHandle(), callback
changeMessageVisibilityTimeout: (secondsFromFunctionCall, callback) ->
@parentQueue.changeMessageVisibilityTimeout secondsFromFunctionCall,@getReceiptHandle(), callback
getReceiptHandle: -> @originalMessage.Messages[0].ReceiptHandle
class MongoQueueClient
registerQueue: (queueName, options, callback) ->
newQueue = new MongoQueue queueName,options,@messageModel
callback(null, newQueue)
constructor: ->
@_configure()
@_createMongoConnection()
@messageModel = @_generateMessageModel()
_configure: -> @databaseAddress = db.generateMongoConnectionString()
_createMongoConnection: ->
@mongooseConnection = mongoose.createConnection @databaseAddress
@mongooseConnection.on 'error', -> log.error "There was an error connecting to the queue in MongoDB"
@mongooseConnection.once 'open', -> log.info "Successfully connected to MongoDB queue!"
_generateMessageModel: ->
schema = new mongoose.Schema
messageBody: Object,
queue: {type: String, index:true}
scheduledVisibilityTime: {type: Date, index: true}
receiptHandle: {type: String, index: true}
@mongooseConnection.model 'messageQueue',schema
class MongoQueue extends events.EventEmitter
constructor: (queueName, options, messageModel) ->
@Message = messageModel
@queueName = queueName
subscribe: (eventName, callback) -> @on eventName, callback
unsubscribe: (eventName, callback) -> @removeListener eventName, callback
receiveMessage: (callback) ->
conditions =
queue: @queueName
scheduledVisibilityTime:
$lt: new Date()
options =
sort: 'scheduledVisibilityTime'
update =
$set:
receiptHandle: @_generateRandomReceiptHandle()
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate()
@Message.findOneAndUpdate conditions, update, options, (err, data) =>
return @emit 'error',err,data if err?
originalData = data
data = new MongoMessage originalData, this
@emit 'message',err,data
callback? err,data
deleteMessage: (receiptHandle, callback) ->
conditions =
queue: @queueName
receiptHandle: receiptHandle
scheduledVisibilityTime:
$lt: new Date()
@Message.findOneAndRemove conditions, {}, (err, data) =>
if err? then @emit 'error',err,data else @emit 'delete',err,data
callback? err,data
sendMessage: (messageBody, delaySeconds, callback) ->
messageToSend = new @Message
messageBody: messageBody
queue: @queueName
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate delaySeconds
messageToSend.save (err,data) =>
if err? then @emit 'error',err,data else @emit 'sent',err, data
callback? err,data
changeMessageVisibilityTimeout: (secondsFromNow, receiptHandle, callback) ->
conditions =
queue: @queueName
receiptHandle: receiptHandle
scheduledVisibilityTime:
$lt: new Date()
update =
$set:
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate secondsFromNow
@Message.findOneAndUpdate conditions, update, (err, data) =>
if err? then @emit 'error',err,data else @emit 'update',err,data
callback? err, data
listenForever: => async.forever (asyncCallback) => @recieveMessage (err, data) -> asyncCallback(null)
_constructDefaultVisibilityTimeoutDate: (timeoutSeconds) ->
timeoutSeconds ?= defaultMessageVisibilityTimeoutInSeconds
newDate = new Date()
newDate = new Date(newDate.getTime() + 1000 * timeoutSeconds)
newDate
_generateRandomReceiptHandle: -> crypto.randomBytes(20).toString('hex')
class MongoMessage
constructor: (@originalMessage, @parentQueue) ->
isEmpty: -> not @originalMessage
getBody: -> @originalMessage.messageBody
getID: -> @originalMesage._id
removeFromQueue: (callback) -> @parentQueue.deleteMessage @getReceiptHandle(), callbacks
requeue: (callback) -> @parentQueue.changeMessageVisibilityTimeout 0, @getReceiptHandle(), callback
changeMessageVisibilityTimeout: (secondsFromFunctionCall, callback) ->
@parentQueue.changeMessageVisibilityTimeout secondsFromFunctionCall,@getReceiptHandle(), callback
getReceiptHandle: -> @originalMessage.receiptHandle

View file

@ -7,7 +7,7 @@ class LevelSessionHandler extends Handler
modelClass: LevelSession
editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state',
'levelName', 'creatorName', 'levelID', 'screenshot',
'chat']
'chat', 'teamSpells']
getByRelationship: (req, res, args...) ->
return @sendNotFoundError(res) unless args.length is 2 and args[1] is 'active'

View file

@ -55,9 +55,18 @@ _.extend LevelSessionSchema.properties,
# TODO: specify this more
code: { type: 'object' }
teamSpells:
type: 'object'
additionalProperties:
type: 'array'
players: { type: 'object' }
chat: { type: 'array' }
meanStrength: {type: 'number', default: 25}
standardDeviation: {type:'number', default:25/3, minimum: 0}
totalScore: {type: 'number', default: 10}
c.extendBasicProperties LevelSessionSchema, 'level.session'

View file

@ -0,0 +1,306 @@
config = require '../../server_config'
log = require 'winston'
mongoose = require 'mongoose'
async = require 'async'
errors = require '../commons/errors'
aws = require 'aws-sdk'
db = require './../routes/db'
mongoose = require 'mongoose'
queues = require '../commons/queue'
LevelSession = require '../levels/sessions/LevelSession'
TaskLog = require './task/ScoringTask'
bayes = new (require 'bayesian-battle')()
scoringTaskQueue = undefined
scoringTaskTimeoutInSeconds = 400
module.exports.setup = (app) -> connectToScoringQueue()
connectToScoringQueue = ->
queues.initializeQueueClient ->
queues.queueClient.registerQueue "scoring", {}, (err,data) ->
throwScoringQueueRegistrationError(err) if err?
scoringTaskQueue = data
log.info "Connected to scoring task queue!"
throwScoringQueueRegistrationError = (error) ->
log.error "There was an error registering the scoring queue: #{error}"
throw new Error "There was an error registering the scoring queue."
module.exports.createNewTask = (req, res) ->
scoringTaskQueue.sendMessage req.body, 0, (err, data) ->
return errors.badInput res, "There was an error creating the message, reason: #{err}" if err?
res.send data
res.end()
module.exports.dispatchTaskToConsumer = (req, res) ->
userID = getUserIDFromRequest req,res
return errors.forbidden res, "You need to be logged in to simulate games" if isUserAnonymous req
scoringTaskQueue.receiveMessage (taskQueueReceiveError, message) ->
if (not message?) or message.isEmpty() or taskQueueReceiveError?
return errors.gatewayTimeoutError res, "No messages were receieved from the queue. Msg:#{taskQueueReceiveError}"
messageBody = parseTaskQueueMessage req, res, message
return errors.serverError res, "There was an error parsing the queue message" unless messageBody?
constructTaskObject messageBody, (taskConstructionError, taskObject) ->
return errors.serverError res, "There was an error constructing the scoring task" if taskConstructionError?
message.changeMessageVisibilityTimeout scoringTaskTimeoutInSeconds
constructTaskLogObject userID,message.getReceiptHandle(), (taskLogError, taskLogObject) ->
return errors.serverError res, "There was an error creating the task log object." if taskLogError?
setTaskObjectTaskLogID taskObject, taskLogObject._id
sendResponseObject req, res, taskObject
getUserIDFromRequest = (req) -> if req.user? then return req.user._id else return null
isUserAnonymous = (req) -> if req.user? then return req.user.anonymous else return true
parseTaskQueueMessage = (req, res, message) ->
try
if typeof message.getBody() is "object" then return message.getBody()
return messageBody = JSON.parse message.getBody()
catch e
sendResponseObject req, res, {"error":"There was an error parsing the task.Error: #{e}" }
return null
constructTaskObject = (taskMessageBody, callback) ->
async.map taskMessageBody.sessions, getSessionInformation, (err, sessions) ->
return callback err, data if err?
taskObject =
"messageGenerated": Date.now()
"sessions": []
for session in sessions
sessionInformation =
"sessionID": session.sessionID
"sessionChangedTime": session.changed
"team": session.team ? "No team"
"code": session.code
"teamSpells": session.teamSpells ? {}
"levelID": session.levelID
taskObject.sessions.push sessionInformation
callback err, taskObject
getSessionInformation = (sessionIDString, callback) ->
LevelSession.findOne {"_id": sessionIDString }, (err, session) ->
return callback err, {"error":"There was an error retrieving the session."} if err?
session = session.toObject()
sessionInformation =
"sessionID": session._id
"code": _.cloneDeep session.code
"changed": session.changed
"creator": session.creator
"team": session.team
"teamSpells": session.teamSpells
"levelID": session.levelID
callback err, sessionInformation
constructTaskLogObject = (calculatorUserID, messageIdentifierString, callback) ->
taskLogObject = new TaskLog
"createdAt": new Date()
"calculator":calculatorUserID
"sentDate": Date.now()
"messageIdentifierString":messageIdentifierString
taskLogObject.save callback
setTaskObjectTaskLogID = (taskObject, taskLogObjectID) -> taskObject.taskID = taskLogObjectID
sendResponseObject = (req,res,object) ->
res.setHeader('Content-Type', 'application/json')
res.send(object)
res.end()
module.exports.processTaskResult = (req, res) ->
clientResponseObject = verifyClientResponse req.body, res
if clientResponseObject?
TaskLog.findOne {"_id": clientResponseObject.taskID}, (err, taskLog) ->
return errors.serverError res, "There was an error retrieiving the task log object" if err?
taskLogJSON = taskLog.toObject()
return errors.badInput res, "That computational task has already been performed" if taskLogJSON.calculationTimeMS
return handleTimedOutTask req, res, clientResponseObject if hasTaskTimedOut taskLogJSON.sentDate
logTaskComputation clientResponseObject, taskLog, (loggingError) ->
if loggingError?
return errors.serverError res, "There as a problem logging the task computation: #{loggingError}"
updateScores clientResponseObject, (updatingScoresError, newScores) ->
if updatingScoresError?
return errors.serverError res, "There was an error updating the scores.#{updatingScoresError}"
sendResponseObject req, res, {"message":"The scores were updated successfully!"}
hasTaskTimedOut = (taskSentTimestamp) -> taskSentTimestamp + scoringTaskTimeoutInSeconds * 1000 < Date.now()
handleTimedOutTask = (req, res, taskBody) -> errors.clientTimeout res, "The results weren't provided within the timeout"
verifyClientResponse = (responseObject, res) ->
unless typeof responseObject is "object"
errors.badInput res, "The response to that query is required to be a JSON object."
null
else
responseObject
logTaskComputation = (taskObject,taskLogObject, callback) ->
taskLogObject.calculationTimeMS = taskObject.calculationTimeMS
taskLogObject.sessions = taskObject.sessions
taskLogObject.save callback
updateScores = (taskObject,callback) ->
sessionIDs = _.pluck taskObject.sessions, 'sessionID'
async.map sessionIDs, retrieveOldScoreMetrics, (err, oldScores) ->
callback err, {"error": "There was an error retrieving the old scores"} if err?
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject taskObject, oldScores
newScoreArray = bayes.updatePlayerSkills oldScoreArray
saveNewScoresToDatabase newScoreArray, callback
saveNewScoresToDatabase = (newScoreArray, callback) ->
async.eachSeries newScoreArray, updateScoreInSession, (err) ->
if err? then callback err, null else callback err, {"message":"All scores were saved successfully."}
updateScoreInSession = (scoreObject,callback) ->
sessionObjectQuery =
"_id": scoreObject.id
LevelSession.findOne sessionObjectQuery, (err, session) ->
return callback err, null if err?
session.meanStrength = scoreObject.meanStrength
session.standardDeviation = scoreObject.standardDeviation
session.totalScore = scoreObject.meanStrength - 1.8 * scoreObject.standardDeviation
log.info "Saving session #{session._id}!"
session.save callback
putRankingFromMetricsIntoScoreObject = (taskObject,scoreObject) ->
scoreObject = _.indexBy scoreObject, 'id'
for session in taskObject.sessions
scoreObject[session.sessionID].gameRanking = session.metrics.rank
scoreObject
retrieveOldScoreMetrics = (sessionID, callback) ->
sessionQuery =
"_id":sessionID
LevelSession.findOne sessionQuery, (err, session) ->
return callback err, {"error":"There was an error retrieving the session."} if err?
defaultScore = (25 - 1.8*(25/3))
defaultStandardDeviation = 25/3
oldScoreObject =
"standardDeviation":session.standardDeviation ? defaultStandardDeviation
"meanStrength":session.meanStrength ? 25
"totalScore":session.totalScore ? defaultScore
"id": sessionID
callback err, oldScoreObject
###Sample Messages
sampleQueueMessage =
{
"sessions": ["52dea9b77e486eeb97000001","52d981a73cf02dcf260003cb"]
}
sampleUndoneTaskObject =
"taskID": "507f191e810c19729de860ea"
"sessions" : [
{
"ID":"52dfeb17c8b5f435c7000025"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"team":"humans"
"code": "code goes here"
},
{
"ID":"51eb2714fa058cb20d00fedg"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"team":"ogres"
"code": "code goes here"
}
]
sampleResponseObject =
"taskID": "507f191e810c19729de860ea"
"calculationTime":3201
"sessions": [
{
"ID":"52dfeb17c8b5f435c7000025"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"metrics": {
"rank":2
}
},
{
"ID":"51eb2714fa058cb20d00fedg"
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
"metrics": {
"rank":1
}
}
]
sampleTaskLogObject=
{
"_id":ObjectId("507f191e810c19729de860ea") #datestamp is built into objectId
"calculatedBy":ObjectId("51eb2714fa058cb20d0006ef")
"calculationTime":3201
timedOut: false
"sessions":[
{
"ID":ObjectId("52dfeb17c8b5f435c7000025")
"metrics": {
"rank":2
}
},
{
"ID":ObjectId("51eb2714fa058cb20d00feda")
"metrics": {
"rank":1
}
}
]
}
###

View file

View file

@ -0,0 +1,12 @@
mongoose = require('mongoose')
ScoringTaskSchema = new mongoose.Schema(
createdAt: {type: Date, expires: 3600} #expire document 1 hour after they are created
calculator: {type:mongoose.Schema.Types.ObjectId}
sentDate: {type: Number}
messageIdentifierString: {type: String}
calculationTimeMS: {type: Number, default: 0}
sessions: {type: Array, default: []}
)
module.exports = mongoose.model('scoringTask', ScoringTaskSchema)

View file

@ -0,0 +1,50 @@
log = require 'winston'
errors = require '../commons/errors'
scoringQueue = require '../queues/scoring'
module.exports.setup = (app) ->
scoringQueue.setup()
app.all '/queue/*', (req, res) ->
setResponseHeaderToJSONContentType res
queueName = getQueueNameFromPath req.path
try
handler = loadQueueHandler queueName
if isHTTPMethodGet req
handler.dispatchTaskToConsumer req,res
else if isHTTPMethodPut req
handler.processTaskResult req,res
else if isHTTPMethodPost req
handler.createNewTask req, res #TODO: do not use this in production
else
sendMethodNotSupportedError req, res
catch error
log.error error
sendQueueError req, res, error
setResponseHeaderToJSONContentType = (res) -> res.setHeader('Content-Type', 'application/json')
getQueueNameFromPath = (path) ->
pathPrefix = '/queue/'
pathAfterPrefix = path[pathPrefix.length..]
partsOfURL = pathAfterPrefix.split '/'
queueName = partsOfURL[0]
queueName
loadQueueHandler = (queueName) -> require ('../queues/' + queueName)
isHTTPMethodGet = (req) -> return req.route.method is 'get'
isHTTPMethodPost = (req) -> return req.route.method is 'post'
isHTTPMethodPut = (req) -> return req.route.method is 'put'
sendMethodNotSupportedError = (req, res) -> errors.badMethod(res,"Queues do not support the HTTP method used." )
sendQueueError = (req,res, error) -> errors.serverError(res, "Route #{req.path} had a problem: #{error}")

View file

@ -1,6 +1,11 @@
config = require '../server_config'
sendwithusAPI = require 'sendwithus'
swuAPIKey = config.mail.sendwithusAPIKey
queues = require './commons/queue'
module.exports.setupRoutes = (app) ->
return
options = { DEBUG: not config.isProduction }
module.exports.api = new sendwithusAPI swuAPIKey, options

View file

@ -31,6 +31,14 @@ config.mail.mailchimpAPIKey = process.env.COCO_MAILCHIMP_API_KEY || '';
config.mail.mailchimpWebhook = process.env.COCO_MAILCHIMP_WEBHOOK || '/mail/webhook';
config.mail.sendwithusAPIKey = process.env.COCO_SENDWITHUS_API_KEY || '';
config.queue = {};
config.queue.accessKeyId = process.env.COCO_AWS_ACCESS_KEY_ID || '';
config.queue.secretAccessKey = process.env.COCO_AWS_SECRET_ACCESS_KEY || '';
config.queue.region = 'us-east-1';
config.queue.simulationQueueName = "simulationQueue";
config.mongoQueue = {};
config.mongoQueue.queueDatabaseName = "coco_queue";
config.salt = process.env.COCO_SALT || 'pepper';
config.cookie_secret = process.env.COCO_COOKIE_SECRET || 'chips ahoy';

View file

@ -80,6 +80,7 @@ setupFacebookCrossDomainCommunicationRoute = (app) ->
exports.setupRoutes = (app) ->
app.use app.router
baseRoute.setup app
setupFacebookCrossDomainCommunicationRoute app
setupFallbackRouteToIndex app