Add some ergonomic features: Pipe, runner, and Config#61
Draft
mhagger wants to merge 9 commits into
Draft
Conversation
The reason for the filename will be apparent in a moment.
Rename `Event` to `EventError`, and make it satisfy the `error` interface and also implement `Unwrap() error`. Add an alias `Event` for backwards-compatibility.
When there is an `EventError`, return it directly rather than passing the `EventError` to the `ErrorHandler` and returning a slightly differently-formatted message as the `error` result. This slightly changes the error's messages, so adjust the tests accordingly.
Add `Pipe`, a `Stage` that itself runs multiple other stages with their stdouts and stdins piped to one another. This is a useful concept, because it allows pipelines to be created in modular form more easily. Also, now that we have this type, we can is it in the implementation of `Pipeline`. Specifically, `Pipeline` now embeds a single `Pipe`, which takes care of managing multiple stages and piping them together. This means that `Pipeline` only needs to know how to run a single stage. More changes are still to come…
`runner` is an object that can run a single `Stage` of any type. Use `runner` in the implementation of `Pipeline`. Now `Pipeline` is just a thin wrapper around `Pipe` and `runner`.
Add `Config`, a new type to hold a shared pipe configuration that can be used when creating multiple pipes. `Config` can run a `Stage` directly, in the usual ways: * Start(ctx, stage, options...) * Run(ctx, stage, options...) * Output(ctx, stage, options...) It can also create a pre-configured `Pipeline` for you: * NewPipeline(options...) But we don't want to allow just any `Option` to be included in a `Config`. For example, it makes no sense to use `WithStdin()` for a `Config`, because stdin can't be reused for multiple pipes. So a big change to make this possible is splitting up the concept of `Option` into two concepts: * `ConfigOption`: — an option that can be applied to a `Config` _or_ at start time. * `Option`: — an option that can be applied _only_ at start time. Currently these are `WithStdin()`, `WithStdout()`, and `WithStdoutCloser()`. This involves some boilerplate, but means that the client code to use a `Pipeline` doesn't have to change unless/until it wants to use the new features.
Consumers, not producers, should define interfaces. This one is not used anywhere in the library so it shouldn't be defined here. But you might notice that `Config.NewPipeline()` satisfies what used to be called the `NewPipeFn` interface. This is no accident; `Config` is meant to be a better replacement for `NewPipeFn`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Some abstractions were struggling desperately to get out. Here they are.
EventErrorThe
Eventtype is a little bit weird. It's construed as an event, but really it represents errors, and its actual raison d'être is for logging. I'm still not happy about the whole idea, but for now I've changedEventto implementerrorand renamed it toEventError(also aliased toEvent). This means that theeventHandlerdoesn't have to be handed down through the stages when running a pipeline, which is useful since we now support compound stages (for more about that, read on…).PipeThe only thing that could be run by this package was a
Pipeline, which was a chain ofStages strung together with pipes. But "a chain ofStages strung together with pipes" is itself a thing that behaves like aStage. So add a new type,Pipe, that represents exactly that and implements theStageinterface. APipecan be used wherever any otherStagecan be used. In particular, somebody could hand you aPipeas a sort of prefabricated part of a pipeline, and you could insert it into your ownPipelineas a component, along with other stages that feed it inputs or process its outputs.But then once we have the concept of a
Pipe, which knows how to manage sub-stages and run them as a unit,Pipelinedoesn't need to duplicate that knowledge. So change the implementation ofPipelineto use aPipeinternally. When it comes time to run aPipeline, it only has to start that singlePipestage, and the latter does all of the work.runnerNow you notice that, aside from helper methods to assemble the
Pipe, aPipelinedoesn't really care that the (single!) stage that it runs has to be aPipe. It could be any kind of singleStage!So extract the code for starting up a stage into a new private type,
runner. Arunnerknows how to set up the environment as well as the stdin and stdout for anyStageand then run the stage using methodsstart(),run(), oroutput(). ChangePipelineinto a thin wrapper around arunnerand aPipe(while preserving its same external interface).ConfigFinally, client code has shown a need to use a
NewPipeFninterface to createPipelines. The reason is that often a whole app wants to add the same options uniformly to all pipelines that are created within that app. This is something that the library can make easier.So add a
Configtype, which allows options to be stored in it. This type is immutable, but oneConfigcan be derived from another with added options:func (cfg *Config) WithOption(option ConfigOption) *Configfunc (cfg *Config) WithOptions(options ...ConfigOption) *ConfigConfighas a method that allow a pipeline to be created using it:func (cfg *Config) NewPipeline(options ...Option) *PipelineIt also has methods that allow stages (i.e., including compound stages like
Pipe) to be run directly, allowing additional options to be specified at runtime:func (cfg *Config) Start(ctx context.Context, stage Stage, options ...Option) (WaitFunc, error)func (cfg *Config) Run(ctx context.Context, stage Stage, options ...Option) errorfunc (cfg *Config) Output(ctx context.Context, stage Stage, options ...Option) ([]byte, error)I anticipate that this new style will be more natural and will gradually replace the old. Instead of
the new alternative looks like
Moreover,
Configis a place that we can add more configurability in the future; for example, we could add aNewCommand()method that by default callsexec.Command(), but that allows the user to configure it to do something different, like running the command with reduced permissions or with resource limitations.Conclusion
The changes in this PR didn't require any changes to the test suite aside from a couple trivial changes to tests that used private methods. Well, actually it still needs some tests that use the new features, but at least we know that
Pipelinestill works as it did before.It does, however, change some types in ways that would not strictly be backwards compatible (especially, the
Optionstype is now an interface, not a function type). That is why I would like to get these changes in before v2 is officially released./cc @znull