Skip to content

Commit a86cf5f

Browse files
authored
Merge pull request #59 from github/stage-joiner
Use a `stageJoiner` to join stages together
2 parents a274f74 + 7072f01 commit a86cf5f

6 files changed

Lines changed: 283 additions & 164 deletions

File tree

pipe/command.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ func (s *commandStage) Process() *os.Process {
8181

8282
func (s *commandStage) Requirements() StageRequirements {
8383
return StageRequirements{
84-
StdinNeedsFile: true,
85-
StdoutNeedsFile: true,
84+
Stdin: StreamPreferFile,
85+
Stdout: StreamPreferFile,
8686
}
8787
}
8888

pipe/pipe_matching_test.go

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,10 @@ func writeCloser() io.WriteCloser {
4949
}
5050

5151
func newPipeSniffingStage(
52-
stdinNeedsFile bool, stdinExpectation ioExpectation,
53-
stdoutNeedsFile bool, stdoutExpectation ioExpectation,
52+
req pipe.StageRequirements, stdinExpectation, stdoutExpectation ioExpectation,
5453
) *pipeSniffingStage {
5554
return &pipeSniffingStage{
56-
requirements: pipe.StageRequirements{
57-
StdinNeedsFile: stdinNeedsFile,
58-
StdoutNeedsFile: stdoutNeedsFile,
59-
},
55+
requirements: req,
6056
expect: pipeExpectations{
6157
stdin: stdinExpectation,
6258
stdout: stdoutExpectation,
@@ -68,17 +64,23 @@ func newPipeSniffingFunc(
6864
stdinExpectation, stdoutExpectation ioExpectation,
6965
) *pipeSniffingStage {
7066
return newPipeSniffingStage(
71-
false, stdinExpectation,
72-
false, stdoutExpectation,
67+
pipe.StageRequirements{
68+
Stdin: pipe.StreamAcceptAny,
69+
Stdout: pipe.StreamAcceptAny,
70+
},
71+
stdinExpectation, stdoutExpectation,
7372
)
7473
}
7574

7675
func newPipeSniffingCmd(
7776
stdinExpectation, stdoutExpectation ioExpectation,
7877
) *pipeSniffingStage {
7978
return newPipeSniffingStage(
80-
true, stdinExpectation,
81-
true, stdoutExpectation,
79+
pipe.StageRequirements{
80+
Stdin: pipe.StreamPreferFile,
81+
Stdout: pipe.StreamPreferFile,
82+
},
83+
stdinExpectation, stdoutExpectation,
8284
)
8385
}
8486

@@ -325,16 +327,25 @@ func TestPipeTypes(t *testing.T) {
325327
opts: []pipe.Option{},
326328
stages: []pipe.Stage{
327329
newPipeSniffingStage(
328-
false, expectNil,
329-
false, expectOther,
330+
pipe.StageRequirements{
331+
Stdin: pipe.StreamAcceptAny,
332+
Stdout: pipe.StreamAcceptAny,
333+
},
334+
expectNil, expectOther,
330335
),
331336
newPipeSniffingStage(
332-
false, expectOther,
333-
true, expectFile,
337+
pipe.StageRequirements{
338+
Stdin: pipe.StreamAcceptAny,
339+
Stdout: pipe.StreamPreferFile,
340+
},
341+
expectOther, expectFile,
334342
),
335343
newPipeSniffingStage(
336-
false, expectFile,
337-
false, expectNil,
344+
pipe.StageRequirements{
345+
Stdin: pipe.StreamAcceptAny,
346+
Stdout: pipe.StreamAcceptAny,
347+
},
348+
expectFile, expectNil,
338349
),
339350
},
340351
},
@@ -343,16 +354,25 @@ func TestPipeTypes(t *testing.T) {
343354
opts: []pipe.Option{},
344355
stages: []pipe.Stage{
345356
newPipeSniffingStage(
346-
false, expectNil,
347-
false, expectFile,
357+
pipe.StageRequirements{
358+
Stdin: pipe.StreamAcceptAny,
359+
Stdout: pipe.StreamAcceptAny,
360+
},
361+
expectNil, expectFile,
348362
),
349363
newPipeSniffingStage(
350-
true, expectFile,
351-
false, expectOther,
364+
pipe.StageRequirements{
365+
Stdin: pipe.StreamPreferFile,
366+
Stdout: pipe.StreamAcceptAny,
367+
},
368+
expectFile, expectOther,
352369
),
353370
newPipeSniffingStage(
354-
false, expectOther,
355-
false, expectNil,
371+
pipe.StageRequirements{
372+
Stdin: pipe.StreamAcceptAny,
373+
Stdout: pipe.StreamAcceptAny,
374+
},
375+
expectOther, expectNil,
356376
),
357377
},
358378
},

pipe/pipeline.go

Lines changed: 64 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"errors"
77
"fmt"
88
"io"
9-
"os"
109
"sync/atomic"
1110
)
1211

@@ -218,50 +217,6 @@ func (p *Pipeline) AddWithIgnoredError(em ErrorMatcher, stages ...Stage) {
218217
}
219218
}
220219

221-
type stageStarter struct {
222-
requirements StageRequirements
223-
stdin *InputStream
224-
stdout *OutputStream
225-
}
226-
227-
func (requirement StreamRequirement) validate() error {
228-
switch requirement {
229-
case StreamOptional, StreamForbidden:
230-
return nil
231-
default:
232-
return fmt.Errorf("invalid stream requirement %d", requirement)
233-
}
234-
}
235-
236-
func (requirements StageRequirements) validate(s Stage, stdinConnected, stdoutConnected bool) error {
237-
if err := requirements.Stdin.validate(); err != nil {
238-
return fmt.Errorf("stdin: %w", err)
239-
}
240-
if err := requirements.Stdout.validate(); err != nil {
241-
return fmt.Errorf("stdout: %w", err)
242-
}
243-
if requirements.Stdin == StreamForbidden && stdinConnected {
244-
return fmt.Errorf("stage %q forbids stdin, but stdin is connected", s.Name())
245-
}
246-
if requirements.Stdout == StreamForbidden && stdoutConnected {
247-
return fmt.Errorf("stage %q forbids stdout, but stdout is connected", s.Name())
248-
}
249-
return nil
250-
}
251-
252-
func (p *Pipeline) abortBeforeStart(s Stage, err error) error {
253-
_ = p.stdout.Close()
254-
p.cancel()
255-
p.eventHandler(&Event{
256-
Command: s.Name(),
257-
Msg: "failed to start pipeline stage",
258-
Err: err,
259-
})
260-
return fmt.Errorf(
261-
"starting pipeline stage %q: %w", s.Name(), err,
262-
)
263-
}
264-
265220
func (p *Pipeline) stageOptions() StageOptions {
266221
return StageOptions{Env: p.env, PanicHandler: p.panicHandler}
267222
}
@@ -309,50 +264,68 @@ func (p *Pipeline) Start(ctx context.Context) error {
309264
// We need to decide how to start the stages, especially what
310265
// pipes to use to connect adjacent stages (`os.Pipe()` vs.
311266
// `io.Pipe()`) based on the two stages' requirements.
312-
stageStarters := make([]stageStarter, len(p.stages))
267+
stageJoiners := make([]stageJoiner, len(p.stages)+1)
268+
269+
// Arrange for the input of the 0th stage to come from `p.stdin`:
270+
stageJoiners[0].nextStdin = p.stdin
271+
272+
// Arrange for the output of the last stage to go to `p.stdout`:
273+
stageJoiners[len(p.stages)].prevStdout = p.stdout
274+
275+
// closePipes closes all of the streams that are currently stored
276+
// in the joiners. This should be called if startup fails. As we
277+
// call `Stage.Start()` and pass that method streams, we clear
278+
// them from the corresponding joiners to avoid closing them
279+
// twice.
280+
closePipes := func() {
281+
for _, sj := range stageJoiners {
282+
_ = sj.closePipe()
283+
}
284+
}
313285

314-
// Collect information about each stage's type and requirements:
286+
// Store the stages in the joiners, and verify that the stages'
287+
// requirements are well-formed:
315288
for i, s := range p.stages {
316-
stageStarters[i].requirements = s.Requirements()
317-
318-
err := stageStarters[i].requirements.validate(
319-
s,
320-
i > 0 || p.stdin != nil,
321-
i < len(p.stages)-1 || p.stdout != nil,
322-
)
323-
if err != nil {
324-
return p.abortBeforeStart(s, err)
289+
// Make sure that the stage's requirements are well-formed:
290+
requirements := s.Requirements()
291+
if err := requirements.Stdin.Validate(); err != nil {
292+
return fmt.Errorf("stdin: %w", err)
293+
}
294+
if err := requirements.Stdout.Validate(); err != nil {
295+
return fmt.Errorf("stdout: %w", err)
325296
}
326-
}
327297

328-
if p.stdin != nil {
329-
// Arrange for the input of the 0th stage to come from
330-
// `p.stdin`:
331-
stageStarters[0].stdin = p.stdin
298+
stageJoiners[i].nextStage = s
299+
stageJoiners[i].nextStageReq = requirements
300+
stageJoiners[i+1].prevStage = s
301+
stageJoiners[i+1].prevStageReq = requirements
332302
}
333303

334-
if p.stdout != nil {
335-
// Arrange for the output of the last stage to go to
336-
// `p.stdout`:
337-
stageStarters[len(p.stages)-1].stdout = p.stdout
304+
// Check that each of the stages' requirements are satisfiable:
305+
for i := range stageJoiners {
306+
if err := stageJoiners[i].validate(); err != nil {
307+
closePipes()
308+
return err
309+
}
338310
}
339311

340-
// Clean up any processes and pipes that have been created. `i` is the
341-
// index of the stage that failed to start. If the stage already received
342-
// its streams, it owns any closing stream.
343-
abort := func(i int, err error, closeFailedStageStdin bool) error {
344-
// If the failing stage never received its stdin, close the pipe that
345-
// the previous stage was writing to. That should cause it to exit
346-
// even if it's not minding its context.
347-
if closeFailedStageStdin {
348-
_ = stageStarters[i].stdin.Close()
312+
// Create the "inner" pipes (i.e, all but the first and last
313+
// `stageJoiners`):
314+
for i := 1; i < len(stageJoiners)-1; i++ {
315+
if err := stageJoiners[i].createPipe(); err != nil {
316+
closePipes()
317+
return err
349318
}
319+
}
350320

351-
// If stdout was supplied with WithStdoutCloser but the final stage
352-
// was never started, then the pipeline still owns that closer.
353-
if i < len(p.stages)-1 {
354-
_ = p.stdout.Close()
355-
}
321+
// We're about to start up the stages, one by one. If something
322+
// goes wrong during that process, this function should be called
323+
// to kill any stages that have already been started and to close
324+
// any pipes that have not yet been passed to a stage. `i` is the
325+
// index of the stage that failed to start. If the stage already
326+
// received its streams, it is responsible for closing them.
327+
abort := func(i int, err error) error {
328+
closePipes()
356329

357330
// Kill and wait for any stages that have been started
358331
// already to finish:
@@ -370,51 +343,20 @@ func (p *Pipeline) Start(ctx context.Context) error {
370343
)
371344
}
372345

373-
// Loop over all but the last stage, starting them. By the time we
374-
// get to a stage, its stdin will have already been determined,
375-
// but we still need to figure out its stdout and set the stdin
376-
// that will be used for the subsequent stage.
377-
for i, s := range p.stages[:len(p.stages)-1] {
378-
ss := &stageStarters[i]
379-
nextSS := &stageStarters[i+1]
380-
381-
// We need to generate a pipe pair for this stage to use
382-
// to communicate with its successor:
383-
if ss.requirements.StdoutNeedsFile || nextSS.requirements.StdinNeedsFile {
384-
// Use an OS-level pipe for the communication:
385-
nextStdin, stdout, err := os.Pipe()
386-
if err != nil {
387-
return abort(i, err, true)
388-
}
389-
nextSS.stdin = ClosingInput(nextStdin)
390-
ss.stdout = ClosingOutput(stdout)
391-
} else {
392-
nextStdin, stdout := io.Pipe()
393-
nextSS.stdin = ClosingInput(nextStdin)
394-
ss.stdout = ClosingOutput(stdout)
395-
}
396-
if err := s.Start(
397-
ctx, p.stageOptions(),
398-
ss.stdin, ss.stdout,
399-
); err != nil {
400-
_ = nextSS.stdin.Close()
401-
return abort(i, err, false)
402-
}
403-
}
346+
// Loop over all of the stages, starting them in order.
347+
for i, s := range p.stages {
348+
prevSJ := &stageJoiners[i]
349+
nextSJ := &stageJoiners[i+1]
404350

405-
// The last stage needs special handling, because its stdout
406-
// doesn't need to flow into another stage (it's already set in
407-
// `ss.stdout` if it's needed).
408-
{
409-
i := len(p.stages) - 1
410-
s := p.stages[i]
411-
ss := &stageStarters[i]
351+
err := s.Start(ctx, p.stageOptions(), prevSJ.nextStdin, nextSJ.prevStdout)
412352

413-
if err := s.Start(
414-
ctx, p.stageOptions(),
415-
ss.stdin, ss.stdout,
416-
); err != nil {
417-
return abort(i, err, false)
353+
// Even if that stage failed to start, we are no longer
354+
// responsible for closing its streams:
355+
prevSJ.nextStdin = nil
356+
nextSJ.prevStdout = nil
357+
358+
if err != nil {
359+
return abort(i, err)
418360
}
419361
}
420362

pipe/stage.go

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,25 +81,11 @@ type StageOptions struct {
8181
// StagePanicHandler is a function that handles panics in a pipeline's stages.
8282
type StagePanicHandler func(p any) error
8383

84-
type StreamRequirement int
85-
86-
const (
87-
// StreamOptional means the stream may be connected or nil.
88-
StreamOptional StreamRequirement = iota
89-
90-
// StreamForbidden means the stream must be nil.
91-
StreamForbidden
92-
)
93-
94-
// StageRequirements describes what a Stage needs from the streams connected to
95-
// its stdin and stdout. The zero value is correct for stages that are happy
96-
// with arbitrary io.Reader/io.Writer streams, such as Function stages.
84+
// StageRequirements describes what a Stage needs from the streams
85+
// connected to its stdin and stdout. The zero value is correct for
86+
// stages that are happy with arbitrary io.Reader/io.Writer streams,
87+
// such as Function stages.
9788
type StageRequirements struct {
9889
Stdin StreamRequirement
9990
Stdout StreamRequirement
100-
101-
// {Stdin,Stdout}NeedsFile indicate that, if stdio is connected, the
102-
// stage requires it to be backed by an *os.File (a real file descriptor)
103-
StdinNeedsFile bool
104-
StdoutNeedsFile bool
10591
}

0 commit comments

Comments
 (0)