github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/persistence/plugin_test.go (about) 1 package persistence 2 3 import ( 4 "fmt" 5 "sync" 6 "testing" 7 8 "google.golang.org/protobuf/proto" 9 10 "github.com/asynkron/protoactor-go/actor" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 ) 14 15 /* 16 Use some common types from persistence example to setup 17 test cases 18 */ 19 20 const ActorName = "demo.actor" 21 22 var system = actor.NewActorSystem() 23 24 type dataStore struct { 25 providerState ProviderState 26 } 27 28 // initData sets up a data store 29 // it adds one event to set state for every sting passed in 30 // set the last snapshot to given index of those events 31 func initData(snapshotInterval, lastSnapshot int, states ...string) *dataStore { 32 // add all events 33 state := NewInMemoryProvider(snapshotInterval) 34 for i, s := range states { 35 state.PersistEvent(ActorName, i, newMessage(s)) 36 } 37 // mark one as a snapshot 38 if lastSnapshot < len(states) { 39 snapshot := states[lastSnapshot] 40 state.PersistSnapshot( 41 ActorName, lastSnapshot, newSnapshot(snapshot), 42 ) 43 } 44 return &dataStore{providerState: state} 45 } 46 47 func (p *dataStore) GetState() ProviderState { 48 return p.providerState 49 } 50 51 type protoMsg struct { 52 proto.Message 53 state string 54 } 55 56 func (p *protoMsg) Reset() {} 57 func (p *protoMsg) String() string { return p.state } 58 func (p *protoMsg) ProtoMessage() {} 59 60 type ( 61 Message struct{ protoMsg } 62 Snapshot struct{ protoMsg } 63 Query struct{ protoMsg } 64 ) 65 66 func newMessage(state string) *Message { 67 return &Message{protoMsg: protoMsg{state: state}} 68 } 69 70 func newSnapshot(state string) *Snapshot { 71 return &Snapshot{protoMsg: protoMsg{state: state}} 72 } 73 74 type myActor struct { 75 Mixin 76 state string 77 } 78 79 var _ actor.Actor = (*myActor)(nil) 80 81 func makeActor() actor.Actor { 82 return &myActor{} 83 } 84 85 var ( 86 queryWg sync.WaitGroup 87 queryState string 88 ) 89 90 func (a *myActor) Receive(ctx actor.Context) { 91 switch msg := ctx.Message().(type) { 92 case *RequestSnapshot: 93 // PersistSnapshot when requested 94 a.PersistSnapshot(newSnapshot(a.state)) 95 case *Snapshot: 96 // Restore from Snapshot 97 a.state = msg.state 98 case *Message: 99 // Persist all events received outside of recovery 100 if !a.Recovering() { 101 a.PersistReceive(msg) 102 } 103 // Set state to whatever message says 104 a.state = msg.state 105 case *Query: 106 // TODO: this is poorly writen... 107 // I have no idea how to synchronously block on the 108 // receipt of a message for test cases. 109 queryState = a.state 110 queryWg.Done() 111 } 112 } 113 114 /****** test code *******/ 115 116 func TestRecovery(t *testing.T) { 117 cases := []struct { 118 init *dataStore 119 msgs []string 120 afterMsgs string 121 }{ 122 // replay with no state 123 0: {initData(5, 0), nil, ""}, 124 125 // replay directly on snapshot, no more messages 126 1: {initData(8, 2, "a", "b", "c"), nil, "c"}, 127 128 // replay with snapshot and events, add another event 129 2: {initData(8, 1, "a", "b", "c"), []string{"d"}, "d"}, 130 131 // replay state and add an event, which triggers snapshot 132 3: {initData(4, 1, "a", "b", "c"), []string{"d"}, "d"}, 133 134 // replay state and add an event, which triggers snapshot, 135 // and then another one 136 4: {initData(4, 1, "a", "b", "c"), []string{"d", "e"}, "e"}, 137 } 138 139 for i, tc := range cases { 140 t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { 141 rootContext := system.Root 142 props := actor.PropsFromProducer(makeActor, 143 actor.WithReceiverMiddleware(Using(tc.init))) 144 pid, err := rootContext.SpawnNamed(props, ActorName) 145 require.NoError(t, err) 146 147 // send a bunch of messages 148 for _, msg := range tc.msgs { 149 rootContext.Send(pid, newMessage(msg)) 150 } 151 152 // ugly way to block on a response.... 153 // TODO: I need some help here 154 queryWg.Add(1) 155 rootContext.Send(pid, &Query{}) 156 queryWg.Wait() 157 // check the state after all these messages 158 assert.Equal(t, tc.afterMsgs, queryState) 159 160 // wait for shutdown 161 _ = rootContext.PoisonFuture(pid).Wait() 162 163 pid, err = rootContext.SpawnNamed(props, ActorName) 164 require.NoError(t, err) 165 166 // ugly way to block on a response.... 167 // TODO: I need some help here 168 queryWg.Add(1) 169 rootContext.Send(pid, &Query{}) 170 queryWg.Wait() 171 // check the state after all these messages 172 assert.Equal(t, tc.afterMsgs, queryState) 173 174 // shutdown at end of test for cleanup 175 _ = rootContext.PoisonFuture(pid).Wait() 176 }) 177 } 178 }