fast-safe-stringify: Infinite loop when called on a Sequelize object
In https://github.com/visionmedia/superagent/commit/2e5d6fdbe026c1934c0a0fbe2ec933a13b60af1a the Superagent library changed from using JSON.stringify to using fast-safe-stringify for it’s default object serializer when sending JSON data.
We use Sequelize for our ORM. Sequelize uses their own custom object structure, which supports a .toJSON call.
When Superagent was using JSON.stringify, that called the .toJSON method on our Sequelize objects, properly converting them to JSON. However, with the change to fast-safe-stringify, .toJSON is no longer called, even if it exists.
I’m not sure how to provide a copy of the object in question since getting its JSON representation is unlikely to be helpful, since all the Sequelize specifics are stripped from that.
Is there a reason this library does not call .toJSON if it exists?
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 1
- Comments: 16 (4 by maintainers)
Okay, I need to move on for now, but here is where I got in debugging:
I set a conditional breakpoint for when the stack hits 100 length. A few highlights from the image:
Array(407)is the 407 domain objects I’m saving to the databaseStageDefinitionis one of our domain objectsStageDefinition’s collection pointing to NUnitDefinitiondomain objectsUnitDefinitionobjectsUnitDefinitionhaving NActivityDefinitions having NTaskDefinitions having NTaskDefinitionPredecessordependencies.(Basically our domain model is a highly-inter-connected graph of a "project with a bunch of tasks, various levels/groupings of tasks (stage/unit/activity), and then dependencies (predecessors/successors) between the tasks. Obviously they become individuals rows in the database, but while in memory our ORM basically graph-izes all the references (intelligently/lazily/no+N+1-ily) so we can easily do things like critical path calculation, which is essentially walking a graph.)
As I continue looking down the
stackentries, everything “looks right”,decircis just continually finding “oh I haven’t seen that yet” references…although they are really all part of that originalArray(407), each one just hasn’t been marked/added to the stack proper yet (i.e. as an explicit element that we know about-but-have-not-visited yet) because we’re not ‘done’ with each one).I.e. even though the 6th stack entry of
Array(407)has all of the instances as implicitly “already seen but not processed”, we process the 1st one (…and find 406 others), then do the 2nd one (…and find 405 others), then do the 3rd one (…and find 403 others), where each of these “…and find XXX others” is not actually linear, but itself recursing through “seen but not yet processed” values.So, could we somehow pre-tell
decirc“yes we know about all 407 of these, we are getting to them, but don’t recurse into them if you find them again”.…i.e. here:
Put every element in
valdirectly on the stack before callingdecircbecause otherwise the current “pre-recursion before we put val[i] in the stack” is going to not realize we already know about the other N values inval.Basically some differentiation of nodes between “has seen and not yet visited/processed” (not currently in the code AFAIU) and “has finished visiting/actually processed” (an entry in stack AFAIU).
I am not an algorithms expert, but I vaguely recall seeing this distinction in graph traversal algorithms last time I was studying for interviews. 😛 😃
Perhaps the code is already doing this and I’m just missing it. Not sure.
Also fwiw I tried to re-create our graph with just a bunch of in-memory nodes with like 2-levels of 100 “parents” and 5 children-per-parent and failed to trigger this behavior. I think the input really does need to be pathologically circular, which is hard to create by hand (the graph I’m using is an actual one from production, hence the inherent size/complexity). I can try to synthesize also pathological input here & there/as I get time.
(Also, just to be clear, we never wanted
fast-safe-stringifyto actually output a sane value for this data structure/graph, it just happened to leak into alogger.error({ err: ... }, ...)without really meaning to, and then tada our CPU is pegged and the app is dead. We’ve since implementedtoJSONon our entities andEntityManagerto stop this from happening, but I followed up here as an extra/future precaution + FYI.)…ah yeah, so this is really ugly, but just doing:
Stopped the pathological behavior. I have no idea is the resulting behavior is correct (i.e. I didn’t inspect the output and it could be doing ‘wrong things’), just that it stops the “not technically infinite but just a lot of it” recursion.
Hopefully that is helpful…
@davidmarkclements sure.
Code to reproduce:
I also have added logging for traverse:
Sample log is attached. traversedProps.txt
bufferprops are realBufferobjects:user._modelOptions.sequelize.connectionManager.pool._availableObjects._list.head.data.obj.connection.writer.buffer instanceof Buffer === true