mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
Phase 3 of plans/exit.md: the streaming-interface change, the load-bearing piece for #341 (DSL exit statement) and #440 (strict mode). - RecordTransformer.Transform and RecordTransformerFunc now return error. All 69 Transform implementations and their dispatch helpers updated (mechanical rewrite, compiler- and errcheck-verified). - runSingleTransformerBatch, on a Transform error, forwards any output produced before the failure plus an end-of-stream marker downstream, so the rest of the chain and the record-writer drain and finish cleanly; runSingleTransformer then surfaces the error to stream.Stream's select loop (non-blocking send; first error wins) and signals upstream-done so the record-reader stops. This is exactly the flush-then-exit sequencing a future DSL 'exit N' needs. - dataProcessingErrorChannel and FileOutputHandler.recordErroredChannel are now chan error instead of chan bool; ChannelWriter still prints write- error details at the site and sends the 'exiting due to data error' sentinel, preserving the exact stderr shape pinned by regression cases. - Mid-stream os.Exit sites converted to returned errors: put/filter DSL begin/main/end-block errors and the non-boolean filter-expression case, tee write/close failures, split write/open/close failures, join left-file ingest failures (both half-streaming and sorted paths, with full error plumbing through JoinBucketKeeper), histogram/stats2 ingest errors, surv fit errors, and step stepper allocation (tStepperAllocator now returns (tStepper, error); bad EWMA coefficients propagate; negative slwin parameters are reported by the CLI parser via the existing bad-stepper-name pattern). - The two genuinely internal join-bucket-keeper states now use lib.InternalCodingErrorWithMessageIf instead of hand-rolled print+exit. - pkg/transformers is now os.Exit-free. Behavior notes: the non-boolean filter message gains the standard 'mlr: ' prefix and a newline (it previously printed with neither); tee errors now include the underlying cause. All 4779 regression cases pass unchanged; mlr head early-out latency is unaffected (0.02s over 50M records). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
319 lines
11 KiB
Go
319 lines
11 KiB
Go
package transformers
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/johnkerl/miller/v6/pkg/cli"
|
|
"github.com/johnkerl/miller/v6/pkg/types"
|
|
)
|
|
|
|
// ChainTransformer is a refinement of Miller's high-level sketch in stream.go.
|
|
// As far as stream.go is concerned, the transformer-chain is a box which reads
|
|
// from an input-record channel (from the record-reader object) and writes to
|
|
// an output-record channel (to the record-writer object). Inside that box is a
|
|
// bit more complexity, including channels between transformers in the chain.
|
|
//
|
|
// Channel structure from outside the box:
|
|
// * readerInputRecordChannel "ichan" passes records from the record-reader
|
|
// to the transformer chain.
|
|
// * writerOutputRecordChannel "ochan" passes records from the transformer chain
|
|
// to the record-writer.
|
|
// * readerDownstreamDoneChannel "dchan" signifies back to the record-reader
|
|
// that it can stop reading files, since all downstream consumers aren't
|
|
// interested in any more data.
|
|
//
|
|
// Record-reader
|
|
// | ichan ^ dchan
|
|
// v |
|
|
// +---------------+
|
|
// | Transformer 0 |
|
|
// | Transformer 1 |
|
|
// | Transformer 2 |
|
|
// | Transformer 3 |
|
|
// +---------------+
|
|
// | ochan
|
|
// v
|
|
// Record-writer
|
|
//
|
|
// Channel structure from inside the box:
|
|
//
|
|
// Record-reader
|
|
// | ichan ^ dchan
|
|
// v |
|
|
// +---------------+
|
|
// | Transformer 0 |
|
|
// | | ^ |
|
|
// | v | |
|
|
// | Transformer 1 |
|
|
// | | ^ |
|
|
// | v | |
|
|
// | Transformer 2 |
|
|
// | | ^ |
|
|
// | v | |
|
|
// | Transformer 3 |
|
|
// +---------------+
|
|
// | ochan
|
|
// v
|
|
// Record-writer
|
|
//
|
|
// Each transformer has four channels from its point of view:
|
|
//
|
|
// | irchan odchan |
|
|
// | | ^ |
|
|
// | v | |
|
|
// | Transformer i |
|
|
// | | ^ |
|
|
// | v | |
|
|
// | orchan idchan |
|
|
//
|
|
// * inputRecordChannel "irchan" is where it reads records from.
|
|
// o If the transformer is the first in the chain, this is the
|
|
// readerInputRecordChannel passed into ChainTransformer.
|
|
// o Otherwise it's an intermediary channel from transformer i-1's output,
|
|
// created and connected by ChainTransformer.
|
|
//
|
|
// * outputRecordChannel "orchan" is where it writes records to.
|
|
// o If the transformer is the last in the chain, this is the
|
|
// writerOutputRecordChannel passed into ChainTransformer.
|
|
// o Otherwise it's an intermediary channel to transformer i+1's input,
|
|
// created and connected by ChainTransformer.
|
|
//
|
|
// * inputDownstreamDoneChannel "idchan" is where it reads a downstream-done flag from.
|
|
// o These are all created internally by ChainTransformer.
|
|
// o If the transform is the last in the chain, nothing writes to its idchan.
|
|
// o Otherwise transformer i's idchan is connected to transformer i+1's odchan,
|
|
// so transformer i can accept a downstream-done flag.
|
|
//
|
|
// * outputDownstreamDoneChannel "odchan" is where it writes a downstream-done flag to.
|
|
// o If the transformer is the first in the chain, this is the
|
|
// readerDownstreamDoneChannel passed into ChainTransformer.
|
|
// o Otherwise it's an intermediary channel connected to transformer i-1's idchan,
|
|
// so transformer i can produce a downstream-done flag.
|
|
//
|
|
// Handling in practice:
|
|
//
|
|
// * Most verbs pass a downstrem-done flag on their idchan upstream to their odchan.
|
|
// The HandleDefaultDownstreamDone function is for this purpose: most verbs use it.
|
|
//
|
|
// * The exceptional verbs are HEAD, TEE, and SEQGEN.
|
|
//
|
|
// * mlr head is the reason this exists. The problem to solve is that people
|
|
// want to do 'mlr head -n 10 myhugefile.dat' and have mlr exit as soon as those
|
|
// 10 records have been read.
|
|
//
|
|
// * However, if someone does 'mlr cut -f foo then tee bar.dat then head -n 10',
|
|
// they want bar.dat to have all the records seen by tee; head -n 10 should produce
|
|
// only the first 10 but bar.dat should have them all.
|
|
//
|
|
// * Likewise, 'mlr seqgen --stop 1000000000 then head -n 10' should result
|
|
// in seqgen breaking out of its data-production loop.
|
|
//
|
|
// * In head.go, tee.go, and seqgen.go you will see specific handling of
|
|
// reading idchan and writing odchan.
|
|
//
|
|
// TESTING
|
|
//
|
|
// * This is a bit awkward with regard to regression-test -- we don't want a
|
|
// multi-GB data file in our repo for the continuous integration job to check
|
|
// that the processing finishes quickly.
|
|
//
|
|
// * Nonetheless: all these should finish quickly/
|
|
//
|
|
// mlr head -n 10 ~/tmp/huge
|
|
// mlr cat then head -n 10 ~/tmp/huge
|
|
// mlr head -n 100 then tee foo.txt then head -n 10 ~/tmp/huge
|
|
// check `wc -l foo.txt` is 100
|
|
// mlr head -n 100 then tee foo.txt then head -n 10 then tee bar.txt ~/tmp/huge
|
|
// check `wc -l foo.txt` is 100 and `wc -l bar.txt` is 10
|
|
// mlr seqgen --stop 100000000 then head -n 10
|
|
//
|
|
|
|
// ChainTransformer is a refinement of Miller's high-level sketch in stream.go.
|
|
// While stream.go sees goroutines for record reader, transformer chain, and
|
|
// record writer, with input channel from record-reader to transformer chain
|
|
// and output channel from transformer chain to record-writer, ChainTransformer
|
|
// subdivides goroutines for each transformer in the chain, with intermediary
|
|
// channels between them.
|
|
func ChainTransformer(
|
|
readerRecordChannel <-chan []*types.RecordAndContext, // list of *types.RecordAndContext
|
|
readerDownstreamDoneChannel chan<- bool, // for mlr head -- see also stream.go
|
|
recordTransformers []RecordTransformer, // not *recordTransformer since this is an interface
|
|
writerRecordChannel chan<- []*types.RecordAndContext, // list of *types.RecordAndContext
|
|
dataProcessingErrorChannel chan<- error, // for mid-stream transformer errors -- see stream.go
|
|
options *cli.TOptions,
|
|
) {
|
|
i := 0
|
|
n := len(recordTransformers)
|
|
|
|
intermediateRecordChannels := make([]chan []*types.RecordAndContext, n-1) // list of *types.RecordAndContext
|
|
for i = 0; i < n-1; i++ {
|
|
intermediateRecordChannels[i] = make(chan []*types.RecordAndContext, 1) // list of *types.RecordAndContext
|
|
}
|
|
|
|
intermediateDownstreamDoneChannels := make([]chan bool, n)
|
|
for i = range n {
|
|
intermediateDownstreamDoneChannels[i] = make(chan bool, 1)
|
|
}
|
|
|
|
for i, recordTransformer := range recordTransformers {
|
|
// Downstream flow: channel a given transformer reads records from
|
|
irchan := readerRecordChannel
|
|
// Downstream flow: channel a given transformer writes transformed
|
|
// records to
|
|
orchan := writerRecordChannel
|
|
// Upstream signaling: channel a given transformer reads to see if
|
|
// downstream transformers are done (e.g. mlr head)
|
|
idchan := intermediateDownstreamDoneChannels[i]
|
|
// Upstream signaling: channel a given transformer (e.g. mlr head)
|
|
// writes to signal to upstream transformers that it will ignore
|
|
// further input.
|
|
odchan := readerDownstreamDoneChannel
|
|
|
|
if i > 0 {
|
|
irchan = intermediateRecordChannels[i-1]
|
|
odchan = intermediateDownstreamDoneChannels[i-1]
|
|
}
|
|
if i < n-1 {
|
|
orchan = intermediateRecordChannels[i]
|
|
}
|
|
|
|
go runSingleTransformer(
|
|
recordTransformer,
|
|
i == 0,
|
|
irchan,
|
|
orchan,
|
|
idchan,
|
|
odchan,
|
|
dataProcessingErrorChannel,
|
|
options,
|
|
)
|
|
}
|
|
}
|
|
|
|
func runSingleTransformer(
|
|
recordTransformer RecordTransformer,
|
|
isFirstInChain bool,
|
|
inputRecordChannel <-chan []*types.RecordAndContext, // list of *types.RecordAndContext
|
|
outputRecordChannel chan<- []*types.RecordAndContext, // list of *types.RecordAndContext
|
|
inputDownstreamDoneChannel <-chan bool,
|
|
outputDownstreamDoneChannel chan<- bool,
|
|
dataProcessingErrorChannel chan<- error,
|
|
options *cli.TOptions,
|
|
) {
|
|
|
|
done := false
|
|
for !done {
|
|
recordsAndContexts := <-inputRecordChannel
|
|
var err error
|
|
done, err = runSingleTransformerBatch(
|
|
recordsAndContexts,
|
|
recordTransformer,
|
|
isFirstInChain,
|
|
outputRecordChannel,
|
|
inputDownstreamDoneChannel,
|
|
outputDownstreamDoneChannel,
|
|
options,
|
|
)
|
|
if err != nil {
|
|
// Surface the error to stream.Stream's select loop. Non-blocking
|
|
// send: if another goroutine errored first, that error wins.
|
|
select {
|
|
case dataProcessingErrorChannel <- err:
|
|
default:
|
|
}
|
|
// Tell upstream (transformers and ultimately the record-reader,
|
|
// via the mlr-head mechanism) that we'll ignore further input.
|
|
select {
|
|
case outputDownstreamDoneChannel <- true:
|
|
default:
|
|
}
|
|
// runSingleTransformerBatch has already forwarded an end-of-stream
|
|
// marker downstream, so the record-writer drains and finishes,
|
|
// upon which stream.Stream returns the error we sent above.
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// runSingleTransformerBatch passes one batch of records through the
|
|
// transformer. The boolean return is true on end of record stream. A non-nil
|
|
// error is a mid-stream transformer failure: any output produced before the
|
|
// failure, plus an end-of-stream marker, has already been forwarded
|
|
// downstream so the rest of the chain and the record-writer can drain and
|
|
// finish cleanly.
|
|
func runSingleTransformerBatch(
|
|
inputRecordsAndContexts []*types.RecordAndContext, // list of types.RecordAndContext
|
|
recordTransformer RecordTransformer,
|
|
isFirstInChain bool,
|
|
outputRecordChannel chan<- []*types.RecordAndContext, // list of *types.RecordAndContext
|
|
inputDownstreamDoneChannel <-chan bool,
|
|
outputDownstreamDoneChannel chan<- bool,
|
|
options *cli.TOptions,
|
|
) (bool, error) {
|
|
outputRecordsAndContexts := make([]*types.RecordAndContext, 0, len(inputRecordsAndContexts))
|
|
done := false
|
|
|
|
for _, inputRecordAndContext := range inputRecordsAndContexts {
|
|
// --nr-progress-mod
|
|
// TODO: function-pointer this away to reduce instruction count in the
|
|
// normal case which it isn't used at all. No need to test if {static thing} != 0
|
|
// on every record.
|
|
if options.NRProgressMod != 0 {
|
|
if isFirstInChain && inputRecordAndContext.Record != nil {
|
|
context := &inputRecordAndContext.Context
|
|
if context.NR%options.NRProgressMod == 0 {
|
|
fmt.Fprintf(os.Stderr, "NR=%d FNR=%d FILENAME=%s\n", context.NR, context.FNR, context.FILENAME)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Three things can come through:
|
|
//
|
|
// * End-of-stream marker
|
|
// * Non-nil records to be printed
|
|
// * Strings to be printed from put/filter DSL print/dump/etc
|
|
// statements. They are handled here rather than fmt.Println directly
|
|
// in the put/filter handlers since we want all print statements and
|
|
// record-output to be in the same goroutine, for deterministic
|
|
// output ordering.
|
|
//
|
|
// The first two are passed to the transformer. The third we send along
|
|
// the output channel without involving the record-transformer, since
|
|
// there is no record to be transformed.
|
|
|
|
if inputRecordAndContext.EndOfStream || inputRecordAndContext.Record != nil {
|
|
err := recordTransformer.Transform(
|
|
inputRecordAndContext,
|
|
&outputRecordsAndContexts,
|
|
// TODO: maybe refactor these out of each transformer.
|
|
// And/or maybe poll them once per batch not once per record.
|
|
inputDownstreamDoneChannel,
|
|
outputDownstreamDoneChannel,
|
|
)
|
|
if err != nil {
|
|
// Forward what was produced before the failure, plus an
|
|
// end-of-stream marker so downstream drains and finishes.
|
|
outputRecordsAndContexts = append(outputRecordsAndContexts,
|
|
types.NewEndOfStreamMarker(&inputRecordAndContext.Context))
|
|
outputRecordChannel <- outputRecordsAndContexts
|
|
return true, err
|
|
}
|
|
} else {
|
|
outputRecordsAndContexts = append(outputRecordsAndContexts, inputRecordAndContext)
|
|
}
|
|
|
|
if inputRecordAndContext.EndOfStream {
|
|
done = true
|
|
break
|
|
}
|
|
|
|
if done {
|
|
break
|
|
}
|
|
}
|
|
|
|
outputRecordChannel <- outputRecordsAndContexts
|
|
|
|
return done, nil
|
|
}
|