Skip to content
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

Add ability to create unpersistent versions of persistent behaviors #31464

Merged
merged 20 commits into from
Oct 18, 2022

Conversation

leviramsey
Copy link
Contributor

Provides an alternative interpreter for EventSourcedBehavior and DurableStateBehavior that does not actually involve any persistence: persistence operations are exposed via a side-channel from the actor.

The major benefit of this approach is that the lower-overhead fully-synchronous BehaviorTestKit can now be used to test many such behaviors (conditional, of course, on the behavior in question not performing operations which are unsupported in the BehaviorTestKit: #30050 removes ActorContext.ask from that set of operations). This lower overhead results in somewhat faster tests (see https://gist.github.com/leviramsey/741e64fc9de472e7782aad3ddf40804c) and makes things like property-based testing of persistent behaviors practical.

Other minor benefits of this approach might include:

  • same testing style for both durable-state and event-sourcing
  • better ergonomics for behaviors which delay replies until some other message is received (stash, later performing an ask) than the EventSourcedBehaviorTestKit
  • middle ground for testing mostly domain logic along with some of how the domain logic affects the world outside (compare with an approach that, e.g. has domain command handlers returning a sequence of events and some reification of a program with side effects (e.g. a free monad))

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good as an alternative synchronous testkit similar to BehaviorTestKit.

"Unpersistent" sounds a bit funny to me.

@leviramsey
Copy link
Contributor Author

Yeah, I bounced between unpersistent and nonpersistent... NotPersistent could also be an option.

}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inadvertently did an applyCodeStyle while in Scala-3 mode...

@leviramsey
Copy link
Contributor Author

One bit of functionality that's not implemented yet is the lastSequenceNumber. The easiest way to do that could be to have the unpersistent and persistent versions extend a HasSequenceNumber trait (to avoid having EventSourcedBehavior.lastSequenceNumber have to discriminate between possible implementations), which would have binary compatibility implications?

@johanandren
Copy link
Member

Running.WithSeqNrAccessible already is a trait, and is completely internal so should be fine to use in more places, couldn't that work out?

@leviramsey
Copy link
Contributor Author

Okay, hadn't noticed that @johanandren

@lightbend-cla-validator

Hi @leviramsey,

Thank you for your contribution! We really value the time you've taken to put this together.

We see that you have signed the Lightbend Contributors License Agreement before, however, the CLA has changed since you last signed it.
Please review the new CLA and sign it before we proceed with reviewing this pull request:

https://www.lightbend.com/contribute/cla

@leviramsey
Copy link
Contributor Author

rebasing on BSL...

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. I guess I'm slowly getting used to the naming Unpersistent.

object UnpersistentBehavior {

type BehaviorAndChanges[Command, Event, State] =
(Behavior[Command], ConcurrentLinkedQueue[ChangePersisted[State, Event]])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be more clear to define this as a case class instead of a tuple

*/
def fromEventSourced[Command, Event, State](
behavior: Behavior[Command],
fromStateAndOffset: Option[(State, Long)] = None): (Behavior[Command], PersistenceProbe[Event], PersistenceProbe[State]) = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The javadsl exposes a wrapper class instead of a Tuple3, though in the Java API, that exposes a "guaranteed-to-fail" probe for durable state. Could get around that with 4 substantially-identical result types ("Java or Scala" x "Event-sourced or Durable-state"), but that strikes me as a "cure might be worse than the disease" situation

@leviramsey
Copy link
Contributor Author

leviramsey commented Sep 21, 2022

Have not yet adjusted any of the tests or docs for the new PersistenceProbe API, btw.

@patriknw
Copy link
Member

patriknw commented Oct 7, 2022

@leviramsey Where are we here? Ready for final review?

@leviramsey leviramsey marked this pull request as ready for review October 7, 2022 14:02
@leviramsey
Copy link
Contributor Author

Basically just docs (including adding a testing section for durable state in general, covering some of the other approaches). The code stuff should be OK to review now.

(or should we delay docs for another PR?)

Copy link
Member

@johanandren johanandren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another round of review, mostly looking at the API, would be good with a small Java test to see the ergonomics using that API as well (even if I think they'll be pretty 1:1)

val recoveryDone = TestInbox[Done]()
val behavior = BehaviorUnderTest("test-1", recoveryDone.ref)

val (unpersistent, probe) = UnpersistentBehavior.fromDurableState[Command, State](behavior)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it is just testkit and not production API but returning tuples is not great DX, let's make a result type for Scala as well instead.

Copy link
Contributor Author

@leviramsey leviramsey Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So with a result type patterned on the Java API, you end up with code like

val unpersistent = UnpersistentBehavior.fromDurableState[Command, State](behavior)
val probe = unpersistent.stateProbe
val testkit = unpersistent.behaviorTestKit

or with an extractor, something like the tuple return

val UnpersistentBehavior.DurableState(testkit, probe) =
  UnpersistentBehavior.fromDurableState[Command, State](behavior)

OTOH, in the Scala API, maybe it makes the most sense to have the result type take a (BehaviorTestKit[Command], PersistenceProbe[Event], PersistenceProbe[State]) => Unit (or otherwise for durable state) in addition to the accessors?

So it would be like

UnpersistentBehavior.fromDurableState[Command, State](behavior) { (testkit, probe) =>
  // body of test
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does make the Java API rather different from the Scala API, but I don't get the sense that that sort of resource-like API is at all idiomatic in Java.

Copy link
Member

@johanandren johanandren Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A case class so that it is possible to use the unapply if you like, or the object with named fields if you rater like that sounds good to me (better than 3-tuple).

Edit: ignore, comment on previous state of PR, not sure why GitHub didn't show me the latest one.

@patriknw patriknw added the 2 - pick next Used to mark issues which are next up in the queue to be worked on. The tag is non-binding label Oct 11, 2022
Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking very good. "Only" reference docs remaining.
And a few nitpicks...

@leviramsey
Copy link
Contributor Author

For the doc structure, I'm thinking to make typed/persistence-testing.md be like typed/testing.md with links to the different options (as well as mentioning explicitly that in many cases the event-handling logic can be tested as just a function):

  • typed/persistence-testing-unpersistent.md
  • typed/persistence-testing-eventsourcedbehaviortestkit.md
  • typed/persistence-testing-persistencetestkit.md

And also adding a couple of durable state testing pages:

  • typed/persistence-testing-durablestate.md
  • typed/persistence-testing-durablestate-unpersistent.md
  • typed/persistence-testing-durablestate-async.md

@johanandren
Copy link
Member

Sounds good, but I think it would also be ok to scope the doc effort down and add a section to the existing testing page for typed persistence. Up to you what you have time to complete, we can always add more docs later if needed.

@leviramsey
Copy link
Contributor Author

For now, just adding the section to typed (event-sourced) persistence.

Documenting the testing of durable state behaviors in general is a gap in the docs (AFAIK, the main way before this to test them is to combine the PersistenceTestKitPlugin with PersistenceTestKitDurableStateStorePlugin and configure the ActorTestKit appropriately). Documenting the previous method and this can be a separate issue I guess.

If #31673 is merged before this, I will probably adjust the doc snippets to use that (or vice versa, do it in that PR).

Copy link
Member

@johanandren johanandren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some tiny things then this looks good to go

val recoveryDone = TestInbox[Done]()
val behavior = BehaviorUnderTest("test-1", recoveryDone.ref)

val (unpersistent, probe) = UnpersistentBehavior.fromDurableState[Command, State](behavior)
Copy link
Member

@johanandren johanandren Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A case class so that it is possible to use the unapply if you like, or the object with named fields if you rater like that sounds good to me (better than 3-tuple).

Edit: ignore, comment on previous state of PR, not sure why GitHub didn't show me the latest one.

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Member

@johanandren johanandren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (I don't think my comments are blocking merge on second thought)

@johanandren johanandren merged commit e7448f8 into akka:main Oct 18, 2022
@johanandren johanandren added this to the 2.7.0 milestone Oct 18, 2022
@leviramsey leviramsey deleted the unpersistent branch October 18, 2022 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 - pick next Used to mark issues which are next up in the queue to be worked on. The tag is non-binding
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants