Repository: Tochemey/goakt
Pull request #120 (2023-09-19 07:32:30)

feat: add forward method to context

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Tochemey/goakt/pull/120?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) Report
Patch coverage: **`100.00%`** and project coverage change: **`+0.07%`** :tada:
> Comparison is base [(`be1ea33`)](https://app.codecov.io/gh/Tochemey/goakt/commit/be1ea33dd20eb9816062e23cbf8af98b35d9ae74?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) 89.73% compared to head [(`c875d4c`)](https://app.codecov.io/gh/Tochemey/goakt/pull/120?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) 89.80%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #120      +/-   ##
==========================================
+ Coverage   89.73%   89.80%   +0.07%     
==========================================
  Files          38       38              
  Lines        3302     3326      +24     
==========================================
+ Hits         2963     2987      +24     
  Misses        264      264              
  Partials       75       75              
```


| [Files Changed](https://app.codecov.io/gh/Tochemey/goakt/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) | Coverage Δ | |
|---|---|---|
| [actors/context.go](https://app.codecov.io/gh/Tochemey/goakt/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene#diff-YWN0b3JzL2NvbnRleHQuZ28=) | `96.44% <100.00%> (+0.58%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Tochemey/goakt/pull/120?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene).


Patch:
diff --git a/actors/context.go b/actors/context.go
index f26b80a..7c849ef 100644
--- a/actors/context.go
+++ b/actors/context.go
@@ -57,6 +57,11 @@ type ReceiveContext interface {
 	// Ask has a timeout which can cause the sender to panic. When ask times out, the receiving actor does not know and may still process the message.
 	// It is recommended to set a good timeout to quickly receive response and try to avoid false positives
 	Ask(to PID, message proto.Message) (response proto.Message)
+	// Forward method works similarly to the Tell() method except that the sender of a forwarded message is kept as the original sender.
+	// As a result, the actor receiving the forwarded messages knows who the actual sender of the message is.
+	// The message that is forwarded is the current message received by the received context.
+	// This operation does nothing when the receiving actor is not running
+	Forward(to PID)
 	// RemoteTell sends a message to an actor remotely without expecting any reply
 	RemoteTell(to *addresspb.Address, message proto.Message)
 	// RemoteAsk is used to send a message to an actor remotely and expect a response
@@ -332,3 +337,33 @@ func (c *receiveContext) Stop(child PID) {
 		panic(err)
 	}
 }
+
+// Forward method works similarly to the Tell() method except that the sender of a forwarded message is kept as the original sender.
+// As a result, the actor receiving the forwarded messages knows who the actual sender of the message is.
+// The message that is forwarded is the current message received by the received context.
+// This operation does nothing when the receiving actor is not running
+func (c *receiveContext) Forward(to PID) {
+	// grab the actual message
+	message := c.Message()
+	// grab the sender of the message
+	sender := c.Sender()
+	// forward the actual message
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	// only forward when the receiving actor is running
+	if to.IsRunning() {
+		// create a new context from the parent context
+		ctx := context.WithoutCancel(c.ctx)
+		// create a receiveContext
+		receiveContext := &receiveContext{
+			ctx:            ctx,
+			message:        message,
+			sender:         sender,
+			recipient:      to,
+			mu:             sync.Mutex{},
+			isAsyncMessage: false,
+		}
+		// forward the actual message
+		to.doReceive(receiveContext)
+	}
+}


Test patch:
diff --git a/actors/actor_test.go b/actors/actor_test.go
index 5a0d78a..712d4be 100644
--- a/actors/actor_test.go
+++ b/actors/actor_test.go
@@ -299,3 +299,24 @@ func (x *RestartBreaker) PostStop(context.Context) error {
 }
 
 var _ Actor = &RestartBreaker{}
+
+type Forwarder struct {
+	actorRef PID
+}
+
+func (x *Forwarder) PreStart(context.Context) error {
+	return nil
+}
+
+func (x *Forwarder) Receive(ctx ReceiveContext) {
+	switch ctx.Message().(type) {
+	case *testspb.TestBye:
+		ctx.Forward(x.actorRef)
+	}
+}
+
+func (x *Forwarder) PostStop(context.Context) error {
+	return nil
+}
+
+var _ Actor = &Forwarder{}
diff --git a/actors/context_test.go b/actors/context_test.go
index 0a647dc..dbb2223 100644
--- a/actors/context_test.go
+++ b/actors/context_test.go
@@ -1077,4 +1077,51 @@ func TestReceiveContext(t *testing.T) {
 			context.Shutdown()
 		})
 	})
+	t.Run("With happy Forward", func(t *testing.T) {
+		ctx := context.TODO()
+		// create a Ping actor
+		opts := []pidOption{
+			withInitMaxRetries(1),
+			withCustomLogger(log.DiscardLogger),
+		}
+
+		// create actorA
+		actorA := &Exchanger{}
+		actorPath1 := NewPath("actorA", NewAddress("sys", "host", 1))
+		pidA, err := newPID(ctx, actorPath1, actorA, opts...)
+		require.NoError(t, err)
+		require.NotNil(t, pidA)
+
+		// create actorC
+		actorC := &Exchanger{}
+		actorPath3 := NewPath("actorC", NewAddress("sys", "host", 1))
+		pidC, err := newPID(ctx, actorPath3, actorC, opts...)
+		require.NoError(t, err)
+		require.NotNil(t, pidC)
+
+		// create actorB
+		actorB := &Forwarder{
+			actorRef: pidC,
+		}
+		actorPath2 := NewPath("actorB", NewAddress("sys", "host", 1))
+		pidB, err := newPID(ctx, actorPath2, actorB, opts...)
+		require.NoError(t, err)
+		require.NotNil(t, pidB)
+
+		// actor A is killing actor C using a forward pattern
+		// actorA tell actorB forward actorC
+		die := new(testpb.TestBye)
+		err = pidA.Tell(ctx, pidB, die)
+		require.NoError(t, err)
+
+		// wait for the async call to properly complete
+		time.Sleep(time.Second)
+		require.True(t, pidA.IsRunning())
+		require.True(t, pidB.IsRunning())
+		require.False(t, pidC.IsRunning())
+
+		// let us shutdown the rest
+		require.NoError(t, pidA.Shutdown(ctx))
+		require.NoError(t, pidB.Shutdown(ctx))
+	})
 }