Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Task<int> UpdateAllAsync(
Task<int> DeleteAllAsync(Expression<Func<T, bool>> filter, CancellationToken cancellationToken = default);
Task<ISubscription<T>> SubscribeAsync(
Expression<Func<T, bool>> filter,
IEnumerable<(Expression<Func<T, object?>> Field, SortOrder SortOrder)>? sort = null,
CancellationToken cancellationToken = default
);
}
7 changes: 7 additions & 0 deletions src/DataAccess/src/SIL.DataAccess.Abstractions/SortOrder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SIL.DataAccess;

public enum SortOrder
{
Ascending,
Descending,
}
22 changes: 21 additions & 1 deletion src/DataAccess/src/SIL.DataAccess/MemoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,33 @@ public async Task<int> DeleteAllAsync(

public async Task<ISubscription<T>> SubscribeAsync(
Expression<Func<T, bool>> filter,
IEnumerable<(Expression<Func<T, object?>> Field, SortOrder SortOrder)>? sort = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
using (await _lock.LockAsync(cancellationToken))
{
T? initialEntity = Entities.AsQueryable().FirstOrDefault(filter);
IQueryable<T> query = Entities.AsQueryable().Where(filter);
if (sort is not null && sort.Any())
{
(Expression<Func<T, object?>> firstField, SortOrder firstSortOrder) = sort.First();
IOrderedQueryable<T> orderedQuery =
firstSortOrder == SortOrder.Ascending
? query.OrderBy(firstField)
: query.OrderByDescending(firstField);

foreach ((Expression<Func<T, object?>> field, SortOrder sortOrder) in sort.Skip(1))
{
orderedQuery =
sortOrder == SortOrder.Ascending
? orderedQuery.ThenBy(field)
: orderedQuery.ThenByDescending(field);
}

query = orderedQuery;
}
T? initialEntity = query.FirstOrDefault();
var subscription = new MemorySubscription<T>(initialEntity, RemoveSubscription);
_subscriptions[subscription] = filter.Compile();
return subscription;
Expand Down
16 changes: 16 additions & 0 deletions src/DataAccess/src/SIL.DataAccess/MongoRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ public async Task<int> DeleteAllAsync(

public async Task<ISubscription<T>> SubscribeAsync(
Expression<Func<T, bool>> filter,
IEnumerable<(Expression<Func<T, object?>> Field, SortOrder SortOrder)>? sort = null,
CancellationToken cancellationToken = default
)
{
Expand All @@ -253,6 +254,21 @@ public async Task<ISubscription<T>> SubscribeAsync(
{ "limit", 1 },
{ "singleBatch", true },
};
if (sort is not null && sort.Any())
{
findCommand.Add(
"sort",
Builders<T>
.Sort.Combine(
sort.Select(s =>
s.SortOrder == SortOrder.Ascending
? Builders<T>.Sort.Ascending(s.Field)
: Builders<T>.Sort.Descending(s.Field)
)
)
.Render(new RenderArgs<T>(_collection.DocumentSerializer, _collection.Settings.SerializerRegistry))
);
}
BsonDocument result;
if (_context.Session is not null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ public async Task<T> ReaderLockAsync<T>(
(bool acquired, DateTime expiresAt) = await TryAcquireReaderLock(lockId, resolvedLifetime, cancellationToken);
if (!acquired)
{
using ISubscription<RWLock> sub = await _locks.SubscribeAsync(rwl => rwl.Id == _id, cancellationToken);
using ISubscription<RWLock> sub = await _locks.SubscribeAsync(
rwl => rwl.Id == _id,
cancellationToken: cancellationToken
);
do
{
RWLock? rwLock = sub.Change.Entity;
Expand Down Expand Up @@ -134,7 +137,10 @@ await _locks.UpdateAsync(
);
try
{
using ISubscription<RWLock> sub = await _locks.SubscribeAsync(rwl => rwl.Id == _id, cancellationToken);
using ISubscription<RWLock> sub = await _locks.SubscribeAsync(
rwl => rwl.Id == _id,
cancellationToken: cancellationToken
);
do
{
RWLock? rwLock = sub.Change.Entity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
e.CurrentBuild != null
&& e.CurrentBuild.BuildJobRunner == BuildJobRunnerType.Local
&& e.CurrentBuild.JobState == BuildJobState.Pending,
stoppingToken
cancellationToken: stoppingToken
);
using ISubscription<WordAlignmentEngine> wordAlignmentSub = await wordAlignmentEngines.SubscribeAsync(
e =>
e.CurrentBuild != null
&& e.CurrentBuild.BuildJobRunner == BuildJobRunnerType.Local
&& e.CurrentBuild.JobState == BuildJobState.Pending,
stoppingToken
cancellationToken: stoppingToken
);

await RecoverPendingJobsAsync(scope.ServiceProvider, stoppingToken);
Expand Down
2 changes: 1 addition & 1 deletion src/Serval/src/Serval.ApiServer/nswag.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"generateContractsOutput": false,
"contractsNamespace": null,
"contractsOutputFilePath": null,
"parameterDateTimeFormat": "u",
"parameterDateTimeFormat": "o",
"parameterDateFormat": "yyyy-MM-dd",
"generateUpdateJsonSerializerSettingsMethod": true,
"useRequestAndResponseSerializationSettings": false,
Expand Down
34 changes: 16 additions & 18 deletions src/Serval/src/Serval.Client/Client.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7398,22 +7398,21 @@ public partial interface ITranslationBuildsClient
/// <summary>
/// Get all builds for your translation engines that are created after the specified date.
/// </summary>
/// <param name="createdAfter">The date and time in UTC that the builds were created after (optional).</param>
/// <param name="createdAfter">The date and time (either in UTC or with offset) that the builds were created after (optional).</param>
/// <returns>The engines</returns>
/// <exception cref="ServalApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task<System.Collections.Generic.IList<TranslationBuild>> GetAllBuildsCreatedAfterAsync(System.DateTimeOffset? createdAfter = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Get the next build that finished after the specified date and time.
/// <br/>If not build has yet completed after that timestamp,
/// <br/>Serval will wait until a build is finished after that date and time.
/// Get the next build that finishes after the specified build id.
/// <br/>If no build has yet completed after that id, or you do not specify the id,
/// <br/>Serval will wait until the next build is finished.
/// </summary>
/// <param name="finishedAfter">The date and time in UTC that the next build should have finished after.
/// <br/>You should use the finished timestamp of the build previously returned when calling this endpoint.</param>
/// <returns>The engines</returns>
/// <param name="finishedAfter">The id of the build that the next build must finish after (optional)</param>
/// <returns>The build</returns>
/// <exception cref="ServalApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task<TranslationBuild> GetNextFinishedBuildAsync(System.DateTimeOffset? finishedAfter = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
System.Threading.Tasks.Task<TranslationBuild> GetNextFinishedBuildAsync(string? finishedAfter = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));

}

Expand Down Expand Up @@ -7469,7 +7468,7 @@ public string BaseUrl
/// <summary>
/// Get all builds for your translation engines that are created after the specified date.
/// </summary>
/// <param name="createdAfter">The date and time in UTC that the builds were created after (optional).</param>
/// <param name="createdAfter">The date and time (either in UTC or with offset) that the builds were created after (optional).</param>
/// <returns>The engines</returns>
/// <exception cref="ServalApiException">A server side error occurred.</exception>
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.IList<TranslationBuild>> GetAllBuildsCreatedAfterAsync(System.DateTimeOffset? createdAfter = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
Expand All @@ -7490,7 +7489,7 @@ public string BaseUrl
urlBuilder_.Append('?');
if (createdAfter != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("created-after")).Append('=').Append(System.Uri.EscapeDataString(createdAfter.Value.ToString("u", System.Globalization.CultureInfo.InvariantCulture))).Append('&');
urlBuilder_.Append(System.Uri.EscapeDataString("created-after")).Append('=').Append(System.Uri.EscapeDataString(createdAfter.Value.ToString("o", System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
urlBuilder_.Length--;

Expand Down Expand Up @@ -7566,15 +7565,14 @@ public string BaseUrl

/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <summary>
/// Get the next build that finished after the specified date and time.
/// <br/>If not build has yet completed after that timestamp,
/// <br/>Serval will wait until a build is finished after that date and time.
/// Get the next build that finishes after the specified build id.
/// <br/>If no build has yet completed after that id, or you do not specify the id,
/// <br/>Serval will wait until the next build is finished.
/// </summary>
/// <param name="finishedAfter">The date and time in UTC that the next build should have finished after.
/// <br/>You should use the finished timestamp of the build previously returned when calling this endpoint.</param>
/// <returns>The engines</returns>
/// <param name="finishedAfter">The id of the build that the next build must finish after (optional)</param>
/// <returns>The build</returns>
/// <exception cref="ServalApiException">A server side error occurred.</exception>
public virtual async System.Threading.Tasks.Task<TranslationBuild> GetNextFinishedBuildAsync(System.DateTimeOffset? finishedAfter = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
public virtual async System.Threading.Tasks.Task<TranslationBuild> GetNextFinishedBuildAsync(string? finishedAfter = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var client_ = _httpClient;
var disposeClient_ = false;
Expand All @@ -7592,7 +7590,7 @@ public string BaseUrl
urlBuilder_.Append('?');
if (finishedAfter != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("finished-after")).Append('=').Append(System.Uri.EscapeDataString(finishedAfter.Value.ToString("u", System.Globalization.CultureInfo.InvariantCulture))).Append('&');
urlBuilder_.Append(System.Uri.EscapeDataString("finished-after")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(finishedAfter, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
urlBuilder_.Length--;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public static IServalConfigurator AddTranslationDataAccess(this IServalConfigura
.Merge(c, new MergeStageOptions<Build> { WhenMatched = MergeStageWhenMatched.Replace })
.ToListAsync(),
MongoMigrations.MigrateTargetQuoteConvention,
c =>
c.Indexes.CreateOrUpdateAsync(
new CreateIndexModel<Build>(Builders<Build>.IndexKeys.Ascending(b => b.DateFinished))
),
]
);
configurator.DataAccess.AddRepository<Pretranslation>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ public partial class TranslationBuildsController
/// <summary>
/// Get all builds for your translation engines that are created after the specified date.
/// </summary>
/// <param name="createdAfter">The date and time in UTC that the builds were created after (optional).</param>
/// <param name="createdAfter">
/// The date and time (either in UTC or with offset) that the builds were created after (optional).
/// </param>
/// <param name="cancellationToken"></param>
/// <response code="200">The engines</response>
/// <response code="401">The client is not authenticated.</response>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Serval.Translation.Features.Builds;

public record GetNextFinishedBuild(string Owner, DateTime FinishedAfter) : IRequest<GetNextFinishedBuildResponse>;
public record GetNextFinishedBuild(string Owner, string? Id = null) : IRequest<GetNextFinishedBuildResponse>;

public record GetNextFinishedBuildResponse(
[property: MemberNotNullWhen(false, nameof(Build))] bool TimedOut,
Expand All @@ -18,10 +18,14 @@ public async Task<GetNextFinishedBuildResponse> HandleAsync(
CancellationToken cancellationToken = default
)
{
DateTime finishedAfter =
request.FinishedAfter.Kind == DateTimeKind.Unspecified
? DateTime.SpecifyKind(request.FinishedAfter, DateTimeKind.Utc)
: request.FinishedAfter.ToUniversalTime();
DateTime dateFinished = DateTime.UtcNow;
string? id = request.Id;
if (id is not null)
{
Build? build = await builds.GetAsync(id, cancellationToken);
if (build is not null)
dateFinished = build.DateFinished ?? DateTime.UtcNow;
}

(_, EntityChange<Build> change) = await TaskEx.Timeout(
async ct =>
Expand All @@ -32,7 +36,10 @@ public async Task<GetNextFinishedBuildResponse> HandleAsync(
&& (
b.State == JobState.Completed || b.State == JobState.Canceled || b.State == JobState.Faulted
)
&& b.DateFinished > finishedAfter,
&& (
b.DateFinished > dateFinished || (b.DateFinished == dateFinished && b.Id.CompareTo(id) > 0)
),
[(b => b.DateFinished, SortOrder.Ascending), (b => b.Id, SortOrder.Ascending)],
ct
);
EntityChange<Build> curChange = subscription.Change;
Expand Down Expand Up @@ -61,16 +68,13 @@ await subscription.WaitForChangeAsync(
public partial class TranslationBuildsController
{
/// <summary>
/// Get the next build that finished after the specified date and time.
/// If not build has yet completed after that timestamp,
/// Serval will wait until a build is finished after that date and time.
/// Get the next build that finishes after the specified build id.
/// If no build has yet completed after that id, or you do not specify the id,
/// Serval will wait until the next build is finished.
/// </summary>
/// <param name="finishedAfter">
/// The date and time in UTC that the next build should have finished after.
/// You should use the <c>finished</c> timestamp of the build previously returned when calling this endpoint.
/// </param>
/// <param name="finishedAfter">The id of the build that the next build must finish after (optional)</param>
/// <param name="cancellationToken"></param>
/// <response code="200">The engines</response>
/// <response code="200">The build</response>
/// <response code="401">The client is not authenticated.</response>
/// <response code="403">The authenticated client cannot perform the operation.</response>
/// <response code="408">The long polling request timed out.</response>
Expand All @@ -83,7 +87,7 @@ public partial class TranslationBuildsController
[ProducesResponseType(typeof(void), StatusCodes.Status408RequestTimeout)]
[ProducesResponseType(typeof(void), StatusCodes.Status503ServiceUnavailable)]
public async Task<ActionResult<TranslationBuildDto>> GetNextFinishedBuildAsync(
[FromQuery(Name = "finished-after")] DateTime finishedAfter,
[FromQuery(Name = "finished-after")] string? finishedAfter,
[FromServices] IRequestHandler<GetNextFinishedBuild, GetNextFinishedBuildResponse> handler,
CancellationToken cancellationToken
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ internal static async Task<EntityChange<Build>> GetNewerRevisionAsync(
CancellationToken cancellationToken = default
)
{
using ISubscription<Build> subscription = await repository.SubscribeAsync(filter, cancellationToken);
using ISubscription<Build> subscription = await repository.SubscribeAsync(
filter,
cancellationToken: cancellationToken
);
EntityChange<Build> curChange = subscription.Change;
if (curChange.Type == EntityChangeType.Delete && minRevision > 1)
return curChange;
Expand Down
5 changes: 4 additions & 1 deletion src/Serval/src/Serval.WordAlignment/Services/BuildService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ private async Task<EntityChange<Build>> GetNewerRevisionAsync(
CancellationToken cancellationToken = default
)
{
using ISubscription<Build> subscription = await Entities.SubscribeAsync(filter, cancellationToken);
using ISubscription<Build> subscription = await Entities.SubscribeAsync(
filter,
cancellationToken: cancellationToken
);
EntityChange<Build> curChange = subscription.Change;
if (curChange.Type == EntityChangeType.Delete && minRevision > 1)
return curChange;
Expand Down
Loading
Loading