I considered Do semantics as well (it can be added easily) but rejected:
- User can add some code between Do calls - it will execute out of any stage (breaks backpressure)
- The main pro of “per-item” execution (opposite to chans) is that you can store the state of item processing in local variables inside fn and use them at any stage. Variables defined in one Do func will not be visible in another which forces you to pre-define variables at the top
- One of my goals is to support easy migration for old 1-goroutine sequential code. In case of MoveTo you don’t need to extract stages in functions and extract variables, just put MoveTo calls at the boundaries
- The init “read” stage is implicit and will not have Do anyway
- Retain functionality will look less natural
- We need to think how to react if Do is called inside another Do: it can be used to replace Retain (is a goroutine is used) but code like that diffucult to read and reason about
And you can still have deadlocks
Calling MoveTo for already entered stage or earlier stages panics
I find SetLimit() has some pitfalls
SetLimit is to allow multiple batches (items in conveyor terms), to be in a stage at the same time. It’s not for messages from your example.
In a simple example:
// implicit read stage
var batch []Message = read100Messages()
if err := processing.MoveTo(ctx); err != nil { ... }
// just spin errgroup with a goroutine for each of the message
// and wait
So conveyor is batch-centric and a batch moves through the stages normally till the last “commit” stage.
If you need gather/scatter and want to have “message processor” with limited concurrency (e.g. 150 messages) that will provide backpressure (which is absent in the example above), use FanOut stage with AddPool().SetLimit(150). Processing a message is a “Task” in it (similar to errgroup). Tasks of earlier batches/items have priority. If there are multiple pools, you claim tasks beforehand for all of them which eliminates the possibility of deadlocks with other items.
You can also use AddLane to create a sub-conveyor for messages (not batches)
True back-pressure is more than just breaking when the queue is full.
I agree that there are more sophisticated strategies than the basic one when back-pressure propagates through the stages one by one. Your example makes sense and I completely agree it may benefit in the cases you mention.
But in the lib I don’t know which strategy works better for your specific stages. Basic one behaves in the same way as channels would do. My goal is not to create a heavy framework but more like sync primitives.
There is observability support. Queue sizes and concurrency limits for stages are dynamic (can be gracefully adjusted in runtime). Given that features and understanding the nature of your specific stages you can implement a custom strategy.
Observability is pull-based in order not to lock on slow consumers. In the first version I didn’t come up with a clear and simple API for push-based “back-pressure controller/monitor”