Skip to content

Commit

Permalink
refactor: refactor ExpectMessage and ExpectMessageWithin
Browse files Browse the repository at this point in the history
  • Loading branch information
Tochemey committed Oct 15, 2024
1 parent d232377 commit 8616d23
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 47 deletions.
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Go-Akt
# GoAkt

[![build](https://img.shields.io/github/actions/workflow/status/Tochemey/goakt/build.yml?branch=main)](https://github.com/Tochemey/goakt/actions/workflows/build.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/tochemey/goakt.svg)](https://pkg.go.dev/github.com/tochemey/goakt)
Expand Down Expand Up @@ -89,7 +89,7 @@ go get github.com/tochemey/goakt/v2

## Versioning

The version system adopted in Go-Akt deviates a bit from the standard semantic versioning system.
The version system adopted in GoAkt deviates a bit from the standard semantic versioning system.
The version format is as follows:

- The `MAJOR` part of the version will stay at `v2` for the meantime.
Expand All @@ -106,15 +106,15 @@ Kindly check out the [examples'](https://github.com/Tochemey/goakt-examples) rep

### Actors

The fundamental building blocks of Go-Akt are actors.
The fundamental building blocks of GoAkt are actors.

- They are independent, isolated unit of computation with their own state.
- They can be _long-lived_ actors or be _passivated_ after some period of time that is configured during their
creation. Use this feature with care when dealing with persistent actors (actors that require their state to be persisted).
- They are automatically thread-safe without having to use locks or any other shared-memory synchronization
mechanisms.
- They can be stateful and stateless depending upon the system to build.
- Every actor in Go-Akt:
- Every actor in GoAkt:
- has a process id [`PID`](./actors/pid.go). Via the process id any allowable action can be executed by the
actor.
- has a lifecycle via the following methods: [`PreStart`](./actors/actor.go), [`PostStop`](./actors/actor.go).
Expand Down Expand Up @@ -150,9 +150,9 @@ When cluster mode is enabled, passivated actors are removed from the entire clus

### Supervision

In Go-Akt, supervision allows to define the various strategies to apply when a given actor is faulty.
In GoAkt, supervision allows to define the various strategies to apply when a given actor is faulty.
The supervisory strategy to adopt is set during the creation of the actor system.
In Go-Akt each child actor is treated separately. There is no concept of one-for-one and one-for-all strategies.
In GoAkt each child actor is treated separately. There is no concept of one-for-one and one-for-all strategies.
The following directives are supported:
- [`Restart`](./actors/supervisor.go): to restart the child actor. One can control how the restart is done using the following options: - `maxNumRetries`: defines the maximum of restart attempts - `timeout`: how to attempt restarting the faulty actor.
- [`Stop`](./actors/supervisor.go): to stop the child actor which is the default one
Expand All @@ -165,10 +165,10 @@ There are only two scenarios where an actor can supervise another actor:

### Actor System

Without an actor system, it is not possible to create actors in Go-Akt. Only a single actor system
is recommended to be created per application when using Go-Akt. At the moment the single instance is not enforced in Go-Akt, this simple implementation is left to the discretion of the developer. To
Without an actor system, it is not possible to create actors in GoAkt. Only a single actor system
is recommended to be created per application when using GoAkt. At the moment the single instance is not enforced in GoAkt, this simple implementation is left to the discretion of the developer. To
create an actor system one just need to use
the [`NewActorSystem`](./actors/actor_system.go) method with the various [Options](./actors/option.go). Go-Akt
the [`NewActorSystem`](./actors/actor_system.go) method with the various [Options](./actors/option.go). GoAkt
ActorSystem has the following characteristics:

- Actors lifecycle management (Spawn, Kill, ReSpawn)
Expand All @@ -181,7 +181,7 @@ ActorSystem has the following characteristics:

### Behaviors

Actors in Go-Akt have the power to switch their behaviors at any point in time. When you change the actor behavior, the new
Actors in GoAkt have the power to switch their behaviors at any point in time. When you change the actor behavior, the new
behavior will take effect for all subsequent messages until the behavior is changed again. The current message will
continue processing with the existing behavior. You can use [Stashing](#stashing) to reprocess the current
message with the new behavior.
Expand All @@ -201,7 +201,7 @@ When the router receives a message to broadcast, every routee is checked whether
When a routee is not alive the router removes it from its set of routees.
When the last routee stops the router itself stops.

Go-Akt comes shipped with the following routing strategies:
GoAkt comes shipped with the following routing strategies:

- `Fan-Out`: This strategy broadcasts the given message to all its available routees in parallel.
- `Random`: This strategy randomly picks a routee in its set of routees and send the message to it.
Expand All @@ -213,7 +213,7 @@ Router as well as their routees are not passivated.
### Mailbox

Once can implement a custom mailbox. See [Mailbox](./actors/mailbox.go).
Go-Akt comes with the following mailboxes built-in:
GoAkt comes with the following mailboxes built-in:

- [`UnboundedMailbox`](./actors/unbounded_mailbox.go): this is the default mailbox. It is implemented using the lock-free Multi-Producer-Single-Consumer Queue.
- [`BoundedMailbox`](./actors/bounded_mailbox.go): this is a thread-safe mailbox implemented using the Ring-Buffer Queue. When the mailbox is full any new message is sent to the deadletter queue. Setting a reasonable capacity for the queue can enhance throughput.
Expand All @@ -240,7 +240,7 @@ The subscription methods can be found on the `ActorSystem` interface.

### Messaging

Communication between actors is achieved exclusively through message passing. In Go-Akt _Google
Communication between actors is achieved exclusively through message passing. In GoAkt _Google
Protocol Buffers_ is used to define messages.
The choice of protobuf is due to easy serialization over wire and strong schema definition. As stated previously the following messaging patterns are supported:

Expand Down Expand Up @@ -290,7 +290,7 @@ Stashing is a mechanism you can enable in your actors, so they can temporarily s
not handle at the moment.
Another way to see it is that stashing allows you to keep processing messages you can handle while saving for later
messages you can't.
Stashing are handled by Go-Akt out of the actor instance just like the mailbox, so if the actor dies while processing a
Stashing are handled by GoAkt out of the actor instance just like the mailbox, so if the actor dies while processing a
message, all messages in the stash are processed.
This feature is usually used together with [Become/UnBecome](#behaviors), as they fit together very well, but this is
not a requirement.
Expand Down Expand Up @@ -323,13 +323,13 @@ These methods can be used from the [API](./actors/api.go) as well as from the [P

### Cluster

This offers simple scalability, partitioning (sharding), and re-balancing out-of-the-box. Go-Akt nodes are automatically discovered. See [Clustering](#clustering).
This offers simple scalability, partitioning (sharding), and re-balancing out-of-the-box. GoAkt nodes are automatically discovered. See [Clustering](#clustering).
Beware that at the moment, within the cluster the existence of an actor is unique.

### Observability

Observability is key in distributed system. It helps to understand and track the performance of a system.
Go-Akt offers out of the box features that can help track, monitor and measure the performance of a Go-Akt based system.
GoAkt offers out of the box features that can help track, monitor and measure the performance of a GoAkt based system.

#### Metrics

Expand All @@ -346,17 +346,18 @@ A simple logging interface to allow custom logger to be implemented instead of u

### Testkit

Go-Akt comes packaged with a testkit that can help test that actors receive expected messages within _unit tests_.
GoAkt comes packaged with a testkit that can help test that actors receive expected messages within _unit tests_.
The teskit in GoAkt uses underneath the [`https://github.com/stretchr/testify`](https://github.com/stretchr/testify) package.
To test that an actor receive and respond to messages one will have to:

1. Create an instance of the testkit: `testkit := New(ctx, t)` where `ctx` is a go context and `t` the instance of `*testing.T`. This can be done in setup before the run of each test.
2. Create the instance of the actor under test. Example: `pinger := testkit.Spawn(ctx, "pinger", &pinger{})`
3. Create an instance of test probe: `probe := testkit.NewProbe(ctx)` where `ctx` is a go context
3. Create an instance of test probe: `probe := testkit.NewProbe(ctx)` where `ctx` is a go context. One can set some [options](./testkit/option.go)
4. Use the probe to send a message to the actor under test. Example: `probe.Send(pinger, new(testpb.Ping))`
5. Assert that the actor under test has received the message and responded as expected using the probe methods:

- `ExpectMessage(message proto.Message) proto.Message`: asserts that the message received from the test actor is the expected one
- `ExpectMessageWithin(duration time.Duration, message proto.Message) proto.Message`: asserts that the message received from the test actor is the expected one within a time duration
- `ExpectMessage(message proto.Message)`: asserts that the message received from the test actor is the expected one
- `ExpectMessageWithin(duration time.Duration, message proto.Message)`: asserts that the message received from the test actor is the expected one within a time duration
- `ExpectNoMessage()`: asserts that no message is expected
- `ExpectAnyMessage() proto.Message`: asserts that any message is expected
- `ExpectAnyMessageWithin(duration time.Duration) proto.Message`: asserts that any message within a time duration
Expand All @@ -369,7 +370,7 @@ To help implement unit tests in GoAkt-based applications. See [Testkit](./testki

## API

The API interface helps interact with a Go-Akt actor system as kind of client. The following features are available:
The API interface helps interact with a GoAkt actor system as kind of client. The following features are available:

- `Tell`: to send a message to an actor in a fire-and-forget manner
- `Ask`: to send a message to an actor and expect a response within a given timeout
Expand All @@ -386,10 +387,10 @@ The API interface helps interact with a Go-Akt actor system as kind of client. T

## Client

The Go-Akt client facilitates interaction with a specified Go-Akt cluster, contingent upon the activation of cluster mode.
The GoAkt client facilitates interaction with a specified GoAkt cluster, contingent upon the activation of cluster mode.
The client operates without knowledge of the specific node within the cluster that will process the request.
This feature is particularly beneficial when interfacing with a Go-Akt cluster from an external system.
Go-Akt client is equipped with a mini load-balancer that helps route requests to the appropriate node.
This feature is particularly beneficial when interfacing with a GoAkt cluster from an external system.
GoAkt client is equipped with a mini load-balancer that helps route requests to the appropriate node.

### Balancer strategies

Expand Down
54 changes: 54 additions & 0 deletions testkit/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* MIT License
*
* Copyright (c) 2022-2024 Tochemey
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package testkit

import (
"os"

"github.com/tochemey/goakt/v2/log"
)

// Option is the interface that applies a Testkit option.
type Option interface {
// Apply sets the Option value of a config.
Apply(kit *TestKit)
}

// enforce compilation error
var _ Option = OptionFunc(nil)

// OptionFunc implements the Option interface.
type OptionFunc func(kit *TestKit)

func (f OptionFunc) Apply(kit *TestKit) {
f(kit)
}

// WithLogging sets the Testkit logger
func WithLogging(level log.Level) Option {
return OptionFunc(func(kit *TestKit) {
kit.logger = log.New(level, os.Stderr)
})
}
15 changes: 7 additions & 8 deletions testkit/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ const (
// when implementing unit tests with actors
type Probe interface {
// ExpectMessage asserts that the message received from the test actor is the expected one
ExpectMessage(message proto.Message) proto.Message
ExpectMessage(message proto.Message)
// ExpectMessageWithin asserts that the message received from the test actor is the expected one within a time duration
ExpectMessageWithin(duration time.Duration, message proto.Message) proto.Message
ExpectMessageWithin(duration time.Duration, message proto.Message)
// ExpectNoMessage asserts that no message is expected
ExpectNoMessage()
// ExpectAnyMessage asserts that any message is expected
Expand Down Expand Up @@ -134,13 +134,13 @@ func (x *probe) ExpectMessageOfTypeWithin(duration time.Duration, messageType pr
}

// ExpectMessage assert message expectation
func (x *probe) ExpectMessage(message proto.Message) proto.Message {
return x.expectMessage(x.defaultTimeout, message)
func (x *probe) ExpectMessage(message proto.Message) {
x.expectMessage(x.defaultTimeout, message)
}

// ExpectMessageWithin expects message within a time duration
func (x *probe) ExpectMessageWithin(duration time.Duration, message proto.Message) proto.Message {
return x.expectMessage(duration, message)
func (x *probe) ExpectMessageWithin(duration time.Duration, message proto.Message) {
x.expectMessage(duration, message)
}

// ExpectNoMessage expects no message
Expand Down Expand Up @@ -212,13 +212,12 @@ func (x *probe) receiveOne(max time.Duration) proto.Message {
}

// expectMessage assert the expectation of a message within a maximum time duration
func (x *probe) expectMessage(max time.Duration, message proto.Message) proto.Message {
func (x *probe) expectMessage(max time.Duration, message proto.Message) {
// receive one message
received := x.receiveOne(max)
// let us assert the received message
require.NotNil(x.pt, received, fmt.Sprintf("timeout (%v) during expectMessage while waiting for %v", max, message))
require.Equal(x.pt, prototext.Format(message), prototext.Format(received), fmt.Sprintf("expected %v, found %v", message, received))
return received
}

// expectNoMessage asserts that no message is expected
Expand Down
16 changes: 7 additions & 9 deletions testkit/probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/tochemey/goakt/v2/actors"
"github.com/tochemey/goakt/v2/internal/lib"
"github.com/tochemey/goakt/v2/log"
"github.com/tochemey/goakt/v2/test/data/testpb"
)

Expand All @@ -19,7 +20,7 @@ func TestTestProbe(t *testing.T) {
// create a test context
ctx := context.TODO()
// create a test kit
testkit := New(ctx, t)
testkit := New(ctx, t, WithLogging(log.ErrorLevel))

// create the actor
pinger := testkit.Spawn(ctx, "pinger", &pinger{})
Expand All @@ -31,8 +32,7 @@ func TestTestProbe(t *testing.T) {
// send a message to the actor to be tested
probe.Send(pinger, new(testpb.Ping))

actual := probe.ExpectMessage(msg)
require.Equal(t, prototext.Format(msg), prototext.Format(actual))
probe.ExpectMessage(msg)
probe.ExpectNoMessage()

t.Cleanup(func() {
Expand All @@ -44,7 +44,7 @@ func TestTestProbe(t *testing.T) {
// create a test context
ctx := context.TODO()
// create a test kit
testkit := New(ctx, t)
testkit := New(ctx, t, WithLogging(log.ErrorLevel))

// create the actor
pinger := testkit.Spawn(ctx, "pinger", &pinger{})
Expand All @@ -69,7 +69,7 @@ func TestTestProbe(t *testing.T) {
// create a test context
ctx := context.TODO()
// create a test kit
testkit := New(ctx, t)
testkit := New(ctx, t, WithLogging(log.ErrorLevel))

// create the actor
pinger := testkit.Spawn(ctx, "pinger", &pinger{})
Expand All @@ -80,8 +80,7 @@ func TestTestProbe(t *testing.T) {
// send a message to the actor to be tested
probe.Send(pinger, new(testpb.Ping))

actual := probe.ExpectMessage(msg)
require.Equal(t, prototext.Format(msg), prototext.Format(actual))
probe.ExpectMessage(msg)
require.Equal(t, pinger.Address().String(), probe.Sender().Address().String())
probe.ExpectNoMessage()

Expand Down Expand Up @@ -130,8 +129,7 @@ func TestTestProbe(t *testing.T) {
duration := time.Second
probe.Send(pinger, &testpb.Wait{Duration: uint64(duration)})

actual := probe.ExpectMessageWithin(2*time.Second, msg)
require.Equal(t, prototext.Format(msg), prototext.Format(actual))
probe.ExpectMessageWithin(2*time.Second, msg)
probe.ExpectNoMessage()

t.Cleanup(func() {
Expand Down
20 changes: 14 additions & 6 deletions testkit/testkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,25 @@ import (
type TestKit struct {
actorSystem actors.ActorSystem
kt *testing.T
logger log.Logger
}

// New creates an instance of TestKit
func New(ctx context.Context, t *testing.T) *TestKit {
func New(ctx context.Context, t *testing.T, opts ...Option) *TestKit {
// create the testkit instance
testkit := &TestKit{
kt: t,
logger: log.DiscardLogger,
}
// apply the various options
for _, opt := range opts {
opt.Apply(testkit)
}
// create an actor system
system, err := actors.NewActorSystem(
"testkit",
actors.WithPassivationDisabled(),
actors.WithLogger(log.DefaultLogger),
actors.WithLogger(testkit.logger),
actors.WithActorInitTimeout(time.Second),
actors.WithActorInitMaxRetries(5),
actors.WithReplyTimeout(time.Minute))
Expand All @@ -34,10 +44,8 @@ func New(ctx context.Context, t *testing.T) *TestKit {
t.Fatal(err.Error())
}

return &TestKit{
actorSystem: system,
kt: t,
}
testkit.actorSystem = system
return testkit
}

// Spawn create an actor
Expand Down

0 comments on commit 8616d23

Please sign in to comment.