Repository: Tochemey/goakt
Pull request #493 (2024-10-13 12:05:11)

feat: add Schedule and RemoteSchedule to scheduler

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/Tochemey/goakt/pull/493?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) Report
Attention: Patch coverage is `70.73171%` with `12 lines` in your changes missing coverage. Please review.
> Project coverage is 88.08%. Comparing base [(`fbb697f`)](https://app.codecov.io/gh/Tochemey/goakt/commit/fbb697fb6e3ee0f2329a9cdda7eb05abe59ad6bf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) to head [(`40767fc`)](https://app.codecov.io/gh/Tochemey/goakt/commit/40767fc4decfb72bd1d5e9abb62be887e19e8742?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene).

| [Files with missing lines](https://app.codecov.io/gh/Tochemey/goakt/pull/493?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) | Patch % | Lines |
|---|---|---|
| [actors/scheduler.go](https://app.codecov.io/gh/Tochemey/goakt/pull/493?src=pr&el=tree&filepath=actors%2Fscheduler.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene#diff-YWN0b3JzL3NjaGVkdWxlci5nbw==) | 67.56% | [8 Missing and 4 partials :warning: ](https://app.codecov.io/gh/Tochemey/goakt/pull/493?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Arsene) |

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


```diff
@@            Coverage Diff             @@
##             main     #493      +/-   ##
==========================================
- Coverage   88.30%   88.08%   -0.23%     
==========================================
  Files          45       45              
  Lines        3549     3584      +35     
==========================================
+ Hits         3134     3157      +23     
- Misses        300      308       +8     
- Partials      115      119       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Tochemey/goakt/pull/493?dropdown=coverage&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/README.md b/README.md
index 73aaf1b..8c79e45 100644
--- a/README.md
+++ b/README.md
@@ -261,8 +261,10 @@ The choice of protobuf is due to easy serialization over wire and strong schema
 
 You can schedule sending messages to actor that will be acted upon in the future. To achieve that you can use the following methods on the [Actor System](./actors/actor_system.go):
 
-- `ScheduleOnce` - will send the given message to a local actor after a given interval
-- `RemoteScheduleOnce` - will send the given message to a remote actor after a given interval. This requires remoting to be enabled on the actor system.
+- `ScheduleOnce` - will send the given message to a local actor _**once**_ after a given interval
+- `Schedule` - will sendd the given message to a local actor at a given interval
+- `RemoteSchedule` - will send the given message to a remote actor at a given interval. This requires remoting to be enabled on the actor system.
+- `RemoteScheduleOnce` - will send the given message to a remote actor _**once**_ after a given interval. This requires remoting to be enabled on the actor system.
 - `ScheduleWithCron` - will send the given message to a local actor using a [cron expression](#cron-expression-format).
 - `RemoteScheduleWithCron` - will send the given message to a remote actor using a [cron expression](#cron-expression-format). This requires remoting to be enabled on the actor system.
 
diff --git a/actors/actor_system.go b/actors/actor_system.go
index fcd5b8c..3d67431 100644
--- a/actors/actor_system.go
+++ b/actors/actor_system.go
@@ -109,11 +109,18 @@ type ActorSystem interface {
 	// This will send the given message to the actor after the given interval specified.
 	// The message will be sent once
 	ScheduleOnce(ctx context.Context, message proto.Message, pid *PID, interval time.Duration) error
+	// Schedule schedules a message that will be delivered to the receiver actor
+	// This will send the given message to the actor after the given interval specified.
+	Schedule(ctx context.Context, message proto.Message, pid *PID, interval time.Duration) error
 	// RemoteScheduleOnce schedules a message to be sent to a remote actor in the future.
 	// This requires remoting to be enabled on the actor system.
 	// This will send the given message to the actor after the given interval specified
 	// The message will be sent once
 	RemoteScheduleOnce(ctx context.Context, message proto.Message, address *address.Address, interval time.Duration) error
+	// RemoteSchedule schedules a message to be sent to a remote actor in the future.
+	// This requires remoting to be enabled on the actor system.
+	// This will send the given message to the actor after the given interval specified
+	RemoteSchedule(ctx context.Context, message proto.Message, address *address.Address, interval time.Duration) error
 	// ScheduleWithCron schedules a message to be sent to an actor in the future using a cron expression.
 	ScheduleWithCron(ctx context.Context, message proto.Message, pid *PID, cronExpression string) error
 	// RemoteScheduleWithCron schedules a message to be sent to an actor in the future using a cron expression.
@@ -313,6 +320,19 @@ func (x *actorSystem) Register(_ context.Context, actor Actor) error {
 	return nil
 }
 
+// Schedule schedules a message that will be delivered to the receiver actor
+// This will send the given message to the actor at the given interval specified.
+func (x *actorSystem) Schedule(ctx context.Context, message proto.Message, pid *PID, interval time.Duration) error {
+	return x.scheduler.Schedule(ctx, message, pid, interval)
+}
+
+// RemoteSchedule schedules a message to be sent to a remote actor in the future.
+// This requires remoting to be enabled on the actor system.
+// This will send the given message to the actor at the given interval specified
+func (x *actorSystem) RemoteSchedule(ctx context.Context, message proto.Message, address *address.Address, interval time.Duration) error {
+	return x.scheduler.RemoteSchedule(ctx, message, address, interval)
+}
+
 // ScheduleOnce schedules a message that will be delivered to the receiver actor
 // This will send the given message to the actor after the given interval specified.
 // The message will be sent once
diff --git a/actors/errors.go b/actors/errors.go
index a907f35..2bcd096 100644
--- a/actors/errors.go
+++ b/actors/errors.go
@@ -83,6 +83,8 @@ var (
 	ErrInvalidHost = errors.New("invalid host")
 	// ErrFullMailbox is returned when the mailbox is full
 	ErrFullMailbox = errors.New("mailbox is full")
+	// ErrSchedulerNotStarted is returned when the scheduler has not started
+	ErrSchedulerNotStarted = errors.New("scheduler has not started")
 )
 
 // eof returns true if the given error is an EOF error
diff --git a/actors/scheduler.go b/actors/scheduler.go
index b51cb92..353a4eb 100644
--- a/actors/scheduler.go
+++ b/actors/scheduler.go
@@ -26,13 +26,13 @@ package actors
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"strconv"
 	"sync"
 	"time"
 
-	"github.com/pkg/errors"
 	"github.com/reugn/go-quartz/job"
 	quartzlogger "github.com/reugn/go-quartz/logger"
 	"github.com/reugn/go-quartz/quartz"
@@ -129,8 +129,7 @@ func (x *scheduler) ScheduleOnce(ctx context.Context, message proto.Message, pid
 	defer x.mu.Unlock()
 
 	if !x.started.Load() {
-		// TODO: add a custom error
-		return errors.New("messages scheduler is not started")
+		return ErrSchedulerNotStarted
 	}
 
 	job := job.NewFunctionJob[bool](func(ctx context.Context) (bool, error) {
@@ -151,6 +150,33 @@ func (x *scheduler) ScheduleOnce(ctx context.Context, message proto.Message, pid
 	return x.quartzScheduler.ScheduleJob(jobDetails, quartz.NewRunOnceTrigger(interval))
 }
 
+// Schedule schedules a message that will be delivered to the receiving actor using the given interval specified
+func (x *scheduler) Schedule(ctx context.Context, message proto.Message, pid *PID, interval time.Duration) error {
+	x.mu.Lock()
+	defer x.mu.Unlock()
+
+	if !x.started.Load() {
+		return ErrSchedulerNotStarted
+	}
+
+	job := job.NewFunctionJob[bool](func(ctx context.Context) (bool, error) {
+		if err := Tell(ctx, pid, message); err != nil {
+			return false, err
+		}
+		return true, nil
+	})
+
+	jobDetails := quartz.NewJobDetail(job, quartz.NewJobKey(pid.Address().String()))
+	if err := x.distributeJobKeyOrNot(ctx, jobDetails); err != nil {
+		if errors.Is(err, errSkipJobScheduling) {
+			return nil
+		}
+		return err
+	}
+
+	return x.quartzScheduler.ScheduleJob(jobDetails, quartz.NewSimpleTrigger(interval))
+}
+
 // RemoteScheduleOnce schedules a message to be sent to a remote actor in the future.
 // This requires remoting to be enabled on the actor system.
 // This will send the given message to the actor after the given interval specified
@@ -160,8 +186,7 @@ func (x *scheduler) RemoteScheduleOnce(ctx context.Context, message proto.Messag
 	defer x.mu.Unlock()
 
 	if !x.started.Load() {
-		// TODO: add a custom error
-		return errors.New("messages scheduler is not started")
+		return ErrSchedulerNotStarted
 	}
 	job := job.NewFunctionJob[bool](func(ctx context.Context) (bool, error) {
 		if err := RemoteTell(ctx, address, message); err != nil {
@@ -182,12 +207,41 @@ func (x *scheduler) RemoteScheduleOnce(ctx context.Context, message proto.Messag
 	return x.quartzScheduler.ScheduleJob(jobDetails, quartz.NewRunOnceTrigger(interval))
 }
 
+// RemoteSchedule schedules a message to be sent to a remote actor in the future.
+// This requires remoting to be enabled on the actor system.
+// This will send the given message to the actor at the given interval specified
+func (x *scheduler) RemoteSchedule(ctx context.Context, message proto.Message, address *address.Address, interval time.Duration) error {
+	x.mu.Lock()
+	defer x.mu.Unlock()
+
+	if !x.started.Load() {
+		return ErrSchedulerNotStarted
+	}
+	job := job.NewFunctionJob[bool](func(ctx context.Context) (bool, error) {
+		if err := RemoteTell(ctx, address, message); err != nil {
+			return false, err
+		}
+		return true, nil
+	})
+
+	key := fmt.Sprintf("%s@%s", address.GetName(), net.JoinHostPort(address.GetHost(), strconv.Itoa(int(address.GetPort()))))
+	jobDetails := quartz.NewJobDetail(job, quartz.NewJobKey(key))
+	if err := x.distributeJobKeyOrNot(ctx, jobDetails); err != nil {
+		if errors.Is(err, errSkipJobScheduling) {
+			return nil
+		}
+		return err
+	}
+	// schedule the job
+	return x.quartzScheduler.ScheduleJob(jobDetails, quartz.NewSimpleTrigger(interval))
+}
+
 // ScheduleWithCron schedules a message to be sent to an actor in the future using a cron expression.
 func (x *scheduler) ScheduleWithCron(ctx context.Context, message proto.Message, pid *PID, cronExpression string) error {
 	x.mu.Lock()
 	defer x.mu.Unlock()
 	if !x.started.Load() {
-		return errors.New("messages scheduler is not started")
+		return ErrSchedulerNotStarted
 	}
 
 	job := job.NewFunctionJob[bool](func(ctx context.Context) (bool, error) {
@@ -208,7 +262,7 @@ func (x *scheduler) ScheduleWithCron(ctx context.Context, message proto.Message,
 	location := time.Now().Location()
 	trigger, err := quartz.NewCronTriggerWithLoc(cronExpression, location)
 	if err != nil {
-		x.logger.Error(errors.Wrap(err, "failed to schedule message"))
+		x.logger.Error(fmt.Errorf("failed to schedule message: %w", err))
 		return err
 	}
 
@@ -221,7 +275,7 @@ func (x *scheduler) RemoteScheduleWithCron(ctx context.Context, message proto.Me
 	defer x.mu.Unlock()
 
 	if !x.started.Load() {
-		return errors.New("messages scheduler is not started")
+		return ErrSchedulerNotStarted
 	}
 
 	job := job.NewFunctionJob[bool](func(ctx context.Context) (bool, error) {
@@ -244,7 +298,7 @@ func (x *scheduler) RemoteScheduleWithCron(ctx context.Context, message proto.Me
 	location := time.Now().Location()
 	trigger, err := quartz.NewCronTriggerWithLoc(cronExpression, location)
 	if err != nil {
-		x.logger.Error(errors.Wrap(err, "failed to schedule message"))
+		x.logger.Error(fmt.Errorf("failed to schedule message: %w", err))
 		return err
 	}
 


Test patch:
diff --git a/actors/scheduler_test.go b/actors/scheduler_test.go
index 2c0b9b6..1085f71 100644
--- a/actors/scheduler_test.go
+++ b/actors/scheduler_test.go
@@ -83,6 +83,51 @@ func TestScheduler(t *testing.T) {
 		err = newActorSystem.Stop(ctx)
 		assert.NoError(t, err)
 	})
+	t.Run("With ScheduleOnce when actor not started", func(t *testing.T) {
+		// create the context
+		ctx := context.TODO()
+		// define the logger to use
+		logger := log.DiscardLogger
+		// create the actor system
+		newActorSystem, err := NewActorSystem("test",
+			WithLogger(logger),
+			WithJanitorInterval(time.Minute),
+			WithPassivationDisabled())
+		// assert there are no error
+		require.NoError(t, err)
+
+		// start the actor system
+		err = newActorSystem.Start(ctx)
+		assert.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		// create a test actor
+		actorName := "test"
+		actor := newTestActor()
+		actorRef, err := newActorSystem.Spawn(ctx, actorName, actor)
+		require.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		lib.Pause(time.Second)
+		require.NoError(t, newActorSystem.Kill(ctx, actorName))
+
+		// send a message to the actor after 100 ms
+		message := new(testpb.TestSend)
+		err = newActorSystem.ScheduleOnce(ctx, message, actorRef, 100*time.Millisecond)
+		require.NoError(t, err)
+
+		lib.Pause(time.Second)
+		typedSystem := newActorSystem.(*actorSystem)
+		keys, err := typedSystem.scheduler.quartzScheduler.GetJobKeys()
+		require.NoError(t, err)
+		assert.Empty(t, keys)
+		assert.EqualValues(t, 0, actorRef.ProcessedCount())
+
+		// stop the actor
+		err = newActorSystem.Stop(ctx)
+		assert.NoError(t, err)
+	})
 	t.Run("With ScheduleOnce with scheduler not started", func(t *testing.T) {
 		ctx := context.TODO()
 		scheduler := newScheduler(log.DiscardLogger, time.Second)
@@ -106,7 +151,7 @@ func TestScheduler(t *testing.T) {
 		message := new(testpb.TestSend)
 		err = scheduler.ScheduleOnce(ctx, message, pid, 100*time.Millisecond)
 		require.Error(t, err)
-		assert.EqualError(t, err, "messages scheduler is not started")
+		assert.EqualError(t, err, ErrSchedulerNotStarted.Error())
 
 		err = pid.Shutdown(ctx)
 		assert.NoError(t, err)
@@ -208,7 +253,7 @@ func TestScheduler(t *testing.T) {
 		message := new(testpb.TestSend)
 		err = newActorSystem.RemoteScheduleOnce(ctx, message, addr, 100*time.Millisecond)
 		require.Error(t, err)
-		assert.EqualError(t, err, "messages scheduler is not started")
+		assert.EqualError(t, err, ErrSchedulerNotStarted.Error())
 
 		// stop the actor
 		err = newActorSystem.Stop(ctx)
@@ -331,7 +376,7 @@ func TestScheduler(t *testing.T) {
 		const expr = "* * * ? * *"
 		err = newActorSystem.ScheduleWithCron(ctx, message, actorRef, expr)
 		require.Error(t, err)
-		assert.EqualError(t, err, "messages scheduler is not started")
+		assert.EqualError(t, err, ErrSchedulerNotStarted.Error())
 
 		// stop the actor
 		err = newActorSystem.Stop(ctx)
@@ -480,7 +525,313 @@ func TestScheduler(t *testing.T) {
 		const expr = "* * * ? * *"
 		err = newActorSystem.RemoteScheduleWithCron(ctx, message, addr, expr)
 		require.Error(t, err)
-		assert.EqualError(t, err, "messages scheduler is not started")
+		assert.EqualError(t, err, ErrSchedulerNotStarted.Error())
+
+		// stop the actor
+		err = newActorSystem.Stop(ctx)
+		assert.NoError(t, err)
+	})
+	t.Run("With Schedule", func(t *testing.T) {
+		// create the context
+		ctx := context.TODO()
+		// define the logger to use
+		logger := log.DiscardLogger
+		// create the actor system
+		newActorSystem, err := NewActorSystem("test",
+			WithLogger(logger),
+			WithPassivationDisabled())
+		// assert there are no error
+		require.NoError(t, err)
+
+		// start the actor system
+		err = newActorSystem.Start(ctx)
+		assert.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		// create a test actor
+		actorName := "test"
+		actor := newTestActor()
+		actorRef, err := newActorSystem.Spawn(ctx, actorName, actor)
+		require.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		lib.Pause(time.Second)
+
+		// send a message to the actor after one second
+		message := new(testpb.TestSend)
+		err = newActorSystem.Schedule(ctx, message, actorRef, time.Second)
+		require.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		typedSystem := newActorSystem.(*actorSystem)
+		keys, err := typedSystem.scheduler.quartzScheduler.GetJobKeys()
+		require.NoError(t, err)
+		require.NotEmpty(t, keys)
+		require.Len(t, keys, 1)
+
+		lib.Pause(500 * time.Millisecond)
+		require.EqualValues(t, 1, actorRef.ProcessedCount()-1)
+		lib.Pause(500 * time.Millisecond)
+		require.EqualValues(t, 2, actorRef.ProcessedCount()-1)
+
+		// stop the actor
+		err = newActorSystem.Stop(ctx)
+		assert.NoError(t, err)
+	})
+	t.Run("With Schedule when actor not started", func(t *testing.T) {
+		// create the context
+		ctx := context.TODO()
+		// define the logger to use
+		logger := log.DiscardLogger
+		// create the actor system
+		newActorSystem, err := NewActorSystem("test",
+			WithLogger(logger),
+			WithJanitorInterval(time.Minute),
+			WithPassivationDisabled())
+		// assert there are no error
+		require.NoError(t, err)
+
+		// start the actor system
+		err = newActorSystem.Start(ctx)
+		assert.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		// create a test actor
+		actorName := "test"
+		actor := newTestActor()
+		actorRef, err := newActorSystem.Spawn(ctx, actorName, actor)
+		require.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		lib.Pause(time.Second)
+		require.NoError(t, newActorSystem.Kill(ctx, actorName))
+
+		// send a message to the actor after one second
+		message := new(testpb.TestSend)
+		err = newActorSystem.Schedule(ctx, message, actorRef, time.Second)
+		require.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		typedSystem := newActorSystem.(*actorSystem)
+		keys, err := typedSystem.scheduler.quartzScheduler.GetJobKeys()
+		require.NoError(t, err)
+		require.NotEmpty(t, keys)
+		require.Len(t, keys, 1)
+
+		lib.Pause(500 * time.Millisecond)
+		require.EqualValues(t, 0, actorRef.ProcessedCount())
+
+		// stop the actor
+		err = newActorSystem.Stop(ctx)
+		assert.NoError(t, err)
+	})
+	t.Run("With Schedule with scheduler not started", func(t *testing.T) {
+		// create the context
+		ctx := context.TODO()
+		// define the logger to use
+		logger := log.DiscardLogger
+		// create the actor system
+		newActorSystem, err := NewActorSystem("test",
+			WithLogger(logger),
+			WithPassivationDisabled())
+		// assert there are no error
+		require.NoError(t, err)
+
+		// start the actor system
+		err = newActorSystem.Start(ctx)
+		assert.NoError(t, err)
+
+		lib.Pause(time.Second)
+		// test purpose only
+		typedSystem := newActorSystem.(*actorSystem)
+		typedSystem.scheduler.Stop(ctx)
+
+		// create a test actor
+		actorName := "test"
+		actor := newTestActor()
+		actorRef, err := newActorSystem.Spawn(ctx, actorName, actor)
+		require.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		lib.Pause(time.Second)
+
+		// send a message to the actor after one second
+		message := new(testpb.TestSend)
+		err = newActorSystem.Schedule(ctx, message, actorRef, time.Second)
+		require.Error(t, err)
+		assert.EqualError(t, err, ErrSchedulerNotStarted.Error())
+
+		// stop the actor
+		err = newActorSystem.Stop(ctx)
+		assert.NoError(t, err)
+	})
+	t.Run("With RemoteSchedule", func(t *testing.T) {
+		// create the context
+		ctx := context.TODO()
+		// define the logger to use
+		logger := log.DiscardLogger
+		// generate the remoting port
+		nodePorts := dynaport.Get(1)
+		remotingPort := nodePorts[0]
+		host := "0.0.0.0"
+
+		// create the actor system
+		newActorSystem, err := NewActorSystem("test",
+			WithLogger(logger),
+			WithPassivationDisabled(),
+			WithRemoting(host, int32(remotingPort)),
+		)
+		// assert there are no error
+		require.NoError(t, err)
+
+		// start the actor system
+		err = newActorSystem.Start(ctx)
+		assert.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		// create a test actor
+		actorName := "test"
+		actor := newTestActor()
+		actorRef, err := newActorSystem.Spawn(ctx, actorName, actor)
+		require.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		// get the address of the actor
+		addr, err := RemoteLookup(ctx, host, remotingPort, actorName)
+		require.NoError(t, err)
+
+		// send a message to the actor after 100 ms
+		message := new(testpb.TestSend)
+		err = newActorSystem.RemoteSchedule(ctx, message, addr, time.Second)
+		require.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		typedSystem := newActorSystem.(*actorSystem)
+		keys, err := typedSystem.scheduler.quartzScheduler.GetJobKeys()
+		require.NoError(t, err)
+		require.NotEmpty(t, keys)
+		require.Len(t, keys, 1)
+
+		lib.Pause(500 * time.Millisecond)
+		require.EqualValues(t, 1, actorRef.ProcessedCount()-1)
+		lib.Pause(800 * time.Millisecond)
+		require.EqualValues(t, 2, actorRef.ProcessedCount()-1)
+
+		// stop the actor
+		err = newActorSystem.Stop(ctx)
+		assert.NoError(t, err)
+	})
+	t.Run("With RemoteSchedule with scheduler not started", func(t *testing.T) {
+		// create the context
+		ctx := context.TODO()
+		// define the logger to use
+		logger := log.DiscardLogger
+		// generate the remoting port
+		nodePorts := dynaport.Get(1)
+		remotingPort := nodePorts[0]
+		host := "0.0.0.0"
+
+		// create the actor system
+		newActorSystem, err := NewActorSystem("test",
+			WithLogger(logger),
+			WithPassivationDisabled(),
+			WithRemoting(host, int32(remotingPort)),
+		)
+		// assert there are no error
+		require.NoError(t, err)
+
+		// start the actor system
+		err = newActorSystem.Start(ctx)
+		assert.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		// test purpose only
+		typedSystem := newActorSystem.(*actorSystem)
+		typedSystem.scheduler.Stop(ctx)
+
+		// create a test actor
+		actorName := "test"
+		actor := newTestActor()
+		actorRef, err := newActorSystem.Spawn(ctx, actorName, actor)
+		require.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		// get the address of the actor
+		addr, err := RemoteLookup(ctx, host, remotingPort, actorName)
+		require.NoError(t, err)
+
+		// send a message to the actor after 100 ms
+		message := new(testpb.TestSend)
+		err = newActorSystem.RemoteSchedule(ctx, message, addr, 100*time.Millisecond)
+		require.Error(t, err)
+		assert.EqualError(t, err, ErrSchedulerNotStarted.Error())
+
+		// stop the actor
+		err = newActorSystem.Stop(ctx)
+		assert.NoError(t, err)
+	})
+	t.Run("With RemoteSchedule when actor not started", func(t *testing.T) {
+		// create the context
+		ctx := context.TODO()
+		// define the logger to use
+		logger := log.DiscardLogger
+		// generate the remoting port
+		nodePorts := dynaport.Get(1)
+		remotingPort := nodePorts[0]
+		host := "0.0.0.0"
+
+		// create the actor system
+		newActorSystem, err := NewActorSystem("test",
+			WithLogger(logger),
+			WithPassivationDisabled(),
+			WithJanitorInterval(time.Minute),
+			WithRemoting(host, int32(remotingPort)),
+		)
+		// assert there are no error
+		require.NoError(t, err)
+
+		// start the actor system
+		err = newActorSystem.Start(ctx)
+		assert.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		// create a test actor
+		actorName := "test"
+		actor := newTestActor()
+		actorRef, err := newActorSystem.Spawn(ctx, actorName, actor)
+		require.NoError(t, err)
+		assert.NotNil(t, actorRef)
+
+		// get the address of the actor
+		addr, err := RemoteLookup(ctx, host, remotingPort, actorName)
+		require.NoError(t, err)
+
+		require.NoError(t, newActorSystem.Kill(ctx, actorName))
+		lib.Pause(time.Second)
+
+		// send a message to the actor after 100 ms
+		message := new(testpb.TestSend)
+		err = newActorSystem.RemoteSchedule(ctx, message, addr, time.Second)
+		require.NoError(t, err)
+
+		lib.Pause(time.Second)
+
+		typedSystem := newActorSystem.(*actorSystem)
+		keys, err := typedSystem.scheduler.quartzScheduler.GetJobKeys()
+		require.NoError(t, err)
+		require.NotEmpty(t, keys)
+		require.Len(t, keys, 1)
+
+		lib.Pause(500 * time.Millisecond)
+		require.Zero(t, actorRef.ProcessedCount())
 
 		// stop the actor
 		err = newActorSystem.Stop(ctx)