@@ -25,6 +25,7 @@ import kalix.protocol.component.{ Failure, MetadataEntry }
2525import org .slf4j .{ Logger , LoggerFactory , MDC }
2626
2727import java .util .Optional
28+ import scala .concurrent .ExecutionContext
2829import scala .concurrent .Future
2930import scala .jdk .CollectionConverters .SeqHasAsJava
3031import scala .jdk .OptionConverters ._
@@ -101,9 +102,10 @@ private[javasdk] final class ActionsImpl(_system: ActorSystem, services: Map[Str
101102
102103 import ActionsImpl ._
103104 import _system .dispatcher
104- implicit val system : ActorSystem = _system
105+ private implicit val system : ActorSystem = _system
106+ private val sdkEc : ExecutionContext = SdkExecutionContext (system)
105107 private val telemetry = Telemetry (system)
106- lazy val telemetries : Map [String , Instrumentation ] = services.values.map { s =>
108+ private lazy val telemetries : Map [String , Instrumentation ] = services.values.map { s =>
107109 (s.serviceName, telemetry.traceInstrumentation(s.serviceName, ActionCategory ))
108110 }.toMap
109111
@@ -176,24 +178,29 @@ private[javasdk] final class ActionsImpl(_system: ActorSystem, services: Map[Str
176178 services.get(in.serviceName) match {
177179 case Some (service) =>
178180 val span = telemetries(service.serviceName).buildSpan(service, in)
179- span.foreach(s => MDC .put( Telemetry . TRACE_ID , s.getSpanContext.getTraceId))
181+
180182 val fut =
181- try {
182- val context = createContext(in, service.messageCodec, span.map(_.getSpanContext), service.serviceName)
183- val decodedPayload = service.messageCodec.decodeMessage(
184- in.payload.getOrElse(throw new IllegalArgumentException (" No command payload" )))
185- val effect = service.factory
186- .create(context)
187- .handleUnary(in.name, MessageEnvelope .of(decodedPayload, context.metadata()), context)
188- effectToResponse(service, in, effect, service.messageCodec)
189- } catch {
190- case NonFatal (ex) =>
191- // command handler threw an "unexpected" error
192- span.foreach(_.end())
193- Future .successful(handleUnexpectedException(service, in, ex))
194- } finally {
195- MDC .remove(Telemetry .TRACE_ID )
196- }
183+ // Note: invocation in future to guarantee create and invocation is running on sdk dispatcher with virtual thread support
184+ Future {
185+ try {
186+ span.foreach(s => MDC .put(Telemetry .TRACE_ID , s.getSpanContext.getTraceId))
187+ val context = createContext(in, service.messageCodec, span.map(_.getSpanContext), service.serviceName)
188+ val decodedPayload = service.messageCodec.decodeMessage(
189+ in.payload.getOrElse(throw new IllegalArgumentException (" No command payload" )))
190+ val effect = service.factory
191+ .create(context)
192+ .handleUnary(in.name, MessageEnvelope .of(decodedPayload, context.metadata()), context)
193+ effectToResponse(service, in, effect, service.messageCodec)
194+ } catch {
195+ case NonFatal (ex) =>
196+ // command handler threw an "unexpected" error
197+ span.foreach(_.end())
198+ Future .successful(handleUnexpectedException(service, in, ex))
199+ } finally {
200+ MDC .remove(Telemetry .TRACE_ID )
201+ }
202+ }(sdkEc).flatten
203+
197204 fut.andThen { case _ =>
198205 span.foreach(_.end())
199206 }
@@ -246,7 +253,7 @@ private[javasdk] final class ActionsImpl(_system: ActorSystem, services: Map[Str
246253 Future .successful(
247254 ActionResponse (ActionResponse .Response .Failure (Failure (0 , " Unknown service: " + call.serviceName))))
248255 }
249- }
256+ }(sdkEc)
250257
251258 /**
252259 * Handle a streamed out command. The input command will contain the service name, command name, request metadata and
@@ -258,25 +265,32 @@ private[javasdk] final class ActionsImpl(_system: ActorSystem, services: Map[Str
258265 override def handleStreamedOut (in : ActionCommand ): Source [ActionResponse , NotUsed ] =
259266 services.get(in.serviceName) match {
260267 case Some (service) =>
261- try {
262- val context = createContext(in, service.messageCodec, None , service.serviceName)
263- val decodedPayload = service.messageCodec.decodeMessage(
264- in.payload.getOrElse(throw new IllegalArgumentException (" No command payload" )))
265- service.factory
266- .create(context)
267- .handleStreamedOut(in.name, MessageEnvelope .of(decodedPayload, context.metadata()), context)
268- .asScala
269- .mapAsync(1 )(effect => effectToResponse(service, in, effect, service.messageCodec))
270- .recover { case NonFatal (ex) =>
271- // user stream failed with an "unexpected" error
272- handleUnexpectedException(service, in, ex)
268+ // Note: invocation in future to guarantee create and invocation is running on sdk dispatcher with virtual thread support
269+ Source
270+ .futureSource(Future {
271+ try {
272+ val context = createContext(in, service.messageCodec, None , service.serviceName)
273+ val decodedPayload = service.messageCodec.decodeMessage(
274+ in.payload.getOrElse(throw new IllegalArgumentException (" No command payload" )))
275+ service.factory
276+ .create(context)
277+ .handleStreamedOut(in.name, MessageEnvelope .of(decodedPayload, context.metadata()), context)
278+ .asScala
279+ .mapAsync(1 )(effect => effectToResponse(service, in, effect, service.messageCodec))
280+ .recover { case NonFatal (ex) =>
281+ // user stream failed with an "unexpected" error
282+ handleUnexpectedException(service, in, ex)
283+ }
284+ // run the stream itself on the virtual thread dispatcher in case the user blocks in stream
285+ .addAttributes(SdkExecutionContext .streamDispatcher)
286+ } catch {
287+ case NonFatal (ex) =>
288+ // command handler threw an "unexpected" error
289+ Source .single(handleUnexpectedException(service, in, ex))
273290 }
274- .async
275- } catch {
276- case NonFatal (ex) =>
277- // command handler threw an "unexpected" error
278- Source .single(handleUnexpectedException(service, in, ex))
279- }
291+ }(sdkEc))
292+ .mapMaterializedValue(_ => NotUsed )
293+
280294 case None =>
281295 Source .single(ActionResponse (ActionResponse .Response .Failure (Failure (0 , " Unknown service: " + in.serviceName))))
282296 }
@@ -303,25 +317,32 @@ private[javasdk] final class ActionsImpl(_system: ActorSystem, services: Map[Str
303317 case (Seq (call), messages) =>
304318 services.get(call.serviceName) match {
305319 case Some (service) =>
320+ // Note: invocation in future to guarantee create and invocation is running on sdk dispatcher with virtual thread support
306321 try {
307- val context = createContext(call, service.messageCodec, None , service.serviceName)
308- service.factory
309- .create(context)
310- .handleStreamed(
311- call.name,
312- messages.map { message =>
313- val metadata = MetadataImpl .of(message.metadata.map(_.entries.toVector).getOrElse(Nil ))
314- val decodedPayload = service.messageCodec.decodeMessage(
315- message.payload.getOrElse(throw new IllegalArgumentException (" No command payload" )))
316- MessageEnvelope .of(decodedPayload, metadata)
317- }.asJava,
318- context)
319- .asScala
320- .mapAsync(1 )(effect => effectToResponse(service, call, effect, service.messageCodec))
321- .recover { case NonFatal (ex) =>
322- // user stream failed with an "unexpected" error
323- handleUnexpectedException(service, call, ex)
324- }
322+ Source
323+ .futureSource(Future {
324+ val context = createContext(call, service.messageCodec, None , service.serviceName)
325+ service.factory
326+ .create(context)
327+ .handleStreamed(
328+ call.name,
329+ messages.map { message =>
330+ val metadata = MetadataImpl .of(message.metadata.map(_.entries.toVector).getOrElse(Nil ))
331+ val decodedPayload = service.messageCodec.decodeMessage(
332+ message.payload.getOrElse(throw new IllegalArgumentException (" No command payload" )))
333+ MessageEnvelope .of(decodedPayload, metadata)
334+ }.asJava,
335+ context)
336+ .asScala
337+ .mapAsync(1 )(effect => effectToResponse(service, call, effect, service.messageCodec))
338+ .recover { case NonFatal (ex) =>
339+ // user stream failed with an "unexpected" error
340+ handleUnexpectedException(service, call, ex)
341+ }
342+ // run the stream itself on the virtual thread dispatcher in case the user blocks in stream
343+ .addAttributes(SdkExecutionContext .streamDispatcher)
344+ }(sdkEc))
345+ .mapMaterializedValue(_ => NotUsed )
325346 } catch {
326347 case NonFatal (ex) =>
327348 // command handler threw an "unexpected" error
0 commit comments