Given:
AtoB: GraphStageWithMaterializedValue[A, B, Future[AtoBRuntimeApi]]
;
BtoC: GraphStageWithMaterializedValue[B, C, Future[BtoCRuntimeApi]]
.
Wanted: AtoC: GraphStageWithMaterializedValue[A, C, Future[AtoCRuntimeApi]]
.
In my particular case it's really convenient to implement AtoCRuntimeApi
in terms of both AtoBRuntimeApi
and BtoARuntimeApi
.
So I would like to define AtoCRuntimeApi
as case class AtoCRuntimeApi(a2b: AtoBRuntimeApi, b2c: BtoCRuntimeApi)
;
And to define the new compound stage as stageAtoB.viaMat(stageBtoC)(combineIntoAtoC)
where
combineIntoAtoC: (Future[AtoBRuntimeApi], Future[B2CRuntimeApi]) => Future[AtoCRuntimeApi]
.
Obviously the implementation of combineIntoAtoC
requires some instance of ExecutionContext
in order to map the futures.
The question: what execution context should I use in the described case?
Options, I would rather avoid:
bake in an instance that is currently available (while composition of stages) — the "blueprint" will not be safe to materialise if that execution-context becomes unavailable;
ExecutionContext.global
— well, it's global. It seems terribly wrong to use it (probably, once per materialisation — is not that big deal).
The most wanted execution-context is the one that is available as the property of materializer (mat.executionContext
). But there is no way I can access it within that combine-function.
I usually use the actor system context in such cases, which is indeed by default can be referred to from the materializer. As a general solution, though, you can pass the execution context to whatever function you construct the stream graph in with an implicit parameter:
def makeGraph(...)(implicit ec: ExecutionContext): RunnableGraph = {
def combineIntoAtoC(...) = ... // uses ec implicitly
}
This allows you to push the decision about which context to use up the call stack. At the appropriate level there most certainly will be some kind of access to the actor system's dispatcher.
The reason why I prefer to use the actor system's dispatcher instead of the global one is because it reduces the surface of dependencies - all execution contexts in this case come from one source, and you know how to configure them if the need arises.