Files
UnrealEngineUWP/Engine/Source/Programs/Horde/Horde.Build/Issues/IssueCollection.cs
Ben Marsh 330924441f Horde: Report "Horde" as the user name when an issue is closed automatically.
#preflight none

[CL 22749933 by Ben Marsh in ue5-main branch]
2022-10-25 09:53:49 -04:00

1430 lines
47 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using EpicGames.Redis.Utility;
using Horde.Build.Auditing;
using Horde.Build.Jobs;
using Horde.Build.Jobs.Graphs;
using Horde.Build.Logs;
using Horde.Build.Server;
using Horde.Build.Streams;
using Horde.Build.Users;
using Horde.Build.Utilities;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
namespace Horde.Build.Issues
{
using JobId = ObjectId<IJob>;
using LogId = ObjectId<ILogFile>;
using StreamId = StringId<IStream>;
using TemplateId = StringId<ITemplateRef>;
using UserId = ObjectId<IUser>;
class IssueCollection : IIssueCollection
{
[SingletonDocument("issue-ledger", "5e4c226440ce25fa3207a9af")]
class IssueLedger : SingletonBase
{
public int NextId { get; set; }
}
class Issue : IIssue
{
[BsonId]
public int Id { get; set; }
public string Summary { get; set; }
[BsonIgnoreIfNull]
public string? UserSummary { get; set; }
[BsonIgnoreIfNull]
public string? Description { get; set; }
[BsonIgnoreIfNull]
public IssueFingerprint? Fingerprint { get; set; }
public List<IssueFingerprint>? Fingerprints { get; set; }
public IssueSeverity Severity { get; set; }
[BsonElement("PromotedV2")]
public bool Promoted { get; set; }
[BsonIgnoreIfNull]
public bool? ManuallyPromoted { get; set; }
[BsonIgnoreIfNull, BsonElement("Promoted")]
public bool? ManuallyPromotedDeprecated { get; set; }
[BsonIgnoreIfNull]
public UserId? OwnerId { get; set; }
[BsonIgnoreIfNull]
public UserId? DefaultOwnerId { get; set; }
[BsonIgnoreIfNull]
public UserId? NominatedById { get; set; }
public DateTime CreatedAt { get; set; }
[BsonIgnoreIfNull]
public DateTime? NominatedAt { get; set; }
[BsonIgnoreIfNull]
public DateTime? AcknowledgedAt { get; set; }
[BsonIgnoreIfNull]
public DateTime? ResolvedAt { get; set; }
[BsonIgnoreIfNull]
public UserId? ResolvedById { get; set; }
[BsonIgnoreIfNull]
public DateTime? VerifiedAt { get; set; }
public DateTime LastSeenAt { get; set; }
[BsonIgnoreIfNull]
public int? FixChange { get; set; }
public List<IssueStream> Streams { get; set; } = new List<IssueStream>();
public int MinSuspectChange { get; set; }
public int MaxSuspectChange { get; set; }
[BsonElement("Suspects"), BsonIgnoreIfNull]
public List<IssueSuspect>? SuspectsDeprecated { get; set; }
[BsonIgnoreIfNull]
public List<ObjectId>? ExcludeSpans { get; set; }
public int UpdateIndex { get; set; }
IReadOnlyList<IIssueFingerprint> IIssue.Fingerprints => Fingerprints ?? ((Fingerprint == null)? new List<IssueFingerprint>() : new List<IssueFingerprint> { Fingerprint });
UserId? IIssue.OwnerId => OwnerId ?? DefaultOwnerId ?? GetDefaultOwnerId();
IReadOnlyList<IIssueStream> IIssue.Streams => Streams;
DateTime IIssue.LastSeenAt => (LastSeenAt == default) ? DateTime.UtcNow : LastSeenAt;
[BsonIgnoreIfNull]
public string? ExternalIssueKey { get; set; }
[BsonIgnoreIfNull]
public UserId? QuarantinedByUserId { get; set; }
[BsonIgnoreIfNull]
public DateTime? QuarantineTimeUtc { get; set; }
[BsonIgnoreIfNull]
public UserId? ForceClosedByUserId { get; set; }
[BsonConstructor]
private Issue()
{
Summary = String.Empty;
Fingerprint = null!;
}
public Issue(int id, string summary)
{
Id = id;
Summary = summary;
CreatedAt = DateTime.UtcNow;
LastSeenAt = DateTime.UtcNow;
}
UserId? GetDefaultOwnerId()
{
if (SuspectsDeprecated != null && SuspectsDeprecated.Count > 0)
{
UserId possibleOwner = SuspectsDeprecated[0].AuthorId;
if (SuspectsDeprecated.All(x => x.AuthorId == possibleOwner) && SuspectsDeprecated.Any(x => x.DeclinedAt == null))
{
return SuspectsDeprecated[0].AuthorId;
}
}
return null;
}
public string FingerprintsDesc
{
get
{
if (Fingerprints == null || Fingerprints.Count == 0)
{
return String.Empty;
}
return String.Join(", ", Fingerprints.Select(x => {
return $"(Type: {x.Type} / Keys: {String.Join(", ", x.Keys)} / RejectKeys: {String.Join(", ", x.RejectKeys ?? new CaseInsensitiveStringSet(new string[] {"No Reject Keys"}))})";
}));
}
}
}
class IssueStream : IIssueStream
{
public StreamId StreamId { get; set; }
public bool? MergeOrigin { get; set; }
public bool? ContainsFix { get; set; }
public bool? FixFailed { get; set; }
public IssueStream()
{
}
public IssueStream(IIssueStream other)
{
StreamId = other.StreamId;
MergeOrigin = other.MergeOrigin;
ContainsFix = other.ContainsFix;
FixFailed = other.FixFailed;
}
}
class IssueSuspect : IIssueSuspect
{
public ObjectId Id { get; set; }
public int IssueId { get; set; }
public UserId AuthorId { get; set; }
public int Change { get; set; }
public DateTime? DeclinedAt { get; set; }
public DateTime? ResolvedAt { get; set; } // Degenerate
private IssueSuspect()
{
}
public IssueSuspect(int issueId, NewIssueSuspectData newSuspect, DateTime? resolvedAt)
{
Id = ObjectId.GenerateNewId();
IssueId = issueId;
AuthorId = newSuspect.AuthorId;
Change = newSuspect.Change;
ResolvedAt = resolvedAt;
}
public IssueSuspect(int issueId, IIssueSpanSuspect suspect)
: this(issueId, suspect.AuthorId, suspect.OriginatingChange ?? suspect.Change, null, null)
{
}
public IssueSuspect(int issueId, UserId authorId, int change, DateTime? declinedAt, DateTime? resolvedAt)
{
Id = ObjectId.GenerateNewId();
IssueId = issueId;
AuthorId = authorId;
Change = change;
DeclinedAt = declinedAt;
ResolvedAt = resolvedAt;
}
}
class IssueFingerprint : IIssueFingerprint
{
public string Type { get; set; }
public CaseInsensitiveStringSet Keys { get; set; }
public CaseInsensitiveStringSet? RejectKeys { get; set; }
public CaseInsensitiveStringSet? Metadata { get; set; }
[BsonConstructor]
private IssueFingerprint()
{
Type = String.Empty;
Keys = new CaseInsensitiveStringSet();
}
public IssueFingerprint(IIssueFingerprint fingerprint)
{
Type = fingerprint.Type;
Keys = fingerprint.Keys;
RejectKeys = fingerprint.RejectKeys;
Metadata = fingerprint.Metadata;
}
}
class IssueSpan : IIssueSpan
{
public ObjectId Id { get; set; }
[BsonRequired]
public StreamId StreamId { get; set; }
[BsonRequired]
public string StreamName { get; set; }
[BsonRequired]
public TemplateId TemplateRefId { get; set; }
[BsonRequired]
public string NodeName { get; set; }
public DateTime? ResolvedAt { get; set; } // Propagated from the owning issue
[BsonRequired]
public IssueFingerprint Fingerprint { get; set; }
public int MinChange { get; set; }
public int MaxChange { get; set; } = Int32.MaxValue;
public IssueStep? LastSuccess { get; set; }
[BsonRequired]
public IssueStep FirstFailure { get; set; }
[BsonRequired]
public IssueStep LastFailure { get; set; }
public IssueStep? NextSuccess { get; set; }
public bool? PromoteByDefault { get; set; }
[BsonElement("NotifySuspects"), BsonIgnoreIfDefault(false)]
public bool NotifySuspectsDeprecated { get; set; }
bool IIssueSpan.PromoteByDefault => PromoteByDefault ?? NotifySuspectsDeprecated;
public List<IssueSpanSuspect> Suspects { get; set; }
public int IssueId { get; set; }
public int UpdateIndex { get; set; }
IIssueStep? IIssueSpan.LastSuccess => LastSuccess;
IIssueStep IIssueSpan.FirstFailure => FirstFailure;
IIssueStep IIssueSpan.LastFailure => LastFailure;
IIssueStep? IIssueSpan.NextSuccess => NextSuccess;
IReadOnlyList<IIssueSpanSuspect> IIssueSpan.Suspects => Suspects;
IIssueFingerprint IIssueSpan.Fingerprint => Fingerprint;
private IssueSpan()
{
StreamName = null!;
NodeName = null!;
Fingerprint = null!;
FirstFailure = null!;
LastFailure = null!;
Suspects = new List<IssueSpanSuspect>();
}
public IssueSpan(int issueId, NewIssueSpanData newSpan)
{
Id = ObjectId.GenerateNewId();
StreamId = newSpan.StreamId;
StreamName = newSpan.StreamName;
TemplateRefId = newSpan.TemplateRefId;
NodeName = newSpan.NodeName;
Fingerprint = new IssueFingerprint(newSpan.Fingerprint);
if (newSpan.LastSuccess != null)
{
MinChange = newSpan.LastSuccess.Change;
LastSuccess = new IssueStep(Id, newSpan.LastSuccess);
}
FirstFailure = new IssueStep(Id, newSpan.FirstFailure);
LastFailure = new IssueStep(Id, newSpan.FirstFailure);
if (newSpan.NextSuccess != null)
{
MaxChange = newSpan.NextSuccess.Change;
NextSuccess = new IssueStep(Id, newSpan.NextSuccess);
}
PromoteByDefault = newSpan.FirstFailure.PromoteByDefault;
Suspects = newSpan.Suspects.ConvertAll(x => new IssueSpanSuspect(x));
IssueId = issueId;
}
}
class IssueSpanSuspect : IIssueSpanSuspect
{
public int Change { get; set; }
public UserId AuthorId { get; set; }
public int? OriginatingChange { get; set; }
[BsonConstructor]
private IssueSpanSuspect()
{
}
public IssueSpanSuspect(NewIssueSpanSuspectData newSuspectData)
{
Change = newSuspectData.Change;
AuthorId = newSuspectData.AuthorId;
OriginatingChange = newSuspectData.OriginatingChange;
}
}
class IssueStep : IIssueStep
{
public ObjectId Id { get; set; }
public ObjectId SpanId { get; set; }
public int Change { get; set; }
public IssueSeverity Severity { get; set; }
[BsonRequired]
public string JobName { get; set; }
[BsonRequired]
public JobId JobId { get; set; }
[BsonRequired]
public SubResourceId BatchId { get; set; }
[BsonRequired]
public SubResourceId StepId { get; set; }
public DateTime StepTime { get; set; }
public LogId? LogId { get; set; }
[BsonIgnoreIfNull]
public NodeAnnotations? Annotations { get; set; }
public bool? PromoteByDefault { get; set; }
[BsonElement("NotifySuspects"), BsonIgnoreIfDefault(false)]
public bool NotifySuspectsDeprecated { get; set; }
IReadOnlyNodeAnnotations IIssueStep.Annotations => Annotations ?? NodeAnnotations.Empty;
bool IIssueStep.PromoteByDefault => PromoteByDefault ?? NotifySuspectsDeprecated;
[BsonConstructor]
private IssueStep()
{
JobName = null!;
}
public IssueStep(ObjectId spanId, NewIssueStepData stepData)
{
Id = ObjectId.GenerateNewId();
SpanId = spanId;
Change = stepData.Change;
Severity = stepData.Severity;
JobName = stepData.JobName;
JobId = stepData.JobId;
BatchId = stepData.BatchId;
StepId = stepData.StepId;
StepTime = stepData.StepTime;
LogId = stepData.LogId;
Annotations = stepData.Annotations;
PromoteByDefault = stepData.PromoteByDefault;
}
}
readonly RedisService _redisService;
readonly IUserCollection _userCollection;
readonly ISingletonDocument<IssueLedger> _ledgerSingleton;
readonly IMongoCollection<Issue> _issues;
readonly IMongoCollection<IssueSpan> _issueSpans;
readonly IMongoCollection<IssueStep> _issueSteps;
readonly IMongoCollection<IssueSuspect> _issueSuspects;
readonly IAuditLog<int> _auditLog;
readonly ILogger _logger;
public IssueCollection(MongoService mongoService, RedisService redisService, IUserCollection userCollection, IAuditLogFactory<int> auditLogFactory, ILogger<IssueCollection> logger)
{
_redisService = redisService;
_userCollection = userCollection;
_logger = logger;
_ledgerSingleton = new SingletonDocument<IssueLedger>(mongoService);
List<MongoIndex<Issue>> issueIndexes = new List<MongoIndex<Issue>>();
issueIndexes.Add(keys => keys.Ascending(x => x.ResolvedAt));
issueIndexes.Add(keys => keys.Ascending(x => x.VerifiedAt));
_issues = mongoService.GetCollection<Issue>("IssuesV2");
List<MongoIndex<IssueSpan>> issueSpanIndexes = new List<MongoIndex<IssueSpan>>();
issueSpanIndexes.Add(keys => keys.Ascending(x => x.IssueId));
issueSpanIndexes.Add(keys => keys.Ascending(x => x.StreamId).Ascending(x => x.MinChange).Ascending(x => x.MaxChange));
issueSpanIndexes.Add("StreamChanges", keys => keys.Ascending(x => x.StreamId).Ascending(x => x.TemplateRefId).Ascending(x => x.NodeName).Ascending(x => x.MinChange).Ascending(x => x.MaxChange));
_issueSpans = mongoService.GetCollection<IssueSpan>("IssuesV2.Spans", issueSpanIndexes);
List<MongoIndex<IssueStep>> issueStepIndexes = new List<MongoIndex<IssueStep>>();
issueStepIndexes.Add(keys => keys.Ascending(x => x.SpanId));
issueStepIndexes.Add(keys => keys.Ascending(x => x.JobId).Ascending(x => x.BatchId).Ascending(x => x.StepId));
_issueSteps = mongoService.GetCollection<IssueStep>("IssuesV2.Steps", issueStepIndexes);
List<MongoIndex<IssueSuspect>> issueSuspectIndexes = new List<MongoIndex<IssueSuspect>>();
issueSuspectIndexes.Add(keys => keys.Ascending(x => x.Change));
issueSuspectIndexes.Add(keys => keys.Ascending(x => x.AuthorId).Ascending(x => x.ResolvedAt));
issueSuspectIndexes.Add(keys => keys.Ascending(x => x.IssueId).Ascending(x => x.Change), unique: true);
_issueSuspects = mongoService.GetCollection<IssueSuspect>("IssuesV2.Suspects", issueSuspectIndexes);
_auditLog = auditLogFactory.Create("IssuesV2.History", "IssueId");
if (!mongoService.ReadOnlyMode)
{
}
}
/// <inheritdoc/>
public async Task<IAsyncDisposable> EnterCriticalSectionAsync()
{
Stopwatch timer = Stopwatch.StartNew();
TimeSpan nextNotifyTime = TimeSpan.FromSeconds(2.0);
RedisLock issueLock = new (_redisService.GetDatabase(), "issues/lock");
while (!await issueLock.AcquireAsync(TimeSpan.FromMinutes(1)))
{
if (timer.Elapsed > nextNotifyTime)
{
_logger.LogWarning("Waiting on lock over issue collection for {TimeSpan}", timer.Elapsed);
nextNotifyTime *= 2;
}
await Task.Delay(TimeSpan.FromMilliseconds(100));
}
return issueLock;
}
async Task<Issue?> TryUpdateIssueAsync(IIssue issue, UpdateDefinition<Issue> update)
{
Issue issueDocument = (Issue)issue;
int prevUpdateIndex = issueDocument.UpdateIndex;
update = update.Set(x => x.UpdateIndex, prevUpdateIndex + 1);
FindOneAndUpdateOptions<Issue, Issue> options = new FindOneAndUpdateOptions<Issue, Issue> { ReturnDocument = ReturnDocument.After };
return await _issues.FindOneAndUpdateAsync<Issue>(x => x.Id == issueDocument.Id && x.UpdateIndex == prevUpdateIndex, update, options);
}
async Task<IssueSpan?> TryUpdateSpanAsync(IIssueSpan issueSpan, UpdateDefinition<IssueSpan> update)
{
IssueSpan issueSpanDocument = (IssueSpan)issueSpan;
int prevUpdateIndex = issueSpanDocument.UpdateIndex;
update = update.Set(x => x.UpdateIndex, prevUpdateIndex + 1);
FindOneAndUpdateOptions<IssueSpan, IssueSpan> options = new FindOneAndUpdateOptions<IssueSpan, IssueSpan> { ReturnDocument = ReturnDocument.After };
return await _issueSpans.FindOneAndUpdateAsync<IssueSpan>(x => x.Id == issueSpanDocument.Id && x.UpdateIndex == prevUpdateIndex, update, options);
}
#region Issues
/// <inheritdoc/>
public async Task<IIssue> AddIssueAsync(string summary)
{
IssueLedger ledger = await _ledgerSingleton.UpdateAsync(x => x.NextId++);
Issue newIssue = new Issue(ledger.NextId, summary);
await _issues.InsertOneAsync(newIssue);
ILogger issueLogger = GetLogger(newIssue.Id);
issueLogger.LogInformation("Created issue {IssueId}", newIssue.Id);
return newIssue;
}
async ValueTask<string> GetUserNameAsync(UserId? UserId)
{
if (UserId == null)
{
return "null";
}
else if (UserId == IIssue.ResolvedByUnknownId)
{
return "Horde (Unknown)";
}
else if (UserId == IIssue.ResolvedByTimeoutId)
{
return "Horde (Timeout)";
}
IUser? user = await _userCollection.GetCachedUserAsync(UserId);
if (user == null)
{
return "Unknown user";
}
return user.Name;
}
async Task LogIssueChangesAsync(UserId? initiatedByUserId, Issue oldIssue, Issue newIssue)
{
ILogger issueLogger = GetLogger(oldIssue.Id);
using IDisposable scope = issueLogger.BeginScope("User {UserName} ({UserId})", await GetUserNameAsync(initiatedByUserId), initiatedByUserId ?? UserId.Empty);
await LogIssueChangesImplAsync(issueLogger, oldIssue, newIssue);
}
async Task LogIssueChangesImplAsync(ILogger issueLogger, Issue oldIssue, Issue newIssue)
{
if (newIssue.Severity != oldIssue.Severity)
{
issueLogger.LogInformation("Changed severity to {Severity}", newIssue.Severity);
}
if (newIssue.Summary != oldIssue.Summary)
{
issueLogger.LogInformation("Changed summary to \"{Summary}\"", newIssue.Summary);
}
if (newIssue.Description != oldIssue.Description)
{
issueLogger.LogInformation("Description set to {Value}", newIssue.Description);
}
if (((IIssue)newIssue).Promoted != ((IIssue)oldIssue).Promoted)
{
issueLogger.LogInformation("Promoted set to {Value}", ((IIssue)newIssue).Promoted);
}
if (newIssue.OwnerId != oldIssue.OwnerId)
{
if (newIssue.NominatedById != null)
{
issueLogger.LogInformation("User {UserName} ({UserId}) was nominated by {NominatedByUserName} ({NominatedByUserId})", await GetUserNameAsync(newIssue.OwnerId), newIssue.OwnerId, await GetUserNameAsync(newIssue.NominatedById), newIssue.NominatedById);
}
else
{
issueLogger.LogInformation("User {UserName} ({UserId}) was nominated by default", await GetUserNameAsync(newIssue.OwnerId), newIssue.OwnerId);
}
}
if (newIssue.AcknowledgedAt != oldIssue.AcknowledgedAt)
{
if (newIssue.AcknowledgedAt == null)
{
issueLogger.LogInformation("Issue was un-acknowledged by {UserName} ({UserId})", await GetUserNameAsync(oldIssue.OwnerId), oldIssue.OwnerId);
}
else
{
issueLogger.LogInformation("Issue was acknowledged by {UserName} ({UserId})", await GetUserNameAsync(newIssue.OwnerId), newIssue.OwnerId);
}
}
if (newIssue.FixChange != oldIssue.FixChange)
{
if (newIssue.FixChange == 0)
{
issueLogger.LogInformation("Issue was marked as not fixed");
}
else
{
issueLogger.LogInformation("Issue was marked as fixed in {Change}", newIssue.FixChange);
}
}
if (newIssue.ResolvedById != oldIssue.ResolvedById)
{
if (newIssue.ResolvedById == null)
{
issueLogger.LogInformation("Marking as unresolved");
}
else
{
issueLogger.LogInformation("Resolved by {UserName} ({UserId})", await GetUserNameAsync(newIssue.ResolvedById), newIssue.ResolvedById);
}
}
if (newIssue.ResolvedAt != oldIssue.ResolvedAt)
{
if (newIssue.ResolvedAt == null)
{
issueLogger.LogInformation("Clearing resolved at time");
}
else
{
issueLogger.LogInformation("Setting resolved at time");
}
}
if (newIssue.VerifiedAt != oldIssue.VerifiedAt)
{
if (newIssue.VerifiedAt == null)
{
issueLogger.LogInformation("Clearing verified at time");
}
else
{
issueLogger.LogInformation("Setting verified at time");
}
}
if (newIssue.ExternalIssueKey != oldIssue.ExternalIssueKey)
{
if (newIssue.ExternalIssueKey != null)
{
issueLogger.LogInformation("Linked to external issue {ExternalIssueKey}", newIssue.ExternalIssueKey);
}
else
{
issueLogger.LogInformation("Unlinked from external issue {ExternalIssueKey}", oldIssue.ExternalIssueKey);
}
}
if (newIssue.QuarantinedByUserId != oldIssue.QuarantinedByUserId)
{
if (newIssue.QuarantinedByUserId != null)
{
issueLogger.LogInformation("Quarantined by {UserName} ({UserId})", await GetUserNameAsync(newIssue.QuarantinedByUserId), newIssue.QuarantinedByUserId);
}
else
{
issueLogger.LogInformation("Quarantine cleared");
}
}
if (newIssue.ForceClosedByUserId != oldIssue.ForceClosedByUserId)
{
if (newIssue.ForceClosedByUserId != null)
{
issueLogger.LogInformation("Forced closed by {UserName} ({UserId})", await GetUserNameAsync(newIssue.ForceClosedByUserId), newIssue.ForceClosedByUserId);
}
else
{
issueLogger.LogInformation("Force closed cleared");
}
}
string oldFingerprints = oldIssue.FingerprintsDesc;
string newFingerprints = newIssue.FingerprintsDesc;
if (oldFingerprints != newFingerprints)
{
issueLogger.LogInformation("Fingerprints changed {Fingerprints}", newFingerprints);
}
HashSet<StreamId> oldFixStreams = new HashSet<StreamId>(oldIssue.Streams.Where(x => x.ContainsFix ?? false).Select(x => x.StreamId));
HashSet<StreamId> newFixStreams = new HashSet<StreamId>(newIssue.Streams.Where(x => x.ContainsFix ?? false).Select(x => x.StreamId));
foreach (StreamId streamId in newFixStreams.Where(x => !oldFixStreams.Contains(x)))
{
issueLogger.LogInformation("Marking stream {StreamId} as fixed", streamId);
}
foreach (StreamId streamId in oldFixStreams.Where(x => !newFixStreams.Contains(x)))
{
issueLogger.LogInformation("Marking stream {StreamId} as not fixed", streamId);
}
}
async Task LogIssueSuspectChangesAsync(ILogger issueLogger, List<IssueSuspect> oldIssueSuspects, List<IssueSuspect> newIssueSuspects)
{
HashSet<(UserId, int)> oldSuspects = new HashSet<(UserId, int)>(oldIssueSuspects.Select(x => (x.AuthorId, x.Change)));
HashSet<(UserId, int)> newSuspects = new HashSet<(UserId, int)>(newIssueSuspects.Select(x => (x.AuthorId, x.Change)));
foreach ((UserId userId, int change) in newSuspects.Where(x => !oldSuspects.Contains(x)))
{
issueLogger.LogInformation("Added suspect {UserName} ({UserId}) for change {Change}", await GetUserNameAsync(userId), userId, change);
}
foreach ((UserId userId, int change) in oldSuspects.Where(x => !newSuspects.Contains(x)))
{
issueLogger.LogInformation("Removed suspect {UserName} ({UserId}) for change {Change}", await GetUserNameAsync(userId), userId, change);
}
HashSet<UserId> oldDeclinedBy = new HashSet<UserId>(oldIssueSuspects.Where(x => x.DeclinedAt != null).Select(x => x.AuthorId));
HashSet<UserId> newDeclinedBy = new HashSet<UserId>(newIssueSuspects.Where(x => x.DeclinedAt != null).Select(x => x.AuthorId));
foreach (UserId addDeclinedBy in newDeclinedBy.Where(x => !oldDeclinedBy.Contains(x)))
{
issueLogger.LogInformation("Declined by {UserName} ({UserId})", await GetUserNameAsync(addDeclinedBy), addDeclinedBy);
}
foreach (UserId removeDeclinedBy in oldDeclinedBy.Where(x => !newDeclinedBy.Contains(x)))
{
issueLogger.LogInformation("Un-declined by {UserName} ({UserId})", await GetUserNameAsync(removeDeclinedBy), removeDeclinedBy);
}
}
/// <inheritdoc/>
public async Task<IIssue?> GetIssueAsync(int issueId)
{
Issue issue = await _issues.Find(x => x.Id == issueId).FirstOrDefaultAsync();
return issue;
}
/// <inheritdoc/>
public Task<List<IIssueSuspect>> FindSuspectsAsync(int issueId)
{
return _issueSuspects.Find(x => x.IssueId == issueId).ToListAsync<IssueSuspect, IIssueSuspect>();
}
class ProjectedIssueId
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles")]
public int? _id { get; set; }
}
/// <inheritdoc/>
public async Task<List<IIssue>> FindIssuesAsync(IEnumerable<int>? ids = null, UserId? ownerId = null, StreamId? streamId = null, int? minChange = null, int? maxChange = null, bool? resolved = null, bool? promoted = null, int? index = null, int? count = null)
{
List<Issue> results;
if (ownerId == null)
{
results = await FilterIssuesByStreamIdAsync(ids, streamId, minChange, maxChange, resolved ?? false, promoted, index ?? 0, count);
}
else
{
results = await _issues.Find(x => x.OwnerId == ownerId).ToListAsync();
}
return results.ConvertAll<IIssue>(x => x);
}
async Task<List<Issue>> FilterIssuesByStreamIdAsync(IEnumerable<int>? ids, StreamId? streamId, int? minChange, int? maxChange, bool? resolved, bool? promoted, int index, int? count)
{
if (streamId == null)
{
return await FilterIssuesByOtherFieldsAsync(ids, minChange, maxChange, resolved, promoted, index, count);
}
else
{
FilterDefinition<IssueSpan> filter = Builders<IssueSpan>.Filter.Eq(x => x.StreamId, streamId.Value);
if (ids != null)
{
filter &= Builders<IssueSpan>.Filter.In(x => x.IssueId, ids.Select<int, int?>(x => x));
}
else
{
filter &= Builders<IssueSpan>.Filter.Exists(x => x.IssueId);
}
if (minChange != null)
{
filter &= Builders<IssueSpan>.Filter.Not(Builders<IssueSpan>.Filter.Lt(x => x.MaxChange, minChange.Value));
}
if (maxChange != null)
{
filter &= Builders<IssueSpan>.Filter.Not(Builders<IssueSpan>.Filter.Gt(x => x.MinChange, maxChange.Value));
}
if (resolved != null)
{
if (resolved.Value)
{
filter &= Builders<IssueSpan>.Filter.Ne(x => x.ResolvedAt, null);
}
else
{
filter &= Builders<IssueSpan>.Filter.Eq(x => x.ResolvedAt, null);
}
}
using (IAsyncCursor<ProjectedIssueId> cursor = await _issueSpans.Aggregate().Match(filter).Group(x => x.IssueId, x => new ProjectedIssueId { _id = x.Key }).SortByDescending(x => x._id).ToCursorAsync())
{
List<Issue> results = await PaginatedJoinAsync(cursor, (nextIds, nextIndex, nextCount) => FilterIssuesByOtherFieldsAsync(nextIds, null, null, null, promoted, nextIndex, nextCount), index, count);
if (resolved != null)
{
for (int idx = results.Count - 1; idx >= 0; idx--)
{
Issue issue = results[idx];
if ((issue.ResolvedAt != null) != resolved.Value)
{
_logger.LogWarning("Issue {IssueId} has resolved state out of sync with spans", issue.Id);
results.RemoveAt(idx);
}
}
}
return results;
}
}
}
async Task<List<Issue>> FilterIssuesByOtherFieldsAsync(IEnumerable<int>? ids, int? minChange, int? maxChange, bool? resolved, bool? promoted, int index, int? count)
{
FilterDefinition<Issue> filter = FilterDefinition<Issue>.Empty;
if (ids != null)
{
filter &= Builders<Issue>.Filter.In(x => x.Id, ids);
}
if (resolved != null)
{
if (resolved.Value)
{
filter &= Builders<Issue>.Filter.Ne(x => x.ResolvedAt, null);
}
else
{
filter &= Builders<Issue>.Filter.Eq(x => x.ResolvedAt, null);
}
}
if (minChange != null)
{
filter &= Builders<Issue>.Filter.Not(Builders<Issue>.Filter.Lt(x => x.MaxSuspectChange, minChange.Value));
}
if (maxChange != null)
{
filter &= Builders<Issue>.Filter.Not(Builders<Issue>.Filter.Gt(x => x.MinSuspectChange, maxChange.Value));
}
if (promoted != null)
{
if (promoted.Value)
{
filter &= Builders<Issue>.Filter.Eq(x => x.Promoted, true);
}
else
{
filter &= Builders<Issue>.Filter.Ne(x => x.Promoted, true); // Handle the field not existing as well as being set to false.
}
}
return await _issues.Find(filter).SortByDescending(x => x.Id).Range(index, count).ToListAsync();
}
/// <summary>
/// Performs a client-side join of a filtered set of issues against another query
/// </summary>
/// <param name="cursor"></param>
/// <param name="nextStageFunc"></param>
/// <param name="index"></param>
/// <param name="count"></param>
/// <returns></returns>
static async Task<List<Issue>> PaginatedJoinAsync(IAsyncCursor<ProjectedIssueId> cursor, Func<IEnumerable<int>, int, int?, Task<List<Issue>>> nextStageFunc, int index, int? count)
{
if (count == null)
{
List<ProjectedIssueId> issueIds = await cursor.ToListAsync();
return await nextStageFunc(issueIds.Where(x => x._id != null).Select(x => x._id!.Value), index, null);
}
else
{
List<Issue> results = new List<Issue>();
while (await cursor.MoveNextAsync() && results.Count < count.Value)
{
List<Issue> nextResults = await nextStageFunc(cursor.Current.Where(x => x._id != null).Select(x => x._id!.Value), 0, count.Value - results.Count);
int removeCount = Math.Min(index, nextResults.Count);
nextResults.RemoveRange(0, removeCount);
index -= removeCount;
results.AddRange(nextResults);
}
return results;
}
}
/// <inheritdoc/>
public async Task<List<IIssue>> FindIssuesForChangesAsync(List<int> changes)
{
List<int> issueIds = await (await _issueSuspects.DistinctAsync(x => x.IssueId, Builders<IssueSuspect>.Filter.In(x => x.Change, changes))).ToListAsync();
return await _issues.Find(Builders<Issue>.Filter.In(x => x.Id, issueIds)).ToListAsync<Issue, IIssue>();
}
/// <inheritdoc/>
public async Task<IIssue?> TryUpdateIssueAsync(IIssue issue, UserId? initiatedByUserId, IssueSeverity? newSeverity = null, string? newSummary = null, string? newUserSummary = null, string? newDescription = null, bool? newManuallyPromoted = null, UserId? newOwnerId = null, UserId? newNominatedById = null, bool? newAcknowledged = null, UserId? newDeclinedById = null, int? newFixChange = null, UserId? newResolvedById = null, List<ObjectId>? newExcludeSpanIds = null, DateTime? newLastSeenAt = null, string? newExternaIssueKey = null, UserId? newQuarantinedById = null, UserId? newForceClosedById = null)
{
Issue issueDocument = (Issue)issue;
if (newDeclinedById != null && newDeclinedById == issueDocument.OwnerId)
{
newOwnerId = UserId.Empty;
}
if (issue.ResolvedById == null && (newForceClosedById != null && newResolvedById == null))
{
newResolvedById = newForceClosedById;
}
DateTime utcNow = DateTime.UtcNow;
List<UpdateDefinition<Issue>> updates = new List<UpdateDefinition<Issue>>();
if (newSeverity != null)
{
updates.Add(Builders<Issue>.Update.Set(x => x.Severity, newSeverity.Value));
}
if (newSummary != null)
{
updates.Add(Builders<Issue>.Update.Set(x => x.Summary, newSummary));
}
if (newUserSummary != null)
{
if (newUserSummary.Length == 0)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.UserSummary!));
}
else
{
updates.Add(Builders<Issue>.Update.Set(x => x.UserSummary, newUserSummary));
}
}
if (newDescription != null)
{
if (newDescription.Length == 0)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.Description));
}
else
{
updates.Add(Builders<Issue>.Update.Set(x => x.Description, newDescription));
}
}
if (newManuallyPromoted != null)
{
updates.Add(Builders<Issue>.Update.Set(x => x.ManuallyPromoted, newManuallyPromoted.Value));
}
if (newResolvedById != null)
{
newOwnerId ??= newResolvedById;
newAcknowledged ??= true;
}
if (newOwnerId != null)
{
if (newOwnerId.Value == UserId.Empty)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.OwnerId!));
updates.Add(Builders<Issue>.Update.Unset(x => x.NominatedAt!));
updates.Add(Builders<Issue>.Update.Unset(x => x.NominatedById!));
}
else
{
updates.Add(Builders<Issue>.Update.Set(x => x.OwnerId!, newOwnerId.Value));
updates.Add(Builders<Issue>.Update.Set(x => x.NominatedAt, DateTime.UtcNow));
if (newNominatedById == null)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.NominatedById!));
}
else
{
updates.Add(Builders<Issue>.Update.Set(x => x.NominatedById, newNominatedById.Value));
}
newAcknowledged ??= false;
}
}
if (newAcknowledged != null)
{
if (newAcknowledged.Value)
{
if (issueDocument.AcknowledgedAt == null)
{
updates.Add(Builders<Issue>.Update.Set(x => x.AcknowledgedAt, utcNow));
}
}
else
{
if (issueDocument.AcknowledgedAt != null)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.AcknowledgedAt!));
}
}
}
if (newFixChange != null)
{
if (newFixChange == 0)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.FixChange!));
}
else
{
updates.Add(Builders<Issue>.Update.Set(x => x.FixChange, newFixChange));
}
}
if (newResolvedById != null)
{
if (newResolvedById.Value != UserId.Empty)
{
if (issueDocument.ResolvedAt == null || issueDocument.ResolvedById != newResolvedById)
{
updates.Add(Builders<Issue>.Update.Set(x => x.ResolvedAt, utcNow));
updates.Add(Builders<Issue>.Update.Set(x => x.ResolvedById, newResolvedById.Value));
}
}
else
{
if (issueDocument.ResolvedAt != null)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.ResolvedAt!));
}
if (issueDocument.ResolvedById != null)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.ResolvedById!));
}
}
}
if (newExcludeSpanIds != null)
{
List<ObjectId> newCombinedExcludeSpanIds = newExcludeSpanIds;
if (issue.ExcludeSpans != null)
{
newCombinedExcludeSpanIds = newCombinedExcludeSpanIds.Union(issue.ExcludeSpans).ToList();
}
updates.Add(Builders<Issue>.Update.Set(x => x.ExcludeSpans, newCombinedExcludeSpanIds));
}
if (newLastSeenAt != null)
{
updates.Add(Builders<Issue>.Update.Set(x => x.LastSeenAt, newLastSeenAt.Value));
}
if (newDeclinedById != null)
{
GetLogger(issue.Id).LogInformation("Declined by {UserId}", newDeclinedById.Value);
await _issueSuspects.UpdateManyAsync(x => x.IssueId == issue.Id && x.AuthorId == newDeclinedById.Value, Builders<IssueSuspect>.Update.Set(x => x.DeclinedAt, DateTime.UtcNow));
}
if (newQuarantinedById != null)
{
if (newQuarantinedById.Value == UserId.Empty)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.QuarantinedByUserId));
updates.Add(Builders<Issue>.Update.Unset(x => x.QuarantineTimeUtc));
}
else
{
updates.Add(Builders<Issue>.Update.Set(x => x.QuarantinedByUserId!, newQuarantinedById.Value));
updates.Add(Builders<Issue>.Update.Set(x => x.QuarantineTimeUtc, DateTime.UtcNow));
}
}
if (newForceClosedById != null)
{
if (newForceClosedById.Value == UserId.Empty)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.ForceClosedByUserId));
}
else
{
updates.Add(Builders<Issue>.Update.Set(x => x.ForceClosedByUserId, newForceClosedById.Value));
}
}
if (newExternaIssueKey != null)
{
updates.Add(Builders<Issue>.Update.Set(x => x.ExternalIssueKey, newExternaIssueKey.Length == 0 ? null : newExternaIssueKey));
}
if (updates.Count == 0)
{
return issueDocument;
}
Issue? newIssue = await TryUpdateIssueAsync(issue, Builders<Issue>.Update.Combine(updates));
if(newIssue == null)
{
return null;
}
await LogIssueChangesAsync(initiatedByUserId, issueDocument, newIssue);
return newIssue;
}
/// <inheritdoc/>
public async Task<IIssue?> TryUpdateIssueDerivedDataAsync(IIssue issue, string newSummary, IssueSeverity newSeverity, List<NewIssueFingerprint> newFingerprints, List<NewIssueStream> newStreams, List<NewIssueSuspectData> newSuspects, DateTime? newResolvedAt, DateTime? newVerifiedAt, DateTime newLastSeenAt)
{
Issue issueImpl = (Issue)issue;
// Update all the suspects for this issue
List<IssueSuspect> oldSuspectImpls = await _issueSuspects.Find(x => x.IssueId == issue.Id).ToListAsync();
List<IssueSuspect> newSuspectImpls = await UpdateIssueSuspectsAsync(issue.Id, oldSuspectImpls, newSuspects, newResolvedAt);
// Find the spans for this issue
List<IssueSpan> newSpans = await _issueSpans.Find(x => x.IssueId == issue.Id).ToListAsync();
// Update the resolved time on any issues
List<ObjectId> updateSpanIds = newSpans.Where(x => x.ResolvedAt != newResolvedAt).Select(x => x.Id).ToList();
if (updateSpanIds.Count > 0)
{
FilterDefinition<IssueSpan> filter = Builders<IssueSpan>.Filter.In(x => x.Id, updateSpanIds);
await _issueSpans.UpdateManyAsync(filter, Builders<IssueSpan>.Update.Set(x => x.ResolvedAt, newResolvedAt));
}
// Figure out if this issue should be promoted
bool newPromoted;
if (issueImpl.ManuallyPromoted.HasValue)
{
newPromoted = issueImpl.ManuallyPromoted.Value;
}
else if (issueImpl.ManuallyPromotedDeprecated.HasValue)
{
newPromoted = issueImpl.ManuallyPromotedDeprecated.Value;
}
else
{
newPromoted = newSpans.Any(x => ((IIssueSpan)x).PromoteByDefault);
}
// Find the default owner
UserId? newDefaultOwnerId = null;
string? autoAssignToUser = newSpans.Select(x => x.LastFailure.Annotations?.AutoAssignToUser).Where(x => x != null).FirstOrDefault();
if (autoAssignToUser != null)
{
IUser? user = await _userCollection.FindUserByLoginAsync(autoAssignToUser);
if(user != null)
{
newDefaultOwnerId = user.Id;
}
}
else
{
// Figure out if we can auto-assign an owner
bool canAutoAssign = newPromoted || newSpans.Any(x => x.LastFailure.Annotations?.AutoAssign ?? false);
if (canAutoAssign && newSuspectImpls.Count > 0)
{
UserId possibleOwnerId = newSuspectImpls[0].AuthorId;
if (newSuspectImpls.All(x => x.AuthorId == possibleOwnerId) && newSuspectImpls.Any(x => x.DeclinedAt == null))
{
newDefaultOwnerId = possibleOwnerId;
}
}
}
// Get the range of suspect changes
int newMinSuspectChange = (newSuspects.Count > 0) ? newSuspects.Min(x => x.Change) : 0;
int newMaxSuspectChange = (newSuspects.Count > 0) ? newSuspects.Min(x => x.Change) : 0;
// Perform the actual update with this data
List<UpdateDefinition<Issue>> updates = new List<UpdateDefinition<Issue>>();
if (!String.Equals(issue.Summary, newSummary, StringComparison.Ordinal))
{
updates.Add(Builders<Issue>.Update.Set(x => x.Summary, newSummary));
}
if (issue.Severity != newSeverity)
{
updates.Add(Builders<Issue>.Update.Set(x => x.Severity, newSeverity));
}
if (issue.Promoted != newPromoted)
{
updates.Add(Builders<Issue>.Update.Set(x => x.Promoted, newPromoted));
}
if (issue.Fingerprints.Count != newFingerprints.Count || !newFingerprints.Zip(issue.Fingerprints).All(x => x.First.Equals(x.Second)))
{
updates.Add(Builders<Issue>.Update.Set(x => x.Fingerprints, newFingerprints.Select(x => new IssueFingerprint(x))));
}
if (issue.Streams.Count != newStreams.Count || !newStreams.Zip(issue.Streams).All(x => x.First.StreamId == x.Second.StreamId && x.First.ContainsFix == x.Second.ContainsFix))
{
updates.Add(Builders<Issue>.Update.Set(x => x.Streams, newStreams.Select(x => new IssueStream(x))));
}
if (issueImpl.MinSuspectChange != newMinSuspectChange)
{
updates.Add(Builders<Issue>.Update.Set(x => x.MinSuspectChange, newMinSuspectChange));
}
if (issueImpl.MaxSuspectChange != newMaxSuspectChange)
{
updates.Add(Builders<Issue>.Update.Set(x => x.MaxSuspectChange, newMaxSuspectChange));
}
if (issueImpl.DefaultOwnerId != newDefaultOwnerId)
{
updates.Add(Builders<Issue>.Update.Set(x => x.DefaultOwnerId, newDefaultOwnerId));
}
if (issue.ResolvedAt != newResolvedAt)
{
updates.Add(Builders<Issue>.Update.SetOrUnsetNull(x => x.ResolvedAt, newResolvedAt));
}
if (newResolvedAt == null && issue.ResolvedById != null)
{
updates.Add(Builders<Issue>.Update.Unset(x => x.ResolvedById));
}
if (issue.VerifiedAt != newVerifiedAt)
{
updates.Add(Builders<Issue>.Update.SetOrUnsetNull(x => x.VerifiedAt, newVerifiedAt));
}
if (issue.LastSeenAt != newLastSeenAt)
{
updates.Add(Builders<Issue>.Update.Set(x => x.LastSeenAt, newLastSeenAt));
}
Issue? newIssue = await TryUpdateIssueAsync(issue, Builders<Issue>.Update.Combine(updates));
if(newIssue != null)
{
await LogIssueChangesAsync(null, issueImpl, newIssue);
await LogIssueSuspectChangesAsync(GetLogger(issue.Id), oldSuspectImpls, newSuspectImpls);
return newIssue;
}
return null;
}
async Task<List<IssueSuspect>> UpdateIssueSuspectsAsync(int issueId, List<IssueSuspect> oldSuspectImpls, List<NewIssueSuspectData> newSuspects, DateTime? resolvedAt)
{
List<IssueSuspect> newSuspectImpls = new List<IssueSuspect>(oldSuspectImpls);
// Find the current list of suspects
HashSet<(UserId, int)> curSuspectKeys = new HashSet<(UserId, int)>(oldSuspectImpls.Select(x => (x.AuthorId, x.Change)));
List<IssueSuspect> createSuspects = newSuspects.Where(x => !curSuspectKeys.Contains((x.AuthorId, x.Change))).Select(x => new IssueSuspect(issueId, x, resolvedAt)).ToList();
HashSet<(UserId, int)> newSuspectKeys = new HashSet<(UserId, int)>(newSuspects.Select(x => (x.AuthorId, x.Change)));
List<IssueSuspect> deleteSuspects = oldSuspectImpls.Where(x => !newSuspectKeys.Contains((x.AuthorId, x.Change))).ToList();
// Apply the suspect changes
if (createSuspects.Count > 0)
{
await _issueSuspects.InsertManyIgnoreDuplicatesAsync(createSuspects);
newSuspectImpls.AddRange(createSuspects);
}
if (deleteSuspects.Count > 0)
{
await _issueSuspects.DeleteManyAsync(Builders<IssueSuspect>.Filter.In(x => x.Id, deleteSuspects.Select(y => y.Id)));
newSuspectImpls.RemoveAll(x => !newSuspectKeys.Contains((x.AuthorId, x.Change)));
}
// Make sure all the remaining suspects have the correct resolved time
if (newSuspectImpls.Any(x => x.ResolvedAt != resolvedAt))
{
await _issueSuspects.UpdateManyAsync(Builders<IssueSuspect>.Filter.Eq(x => x.IssueId, issueId), Builders<IssueSuspect>.Update.Set(x => x.ResolvedAt, resolvedAt));
}
return newSuspectImpls;
}
#endregion
#region Spans
/// <inheritdoc/>
public async Task<IIssueSpan> AddSpanAsync(int issueId, NewIssueSpanData newSpan)
{
IssueSpan span = new IssueSpan(issueId, newSpan);
await _issueSpans.InsertOneAsync(span);
return span;
}
/// <inheritdoc/>
public async Task<IIssueSpan?> GetSpanAsync(ObjectId spanId)
{
return await _issueSpans.Find(Builders<IssueSpan>.Filter.Eq(x => x.Id, spanId)).FirstOrDefaultAsync();
}
/// <inheritdoc/>
public async Task<IIssueSpan?> TryUpdateSpanAsync(IIssueSpan span, NewIssueStepData? newLastSuccess = null, NewIssueStepData? newFailure = null, NewIssueStepData? newNextSuccess = null, List<NewIssueSpanSuspectData>? newSuspects = null, int? newIssueId = null)
{
List<UpdateDefinition<IssueSpan>> updates = new List<UpdateDefinition<IssueSpan>>();
if (newLastSuccess != null)
{
updates.Add(Builders<IssueSpan>.Update.Set(x => x.MinChange, newLastSuccess.Change));
updates.Add(Builders<IssueSpan>.Update.Set(x => x.LastSuccess, new IssueStep(span.Id, newLastSuccess)));
}
if (newFailure != null)
{
if (newFailure.Change < span.FirstFailure.Change)
{
updates.Add(Builders<IssueSpan>.Update.Set(x => x.FirstFailure, new IssueStep(span.Id, newFailure)));
}
if (newFailure.Change >= span.LastFailure.Change)
{
updates.Add(Builders<IssueSpan>.Update.Set(x => x.LastFailure, new IssueStep(span.Id, newFailure)));
}
if (newFailure.PromoteByDefault != span.PromoteByDefault && newFailure.Change >= span.LastFailure.Change)
{
updates.Add(Builders<IssueSpan>.Update.Set(x => x.PromoteByDefault, newFailure.PromoteByDefault));
}
}
if (newNextSuccess != null)
{
updates.Add(Builders<IssueSpan>.Update.Set(x => x.MaxChange, newNextSuccess.Change));
updates.Add(Builders<IssueSpan>.Update.Set(x => x.NextSuccess, new IssueStep(span.Id, newNextSuccess)));
}
if (newSuspects != null)
{
updates.Add(Builders<IssueSpan>.Update.Set(x => x.Suspects, newSuspects.ConvertAll(x => new IssueSpanSuspect(x))));
}
if (newIssueId != null)
{
updates.Add(Builders<IssueSpan>.Update.Set(x => x.IssueId, newIssueId.Value));
}
if (updates.Count == 0)
{
return span;
}
IssueSpan? newSpan = await TryUpdateSpanAsync(span, Builders<IssueSpan>.Update.Combine(updates));
if (newSpan != null)
{
ILogger logger = GetLogger(newSpan.IssueId);
if (newLastSuccess != null)
{
logger.LogInformation("Set last success for span {SpanId} to job {JobId} at CL {Change}", newSpan.Id, newLastSuccess.JobId, newLastSuccess.Change);
}
if (newNextSuccess != null)
{
logger.LogInformation("Set next success for span {SpanId} to job {JobId} at CL {Change}", newSpan.Id, newNextSuccess.JobId, newNextSuccess.Change);
}
if (newFailure != null)
{
logger.LogInformation("Added failure for span {SpanId} in job {JobId} at CL {Change}", newSpan.Id, newFailure.JobId, newFailure.Change);
}
}
return newSpan;
}
/// <inheritdoc/>
public async Task<List<IIssueSpan>> FindSpansAsync(int issueId)
{
return await _issueSpans.Find(x => x.IssueId == issueId).ToListAsync<IssueSpan, IIssueSpan>();
}
/// <inheritdoc/>
public Task<List<IIssueSpan>> FindSpansAsync(IEnumerable<ObjectId> spanIds)
{
return _issueSpans.Find(Builders<IssueSpan>.Filter.In(x => x.Id, spanIds)).ToListAsync<IssueSpan, IIssueSpan>();
}
/// <inheritdoc/>
public async Task<List<IIssueSpan>> FindOpenSpansAsync(StreamId streamId, TemplateId templateId, string nodeName, int change)
{
List<IssueSpan> spans = await _issueSpans.Find(x => x.StreamId == streamId && x.TemplateRefId == templateId && x.NodeName == nodeName && change >= x.MinChange && change <= x.MaxChange).ToListAsync();
return spans.ConvertAll<IIssueSpan>(x => x);
}
/// <inheritdoc/>
public Task<List<IIssueSpan>> FindSpansAsync(IEnumerable<ObjectId>? spanIds, IEnumerable<int>? issueIds, StreamId? streamId, int? minChange, int? maxChange, bool? resolved, int? index, int? count)
{
FilterDefinition<IssueSpan> filter = FilterDefinition<IssueSpan>.Empty;
if(spanIds != null)
{
filter &= Builders<IssueSpan>.Filter.In(x => x.Id, spanIds);
}
if (streamId != null)
{
filter &= Builders<IssueSpan>.Filter.Eq(x => x.StreamId, streamId);
}
if (issueIds != null)
{
filter &= Builders<IssueSpan>.Filter.In(x => x.IssueId, issueIds.Select<int, int?>(x => x));
}
if (minChange != null)
{
filter &= Builders<IssueSpan>.Filter.Not(Builders<IssueSpan>.Filter.Lt(x => x.MaxChange, minChange.Value));
}
if (maxChange != null)
{
filter &= Builders<IssueSpan>.Filter.Not(Builders<IssueSpan>.Filter.Gt(x => x.MinChange, maxChange.Value));
}
if (resolved != null)
{
if (resolved.Value)
{
filter &= Builders<IssueSpan>.Filter.Ne(x => x.ResolvedAt, null);
}
else
{
filter &= Builders<IssueSpan>.Filter.Eq(x => x.ResolvedAt, null);
}
}
return _issueSpans.Find(filter).Range(index, count).ToListAsync<IssueSpan, IIssueSpan>();
}
#endregion
#region Steps
/// <inheritdoc/>
public async Task<IIssueStep> AddStepAsync(ObjectId spanId, NewIssueStepData newStep)
{
IssueStep step = new IssueStep(spanId, newStep);
await _issueSteps.InsertOneAsync(step);
return step;
}
/// <inheritdoc/>
public Task<List<IIssueStep>> FindStepsAsync(IEnumerable<ObjectId> spanIds)
{
FilterDefinition<IssueStep> filter = Builders<IssueStep>.Filter.In(x => x.SpanId, spanIds);
return _issueSteps.Find(filter).ToListAsync<IssueStep, IIssueStep>();
}
/// <inheritdoc/>
public Task<List<IIssueStep>> FindStepsAsync(JobId jobId, SubResourceId? batchId, SubResourceId? stepId)
{
FilterDefinition<IssueStep> filter = Builders<IssueStep>.Filter.Eq(x => x.JobId, jobId);
if (batchId != null)
{
filter &= Builders<IssueStep>.Filter.Eq(x => x.BatchId, batchId.Value);
}
if (stepId != null)
{
filter &= Builders<IssueStep>.Filter.Eq(x => x.StepId, stepId.Value);
}
return _issueSteps.Find(filter).ToListAsync<IssueStep, IIssueStep>();
}
#endregion
/// <inheritdoc/>
public IAuditLogChannel<int> GetLogger(int issueId)
{
return _auditLog[issueId];
}
}
}