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

CommandHandlerWithReplyBuilder "state" is null error even if using forNullState #32101

Open
aklikic opened this issue Sep 14, 2023 · 3 comments
Labels
0 - new Ticket is unclear on it's purpose or if it is valid or not t:java Related to the Java APIs t:persistence

Comments

@aklikic
Copy link

aklikic commented Sep 14, 2023

Reproducable:

public class TestEntity extends EventSourcedBehaviorWithEnforcedReplies<TestEntity.Command, TestEntity.Event, TestEntity.State> {

    public interface Command extends CborSerializable {}
    record CreateCommand(ActorRef<Response> replyTo) implements Command {}
    record StartCommand(ActorRef<Response> replyTo) implements Command {}
    record Response() implements CborSerializable {}

    public interface Event extends CborSerializable{}
    record CreatedEvent(Instant timestamp) implements Event {}
    record StartedEvent(Instant timestamp) implements Event {}

    public enum CurrentStatus {
        Created,
        Started
    }
    record State(CurrentStatus status) implements CborSerializable{
        public static State onCreated(CreatedEvent event){
            return new State(CurrentStatus.Created);
        }
        public State onStarted(StartedEvent event){
            return new State(CurrentStatus.Started);
        }
    }

    public TestEntity(String entityId) {
        super(persistenceId(entityId));
    }
    public static final PersistenceId persistenceId(String entityId) {
        return PersistenceId.of(ENTITY_TYPE_KEY.name(), entityId);
    }


    public static final EntityTypeKey<TestEntity.Command> ENTITY_TYPE_KEY =
            EntityTypeKey.create(TestEntity.Command.class, "TestEntity");
    public static Behavior<TestEntity.Command> create(String entityId) {
        return Behaviors.setup(context -> new TestEntity(entityId));
    }

    public static void initSharding(ActorSystem<?> system) {
        ClusterSharding.get(system).init(
                Entity.of(
                        ENTITY_TYPE_KEY,
                        entityContext -> TestEntity.create(entityContext.getEntityId())));
    }

    @Override
    public TestEntity.State emptyState() {
        return null;
    }

    @Override
    public CommandHandlerWithReply<TestEntity.Command, TestEntity.Event, TestEntity.State> commandHandler() {
        CommandHandlerWithReplyBuilder<TestEntity.Command, TestEntity.Event, TestEntity.State> b = newCommandHandlerWithReplyBuilder();
        b.forNullState().onCommand(TestEntity.CreateCommand.class, this::onCreate);
        b.forState(state -> state.status() == TestEntity.CurrentStatus.Created)
                .onCommand(TestEntity.StartCommand.class,this::onStart);
        b.forAnyState()
                .onAnyCommand(() -> Effect().unhandled().thenNoReply());
        return b.build();
    }

    private ReplyEffect<TestEntity.Event, TestEntity.State> onCreate(TestEntity.State state, TestEntity.CreateCommand cmd){
        return Effect().persist(new TestEntity.CreatedEvent(Instant.now()))
                .thenReply(cmd.replyTo,updatedState -> new TestEntity.Response());
    }

    private ReplyEffect<TestEntity.Event, TestEntity.State> onStart(TestEntity.State state, TestEntity.StartCommand cmd){
        return Effect().persist(new TestEntity.StartedEvent(Instant.now()))
                .thenReply(cmd.replyTo,updatedState -> new TestEntity.Response());
    }

    public EventHandler<TestEntity.State, TestEntity.Event> eventHandler() {
        EventHandlerBuilder<TestEntity.State, TestEntity.Event> b = newEventHandlerBuilder();
        b.forNullState()
                .onEvent(TestEntity.CreatedEvent.class, TestEntity.State::onCreated);
        b.forAnyState()
                .onEvent(TestEntity.StartedEvent.class, TestEntity.State::onStarted);
        return b.build();
    }
}
public class TestEntityTest {

    @Test
    public void happyPath(){
        String entityId = "1";
        UnpersistentBehavior<TestEntity.Command,TestEntity.Event,TestEntity.State> test =
                UnpersistentBehavior.fromEventSourced(TestEntity.create(entityId));

        BehaviorTestKit<TestEntity.Command> testkit = test.getBehaviorTestKit();
        ReplyInbox<TestEntity.Response> replyInbox = testkit.runAsk(replyTo -> new TestEntity.StartCommand(replyTo));

        replyInbox = testkit.runAsk(replyTo -> new TestEntity.StartCommand(replyTo));
        replyInbox.expectReply(new TestEntity.Response());
        test.getEventProbe().expectPersistedClass(TestEntity.StartedEvent.class);
        assertFalse(test.getSnapshotProbe().hasEffects());

    }
}

Error:

java.lang.NullPointerException: Cannot invoke "TestEntity$State.status()" because "state" is null
	at TestEntity.lambda$commandHandler$0(TestEntity.java:74)
@aklikic
Copy link
Author

aklikic commented Sep 14, 2023

Akka version 2.8.4
Java 17

@johanandren
Copy link
Member

Thanks, I think there are two things we need to investigate/discuss. Not 100% sure :

  1. Should null ever be fed to the forState predicate?
  2. Is null one of the possible forAnyState's or should it mean any non-null state?

I think a possible workaround with current behavior would be to end the forNullState set of handlers with an onAnyCommand to catch all other commands and return Effect().unhandled() to keep the commands from falling through to the other two state handlers.

@johanandren johanandren added t:persistence 0 - new Ticket is unclear on it's purpose or if it is valid or not t:java Related to the Java APIs labels Sep 14, 2023
@patriknw
Copy link
Member

I would say that it works as designed. null has been chosen to be a valid state and can therefore be passed to the predicate. If there is no matching command handler in the forNullState() it would continue with next forState. It would be the same if you add several forState and the first didn't have a matching command handler, then it would continue with next forState.

Johan's suggestion of capturing all with onAnyCommand is good.

That said, documentation should clarify this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
0 - new Ticket is unclear on it's purpose or if it is valid or not t:java Related to the Java APIs t:persistence
Projects
None yet
Development

No branches or pull requests

3 participants