-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement streaming with state changes #270
Draft
nightscape
wants to merge
12
commits into
zio:master
Choose a base branch
from
nightscape:real_streaming
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 5 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e430361
Implement streaming with state changes
nightscape 43ad58a
fixup! Implement streaming with state changes
nightscape 7c73a46
fixup! Implement streaming with state changes
nightscape 97ed0eb
fixup! Implement streaming with state changes
nightscape 232a216
fixup! Implement streaming with state changes
nightscape 3029945
fixup! Implement streaming with state changes
nightscape daf432d
fixup! Implement streaming with state changes
nightscape b175ea5
fixup! Implement streaming with state changes
nightscape 8d02345
fixup! Implement streaming with state changes
nightscape ffad0a1
fixup! Implement streaming with state changes
nightscape 4ed3f6b
fixup! Implement streaming with state changes
nightscape e16bc8c
fixup! Implement streaming with state changes
nightscape File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,11 +2,59 @@ package zio.actors | |
|
||
import zio.actors.Actor.PendingMessage | ||
import zio.clock.Clock | ||
import zio.stream.ZStream | ||
import zio.{ Supervisor => _, _ } | ||
|
||
import scala.language.implicitConversions | ||
|
||
object Actor { | ||
|
||
private[actors] type PendingMessage[F[_], A] = (F[A], Promise[Throwable, A]) | ||
trait ActorResponse[R, S, +A] { | ||
type ResponseType | ||
|
||
def materialize[AA >: A]( | ||
state: Ref[S], | ||
promise: Promise[Throwable, AA], | ||
supervisor: Supervisor[R] | ||
): URIO[R with Clock, Unit] | ||
} | ||
|
||
object ActorResponse { | ||
implicit def oneTime[R, S, A](response: RIO[R, (S, A)]): ActorResponse[R, S, A] = | ||
new ActorResponse[R, S, A] { | ||
override type ResponseType = RIO[R, (S, A)] | ||
|
||
override def materialize[AA >: A]( | ||
state: Ref[S], | ||
promise: Promise[Throwable, AA], | ||
supervisor: Supervisor[R] | ||
): URIO[R with Clock, Unit] = { | ||
val completer = ((s: S, a: A) => (state.set(s) *> promise.succeed(a)).ignore).tupled | ||
response.foldM( | ||
e => | ||
supervisor | ||
.supervise[R, (S, A)](response, e) | ||
.foldM(_ => promise.fail(e).ignore, completer), | ||
completer | ||
) | ||
} | ||
} | ||
implicit def stream[R, S, E, I](response: ZStream[R, E, (S, I)]): ActorResponse[R, S, ZStream[R, E, I]] = | ||
new ActorResponse[R, S, ZStream[R, E, I]] { | ||
override type ResponseType = ZStream[R, E, (S, I)] | ||
|
||
override def materialize[AA >: ZStream[R, E, I]]( | ||
state: Ref[S], | ||
promise: Promise[Throwable, AA], | ||
supervisor: Supervisor[R] | ||
): URIO[R with Clock, Unit] = { | ||
// TODO: Supervision | ||
nightscape marked this conversation as resolved.
Show resolved
Hide resolved
|
||
val outputStream = response.mapM { case (s, a) => state.set(s) *> UIO(a) } | ||
promise.succeed(outputStream).ignore | ||
} | ||
Comment on lines
+47
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
} | ||
|
||
/** | ||
* Description of actor behavior (can act as FSM) | ||
|
@@ -26,7 +74,9 @@ object Actor { | |
* @tparam A - domain of return entities | ||
* @return effectful result | ||
*/ | ||
def receive[A](state: S, msg: F[A], context: Context): RIO[R, (S, A)] | ||
def receive[A](state: S, msg: F[A], context: Context): RIO[R, (S, A)] = ??? | ||
def receiveS[A](state: S, msg: F[A], context: Context): ActorResponse[R, S, A] = | ||
ActorResponse.oneTime(receive(state, msg, context)) | ||
|
||
/* INTERNAL API */ | ||
|
||
|
@@ -41,24 +91,17 @@ object Actor { | |
for { | ||
s <- state.get | ||
(fa, promise) = msg | ||
receiver = receive(s, fa, context) | ||
completer = ((s: S, a: A) => state.set(s) *> promise.succeed(a)).tupled | ||
_ <- receiver.foldM( | ||
e => | ||
supervisor | ||
.supervise(receiver, e) | ||
.foldM(_ => promise.fail(e), completer), | ||
completer | ||
) | ||
receiver = receiveS(s, fa, context) | ||
_ <- receiver.materialize(state, promise, supervisor) | ||
} yield () | ||
|
||
for { | ||
state <- Ref.make(initial) | ||
queue <- Queue.bounded[PendingMessage[F, _]](mailboxSize) | ||
_ <- (for { | ||
t <- queue.take | ||
_ <- process(t, state) | ||
} yield ()).forever.fork | ||
t <- queue.take | ||
_ <- process(t, state) | ||
} yield ()).forever.fork | ||
} yield new Actor[F](queue)(optOutActorSystem) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,10 @@ package zio.actors | |
|
||
import java.util.concurrent.atomic.AtomicBoolean | ||
|
||
import zio.actors.Actor.Stateful | ||
import zio.stream.Stream | ||
import zio.actors.Supervisor | ||
import zio.actors.Actor.ActorResponse._ | ||
import zio.actors.Actor.{ ActorResponse, Stateful } | ||
import zio.stream.{ Stream, ZStream } | ||
import zio.{ Chunk, IO, Ref, Schedule, Task, UIO } | ||
import zio.test._ | ||
import zio.test.Assertion._ | ||
|
@@ -13,7 +15,7 @@ object CounterUtils { | |
case object Reset extends Message[Unit] | ||
case object Increase extends Message[Unit] | ||
case object Get extends Message[Int] | ||
case class IncreaseUpTo(upper: Int) extends Message[Stream[Nothing, Int]] | ||
case class IncreaseUpTo(upper: Int) extends Message[ZStream[Any, Nothing, Int]] | ||
} | ||
|
||
object TickUtils { | ||
|
@@ -33,16 +35,19 @@ object ActorsSpec extends DefaultRunnableSpec { | |
import CounterUtils._ | ||
|
||
val handler: Stateful[Any, Int, Message] = new Stateful[Any, Int, Message] { | ||
override def receive[A]( | ||
override def receiveS[A]( | ||
state: Int, | ||
msg: Message[A], | ||
context: Context | ||
): UIO[(Int, A)] = | ||
): ActorResponse[Any, Int, A] = | ||
msg match { | ||
case Reset => UIO((0, ())) | ||
case Increase => UIO((state + 1, ())) | ||
case Get => UIO((state, state)) | ||
case IncreaseUpTo(upper) => UIO((upper, Stream.fromIterable(state until upper))) | ||
case Reset => oneTime(UIO((0, ()))) | ||
case Increase => oneTime(UIO((state + 1, ()))) | ||
case Get => oneTime(UIO((state, state))) | ||
case IncreaseUpTo(upper) => | ||
stream[Any, Int, Nothing, Int]( | ||
Stream.fromIterable((state to upper).map(i => (i, i))).asInstanceOf[Stream[Nothing, (Int, Int)]] | ||
) // | ||
} | ||
} | ||
|
||
|
@@ -55,11 +60,11 @@ object ActorsSpec extends DefaultRunnableSpec { | |
_ <- actor ! Reset | ||
c2 <- actor ? Get | ||
c3 <- actor ? IncreaseUpTo(20) | ||
vals <- c3.runCollect | ||
vals <- c3.mapM(i => (actor ? Get).map(i2 => (i, i2))).runCollect | ||
c4 <- actor ? Get | ||
} yield assert(c1)(equalTo(2)) && | ||
assert(c2)(equalTo(0)) && | ||
assert(vals)(equalTo(Chunk.apply(0 until 20: _*))) && | ||
assert(vals)(equalTo(Chunk.apply((0 to 20).map(i => (i, i)): _*))) && | ||
assert(c4)(equalTo(20)) | ||
}, | ||
testM("Error recovery by retrying") { | ||
|
@@ -141,10 +146,10 @@ object ActorsSpec extends DefaultRunnableSpec { | |
} | ||
for { | ||
system <- ActorSystem("test1") | ||
actor <- system.make("actor1", Supervisor.none, (), handler) | ||
_ <- actor ! Letter | ||
_ <- actor ? Letter | ||
dump <- actor.stop | ||
actor <- system.make("actor1", Supervisor.none, (), handler) | ||
_ <- actor ! Letter | ||
_ <- actor ? Letter | ||
dump <- actor.stop | ||
} yield assert(dump)( | ||
isSubtype[List[_]](anything) && | ||
hasField[List[_], Int]("size", _.size, equalTo(0)) | ||
|
@@ -164,7 +169,7 @@ object ActorsSpec extends DefaultRunnableSpec { | |
} | ||
} | ||
for { | ||
system <- ActorSystem("test5") | ||
system <- ActorSystem("test5") | ||
_ <- system.make("actor1-1", Supervisor.none, (), handler) | ||
actor <- system.select[Message]("zio://[email protected]:0000/actor1-1") | ||
_ <- actor ! Tick | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that
ResponseType
here should beZStream[R, Throwable, (S, I)]
.As actors are location transparent and interaction can involve remote communication our interaction patterns
!
and?
result inTask[...]
(so a ZIO that can fail withThrowable
as it might involvewrite/readFromWire
) although in actor's behavior there could be no failures that can occur.Notice that here https://github.com/zio/zio-actors/pull/270/files#diff-c1a93d1001aa018540bc59b83dd7a09dR134 stream elements are created from
Task[Any]
so resulting stream that is returned will beStream[Any, Throwable, Any]
.That is - a stream returned in our
RemoteSpec
will performreadFromWire
to get elements (until receivesStreamEnd
) so every time it might fail with communication error but here it test the signature isZStream[Any, Nothing, String]
which seems that pulling elements out of stream can't fail.I propose to remove
E
fromimplicit def stream[R, S, E, I]
and fix returnedZStream
signature toZStream[R, Throwable, I]
.This might seem unclear but if we notice where communication occurs it should be clear - when we do
?
and the response itoneTime
the call is synchronous and we wait for the response.When the response is a stream on the caller site we only create a recipe with
?
-> the communication occurs when we do.runCollect
and this is a counterpart of getting response out of?
withoneTime
.Please correct me if I got this wrong.
[EDIT]
Problem
I think that support for streaming responses will require overhaul for remoting (due to the fact that we decide when we pull responses out of stream, so perform remote communication) as it breaks current design.
I introduced simple change to our test case:
And it fails - first we do the first ask -> remote actor system writes to the wire - we read it.
Then we ask for a stream response and get the stream - the remote actor system writes all responses to the wire but we do not read them (!) but only define recipe for reading them. Then we ask for another oneTime
result
but we read a bit of those stream responses instead and it fails. This should definitely be covered in a test case like this (intertwine oneTime and stream interactions)Solution?
Off the top of my head - we can perform
runCollect
insendEnvelope
but it misses the point of #268 what you described as your goal.You could just make it fully akka-like and pass
ActorRef
in a message you send as a part of your protocol (so you know where send messages back) - and perform all communication with!
(tell) and drop stream responses.Otherwise it would require rewriting internals (which are really basic now) to support it that way.
WDYT?
Sorry for writing about this issue now - only now I had some time to think about it deeper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding
ZStream
being a pull-based stream, but the responses being pushed over the wire:Would it help to introduce a
Queue
at the receiver side instead of doing arunCollect
?That way the remote actor can push messages at any time, but from the client side we pull from the
Queue
.I had also thought about the Akka way of passing an
ActorRef
and sending responses via!
, but the resulting API would be quite a bit less elegant...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now I think the issue on response producer site is:
If we immediately write stream elements to receiver like this then they need to be read right away (e.g. with runCollect), otherwise remoting can be easily broken (but it makes no sense as we are blocked until receive the whole stream).
I don't know a solution for this right now (maybe another abstraction layer for remote communication for atomic and stream responses where stream response elements will be stored on response producer site and only send over the wire when requested (but it would introduce other issues I think), or (also with that additional abstraction layer) response producer will send it like this but on the other side we need to manage reading
?
responses and those out-of-order stream responses, or maybe something different)Yeah, including sender ActorRef in protocol would make it less elegant but making stream responses that comply with current remoting feels to require major design change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to have a daemon on the receiver side that permanently tries to
readFromWire
and pushes into aQueue
from which bothZStream
s and answers from?
can be read?StreamMsg
would then need some kind of "conversation identifier" so that when multiple streams are running at the same time their messages can be distinguished.If we can come up with a working solution, would you see the overall goal as worthwhile or do you want to have a simple, no-bells-and-whistles library?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, sounds great but I lack in knowledge and experience to elaborate on such solution (what are the pros and cons). Do you know a project where such solution is used?
As this is a design discussion also asking @mijicd @softinio. What do you think about it?