Skip to content

Commit

Permalink
Add rate limiter to omit duplicated notifications in WillPresentNotif…
Browse files Browse the repository at this point in the history
…ication
  • Loading branch information
Thomas Galliker committed Oct 2, 2024
1 parent 63586e7 commit d726d26
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Concurrent;

namespace Plugin.FirebasePushNotifications.Internals
{
internal interface INotificationRateLimiter
{
bool HasReachedLimit(string identifier);
}

internal class NotificationRateLimiter : INotificationRateLimiter
{
private readonly ConcurrentDictionary<string, DateTime> cache = new();
private readonly TimeSpan expirationPeriod;

public NotificationRateLimiter(TimeSpan expirationPeriod)
{
this.expirationPeriod = expirationPeriod;
}

public bool HasReachedLimit(string identifier)
{
var utcNow = DateTime.UtcNow;

if (this.cache.TryGetValue(identifier, out var expirationTime))
{
if (expirationTime > utcNow)
{
return true;
}
}

foreach (var item in this.cache)
{
if (item.Value > utcNow)
{
this.cache.TryRemove(item.Key, out _);
}
}

this.cache[identifier] = utcNow.Add(this.expirationPeriod);
return false;
}

public int Count => this.cache.Count();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Foundation;
using Microsoft.Extensions.Logging;
using Plugin.FirebasePushNotifications.Extensions;
using Plugin.FirebasePushNotifications.Internals;
using UIKit;
using UserNotifications;

Expand All @@ -17,6 +18,7 @@ public class FirebasePushNotificationManager : FirebasePushNotificationManagerBa
UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound;

private readonly Queue<(string Topic, bool Subscribe)> pendingTopics = new Queue<(string, bool)>();
private readonly INotificationRateLimiter willPresentNotificationRateLimiter = new NotificationRateLimiter(TimeSpan.FromMilliseconds(200));
private bool disposed;

internal FirebasePushNotificationManager(
Expand Down Expand Up @@ -247,6 +249,14 @@ private void DidReceiveRemoteNotificationInternal(NSDictionary userInfo)

private void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
if (this.willPresentNotificationRateLimiter.HasReachedLimit(notification.Request.Identifier))
{
this.logger.LogDebug(
$"WillPresentNotification: notification.Request.Identifier \"{notification.Request.Identifier}\" " +
$"has reached the rate limit");
return;
}

var data = notification.Request.Content.UserInfo.GetParameters();
var notificationPresentationOptions = GetNotificationPresentationOptions(data);
this.logger.LogDebug($"WillPresentNotification: UNNotificationPresentationOptions={notificationPresentationOptions}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Diagnostics;
using FluentAssertions;
using Plugin.FirebasePushNotifications.Internals;

namespace Plugin.FirebasePushNotifications.Tests.Internals
{
public class NotificationRateLimiterTests
{
private readonly TimeSpan expirationPeriod;

public NotificationRateLimiterTests()
{
this.expirationPeriod = Debugger.IsAttached ?
TimeSpan.FromMilliseconds(10000) :
TimeSpan.FromMilliseconds(100);
}

[Fact]
public async Task HasReachedLimit_ShouldReturnFalse_WhenIdentifierHasExpired()
{
// Arrange
var rateLimiter = new NotificationRateLimiter(this.expirationPeriod);

// Act
var result1 = rateLimiter.HasReachedLimit("identifier1");
await Task.Delay(this.expirationPeriod);
var result2 = rateLimiter.HasReachedLimit("identifier1");

// Assert
result1.Should().BeFalse();
result2.Should().BeFalse();
rateLimiter.Count.Should().Be(1);
}

[Fact]
public void HasReachedLimit_ShouldReturnFalse_WhenNewIdentifierIsAdded()
{
// Arrange
var rateLimiter = new NotificationRateLimiter(this.expirationPeriod);

// Act
var result1 = rateLimiter.HasReachedLimit("identifier1");
var result2 = rateLimiter.HasReachedLimit("identifier2");

// Assert
result1.Should().BeFalse();
result2.Should().BeFalse();
rateLimiter.Count.Should().Be(1);
}
[Fact]
public void HasReachedLimit_ShouldReturnTrue_WhenIdentifierIsNotExpired()
{
// Arrange
var rateLimiter = new NotificationRateLimiter(this.expirationPeriod);

// Act
var result1 = rateLimiter.HasReachedLimit("identifier1");
var result2 = rateLimiter.HasReachedLimit("identifier1");

// Assert
result1.Should().BeFalse();
result2.Should().BeTrue();
rateLimiter.Count.Should().Be(1);
}
}
}

0 comments on commit d726d26

Please sign in to comment.