Workflow streams user publish executor - #3025
Conversation
StreamPublisher always created its own native platform thread executor to drive the background flush loop, one thread per publisher, with no way to share an executor across publishers. Applications that want to run flushes on virtual threads or a shared pool had no way to opt in. Add a constructor that accepts a ScheduledExecutorService. When one is supplied, the publisher never shuts it down: it only cancels the periodic flush task on close or on a deferred flush timeout, leaving the executor free for its other work. The default (no executor) behavior is unchanged: a lazily created single-thread executor owned and shut down by the publisher.
Every WorkflowStreamClient paid a dedicated platform thread for its publisher's flush loop, with no way to share an executor across clients even though WorkflowStreamClientOptions already allows one for the poll path. Applications that want virtual threads or one shared pool for many clients had no way to opt in. Add setPublishExecutor, mirroring setPollExecutor: the supplied executor drives the background flushes and the client never shuts it down — on close it only stops its own tasks. Default behavior is unchanged. Document the option in the module README and cover it with an integration test sharing one executor across two clients.
Mentioning virtual threads ties the docs to the newest JDKs even though any shared ScheduledExecutorService works. Describe the option in terms of sharing one executor across clients instead.
| toStop = ownedSchedulerLocked(); | ||
| } | ||
| if (toCancel != null) { | ||
| toCancel.cancel(false); |
There was a problem hiding this comment.
After a background flush times out, this only cancels the periodic future when a user executor is supplied. Because the shared executor stays live, a later forceFlush=true or maxBatchSize trigger still submits backgroundFlush and sends the next buffered batch before throwDeferred surfaces the original error. I reproduced this with a user executor: let the first batch time out, clear the signal failure, then publish a second item with forceFlush=true; the second item is delivered. This also contradicts the new test comment that later items stay buffered until explicit flush or close. Please gate all triggered background flushes once deferredError is set, without shutting down the caller-owned executor, or otherwise surface the deferred error before another background send.
brianstrauch
left a comment
There was a problem hiding this comment.
Blocking on the publish-executor timeout behavior described in the inline comment. A caller-owned executor remains able to run forced or size-triggered background flushes after a deferred FlushTimeoutException, allowing later data to be sent before the failure is surfaced.
What was changed
Every
StreamPublisherinstance was creating its own single thread executor to run the background flush. If the worker has high concurrency and many publishers, more and more platform threads will be created (and these mostly just idle).When using virtual threads, it's also not possible to override this executor with a shared one that uses virtual threads. Follow the pattern from workflow stream client and allow configuring a shared publish executor.
Why?
To reduce the number of idle platform threads created for each stream publisher instance.
Checklist
Tests added
Doc updated