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  }