Skip to content

Commit

Permalink
feat: add durable state actor
Browse files Browse the repository at this point in the history
  • Loading branch information
Tochemey committed Dec 16, 2024
1 parent ed4da0f commit 7de6e19
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 26 deletions.
2 changes: 2 additions & 0 deletions durable_state_actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ func (entity *durableStateActor) Receive(ctx *actors.ReceiveContext) {
switch command := ctx.Message().(type) {
case *goaktpb.PostStart:
entity.actorSystem = ctx.ActorSystem()
case *egopb.GetStateCommand:
entity.sendStateReply(ctx)
default:
entity.processCommand(ctx, command)
}
Expand Down
152 changes: 152 additions & 0 deletions durable_state_actor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ import (
"github.com/tochemey/ego/v3/egopb"
"github.com/tochemey/ego/v3/eventstream"
"github.com/tochemey/ego/v3/internal/lib"
"github.com/tochemey/ego/v3/internal/postgres"
"github.com/tochemey/ego/v3/plugins/statestore/memory"
pgstore "github.com/tochemey/ego/v3/plugins/statestore/postgres"
testpb "github.com/tochemey/ego/v3/test/data/pb/v3"
)

Expand Down Expand Up @@ -229,4 +231,154 @@ func TestDurableStateBehavior(t *testing.T) {
lib.Pause(time.Second)
eventStream.Close()
})
t.Run("with state recovery from state store", func(t *testing.T) {
ctx := context.TODO()
actorSystem, err := actors.NewActorSystem("TestActorSystem",
actors.WithPassivationDisabled(),
actors.WithLogger(log.DiscardLogger),
actors.WithActorInitMaxRetries(3),
)
require.NoError(t, err)
assert.NotNil(t, actorSystem)

// start the actor system
err = actorSystem.Start(ctx)
require.NoError(t, err)

lib.Pause(time.Second)

var (
testDatabase = "testdb"
testUser = "testUser"
testDatabasePassword = "testPass"
)

testContainer := postgres.NewTestContainer(testDatabase, testUser, testDatabasePassword)
db := testContainer.GetTestDB()
require.NoError(t, db.Connect(ctx))
schemaUtils := pgstore.NewSchemaUtils(db)
require.NoError(t, schemaUtils.CreateTable(ctx))

config := &pgstore.Config{
DBHost: testContainer.Host(),
DBPort: testContainer.Port(),
DBName: testDatabase,
DBUser: testUser,
DBPassword: testDatabasePassword,
DBSchema: testContainer.Schema(),
}
durableStore := pgstore.NewStateStore(config)
require.NoError(t, durableStore.Connect(ctx))

lib.Pause(time.Second)

persistenceID := uuid.NewString()
behavior := NewAccountDurableStateBehavior(persistenceID)

err = durableStore.Connect(ctx)
require.NoError(t, err)

lib.Pause(time.Second)

eventStream := eventstream.New()

persistentActor := newDurableStateActor(behavior, durableStore, eventStream)
pid, err := actorSystem.Spawn(ctx, behavior.ID(), persistentActor)
require.NoError(t, err)
require.NotNil(t, pid)

lib.Pause(time.Second)

var command proto.Message

command = &testpb.CreateAccount{AccountBalance: 500.00}

reply, err := actors.Ask(ctx, pid, command, time.Second)
require.NoError(t, err)
require.NotNil(t, reply)
require.IsType(t, new(egopb.CommandReply), reply)

commandReply := reply.(*egopb.CommandReply)
require.IsType(t, new(egopb.CommandReply_StateReply), commandReply.GetReply())

state := commandReply.GetReply().(*egopb.CommandReply_StateReply)
assert.EqualValues(t, 1, state.StateReply.GetSequenceNumber())

// marshal the resulting state
resultingState := new(testpb.Account)
err = state.StateReply.GetState().UnmarshalTo(resultingState)
require.NoError(t, err)

expected := &testpb.Account{
AccountId: persistenceID,
AccountBalance: 500.00,
}
assert.True(t, proto.Equal(expected, resultingState))

// send another command to credit the balance
command = &testpb.CreditAccount{
AccountId: persistenceID,
Balance: 250,
}
reply, err = actors.Ask(ctx, pid, command, time.Second)
require.NoError(t, err)
require.NotNil(t, reply)
require.IsType(t, new(egopb.CommandReply), reply)

commandReply = reply.(*egopb.CommandReply)
require.IsType(t, new(egopb.CommandReply_StateReply), commandReply.GetReply())

state = commandReply.GetReply().(*egopb.CommandReply_StateReply)
assert.EqualValues(t, 2, state.StateReply.GetSequenceNumber())

// marshal the resulting state
resultingState = new(testpb.Account)
err = state.StateReply.GetState().UnmarshalTo(resultingState)
require.NoError(t, err)

expected = &testpb.Account{
AccountId: persistenceID,
AccountBalance: 750.00,
}

assert.True(t, proto.Equal(expected, resultingState))
// wait a while
lib.Pause(time.Second)

// restart the actor
pid, err = actorSystem.ReSpawn(ctx, behavior.ID())
require.NoError(t, err)

lib.Pause(time.Second)

// fetch the current state
command = &egopb.GetStateCommand{}
reply, err = actors.Ask(ctx, pid, command, time.Second)
require.NoError(t, err)
require.NotNil(t, reply)
require.IsType(t, new(egopb.CommandReply), reply)

commandReply = reply.(*egopb.CommandReply)
require.IsType(t, new(egopb.CommandReply_StateReply), commandReply.GetReply())

resultingState = new(testpb.Account)
err = state.StateReply.GetState().UnmarshalTo(resultingState)
require.NoError(t, err)
expected = &testpb.Account{
AccountId: persistenceID,
AccountBalance: 750.00,
}
assert.True(t, proto.Equal(expected, resultingState))

err = actorSystem.Stop(ctx)
assert.NoError(t, err)

lib.Pause(time.Second)

// free resources
assert.NoError(t, schemaUtils.DropTable(ctx))
assert.NoError(t, durableStore.Disconnect(ctx))
testContainer.Cleanup()
eventStream.Close()
})
}
2 changes: 1 addition & 1 deletion engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func (engine *Engine) DurableStateEntity(ctx context.Context, behavior DurableSt

// SendCommand sends command to a given entity ref.
// This will return:
// 1. the resulting state after the command has been handled and the emitted event persisted
// 1. the resulting state after the command has been handled and the emitted event/durable state persisted
// 2. nil when there is no resulting state or no event persisted
// 3. an error in case of error
func (engine *Engine) SendCommand(ctx context.Context, entityID string, cmd Command, timeout time.Duration) (resultingState State, revision uint64, err error) {
Expand Down
Loading

0 comments on commit 7de6e19

Please sign in to comment.