Our server app has several methods, called in sequence that iterate through a 20M-row resultset and transform it. Each method in this pipeline stores a 200+ megabyte copy of the data, with predictably bad RAM and GC performance impact.
Each method follows a similar pattern:
public HugeCollection1 Step1 (SomeType sourceData)
{
var transformed = new List<RowType>;
using (var foo = InitializeSomethingExpensive(sourceData))
{
foreach (var row in foo)
{
transformed.Add (TransformRow(row));
}
}
return transformed;
}
Then these methods are called in a pipeline, e.g.
var results1 = Step1(sourceData);
var results2 = Step2(results1);
var results3 = Step3(results2);
...
var finalResults = StepN (resultsNMinus1);
return finalResults; // final results
I'd like to transform this into a more functional solution that iterates through the original source data without ever holding the entire dataset in RAM. I want to end up with a List of the final results without any intermediate collections.
If there were no setup required at each stage of the pipeline, then the solution would be simple: just run each transformation for each row and store only the final result.
var transformed = new List<SmallResult>;
// TODO: How to set up and ensure teardown of the *other* pipeline steps?
using (var foo = InitializeSomethingExpensive(sourceData))
{
foreach (var row in foo)
{
object result = row;
foreach (var step in Pipeline)
{
result = step.Transform (result);
}
transformed.Add (result as SmallResult);
}
}
return transformed;
But today, each of those separate pipeline steps has its own expensive setup and tear-down process that's enforced via a using block.
What's a good pattern to refactor each of these pipeline methods so the setup/teardown code is guaranteed to happen? In pseudo-code, I'd like to end up with this:
- Setup all steps
- Loop through each row
- Transform row through each step
- End loop
- Cleanup all steps, guaranteeing that cleanup always happens
- Return (small) results
It's not practical to combine all the using blocks into a single method because the code in each of these steps is long and shared and I don't want to repeat that shared code in one method.
I know I could manually replace the using block with try/finally, but doing that manually for multiple resources seems harder than necessary.
Is there a simpler solution possible, e.g. using using and yield together in a smart way? Or is there a good "multi-using" class implementation available that makes this coordinated setup/teardown process easy (e.g. its constructor accepts a list of functions that return IDisposable and its Dispose() implementation would ensure that everything is cleaned up)?
Seems like this is a pattern that someone smarter than I has already figured out, so asking here before re-inventing the wheel.