mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #7 from sharelatex/sk-time-async-method
Time-async-method
This commit is contained in:
commit
9fe8d4fd10
7 changed files with 209 additions and 2 deletions
1
libraries/metrics/.gitignore
vendored
1
libraries/metrics/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
test/unit/js/*
|
||||||
|
|
1
libraries/metrics/.nvmrc
Normal file
1
libraries/metrics/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
6.9
|
29
libraries/metrics/Gruntfile.coffee
Normal file
29
libraries/metrics/Gruntfile.coffee
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module.exports = (grunt) ->
|
||||||
|
grunt.initConfig
|
||||||
|
coffee:
|
||||||
|
unit_tests:
|
||||||
|
expand: true
|
||||||
|
cwd: "test/unit/coffee"
|
||||||
|
src: ["**/*.coffee"]
|
||||||
|
dest: "test/unit/js/"
|
||||||
|
ext: ".js"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
unit_tests: ["test/unit/js"]
|
||||||
|
|
||||||
|
mochaTest:
|
||||||
|
unit:
|
||||||
|
options:
|
||||||
|
reporter: grunt.option('reporter') or 'spec'
|
||||||
|
grep: grunt.option("grep")
|
||||||
|
|
||||||
|
src: ["test/unit/js/**/*.js"]
|
||||||
|
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-clean'
|
||||||
|
grunt.loadNpmTasks 'grunt-mocha-test'
|
||||||
|
grunt.loadNpmTasks 'grunt-execute'
|
||||||
|
grunt.loadNpmTasks 'grunt-bunyan'
|
||||||
|
|
||||||
|
grunt.registerTask 'compile:unit_tests', ['clean:unit_tests', 'coffee:unit_tests']
|
||||||
|
grunt.registerTask 'test:unit', ['compile:unit_tests', 'mochaTest:unit']
|
|
@ -10,7 +10,7 @@ destructors = []
|
||||||
|
|
||||||
require "./uv_threadpool_size"
|
require "./uv_threadpool_size"
|
||||||
|
|
||||||
module.exports =
|
module.exports = Metrics =
|
||||||
initialize: (_name) ->
|
initialize: (_name) ->
|
||||||
name = _name
|
name = _name
|
||||||
|
|
||||||
|
@ -48,6 +48,8 @@ module.exports =
|
||||||
event_loop: require "./event_loop"
|
event_loop: require "./event_loop"
|
||||||
memory: require "./memory"
|
memory: require "./memory"
|
||||||
|
|
||||||
|
timeAsyncMethod: require('./timeAsyncMethod')
|
||||||
|
|
||||||
close: () ->
|
close: () ->
|
||||||
for func in destructors
|
for func in destructors
|
||||||
func()
|
func()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "metrics-sharelatex",
|
"name": "metrics-sharelatex",
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"description": "A drop-in metrics and monitoring module for node.js apps",
|
"description": "A drop-in metrics and monitoring module for node.js apps",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -10,5 +10,17 @@
|
||||||
"lynx": "~0.1.1",
|
"lynx": "~0.1.1",
|
||||||
"coffee-script": "1.6.0",
|
"coffee-script": "1.6.0",
|
||||||
"underscore": "~1.6.0"
|
"underscore": "~1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"bunyan": "^1.0.0",
|
||||||
|
"chai": "",
|
||||||
|
"grunt": "^0.4.5",
|
||||||
|
"grunt-bunyan": "^0.5.0",
|
||||||
|
"grunt-contrib-clean": "^0.6.0",
|
||||||
|
"grunt-contrib-coffee": "^0.11.0",
|
||||||
|
"grunt-execute": "^0.2.2",
|
||||||
|
"grunt-mocha-test": "^0.11.0",
|
||||||
|
"sandboxed-module": "",
|
||||||
|
"sinon": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
126
libraries/metrics/test/unit/coffee/timeAsyncMethodTests.coffee
Normal file
126
libraries/metrics/test/unit/coffee/timeAsyncMethodTests.coffee
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
require('coffee-script')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
path = require('path')
|
||||||
|
modulePath = path.join __dirname, '../../../timeAsyncMethod.coffee'
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
sinon = require("sinon")
|
||||||
|
|
||||||
|
|
||||||
|
describe 'timeAsyncMethod', ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
@Timer = {done: sinon.stub()}
|
||||||
|
@TimerConstructor = sinon.stub().returns(@Timer)
|
||||||
|
@metrics = {
|
||||||
|
Timer: @TimerConstructor
|
||||||
|
inc: sinon.stub()
|
||||||
|
}
|
||||||
|
@timeAsyncMethod = SandboxedModule.require modulePath, requires:
|
||||||
|
'./metrics': @metrics
|
||||||
|
|
||||||
|
@testObject = {
|
||||||
|
nextNumber: (n, callback=(err, result)->) ->
|
||||||
|
setTimeout(
|
||||||
|
() ->
|
||||||
|
callback(null, n+1)
|
||||||
|
, 100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it 'should have the testObject behave correctly before wrapping', (done) ->
|
||||||
|
@testObject.nextNumber 2, (err, result) ->
|
||||||
|
expect(err).to.not.exist
|
||||||
|
expect(result).to.equal 3
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'should wrap method without error', (done) ->
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject'
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'should transparently wrap method invocation in timer', (done) ->
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject'
|
||||||
|
@testObject.nextNumber 2, (err, result) =>
|
||||||
|
expect(err).to.not.exist
|
||||||
|
expect(result).to.equal 3
|
||||||
|
expect(@TimerConstructor.callCount).to.equal 1
|
||||||
|
expect(@Timer.done.callCount).to.equal 1
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'should increment success count', (done) ->
|
||||||
|
@metrics.inc = sinon.stub()
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject'
|
||||||
|
@testObject.nextNumber 2, (err, result) =>
|
||||||
|
expect(@metrics.inc.callCount).to.equal 1
|
||||||
|
expect(@metrics.inc.calledWith('someContext.TestObject.nextNumber.success')).to.equal true
|
||||||
|
done()
|
||||||
|
|
||||||
|
describe 'when base method produces an error', ->
|
||||||
|
beforeEach ->
|
||||||
|
@metrics.inc = sinon.stub()
|
||||||
|
@testObject.nextNumber = (n, callback=(err, result)->) ->
|
||||||
|
setTimeout(
|
||||||
|
() ->
|
||||||
|
callback(new Error('woops'))
|
||||||
|
, 100
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'should propagate the error transparently', (done) ->
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject'
|
||||||
|
@testObject.nextNumber 2, (err, result) =>
|
||||||
|
expect(err).to.exist
|
||||||
|
expect(err).to.be.instanceof Error
|
||||||
|
expect(result).to.not.exist
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'should increment failure count', (done) ->
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject'
|
||||||
|
@testObject.nextNumber 2, (err, result) =>
|
||||||
|
expect(@metrics.inc.callCount).to.equal 1
|
||||||
|
expect(@metrics.inc.calledWith('someContext.TestObject.nextNumber.failure')).to.equal true
|
||||||
|
done()
|
||||||
|
|
||||||
|
describe 'when a logger is supplied', ->
|
||||||
|
beforeEach ->
|
||||||
|
@logger = {log: sinon.stub()}
|
||||||
|
|
||||||
|
it 'should also call logger.log', (done) ->
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject', @logger
|
||||||
|
@testObject.nextNumber 2, (err, result) =>
|
||||||
|
expect(err).to.not.exist
|
||||||
|
expect(result).to.equal 3
|
||||||
|
expect(@TimerConstructor.callCount).to.equal 1
|
||||||
|
expect(@Timer.done.callCount).to.equal 1
|
||||||
|
expect(@logger.log.callCount).to.equal 1
|
||||||
|
done()
|
||||||
|
|
||||||
|
describe 'when the wrapper cannot be applied', ->
|
||||||
|
beforeEach ->
|
||||||
|
|
||||||
|
it 'should raise an error', ->
|
||||||
|
badWrap = () =>
|
||||||
|
@timeAsyncMethod @testObject, 'DEFINITELY_NOT_A_REAL_METHOD', 'someContext.TestObject'
|
||||||
|
expect(badWrap).to.throw(
|
||||||
|
/^.*expected object property 'DEFINITELY_NOT_A_REAL_METHOD' to be a function.*$/
|
||||||
|
)
|
||||||
|
|
||||||
|
describe 'when the wrapped function is not using a callback', ->
|
||||||
|
beforeEach ->
|
||||||
|
@realMethod = sinon.stub().returns(42)
|
||||||
|
@testObject.nextNumber = @realMethod
|
||||||
|
|
||||||
|
it 'should not throw an error', ->
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject'
|
||||||
|
badCall = () =>
|
||||||
|
@testObject.nextNumber 2
|
||||||
|
expect(badCall).to.not.throw(Error)
|
||||||
|
|
||||||
|
it 'should call the underlying method', ->
|
||||||
|
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject'
|
||||||
|
result = @testObject.nextNumber(12)
|
||||||
|
expect(@realMethod.callCount).to.equal 1
|
||||||
|
expect(@realMethod.calledWith(12)).to.equal true
|
||||||
|
expect(result).to.equal 42
|
||||||
|
|
||||||
|
|
36
libraries/metrics/timeAsyncMethod.coffee
Normal file
36
libraries/metrics/timeAsyncMethod.coffee
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
module.exports = (obj, methodName, prefix, logger) ->
|
||||||
|
metrics = require('./metrics')
|
||||||
|
|
||||||
|
if typeof obj[methodName] != 'function'
|
||||||
|
throw new Error("[Metrics] expected object property '#{methodName}' to be a function")
|
||||||
|
|
||||||
|
realMethod = obj[methodName]
|
||||||
|
key = "#{prefix}.#{methodName}"
|
||||||
|
|
||||||
|
obj[methodName] = (originalArgs...) ->
|
||||||
|
|
||||||
|
[firstArgs..., callback] = originalArgs
|
||||||
|
|
||||||
|
if !callback? || typeof callback != 'function'
|
||||||
|
if logger?
|
||||||
|
logger.log "[Metrics] expected wrapped method '#{methodName}' to be invoked with a callback"
|
||||||
|
return realMethod.apply this, originalArgs
|
||||||
|
|
||||||
|
timer = new metrics.Timer(key)
|
||||||
|
|
||||||
|
realMethod.call this, firstArgs..., (callbackArgs...) ->
|
||||||
|
elapsedTime = timer.done()
|
||||||
|
possibleError = callbackArgs[0]
|
||||||
|
if possibleError?
|
||||||
|
metrics.inc "#{key}.failure"
|
||||||
|
else
|
||||||
|
metrics.inc "#{key}.success"
|
||||||
|
if logger?
|
||||||
|
loggableArgs = {}
|
||||||
|
try
|
||||||
|
for arg, idx in firstArgs
|
||||||
|
if arg.toString().match(/^[0-9a-f]{24}$/)
|
||||||
|
loggableArgs["#{idx}"] = arg
|
||||||
|
logger.log {key, args: loggableArgs, elapsedTime}, "[Metrics] timed async method call"
|
||||||
|
callback.apply this, callbackArgs
|
Loading…
Reference in a new issue