AK1004 - Warning
Usage of ScheduleTellOnce()
and ScheduleTellRepeatedly()
inside an Akka actor, especially the variant that does not accept an ICancelable
parameter, can cause memory leak and unnecessary CPU usage if they are not canceled properly inside PostStop()
.
Cause
ScheduleTellOnce()
and ScheduleTellRepeatedly()
registers a timer directly inside the HashedWheelTimerScheduler
that will be fired after or each time a time have elapsed, depending on the method used. These timers are disconnected to the actor lifecycle and are not cleaned up nor replaced when the actors are stopped or restarted and will still run in the background even if the actor were stopped.
Memory leak and increased CPU usage will develop if these methods were called multiple times by the same actor code, especially on actors with short lifespan that are restarted repeatedly.
For example, this code will start a new timer every time the actor is started. If it were short lived and restarted repeatedly, all of the started timers will accumulate in memory and build up over time.
using Akka.Actor;
using System.Threading.Tasks;
using System;
public sealed class MyActor : ReceiveActor
{
private sealed class TimerMessage
{
public static readonly TimerMessage Instance = new();
private TimerMessage() { }
}
public MyActor()
{
Context.System.Scheduler.ScheduleTellRepeatedly(
initialDelay: TimeSpan.FromSeconds(3),
interval: TimeSpan.FromSeconds(3),
receiver: Self,
message: TimerMessage.Instance,
sender: Self);
}
}
Resolution
Try to convert your actor to implement IWithTimers
instead. The timers started using IWithTimers.Timers
methods are tied to the actor life cycle and are automatically removed inside the default AroundPostStop()
and AroundPreRestart()
actor lifecycle method.
Here's an example below:
using Akka.Actor;
using System.Threading.Tasks;
using System;
public sealed class MyActor : ReceiveActor, IWithTimers
{
private sealed class TimerKey
{
public static readonly TimerKey Instance = new();
private TimerKey() { }
}
private sealed class TimerMessage
{
public static readonly TimerMessage Instance = new();
private TimerMessage() { }
}
public MyActor()
{
Timers.StartPeriodicTimer(
key: TimerKey.Instance,
msg: TimerMessage.Instance,
initialDelay: TimeSpan.FromSeconds(3),
interval: TimeSpan.FromSeconds(3));
}
public ITimerScheduler Timers { get; set; } = null!;
}
If you have to use any of the ITellScheduler
methods, make sure that you always use the methods that accepts ICancelable
as a parameter and always call the ICancelable.Cancel()
method on actor PostStop()
.
using Akka.Actor;
using System.Threading.Tasks;
using System;
public sealed class MyActor : ReceiveActor
{
private sealed class TimerMessage
{
public static readonly TimerMessage Instance = new();
private TimerMessage() { }
}
private readonly Cancelable _timerCancelable;
public MyActor()
{
_timerCancelable = new Cancelable(Context.System.Scheduler);
Context.System.Scheduler.ScheduleTellRepeatedly(
initialDelay: TimeSpan.FromSeconds(3),
interval: TimeSpan.FromSeconds(3),
receiver: Self,
message: TimerMessage.Instance,
sender: Self
cancelable: _timerCancelable);
}
protected override void PostStop()
{
_timerCancelable.Cancel();
}
}