github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/actor/actor_context_test.go (about)

     1  package actor
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  )
    11  
    12  func FuzzSpawnNamed(f *testing.F) {
    13  	f.Add("parent", "child")
    14  
    15  	f.Fuzz(func(t *testing.T, parentName string, childName string) {
    16  		combined := parentName + "/" + childName
    17  
    18  		pid, _ := spawnMockProcess(parentName)
    19  
    20  		defer removeMockProcess(pid)
    21  
    22  		props := &Props{
    23  			spawner: func(actorSystem *ActorSystem, id string, _ *Props, _ SpawnerContext) (*PID, error) {
    24  				assert.Equal(t, combined, id)
    25  
    26  				return NewPID(actorSystem.Address(), id), nil
    27  			},
    28  		}
    29  
    30  		parent := &actorContext{self: NewPID(localAddress, parentName), props: props, actorSystem: system}
    31  		child, err := parent.SpawnNamed(props, childName)
    32  		assert.NoError(t, err)
    33  		assert.Equal(t, parent.Children()[0], child)
    34  	})
    35  }
    36  
    37  // TestActorContext_Stop verifies if context is stopping and receives a Watch message, it should
    38  // immediately respond with a Terminated message.
    39  func TestActorContext_Stop(t *testing.T) {
    40  	t.Parallel()
    41  
    42  	pid, p := spawnMockProcess("foo")
    43  	defer removeMockProcess(pid)
    44  
    45  	other, o := spawnMockProcess("watcher")
    46  	defer removeMockProcess(other)
    47  
    48  	o.On("SendSystemMessage", other, &Terminated{Who: pid})
    49  
    50  	props := PropsFromProducer(nullProducer, WithSupervisor(DefaultSupervisorStrategy()))
    51  	lc := newActorContext(system, props, nil)
    52  	lc.self = pid
    53  	lc.InvokeSystemMessage(&Stop{})
    54  	lc.InvokeSystemMessage(&Watch{Watcher: other})
    55  
    56  	p.AssertExpectations(t)
    57  	o.AssertExpectations(t)
    58  }
    59  
    60  func TestActorContext_SendMessage_WithSenderMiddleware(t *testing.T) {
    61  	t.Parallel()
    62  
    63  	var wg sync.WaitGroup
    64  
    65  	wg.Add(1)
    66  
    67  	// Define a local context with no-op sender middleware
    68  	mw := func(next SenderFunc) SenderFunc {
    69  		return func(ctx SenderContext, target *PID, envelope *MessageEnvelope) {
    70  			next(ctx, target, envelope)
    71  		}
    72  	}
    73  
    74  	props := PropsFromProducer(nullProducer, WithSupervisor(DefaultSupervisorStrategy()), WithSenderMiddleware(mw))
    75  	ctx := newActorContext(system, props, nil)
    76  
    77  	// Define a receiver to which the local context will send a message
    78  	var counter int
    79  
    80  	receiver := rootContext.Spawn(PropsFromFunc(func(ctx Context) {
    81  		if _, ok := ctx.Message().(bool); ok {
    82  			counter++
    83  			wg.Done()
    84  		}
    85  	}))
    86  
    87  	// Send a message with Tell
    88  	// Then wait a little to allow the receiver to process the message
    89  	// TODO: There should be a better way to wait.
    90  	timeout := 3 * time.Millisecond
    91  
    92  	ctx.Send(receiver, true)
    93  	wg.Wait()
    94  	assert.Equal(t, 1, counter)
    95  
    96  	// Send a message with Request
    97  	counter = 0 // Reset the counter
    98  
    99  	wg.Add(1)
   100  	ctx.Request(receiver, true)
   101  	wg.Wait()
   102  	assert.Equal(t, 1, counter)
   103  
   104  	// Send a message with RequestFuture
   105  	counter = 0 // Reset the counter
   106  
   107  	wg.Add(1)
   108  
   109  	_ = ctx.RequestFuture(receiver, true, timeout).Wait()
   110  	wg.Wait()
   111  	assert.Equal(t, 1, counter)
   112  }
   113  
   114  func BenchmarkActorContext_ProcessMessageNoMiddleware(b *testing.B) {
   115  	var m interface{} = 1
   116  
   117  	ctx := newActorContext(system, PropsFromFunc(nullReceive), nil)
   118  	for i := 0; i < b.N; i++ {
   119  		ctx.processMessage(m)
   120  	}
   121  }
   122  
   123  func TestActorContext_Respond(t *testing.T) {
   124  	t.Parallel()
   125  
   126  	var wg sync.WaitGroup
   127  
   128  	wg.Add(1)
   129  
   130  	// Defined a responder actor
   131  	// It simply echoes a received string.
   132  	responder := rootContext.Spawn(PropsFromFunc(func(ctx Context) {
   133  		if m, ok := ctx.Message().(string); ok {
   134  			ctx.Respond(fmt.Sprintf("Got a string: %v", m))
   135  		}
   136  	}))
   137  
   138  	// Be prepared to catch a response that the responder will send to nil
   139  	var gotResponseToNil bool
   140  
   141  	deadLetterSubscriber := system.EventStream.Subscribe(func(msg interface{}) {
   142  		if deadLetter, ok := msg.(*DeadLetterEvent); ok {
   143  			if deadLetter.PID == nil {
   144  				gotResponseToNil = true
   145  				wg.Done()
   146  			}
   147  		}
   148  	})
   149  
   150  	// Send a message to the responder using Request
   151  	// The responder should send something back.
   152  	timeout := 3 * time.Millisecond
   153  	res, err := rootContext.RequestFuture(responder, "hello", timeout).Result()
   154  	assert.Nil(t, err)
   155  	assert.NotNil(t, res)
   156  
   157  	resStr, ok := res.(string)
   158  	assert.True(t, ok)
   159  	assert.Equal(t, "Got a string: hello", resStr)
   160  
   161  	// Ensure that the responder did not send anything to nil
   162  	assert.False(t, gotResponseToNil)
   163  
   164  	// Send a message using Tell
   165  	rootContext.Send(responder, "hello")
   166  
   167  	// Ensure that the responder actually send something to nil
   168  	wg.Wait()
   169  	assert.True(t, gotResponseToNil)
   170  
   171  	// Cleanup
   172  	system.EventStream.Unsubscribe(deadLetterSubscriber)
   173  }
   174  
   175  func TestActorContext_Forward(t *testing.T) {
   176  	t.Parallel()
   177  	// Defined a response actor
   178  	// It simply responds to the string message
   179  	responder := rootContext.Spawn(PropsFromFunc(func(ctx Context) {
   180  		if m, ok := ctx.Message().(string); ok {
   181  			ctx.Respond(fmt.Sprintf("Got a string: %v", m))
   182  		}
   183  	}))
   184  
   185  	// Defined a forwarder actor
   186  	// It simply forward the string message to responder
   187  	forwarder := rootContext.Spawn(PropsFromFunc(func(ctx Context) {
   188  		if _, ok := ctx.Message().(string); ok {
   189  			ctx.Forward(responder)
   190  		}
   191  	}))
   192  
   193  	// Send a message to the responder using Request
   194  	// The responder should send something back.
   195  	timeout := 3 * time.Millisecond
   196  	res, err := rootContext.RequestFuture(forwarder, "hello", timeout).Result()
   197  	assert.Nil(t, err)
   198  	assert.NotNil(t, res)
   199  
   200  	resStr, ok := res.(string)
   201  	assert.True(t, ok)
   202  	assert.Equal(t, "Got a string: hello", resStr)
   203  }
   204  
   205  func BenchmarkActorContext_ProcessMessageWithMiddleware(b *testing.B) {
   206  	var m interface{} = 1
   207  
   208  	fn := func(next ReceiverFunc) ReceiverFunc {
   209  		return func(ctx ReceiverContext, env *MessageEnvelope) {
   210  			next(ctx, env)
   211  		}
   212  	}
   213  
   214  	props := PropsFromProducer(nullProducer, WithSupervisor(DefaultSupervisorStrategy()), WithReceiverMiddleware(fn))
   215  	ctx := newActorContext(system, props, nil)
   216  
   217  	for i := 0; i < b.N; i++ {
   218  		ctx.processMessage(m)
   219  	}
   220  }
   221  
   222  func benchmarkactorcontextSpawnwithmiddlewaren(n int, b *testing.B) {
   223  	middlewareFn := func(next SenderFunc) SenderFunc {
   224  		return func(ctx SenderContext, pid *PID, env *MessageEnvelope) {
   225  			next(ctx, pid, env)
   226  		}
   227  	}
   228  
   229  	props := PropsFromProducer(nullProducer)
   230  	for i := 0; i < n; i++ {
   231  		props = props.Configure(WithSenderMiddleware(middlewareFn))
   232  	}
   233  
   234  	system := NewActorSystem()
   235  	parent := &actorContext{self: NewPID(localAddress, "foo"), props: props, actorSystem: system}
   236  
   237  	for i := 0; i < b.N; i++ {
   238  		parent.Spawn(props)
   239  	}
   240  }
   241  
   242  func BenchmarkActorContext_SpawnWithMiddleware0(b *testing.B) {
   243  	benchmarkactorcontextSpawnwithmiddlewaren(0, b)
   244  }
   245  
   246  func BenchmarkActorContext_SpawnWithMiddleware1(b *testing.B) {
   247  	benchmarkactorcontextSpawnwithmiddlewaren(1, b)
   248  }
   249  
   250  func BenchmarkActorContext_SpawnWithMiddleware2(b *testing.B) {
   251  	benchmarkactorcontextSpawnwithmiddlewaren(2, b)
   252  }
   253  
   254  func BenchmarkActorContext_SpawnWithMiddleware5(b *testing.B) {
   255  	benchmarkactorcontextSpawnwithmiddlewaren(5, b)
   256  }
   257  
   258  func TestActorContinueFutureInActor(t *testing.T) {
   259  	t.Parallel()
   260  
   261  	pid := rootContext.Spawn(PropsFromFunc(func(ctx Context) {
   262  		if ctx.Message() == "request" {
   263  			ctx.Respond("done")
   264  		}
   265  		if ctx.Message() == "start" {
   266  			f := ctx.RequestFuture(ctx.Self(), "request", 5*time.Second)
   267  			ctx.ReenterAfter(f, func(res interface{}, err error) {
   268  				ctx.Respond(res)
   269  			})
   270  		}
   271  	}))
   272  	res, err := rootContext.RequestFuture(pid, "start", time.Second).Result()
   273  	assert.NoError(t, err)
   274  	assert.Equal(t, "done", res)
   275  }
   276  
   277  type dummyAutoRespond struct{}
   278  
   279  func (*dummyAutoRespond) GetAutoResponse(_ Context) interface{} {
   280  	return &dummyResponse{}
   281  }
   282  
   283  func TestActorContextAutoRespondMessage(t *testing.T) {
   284  	t.Parallel()
   285  
   286  	pid := rootContext.Spawn(PropsFromFunc(func(ctx Context) {}))
   287  
   288  	var msg AutoRespond = &dummyAutoRespond{}
   289  
   290  	res, err := rootContext.RequestFuture(pid, msg, 1*time.Second).Result()
   291  	assert.NoError(t, err)
   292  	assert.IsType(t, &dummyResponse{}, res)
   293  }
   294  
   295  func TestActorContextAutoRespondTouchedMessage(t *testing.T) {
   296  	t.Parallel()
   297  
   298  	pid := rootContext.Spawn(PropsFromFunc(func(ctx Context) {}))
   299  
   300  	var msg AutoRespond = &Touch{}
   301  
   302  	res, err := rootContext.RequestFuture(pid, msg, 1*time.Second).Result()
   303  
   304  	res2, _ := res.(*Touched)
   305  
   306  	assert.NoError(t, err)
   307  	assert.IsType(t, &Touched{}, res)
   308  	assert.True(t, res2.Who.Equal(pid))
   309  }