You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
So, I start seeing that some code that on the surface should be dead simple in textbook form in reality the main entry point and the whole state handling is really complex
For example SortMergeJoin, on the surface the algorithm is simple
child return pending and have to keep state between calls
programming language as opposed to pseudo code
Flow is not linear as pseudo code since you can return only 1 value at a time
needing to handle spill
performance optimization
the actual implementation is really complex which means that doing any sort of PR there is hard to review or to understand
Tradeoffs
So I started with moving some stuff to async generators that solve some of the problems while adding others.
problem that it is solving:
Child return pending and have to keep state between calls
Flow is now linear since we can yield from the code and have the state and code in mind kept
Fewer lines - since less code needed for holding and managing the state
In some cases improve performance since going back to where you was in the state by making the function idompotent could be expensive
Problems that it is creating:
Timing is more annoying, you should pause the elapsed_compute timer between
i. yields - since you shouldn't count the time that the parent has done work between calling you
ii. awaits on child streams - because you don't want to count the child time
iii. awaits on stuff that you do async like reading a spill file - because even though the on the surface this is your work, you might overcount the elapsed time and the spill reading finish earlier but you did not woke up (up for debate)
Reserving memory is less intuitive since the code look linear but you hand off control between awaits so the data that you hold should be reserved for
You need to wrap the async generator with ObservedStream to track end time and output batches/rows/etc
can introduce performance problems since we are adding async machinery if not used with caution
Alternative solutions
We already have RecordBatchReceiverStreamBuilder but the flow of data is different - i.e. it is push based and not pull based which means:
more memory is being in the buffer (have at least 1 item pending
and @pepijnve kindly extracted a trimmed down version for the async generators from genawaiter crate that I previously used in that PR and tokio async-stream crate in:
Status:
SortPreservingMergeStreamSortPreservingMergeStreamto use generators instead of state machine #23407 - Moving to generatorsSortMergeJoinBitwiseSortMergeJoinStreamchore: refactor SortMergeJoin bitwise stream to generators and simplify to be textbook like as possible #23761 - moving to generators and simplifying the codeMaterializingSortMergeJoinStream- chore: refactorMaterializingSortMergeJoinStreaminto generators and simplify code to be textbook like as possible #23976AggregateOrderedPartialAggregateStream- chore(ordered-partial-aggregate): moveOrderedPartialAggregateStreamto generators for readability #23951 - moving to generators, the code is already pretty simpleOrderedFinalAggregateStream- chore(OrderedFinalAggregateStream): refactor to async generator #24008Possible Candidates:
SingleHashAggregateStreamOrderedFinalAggregateStreamPartialHashAggregateStreamPartialReduceHashAggregateStreamFinalHashAggregateStreamSortMergeJoinMaterializingSortMergeJoinStreamBackground
So, I start seeing that some code that on the surface should be dead simple in textbook form in reality the main entry point and the whole state handling is really complex
For example SortMergeJoin, on the surface the algorithm is simple
Algorithm
Taken from Sort-Merge Joins
But due to all the following reasons:
the actual implementation is really complex which means that doing any sort of PR there is hard to review or to understand
Tradeoffs
So I started with moving some stuff to async generators that solve some of the problems while adding others.
problem that it is solving:
Problems that it is creating:
elapsed_computetimer betweeni.
yields - since you shouldn't count the time that the parent has done work between calling youii.
awaits on child streams - because you don't want to count the child timeiii.
awaits on stuff that you doasynclike reading a spill file - because even though the on the surface this is your work, you might overcount the elapsed time and the spill reading finish earlier but you did not woke up (up for debate)awaits so the data that you hold should be reserved forObservedStreamto track end time and output batches/rows/etcAlternative solutions
We already have
RecordBatchReceiverStreamBuilderbut the flow of data is different - i.e. it is push based and not pull based which means:Initial Progress
I started the first PR in
SortPreservingMergeStreamto use generators instead of state machine #23407and @pepijnve kindly extracted a trimmed down version for the async generators from
genawaitercrate that I previously used in that PR and tokioasync-streamcrate in:All the discussion for why we have our own implementation and also why not having macro implementation can be found in:
SortPreservingMergeStreamto use generators instead of state machine #23407 (comment) - for why not using external crate or macroSortPreservingMergeStreamto use generators instead of state machine #23407 - why not using macro in the pr descriptionthe first rewrite PR allowed for having the code rewritten to match simpler form: