Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(data-point): Add reducer stack when reducers throw errors #230

Closed

Conversation

raingerber
Copy link
Contributor

@raingerber raingerber commented Feb 23, 2018

What: closes #149 and closes #160 . I opened a previous PR for this issue, but that implementation was (probably) less efficient than this one.

Why: Helps with debugging

How: Add _stack and _input properties to error objects. It doesn't log anything though - leaving that to the user.

Instead of trying to create a tree data structure, this relies on the fact that resolveReducer is called recursively. That function generates a promise with a catch handler that re-throws the error, so the handler is called for every reducer in a given chain. So, if we have a reducer chain a -> b -> c that throws an error for c, each reducer has a chance to add data to the error object:

This is the sequence for that chain:

  1. c -> creates a stack array: error._stack = ['c']

  2. b -> updates the array: error._stack = ['b', 'c']

  3. a -> updates the array: error._stack = ['a', 'b', 'c']

This means the final error object has a _stack property that records an id for each reducer in the chain that failed. That's the basic idea anyway -- resolveReducer also now takes a key parameter to add extra metadata about the reducer that's being resolved. For example, if we're resolving a ReducerMap on a given array, we need to know what index of the array was being resolved when the reducer failed. In that example, the key parameter would be the current index of the input array, and it would be added to the error._stack array.

NOTE: The stack traces do not working correctly for entities with the [] notation (e.g. hash:thing[]); specifically, it does not indicate what index of the array was being evaluated when the error was thrown. This would be fixed if we do #130.

Checklist:

  • Has Breaking changes
  • Documentation
  • Tests
  • Ready to be merged

@raingerber raingerber self-assigned this Feb 23, 2018
@coveralls
Copy link

coveralls commented Feb 23, 2018

Coverage Status

Coverage remained the same at 100.0% when pulling 4adaea5 on raingerber:reducer-stack-second-attempt into cd77019 on ViacomInc:master.

const value = accumulator.value
const result = Promise.try(() => getResolveFunction(reducer))
// NOTE: recursive call
.then(resolve => resolve(manager, resolveReducer, accumulator, reducer))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapping getResolveFunction so that resolveReducer will always return a promise, and errors thrown from this function will always have the _stack property

function resolveRequest (accumulator, resolveReducer) {
inspect(accumulator)
const acc = utils.set(accumulator, 'value', accumulator.options)
return resolveReducer(acc, requestReducer)
Copy link
Contributor Author

@raingerber raingerber Feb 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets rid of all the custom properties that were added to the error object here. My reasoning for this change is that I'm now adding error._input and error._stack properties that have a lot of the same information, and most of the other properties are just repeating data that's already available in the original error. Now that we're no longer doing console.info with the error message (#210), it seems less important to have this code.

error._value,
'foo'
)
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if this property should be _input instead of _value...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking the same thing. input better matches what this actually is (the input provided to the failing reducer). The description you wrote supports that idea too:

When a reducer throws an error, DataPoint adds two properties to the error object:
 
`_value:*` - the input value to the reducer that failed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -23,7 +23,8 @@ function resolveErrorReducers (manager, error, accumulator, resolveReducer) {
const reducerResolved = resolveReducer(
manager,
errorAccumulator,
errorReducer
errorReducer,
[['error']]
Copy link
Contributor Author

@raingerber raingerber Feb 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the nested array forces stringifyReducerStack to format the output string in a particular way, and I'm using this approach for most of the entity lifecycle reducers. Don't really like it though...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming this is kept it might be worth adding a comment about why it's implemented this way.

@raingerber raingerber closed this Feb 26, 2018
@raingerber raingerber reopened this Feb 27, 2018
Copy link
Collaborator

@paulmolluzzo paulmolluzzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome! And the new tests you wrote for the stack tracing are 💯! 👏 👏 👏

Left a few comments, and I believe most are questions not requests. I would like to have some comment in there about that nested array being used. The other comments I left could just be discussed first.

Fantastic work!


When a reducer throws an error, DataPoint adds two properties to the error object:

`_value:*` - the input value to the reducer that failed
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reading this, changing this to _input makes more sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

expect(result).toHaveProperty('_value')
expect(result).toHaveProperty('_stack')
expect(result._value).toMatchSnapshot()
expect(result._stack).toMatchSnapshot()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we want to use toThrowErrorMatchingSnapshot()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that work with promises?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it doesn't. ☹️

We can do this though, which should write the whole object:

async function testError (reducer, input) {
  await expect(dataPoint.resolve(reducer, input)).rejects.toMatchSnapshot()
}

However this changes the snapshots dramatically, and it looks like we lose some critical information stored in the snapshots. So it might not be a good idea to change this after all.

Related thread: jestjs/jest#3601

}
}
}
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely see the value of defining these entities here, but it's not the same pattern as the other entity fixtures we have (in the test directory).

I'm not asking this to be changed or moved, just commenting in case there's interest in discussing the inclusion of this in this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like defining them in the same file so it's easier to understand the tests, and since these entities are testing very specific cases, I doubt they would ever be used in other files.

If we do use the same entities in multiple tests (which is encouraged by defining them in the test directory), it could also make it harder to make changes to the entities if we ever need to, since any change would affect multiple tests.

Lastly, if every instance of datapoint that we create for the tests contains all if the entities defined in the test directory, that might slow down the tests (since we probably create a lot of instances).

dataPoint.addEntities(schemaA10)

describe('transform entity stack traces', () => {
test('transform:1', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The descriptions could use a little improvement (even though I expect they'll all say something similar). Just thinking that if this is run with --verbose there's no additional information from the descriptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through and added better descriptions; please take a look when you have a chance

racc = utils.set(acc, 'value', _.defaultTo(acc.value, {}))
return resolveReducer(racc, contextTransform)
const racc = utils.set(acc, 'value', _.defaultTo(acc.value, {}))
return resolveReducer(racc, contextTransform, [['value']])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These nested arrays are in a few places. I'm still not exactly sure why, tbh. What's the format that is produced and why is it being done that way? (Sorry if I'm overlooking something obvious.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format can be seen by looking at the _stack strings in the snapshots.

Copy link
Contributor Author

@raingerber raingerber Mar 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The values in the nested arrays will be printed with brackets and no preceding whitespace, so that we get things like 'entry:example[value]' (and the strings are created by the stringifyReducerStack function)

@@ -1,3 +1,5 @@
const Promise = require('bluebird')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a required change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added this because I was getting an error when trying to use Promise.tap in the main reducer resolve function - I'm no longer using tap, but left this in

stack = castArray(key)
}

if (reducer.type === 'ReducerFunction') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might this be better as a switch? I don't know what other types we might have a case for, but at a certain point if..else if..else gets cumbersome to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably won't need more conditions here, since I don't think we'll be adding many more reducer types. Honestly I just really dislike switch syntax...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I just really dislike switch syntax...

Ah ha!!!! 😄

But for real, if we're not going to extend it then a switch isn't really necessary I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to change it though if there's a consensus...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be premature to change it now if we only have to account for these two scenarios.

.catch(e => e)
.then(result => {
expect(result).toBeInstanceOf(Error)
expect(result).toMatchSnapshot()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we want to use toThrowErrorMatchingSnapshot() here?

Copy link
Contributor Author

@raingerber raingerber Mar 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for record keeping, we agreed not to do this because it's using a promise instead of a synchronous function.

*/
function getMatchingCaseStatement (caseStatements, acc, resolveReducer) {
function getMatchingCaseIndex (caseStatements, accumulator, resolveReducer) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returning the index instead of the statement, so that we can add the index to the reducer stack

@codecov
Copy link

codecov bot commented Mar 10, 2018

Codecov Report

Merging #230 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #230   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files         129    130    +1     
  Lines        1700   1737   +37     
  Branches      149    160   +11     
=====================================
+ Hits         1700   1737   +37
Impacted Files Coverage Δ
packages/data-point/lib/reducer-types/factory.js 100% <ø> (ø) ⬆️
...ucer-types/reducer-helpers/reducer-find/resolve.js 100% <ø> (ø) ⬆️
...ata-point/lib/entity-types/entity-entry/resolve.js 100% <100%> (ø) ⬆️
...a-point/lib/entity-types/entity-request/resolve.js 100% <100%> (ø) ⬆️
...ucer-types/reducer-helpers/reducer-pick/resolve.js 100% <100%> (ø) ⬆️
...oint/lib/reducer-types/reducer-function/factory.js 100% <100%> (ø) ⬆️
...data-point/lib/entity-types/entity-hash/resolve.js 100% <100%> (ø) ⬆️
...ata-point/lib/entity-types/entity-model/resolve.js 100% <100%> (ø) ⬆️
...-types/reducer-helpers/reducer-parallel/resolve.js 100% <100%> (ø) ⬆️
packages/data-point/lib/reducer-types/resolve.js 100% <100%> (ø) ⬆️
... and 14 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d212e83...692ce80. Read the comment docs.

@raingerber raingerber closed this Mar 11, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

inputType/outputType describe when each fails Log paths to reducers that throw errors
3 participants