[backend/core] Fix AsChunkedAsyncEnumerable pagination
This commit is contained in:
parent
c02af726e1
commit
c68e0bbd94
4 changed files with 75 additions and 4 deletions
|
@ -114,7 +114,7 @@ public class AdminController(
|
||||||
{
|
{
|
||||||
var jobs = db.Jobs
|
var jobs = db.Jobs
|
||||||
.Where(p => p.Queue == queue && p.Status == Job.JobStatus.Failed)
|
.Where(p => p.Queue == queue && p.Status == Job.JobStatus.Failed)
|
||||||
.AsChunkedAsyncEnumerable(10);
|
.AsChunkedAsyncEnumerable(10, p => p.Id);
|
||||||
|
|
||||||
await foreach (var job in jobs)
|
await foreach (var job in jobs)
|
||||||
await queueSvc.RetryJobAsync(job);
|
await queueSvc.RetryJobAsync(job);
|
||||||
|
@ -127,7 +127,7 @@ public class AdminController(
|
||||||
var jobs = db.Jobs
|
var jobs = db.Jobs
|
||||||
.Where(p => p.Queue == queue && p.Status == Job.JobStatus.Failed)
|
.Where(p => p.Queue == queue && p.Status == Job.JobStatus.Failed)
|
||||||
.Where(p => p.Id >= from && p.Id <= to)
|
.Where(p => p.Id >= from && p.Id <= to)
|
||||||
.AsChunkedAsyncEnumerable(10);
|
.AsChunkedAsyncEnumerable(10, p => p.Id);
|
||||||
|
|
||||||
await foreach (var job in jobs)
|
await foreach (var job in jobs)
|
||||||
await queueSvc.RetryJobAsync(job);
|
await queueSvc.RetryJobAsync(job);
|
||||||
|
|
|
@ -22,6 +22,7 @@ public static class QueryableExtensions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Make sure to call .OrderBy() on the query, otherwise the results will be unpredictable.
|
/// Make sure to call .OrderBy() on the query, otherwise the results will be unpredictable.
|
||||||
|
/// Furthermore, this method is unsuitable for cases where the consumer removes elements from the original collection.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// The result set as an IAsyncEnumerable. Makes one DB roundtrip at the start of each chunk.
|
/// The result set as an IAsyncEnumerable. Makes one DB roundtrip at the start of each chunk.
|
||||||
|
@ -40,6 +41,76 @@ public static class QueryableExtensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="AsChunkedAsyncEnumerable{T}(System.Linq.IQueryable{T},int)" select="summary|returns"/>
|
||||||
|
/// <remarks>
|
||||||
|
/// This overload requires you to pass a predicate to the identifier.
|
||||||
|
/// When <paramref name="isOrdered"/> is set to false, .OrderBy(<paramref name="idPredicate"/>) is appended to the query.
|
||||||
|
/// </remarks>
|
||||||
|
public static async IAsyncEnumerable<TResult> AsChunkedAsyncEnumerable<TResult>(
|
||||||
|
this IQueryable<TResult> query, int chunkSize, Expression<Func<TResult, string>> idPredicate,
|
||||||
|
bool isOrdered = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var pred = idPredicate.Compile();
|
||||||
|
query = isOrdered ? query : query.OrderBy(idPredicate);
|
||||||
|
|
||||||
|
string? last = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
|
var final = last is not null ? query.Where(idPredicate.Compose(p => p.IsGreaterThan(last))) : query;
|
||||||
|
var res = await final.Take(chunkSize).ToArrayAsync();
|
||||||
|
if (res.Length == 0) break;
|
||||||
|
foreach (var item in res) yield return item;
|
||||||
|
if (res.Length < chunkSize) break;
|
||||||
|
last = pred.Invoke(res.Last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="AsChunkedAsyncEnumerable{T}(System.Linq.IQueryable{T},int,Expression{Func{T,string}},bool)"/>
|
||||||
|
public static async IAsyncEnumerable<TResult> AsChunkedAsyncEnumerable<TResult>(
|
||||||
|
this IQueryable<TResult> query, int chunkSize, Expression<Func<TResult, Guid>> idPredicate,
|
||||||
|
bool isOrdered = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var pred = idPredicate.Compile();
|
||||||
|
query = isOrdered ? query : query.OrderBy(idPredicate);
|
||||||
|
|
||||||
|
Guid? last = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
|
var final = last is not null ? query.Where(idPredicate.Compose(p => p > last)) : query;
|
||||||
|
var res = await final.Take(chunkSize).ToArrayAsync();
|
||||||
|
if (res.Length == 0) break;
|
||||||
|
foreach (var item in res) yield return item;
|
||||||
|
if (res.Length < chunkSize) break;
|
||||||
|
last = pred.Invoke(res.Last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="AsChunkedAsyncEnumerable{T}(System.Linq.IQueryable{T},int,Expression{Func{T,string}},bool)"/>
|
||||||
|
public static async IAsyncEnumerable<TResult> AsChunkedAsyncEnumerable<TResult>(
|
||||||
|
this IQueryable<TResult> query, int chunkSize, Expression<Func<TResult, int>> idPredicate,
|
||||||
|
bool isOrdered = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var pred = idPredicate.Compile();
|
||||||
|
query = isOrdered ? query : query.OrderBy(idPredicate);
|
||||||
|
|
||||||
|
int? last = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
|
var final = last is not null ? query.Where(idPredicate.Compose(p => p > last)) : query;
|
||||||
|
var res = await final.Take(chunkSize).ToArrayAsync();
|
||||||
|
if (res.Length == 0) break;
|
||||||
|
foreach (var item in res) yield return item;
|
||||||
|
if (res.Length < chunkSize) break;
|
||||||
|
last = pred.Invoke(res.Last());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static IQueryable<T> Paginate<T>(
|
public static IQueryable<T> Paginate<T>(
|
||||||
this IQueryable<T> query,
|
this IQueryable<T> query,
|
||||||
MastodonPaginationQuery pq,
|
MastodonPaginationQuery pq,
|
||||||
|
|
|
@ -507,7 +507,7 @@ public class ActivityHandlerService(
|
||||||
.OrderBy(p => p.Id)
|
.OrderBy(p => p.Id)
|
||||||
.Select(p => p.Follower)
|
.Select(p => p.Follower)
|
||||||
.PrecomputeRelationshipData(source)
|
.PrecomputeRelationshipData(source)
|
||||||
.AsChunkedAsyncEnumerable(50);
|
.AsChunkedAsyncEnumerable(50, p => p.Id, isOrdered: true);
|
||||||
|
|
||||||
await foreach (var follower in followers)
|
await foreach (var follower in followers)
|
||||||
{
|
{
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class MediaCleanupTask : ICronTask
|
||||||
var cnt = await fileIds.CountAsync();
|
var cnt = await fileIds.CountAsync();
|
||||||
|
|
||||||
logger.LogInformation("Expiring {count} files...", cnt);
|
logger.LogInformation("Expiring {count} files...", cnt);
|
||||||
await foreach (var fileId in fileIds.OrderBy(p => p).AsChunkedAsyncEnumerable(50))
|
await foreach (var fileId in fileIds.AsChunkedAsyncEnumerable(50, p => p))
|
||||||
{
|
{
|
||||||
await queueService.BackgroundTaskQueue.EnqueueAsync(new DriveFileDeleteJobData
|
await queueService.BackgroundTaskQueue.EnqueueAsync(new DriveFileDeleteJobData
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue