Skip to content

Commit

Permalink
Add separate very-long-running job for all users updates
Browse files Browse the repository at this point in the history
  • Loading branch information
stanriders committed Jul 1, 2024
1 parent bd2f34b commit f69e264
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 7 deletions.
2 changes: 1 addition & 1 deletion backend/Mutualify/Contracts/StatsContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public class StatsContract
{
public int RegisteredCount { get; set; }
public long RelationCount { get; set; }
public long UserCount { get; set; }
public int LastDayRegisteredCount { get; set; }
public int EligibleForUpdateCount { get; set; }
public int EligibleForUserUpdateCount { get; set; }
}
116 changes: 116 additions & 0 deletions backend/Mutualify/Jobs/UserAllUpdateJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@

using Hangfire;
using Hangfire.Server;
using Microsoft.EntityFrameworkCore;
using Mutualify.Database;
using Mutualify.Services.Interfaces;

namespace Mutualify.Jobs;
public interface IUserAllUpdateJob
{
Task Run(PerformContext context, CancellationToken token);
}

/// <summary>
/// Apparently there are ~400k users in the database so updating them every day is not really possible
/// </summary>
public class UserAllUpdateJob : IUserAllUpdateJob
{
private readonly DatabaseContext _databaseContext;
private readonly IUsersService _usersService;
private readonly ILogger<UserUpdateJob> _logger;

private const double _interval = 5; // seconds

private static bool _isRunning = false;
private static DateTime _lastStartDate;

public UserAllUpdateJob(IUsersService usersService, ILogger<UserUpdateJob> logger, DatabaseContext databaseContext)
{
_usersService = usersService;
_logger = logger;
_databaseContext = databaseContext;
}

[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 24 * 14)]
public async Task Run(PerformContext context, CancellationToken token)
{
var jobId = context.BackgroundJob.Id;

using var _ = _logger.BeginScope("UserAllUpdateJob");

_logger.LogInformation("[{JobId}] Starting all users update job...", jobId);

if (_isRunning && _lastStartDate.AddDays(1) > DateTime.Now)
{
_logger.LogInformation("[{JobId}] Job is already running, abort!", jobId);
return;
}

_isRunning = true;
_lastStartDate = DateTime.Now;

// since we might be running for a month shouldn't this be updated at some point?
var userUpdateQueue = await _databaseContext.Users.AsNoTracking()
.Where(x => x.UpdatedAt == null || x.UpdatedAt < DateTime.UtcNow.AddDays(-14))
.Select(x => x.Id)
.ToListAsync(cancellationToken: token);

for (var i = 0; i < userUpdateQueue.Count; i++)
{
token.ThrowIfCancellationRequested();

var userId = userUpdateQueue[i];
var startTime = DateTime.Now;

try
{
#if !DEBUG
if (i % 1000 == 0)
{
#endif
_logger.LogInformation("[{JobId}] ({Current}/{Total}) Updating {Id}...", jobId, i + 1,
userUpdateQueue.Count, userId);
#if !DEBUG
}
#endif

await _usersService.Update(userId, false);
}
catch (AggregateException e)
{
if (e.InnerException is HttpRequestException)
{
// don't fail on HttpRequestExceptions, just keep going
continue;
}

_isRunning = false;

throw;
}
catch (DbUpdateConcurrencyException) { } // don't fail on HttpRequestExceptions or DbUpdateConcurrencyException, just keep going
catch (HttpRequestException) { }
catch (OperationCanceledException)
{
_logger.LogWarning("[{JobId}] All users update job has been cancelled!", jobId);

_isRunning = false;
return;
}
finally
{
var endTime = DateTime.Now;

// yea its not really accurate but we don't need precision, just run it approximately every X seconds
var elapsed = endTime - startTime;
var timeout = elapsed.TotalSeconds < _interval ? _interval - (int) elapsed.TotalSeconds : 0;

await Task.Delay((int)(timeout * 1000), token);
}
}

_isRunning = false;
_logger.LogInformation("[{JobId}] Finished all users update job", jobId);
}
}
5 changes: 3 additions & 2 deletions backend/Mutualify/Jobs/UserUpdateJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class UserUpdateJob : IUserUpdateJob
private readonly IUsersService _usersService;
private readonly ILogger<UserUpdateJob> _logger;

private const int _interval = 2; // seconds
private const double _interval = 2; // seconds

private static bool _isRunning = false;
private static DateTime _lastStartDate;
Expand Down Expand Up @@ -48,6 +48,7 @@ public async Task Run(PerformContext context, CancellationToken token)
.Where(x=> x.UpdatedAt == null || x.UpdatedAt < DateTime.UtcNow.AddDays(-1))
.OrderByDescending(x=> x.FollowerCount)
.Select(x => x.Id)
.Take(15000) // see UserAllUpdateJob.cs
.ToListAsync(cancellationToken: token);

for (var i = 0; i < userUpdateQueue.Count; i++)
Expand Down Expand Up @@ -100,7 +101,7 @@ public async Task Run(PerformContext context, CancellationToken token)
var elapsed = endTime - startTime;
var timeout = elapsed.TotalSeconds < _interval ? _interval - (int) elapsed.TotalSeconds : 0;

await Task.Delay(timeout * 1000, token);
await Task.Delay((int)(timeout * 1000), token);
}
}

Expand Down
1 change: 1 addition & 0 deletions backend/Mutualify/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ static Task UnauthorizedRedirect(RedirectContext<CookieAuthenticationOptions> co

RecurringJob.AddOrUpdate<IUserRelationsUpdateJob>("user-relations-update", x => x.Run(null!, CancellationToken.None), Cron.Daily(12));
RecurringJob.AddOrUpdate<IUserUpdateJob>("users-update", x => x.Run(null!, CancellationToken.None), Cron.Daily());
RecurringJob.AddOrUpdate<IUserAllUpdateJob>("users-update-all", x => x.Run(null!, CancellationToken.None), Cron.Monthly(3));
//BackgroundJob.Enqueue<IUserPopulateJob>(x => x.Run(null!, JobCancellationToken.Null));

try
Expand Down
8 changes: 4 additions & 4 deletions backend/Mutualify/Services/UsersService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,15 @@ public async Task<StatsContract> GetStats()
var relationsUpdateEligible = await _databaseContext.Tokens.AsNoTracking()
.CountAsync();

var userUpdateEligible = await _databaseContext.Users.AsNoTracking()
.Where(x => x.UpdatedAt == null || x.UpdatedAt < DateTime.UtcNow.AddDays(-1))
.CountAsync();
var userCount = await _databaseContext.Users.AsNoTracking()
.LongCountAsync();

return new StatsContract
{
RegisteredCount = registeredUsers,
RelationCount = relationCount,
UserCount = userCount,
EligibleForUpdateCount = relationsUpdateEligible,
EligibleForUserUpdateCount = userUpdateEligible,
LastDayRegisteredCount = lastDayRegistered
};
}
Expand Down Expand Up @@ -138,6 +137,7 @@ public async Task Update(int userId, bool useTokens)

if (osuUser is null)
{
_logger.LogError("User {User} doesn't exist according to API!", userId);
return;
}

Expand Down

0 comments on commit f69e264

Please sign in to comment.