This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new b1ff0db36c feat: support default attributes on materializers (#3647)
b1ff0db36c is described below
commit b1ff0db36c44f37f4e5c5b02bf3e4742090dac55
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Oct 23 11:02:33 2026 +0100
feat: support default attributes on materializers (#3647)
* feat: support default attributes on materializers
Motivation:
There is no way to create a materializer that applies a set of default
attributes (for example a dispatcher, buffer sizes or a supervision
strategy) to every stream it materializes; the attributes have to be put
on each stream individually, or changed globally via configuration.
Modification:
- Add `Materializer(contextProvider, defaultAttributes)` /
`Materializer.createMaterializer(contextProvider, defaultAttributes)`
for actor-context bound materializers.
- Add `Materializer(systemProvider, defaultAttributes)` /
`Materializer.createMaterializer(systemProvider, defaultAttributes)`
for system level materializers, backed by a new internal
`SystemMaterializer.createAdditionalSystemMaterializer(Attributes)`.
- Add a new internal `MaterializerGuardian.StartMaterializerWithAttributes`
message; the existing `StartMaterializer` case object is unchanged.
- The given attributes are combined with the configured materializer
settings attributes, so attributes set on a stream still take precedence.
- Fix the Scaladoc of the Java `createMaterializer(systemProvider)` and
mention the new option in the stream basics docs.
Result:
Users can create materializers whose default attributes apply to all
streams they materialize. All changes are additive, no MiMa excludes.
Tests:
- Not run locally - left to CI; scalafmt applied,
stream-tests/Test/headerCreateAll run for the new spec
References:
None - new materializer factory overloads
* test: run MaterializerWithAttributesSpec actor on the stream test
dispatcher
Motivation:
The actor context materializer test created its actor on the default
dispatcher. In stream tests the default mailbox rejects actors from the
org.apache.pekko.stream package on the default dispatcher, so the actor
failed to start and the test timed out.
Modification:
Create the test actor with `withDispatcher("pekko.test.stream-dispatcher")`,
as other stream tests do.
Result:
The actor starts and the test can check the materializer attributes.
Tests:
- Not run locally - left to CI; scalafmt applied
References:
Refs #3647
* Retain Lightbend source notices for adapted materializer code
Motivation:
Review of #3647 noted that MaterializerWithAttributesSpec and the new
Materializer overloads are adapted from Akka 2.9.0 sources that carry 2023
Lightbend copyright notices, and that a docs line had trailing whitespace.
Modification:
- MaterializerWithAttributesSpec keeps its source notice
(Copyright (C) 2023 Lightbend Inc.), with the Apache header added by
headerCreate.
- Update the Materializer.scala notice from 2015-2022 to the source notice
2015-2023.
- Remove the trailing space in stream-flows-and-basics.md.
Result:
Source notices of the adapted code are preserved, and git diff --check
passes.
Tests:
sbt "stream-tests/Test/headerCreate" "stream/headerCheckAll"
"stream-tests/headerCheckAll"
sbt "stream-tests/testOnly
org.apache.pekko.stream.MaterializerWithAttributesSpec" (3 passed)
git diff --check
References:
https://github.com/apache/pekko/pull/3647
---
.../main/paradox/stream/stream-flows-and-basics.md | 9 +-
.../stream/MaterializerWithAttributesSpec.scala | 115 +++++++++++++++++++++
.../org/apache/pekko/stream/Materializer.scala | 62 ++++++++++-
.../apache/pekko/stream/SystemMaterializer.scala | 10 ++
.../pekko/stream/impl/MaterializerGuardian.scala | 7 ++
5 files changed, 198 insertions(+), 5 deletions(-)
diff --git a/docs/src/main/paradox/stream/stream-flows-and-basics.md
b/docs/src/main/paradox/stream/stream-flows-and-basics.md
index 2bb05d8994..71c25c0ef5 100644
--- a/docs/src/main/paradox/stream/stream-flows-and-basics.md
+++ b/docs/src/main/paradox/stream/stream-flows-and-basics.md
@@ -271,8 +271,11 @@ well-known sinks, such as @scala[`runForeach(el =>
...)`]@java[`runForeach(el ->
Materialization is performed synchronously on the materializing thread by an
@apidoc[actor.ActorSystem] global @apidoc[stream.Materializer].
The actual stream processing is handled by actors started up during the
streams materialization,
-which will be running on the thread pools they have been configured to run on
- which defaults to the dispatcher set in
-the `ActorSystem` config or provided as attributes on the stream that is
getting materialized.
+which will be running on the thread pools they have been configured to run on.
By default, the thread pool used is the
+dispatcher set in the `ActorSystem` config, but it is possible to select other
thread pools by providing @apidoc[stream.Attributes] to
+
+ * the stream that is getting materialized
+ * a custom instance of @apidoc[stream.Materializer] created with
`defaultAttributes`
@@@ note
@@ -384,7 +387,7 @@ and emitting the "materialized value". An
@apidoc[actor.ActorSystem] wide `Mater
@apidoc[SystemMaterializer] by @scala[having an implicit `ActorSystem` in
scope]@java[passing the `ActorSystem` to the
various `run` methods] this way there is no need to worry about the
`Materializer` unless there are special requirements.
-The use case that may require a custom instance of `Materializer` is when all
streams materialized in an actor should be tied to the Actor lifecycle and stop
if the Actor stops or crashes.
+One use case that may require a custom instance of `Materializer` is when all
streams materialized in an actor should be tied to the Actor lifecycle and stop
if the Actor stops or crashes.
An important aspect of working with streams and actors is understanding a
`Materializer`'s life-cycle.
The materializer is bound to the lifecycle of the
@apidoc[actor.ActorRefFactory] it is created from, which in practice will
diff --git
a/stream-tests/src/test/scala/org/apache/pekko/stream/MaterializerWithAttributesSpec.scala
b/stream-tests/src/test/scala/org/apache/pekko/stream/MaterializerWithAttributesSpec.scala
new file mode 100644
index 0000000000..f33729dc95
--- /dev/null
+++
b/stream-tests/src/test/scala/org/apache/pekko/stream/MaterializerWithAttributesSpec.scala
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.stream
+
+import scala.concurrent.duration._
+
+import org.apache.pekko
+import pekko.actor.Actor
+import pekko.actor.Props
+import pekko.stream.scaladsl.Sink
+import pekko.stream.scaladsl.Source
+import pekko.stream.stage.GraphStageLogic
+import pekko.stream.stage.GraphStageWithMaterializedValue
+import pekko.stream.stage.OutHandler
+import pekko.testkit.ImplicitSender
+import pekko.testkit.PekkoSpec
+
+object MaterializerWithAttributesSpec {
+
+ /** Completes immediately and materializes the attributes it was
materialized with */
+ object AttributesSource extends
GraphStageWithMaterializedValue[SourceShape[Nothing], Attributes] {
+ val out: Outlet[Nothing] = Outlet[Nothing]("AttributesSource.out")
+ override val shape: SourceShape[Nothing] = SourceShape(out)
+
+ override def createLogicAndMaterializedValue(inheritedAttributes:
Attributes): (GraphStageLogic, Attributes) = {
+ val logic = new GraphStageLogic(shape) with OutHandler {
+ override def preStart(): Unit = completeStage()
+ override def onPull(): Unit = completeStage()
+
+ setHandler(out, this)
+ }
+ (logic, inheritedAttributes)
+ }
+ }
+
+ class StreamActor extends Actor {
+ override def receive: Receive = {
+ case "do it!" =>
+ val materializerWithAttributes: Materializer = Materializer(context,
Attributes.name("bar"))
+
+ val firstAttributes =
+
Source.fromGraph(AttributesSource).to(Sink.ignore).run()(materializerWithAttributes)
+ val secondAttributes =
+
Source.fromGraph(AttributesSource).to(Sink.ignore).run()(Materializer(context))
+
+ sender() ! firstAttributes
+ sender() ! secondAttributes
+
+ context.stop(self)
+ }
+ }
+}
+
+class MaterializerWithAttributesSpec extends PekkoSpec with ImplicitSender {
+ import MaterializerWithAttributesSpec._
+
+ private def names(attributes: Attributes): List[String] =
+ attributes.attributeList.collect { case Attributes.Name(n) => n }
+
+ "A system level materializer with default attributes" should {
+ "apply its attributes to streams it materializes" in {
+ val materializerWithAttributes = Materializer(system,
Attributes.name("foo"))
+
+ val attributes =
Source.fromGraph(AttributesSource).to(Sink.ignore).run()(materializerWithAttributes)
+ names(attributes) should contain("foo")
+
+ val otherAttributes =
Source.fromGraph(AttributesSource).to(Sink.ignore).run()(Materializer(system))
+ names(otherAttributes) shouldNot contain("foo")
+
+ materializerWithAttributes.shutdown()
+ }
+
+ "let attributes on the stream override the materializer default
attributes" in {
+ val materializerWithAttributes = Materializer(system,
Attributes.inputBuffer(1, 1))
+
+ val attributes =
Source.fromGraph(AttributesSource).to(Sink.ignore).run()(materializerWithAttributes)
+ attributes.get[Attributes.InputBuffer] should
===(Some(Attributes.InputBuffer(1, 1)))
+
+ val overridden = Source
+ .fromGraph(AttributesSource)
+ .withAttributes(Attributes.inputBuffer(2, 2))
+ .to(Sink.ignore)
+ .run()(materializerWithAttributes)
+ overridden.get[Attributes.InputBuffer] should
===(Some(Attributes.InputBuffer(2, 2)))
+
+ materializerWithAttributes.shutdown()
+ }
+ }
+
+ "An actor context materializer with default attributes" should {
+ "apply its attributes to streams it materializes" in {
+ val streamActor = system.actorOf(Props(new
StreamActor).withDispatcher("pekko.test.stream-dispatcher"))
+
+ streamActor ! "do it!"
+
+ val responses = receiveN(2, 3.seconds)
+ responses.filterNot(_.isInstanceOf[Attributes]) shouldBe empty
+
+ val allAttributes = responses.map(_.asInstanceOf[Attributes])
+ names(allAttributes.head) should contain("bar")
+ names(allAttributes.last) shouldNot contain("bar")
+ }
+ }
+}
diff --git a/stream/src/main/scala/org/apache/pekko/stream/Materializer.scala
b/stream/src/main/scala/org/apache/pekko/stream/Materializer.scala
index dd5c56b92b..d9e29de278 100644
--- a/stream/src/main/scala/org/apache/pekko/stream/Materializer.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/Materializer.scala
@@ -8,7 +8,7 @@
*/
/*
- * Copyright (C) 2015-2022 Lightbend Inc. <https://www.lightbend.com>
+ * Copyright (C) 2015-2023 Lightbend Inc. <https://www.lightbend.com>
*/
package org.apache.pekko.stream
@@ -27,6 +27,7 @@ import pekko.actor.Props
import pekko.annotation.DoNotInherit
import pekko.annotation.InternalApi
import pekko.event.LoggingAdapter
+import pekko.stream.impl.PhasedFusingActorMaterializer
/**
* The Materializer is the component responsible for turning a stream
blueprint into a running stream.
@@ -212,6 +213,37 @@ object Materializer {
*/
def createMaterializer(contextProvider: ClassicActorContextProvider):
Materializer = apply(contextProvider)
+ /**
+ * Scala API: Create a materializer whose lifecycle will be tied to the one
of the passed actor context.
+ * When the actor stops the materializer will stop and all streams created
with it will be failed with an [[AbruptTerminationExeption]]
+ *
+ * The given `defaultAttributes` are applied to all streams materialized
with this materializer, and can be
+ * overridden by attributes set on the streams themselves.
+ *
+ * You can pass either a classic actor context or a typed actor context.
+ *
+ * @since 2.0.0
+ */
+ def apply(contextProvider: ClassicActorContextProvider, defaultAttributes:
Attributes): Materializer = {
+ val context = contextProvider.classicActorContext
+ val settings = SystemMaterializer(context.system).materializerSettings
+ PhasedFusingActorMaterializer(context, "flow", settings,
settings.toAttributes.and(defaultAttributes))
+ }
+
+ /**
+ * Java API: Create a materializer whose lifecycle will be tied to the one
of the passed actor context.
+ * When the actor stops the materializer will stop and all streams created
with it will be failed with an [[AbruptTerminationExeption]]
+ *
+ * The given `defaultAttributes` are applied to all streams materialized
with this materializer, and can be
+ * overridden by attributes set on the streams themselves.
+ *
+ * You can pass either a classic actor context or a typed actor context.
+ *
+ * @since 2.0.0
+ */
+ def createMaterializer(contextProvider: ClassicActorContextProvider,
defaultAttributes: Attributes): Materializer =
+ apply(contextProvider, defaultAttributes)
+
/**
* Scala API: Create a new materializer that will stay alive as long as the
system does or until it is explicitly stopped.
*
@@ -224,7 +256,7 @@ object Materializer {
SystemMaterializer(systemProvider.classicSystem).createAdditionalSystemMaterializer()
/**
- * Scala API: Create a new materializer that will stay alive as long as the
system does or until it is explicitly stopped.
+ * Java API: Create a new materializer that will stay alive as long as the
system does or until it is explicitly stopped.
*
* *Note* prefer using the default [[SystemMaterializer]] by passing the
`ActorSystem` to the various `run`
* methods on the streams. Only create new system level materializers if you
have specific
@@ -234,6 +266,32 @@ object Materializer {
def createMaterializer(systemProvider: ClassicActorSystemProvider):
Materializer =
apply(systemProvider)
+ /**
+ * Scala API: Create a new materializer that will stay alive as long as the
system does or until it is explicitly stopped.
+ *
+ * The given `defaultAttributes` are applied to all streams materialized
with this materializer, and can be
+ * overridden by attributes set on the streams themselves.
+ *
+ * It is generally advised to limit the number of system level materializers
created.
+ *
+ * @since 2.0.0
+ */
+ def apply(systemProvider: ClassicActorSystemProvider, defaultAttributes:
Attributes): Materializer =
+
SystemMaterializer(systemProvider.classicSystem).createAdditionalSystemMaterializer(defaultAttributes)
+
+ /**
+ * Java API: Create a new materializer that will stay alive as long as the
system does or until it is explicitly stopped.
+ *
+ * The given `defaultAttributes` are applied to all streams materialized
with this materializer, and can be
+ * overridden by attributes set on the streams themselves.
+ *
+ * It is generally advised to limit the number of system level materializers
created.
+ *
+ * @since 2.0.0
+ */
+ def createMaterializer(systemProvider: ClassicActorSystemProvider,
defaultAttributes: Attributes): Materializer =
+ apply(systemProvider, defaultAttributes)
+
}
/**
diff --git
a/stream/src/main/scala/org/apache/pekko/stream/SystemMaterializer.scala
b/stream/src/main/scala/org/apache/pekko/stream/SystemMaterializer.scala
index b388de38bb..412a470101 100644
--- a/stream/src/main/scala/org/apache/pekko/stream/SystemMaterializer.scala
+++ b/stream/src/main/scala/org/apache/pekko/stream/SystemMaterializer.scala
@@ -80,6 +80,16 @@ final class SystemMaterializer(system: ExtendedActorSystem)
extends Extension {
Await.result(started, materializerTimeout.duration).materializer
}
+ /**
+ * INTERNAL API
+ */
+ @InternalApi
+ private[pekko] def createAdditionalSystemMaterializer(defaultAttributes:
Attributes): Materializer = {
+ val started = (materializerGuardian ?
MaterializerGuardian.StartMaterializerWithAttributes(defaultAttributes))
+ .mapTo[MaterializerGuardian.MaterializerStarted]
+ Await.result(started, materializerTimeout.duration).materializer
+ }
+
/**
* INTERNAL API
*/
diff --git
a/stream/src/main/scala/org/apache/pekko/stream/impl/MaterializerGuardian.scala
b/stream/src/main/scala/org/apache/pekko/stream/impl/MaterializerGuardian.scala
index c200bbf651..bb76d2f105 100644
---
a/stream/src/main/scala/org/apache/pekko/stream/impl/MaterializerGuardian.scala
+++
b/stream/src/main/scala/org/apache/pekko/stream/impl/MaterializerGuardian.scala
@@ -21,6 +21,7 @@ import pekko.actor.Actor
import pekko.actor.Props
import pekko.annotation.InternalApi
import pekko.stream.ActorMaterializerSettings
+import pekko.stream.Attributes
import pekko.stream.Materializer
/**
@@ -33,6 +34,7 @@ import pekko.stream.Materializer
private[pekko] object MaterializerGuardian {
case object StartMaterializer
+ final case class StartMaterializerWithAttributes(attributes: Attributes)
final case class MaterializerStarted(materializer: Materializer)
// this is available to keep backwards compatibility with ActorMaterializer
and should
@@ -63,6 +65,11 @@ private[pekko] final class MaterializerGuardian(
override def receive: Receive = {
case StartMaterializer =>
sender() ! MaterializerStarted(startMaterializer(defaultNamePrefix,
None))
+ case StartMaterializerWithAttributes(attributes) =>
+ val materializer =
+ PhasedFusingActorMaterializer(context, defaultNamePrefix,
materializerSettings,
+ defaultAttributes.and(attributes))
+ sender() ! MaterializerStarted(materializer)
case LegacyStartMaterializer(namePrefix, settings) =>
sender() ! MaterializerStarted(startMaterializer(namePrefix,
Some(settings)))
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]