// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EpicGames.Horde.Api; using Horde.Server.Server; using Horde.Server.Streams; using Horde.Server.Utilities; using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Options; using MongoDB.Driver; namespace Horde.Server.Devices { /// /// Collection of device documents /// public class DeviceCollection : IDeviceCollection { /// /// Document representing a device platform /// class DevicePlatformDocument : IDevicePlatform { [BsonRequired, BsonId] public DevicePlatformId Id { get; set; } public string Name { get; set; } public List Models { get; set; } = new List(); IReadOnlyList IDevicePlatform.Models => Models; [BsonConstructor] private DevicePlatformDocument() { Name = null!; } public DevicePlatformDocument(DevicePlatformId id, string name) { Id = id; Name = name; } } /// /// Document representing a pool of devices /// class DevicePoolDocument : IDevicePool { [BsonRequired, BsonId] public DevicePoolId Id { get; set; } [BsonRequired] public DevicePoolType PoolType { get; set; } [BsonIgnoreIfNull] public List? ProjectIds { get; set; } [BsonRequired] public string Name { get; set; } = null!; [BsonConstructor] private DevicePoolDocument() { } public DevicePoolDocument(DevicePoolId id, string name, DevicePoolType poolType, List? projectIds) { Id = id; Name = name; PoolType = poolType; ProjectIds = projectIds; } } /// /// Document representing a reservation of devices /// class DeviceReservationDocument : IDeviceReservation { /// /// Randomly generated unique id for this reservation. /// [BsonRequired, BsonId] public ObjectId Id { get; set; } [BsonRequired] public DevicePoolId PoolId { get; set; } [BsonIgnoreIfNull] public string? StreamId { get; set; } [BsonIgnoreIfNull] public string? JobId { get; set; } [BsonIgnoreIfNull] public string? StepId { get; set; } [BsonIgnoreIfNull] public string? JobName { get; set; } [BsonIgnoreIfNull] public string? StepName { get; set; } /// /// Reservations held by a user, requires a token like download code /// [BsonIgnoreIfNull] public UserId? UserId { get; set; } /// /// The hostname of the machine which has made the reservation /// [BsonIgnoreIfNull] public string? Hostname { get; set; } /// /// Optional string holding details about the reservation /// [BsonIgnoreIfNull] public string? ReservationDetails { get; set; } /// /// DeviceIds in the reservation /// public List Devices { get; set; } = new List(); public List RequestedDevicePlatforms { get; set; } = new List(); public DateTime CreateTimeUtc { get; set; } public DateTime UpdateTimeUtc { get; set; } // Legacy Guid public string LegacyGuid { get; set; } = null!; [BsonConstructor] private DeviceReservationDocument() { } public DeviceReservationDocument(ObjectId id, DevicePoolId poolId, List devices, List requestedDevicePlatforms, DateTime createTimeUtc, string? hostname, string? reservationDetails, string? streamId, string? jobId, string? stepId, string? jobName, string? stepName) { Id = id; PoolId = poolId; Devices = devices; RequestedDevicePlatforms = requestedDevicePlatforms; CreateTimeUtc = createTimeUtc; UpdateTimeUtc = createTimeUtc; Hostname = hostname; ReservationDetails = reservationDetails; StreamId = streamId; JobId = jobId; StepId = stepId; JobName = jobName; StepName = stepName; LegacyGuid = Guid.NewGuid().ToString(); } } /// /// Concrete implementation of an device document /// class DeviceDocument : IDevice { public static int CurrentVersion = 1; [BsonRequired, BsonId] public DeviceId Id { get; set; } public DevicePlatformId PlatformId { get; set; } public DevicePoolId PoolId { get; set; } public string Name { get; set; } = null!; public bool Enabled { get; set; } [BsonIgnoreIfNull] public string? ModelId { get; set; } [BsonIgnoreIfNull] public string? Address { get; set; } [BsonIgnoreIfNull] public string? CheckedOutByUser { get; set; } [BsonIgnoreIfNull] public DateTime? CheckOutTime { get; set; } [BsonIgnoreIfNull] public bool? CheckoutExpiringNotificationSent { get; set; } [BsonIgnoreIfNull] public DateTime? ProblemTimeUtc { get; set; } [BsonIgnoreIfNull] public DateTime? MaintenanceTimeUtc { get; set; } /// /// The last time this device was reserved, used to cycle devices for reservations /// [BsonIgnoreIfNull] public DateTime? ReservationTimeUtc { get; set; } [BsonIgnoreIfNull] public string? ModifiedByUser { get; set; } [BsonIgnoreIfNull] public string? Notes { get; set; } /// /// [DEPRECATED] /// [BsonIgnoreIfNull] public List? Utilization { get; set; } [BsonIgnoreIfNull] public int? Version { get; set; } [BsonConstructor] private DeviceDocument() { } public DeviceDocument(DeviceId id, DevicePlatformId platformId, DevicePoolId poolId, string name, bool enabled, string? address, string? modelId, UserId? userId) { Id = id; PlatformId = platformId; PoolId = poolId; Name = name; Enabled = enabled; Address = address; ModelId = modelId; ModifiedByUser = userId?.ToString(); Version = CurrentVersion; } } /// /// Device telemetry information for an individual device /// class DeviceTelemetryDocument : IDeviceTelemetry { /// /// Id of telemetry document /// [BsonRequired, BsonId] public ObjectId TelemetryId { get; set; } /// /// The device id /// [BsonRequired, BsonElement("did")] public DeviceId DeviceId { get; set; } /// /// The time this telemetry data was created /// [BsonRequired, BsonElement("c")] public DateTime CreateTimeUtc { get; set; } /// /// The stream id which utilized device /// [BsonIgnoreIfNull, BsonElement("sid")] public string? StreamId { get; set; } /// /// The job id which utilized device /// [BsonIgnoreIfNull,BsonElement("job")] public string? JobId { get; set; } /// /// The job name /// [BsonIgnoreIfNull] public string? JobName { get; set; } /// /// The job's step id /// [BsonIgnoreIfNull, BsonElement("step")] public string? StepId { get; set; } /// /// The step name /// [BsonIgnoreIfNull] public string? StepName { get; set; } /// /// Reservation Id (transient, reservations are deleted upon expiration) /// [BsonIgnoreIfNull, BsonElement("rid")] public ObjectId? ReservationId { get; set; } /// /// The time device was reserved /// [BsonIgnoreIfNull, BsonElement("rs")] public DateTime? ReservationStartUtc { get; set; } /// /// The time device was freed /// [BsonIgnoreIfNull, BsonElement("rf")] public DateTime? ReservationFinishUtc { get; set; } /// /// If the device reported a problem /// [BsonIgnoreIfNull, BsonElement("p")] public DateTime? ProblemTimeUtc { get; set; } [BsonConstructor] private DeviceTelemetryDocument() { } public DeviceTelemetryDocument(DeviceId deviceId, ObjectId? reservationId = null, DateTime? reservationStartTime = null, string? streamId = null, string? jobId = null, string? stepId = null, string? jobName = null, string? stepName = null) { TelemetryId = ObjectId.GenerateNewId(); CreateTimeUtc = DateTime.UtcNow; DeviceId = deviceId; ReservationId = reservationId; ReservationStartUtc = reservationStartTime; StreamId = streamId; JobId = jobId; StepId = stepId; JobName = jobName; StepName = stepName; } } class DeviceReservationPoolTelemetryDocument : IDevicePoolReservationTelemetry { /// [BsonRequired, BsonElement("did")] public DeviceId DeviceId { get; set; } [BsonIgnoreIfNull, BsonElement("ji")] public string? JobId { get; set; } [BsonIgnoreIfNull, BsonElement("si")] public string? StepId { get; set; } [BsonIgnoreIfNull, BsonElement("jn")] public string? JobName { get; set; } [BsonIgnoreIfNull, BsonElement("sn")] public string? StepName { get; set; } [BsonConstructor] private DeviceReservationPoolTelemetryDocument() { } public DeviceReservationPoolTelemetryDocument(DeviceId deviceId, string? jobId, string? stepId, string? jobName, string? stepName) { DeviceId = deviceId; JobId = jobId; StepId = stepId; JobName = jobName; StepName = stepName; } } class DevicePlatformTelemetryDocument : IDevicePlatformTelemetry { [BsonRequired, BsonElement("pid")] public DevicePlatformId PlatformId { get; set; } [BsonIgnoreIfNull, BsonElement("a")] public List? Available { get; set; } IReadOnlyList? IDevicePlatformTelemetry.Available => Available; [BsonIgnoreIfNull, BsonElement("m")] public List? Maintenance { get; set; } IReadOnlyList? IDevicePlatformTelemetry.Maintenance => Maintenance; [BsonIgnoreIfNull, BsonElement("p")] public List? Problem { get; set; } IReadOnlyList? IDevicePlatformTelemetry.Problem => Problem; [BsonIgnoreIfNull, BsonElement("d")] public List? Disabled { get; set; } IReadOnlyList? IDevicePlatformTelemetry.Disabled => Disabled; [BsonIgnoreIfNull, BsonElement("r"), BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public Dictionary>? Reserved { get; set; } IReadOnlyDictionary>? IDevicePlatformTelemetry.Reserved => Reserved?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value as IReadOnlyList) ?? new Dictionary>(); [BsonConstructor] private DevicePlatformTelemetryDocument() { } public DevicePlatformTelemetryDocument(DevicePlatformId platformId, List? available, Dictionary>? reserved, List? maintenance, List? problem, List? disabled) { PlatformId = platformId; if (available != null && available.Count > 0) { Available = available; } if (reserved != null && reserved.Count > 0) { Reserved = reserved; } if (maintenance != null && maintenance.Count > 0) { Maintenance = maintenance; } if (problem != null && problem.Count > 0) { Problem = problem; } if (disabled != null && disabled.Count > 0) { Disabled = disabled; } } } /// /// Device telemetry information for pools /// class DevicePoolTelemetryDocument : IDevicePoolTelemetry { /// /// Id of telemetry document /// [BsonRequired, BsonId] public ObjectId TelemetryId { get; set; } /// /// The time this telemetry data was created /// [BsonRequired] public DateTime CreateTimeUtc { get; set; } /// /// Pool platform state /// [BsonRequired] public Dictionary> Pools { get; set; } = new Dictionary>(); IReadOnlyDictionary> IDevicePoolTelemetry.Pools => Pools.ToDictionary(kvp => kvp.Key, kvp => kvp.Value as IReadOnlyList); [BsonConstructor] private DevicePoolTelemetryDocument() { } public DevicePoolTelemetryDocument(Dictionary> pools) { TelemetryId = ObjectId.GenerateNewId(); CreateTimeUtc = DateTime.UtcNow; Pools = pools; } } readonly IMongoCollection _platforms; readonly IMongoCollection _devices; readonly IMongoCollection _pools; readonly IMongoCollection _reservations; readonly IMongoCollection _deviceTelemetry; readonly IMongoCollection _poolTelemetry; readonly ILogger _logger; /// /// Constructor /// public DeviceCollection(MongoService mongoService, ILogger logger) { _logger = logger; _devices = mongoService.GetCollection("Devices", keys => keys.Ascending(x => x.Name), unique: true); _platforms = mongoService.GetCollection("Devices.Platforms", keys => keys.Ascending(x => x.Name), unique: true); _pools = mongoService.GetCollection("Devices.Pools", keys => keys.Ascending(x => x.Name), unique: true); _reservations = mongoService.GetCollection("Devices.Reservations"); List> deviceTelemetryIndexes = new List>(); deviceTelemetryIndexes.Add((keys => keys.Ascending(x => x.CreateTimeUtc))); _deviceTelemetry = mongoService.GetCollection("Devices.DeviceTelemetryV2", deviceTelemetryIndexes); List> poolTelemetryIndexes = new List>(); poolTelemetryIndexes.Add((keys => keys.Ascending(x => x.CreateTimeUtc))); _poolTelemetry = mongoService.GetCollection("Devices.PoolTelemetryV2", poolTelemetryIndexes); } /// public async Task TryAddDeviceAsync(DeviceId id, string name, DevicePlatformId platformId, DevicePoolId poolId, bool? enabled, string? address, string? modelId, UserId? userId) { DeviceDocument newDevice = new DeviceDocument(id, platformId, poolId, name, enabled ?? true, address, modelId, userId); await _devices.InsertOneAsync(newDevice); return newDevice; } /// public async Task TryAddPlatformAsync(DevicePlatformId id, string name) { DevicePlatformDocument newPlatform = new DevicePlatformDocument(id, name); try { await _platforms.InsertOneAsync(newPlatform); return newPlatform; } catch (MongoWriteException ex) { if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { return null; } else { throw; } } } /// public async Task> FindAllPlatformsAsync() { List results = await _platforms.Find(x => true).ToListAsync(); return results.OrderBy(x => x.Name).Select(x => x).ToList(); } /// public async Task UpdatePlatformAsync(DevicePlatformId platformId, string[]? modelIds) { UpdateDefinitionBuilder updateBuilder = Builders.Update; List> updates = new List>(); if (modelIds != null) { updates.Add(updateBuilder.Set(x => x.Models, modelIds.ToList())); } if (updates.Count > 0) { await _platforms.FindOneAndUpdateAsync(x => x.Id == platformId, updateBuilder.Combine(updates)); } return true; } /// public async Task TryAddPoolAsync(DevicePoolId id, string name, DevicePoolType poolType, List? projectIds) { DevicePoolDocument newPool = new DevicePoolDocument(id, name, poolType, projectIds); try { await _pools.InsertOneAsync(newPool); return newPool; } catch (MongoWriteException ex) { if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { return null; } else { throw; } } } /// public async Task UpdatePoolAsync(DevicePoolId id, List? projectIds) { UpdateDefinitionBuilder updateBuilder = Builders.Update; List> updates = new List>(); if (projectIds != null) { updates.Add(updateBuilder.Set(x => x.ProjectIds, projectIds)); } if (updates.Count > 0) { await _pools.FindOneAndUpdateAsync(x => x.Id == id, updateBuilder.Combine(updates)); } } /// public async Task> FindAllDevicesAsync(List? deviceIds = null, DevicePoolId? poolId = null, DevicePlatformId? platformId = null) { FilterDefinition filter = Builders.Filter.Empty; if (deviceIds != null) { filter &= Builders.Filter.In(x => x.Id, deviceIds); } if (poolId != null) { filter &= Builders.Filter.Eq(x => x.PoolId, poolId); } if (platformId != null) { filter &= Builders.Filter.Eq(x => x.PlatformId, platformId); } List results = await _devices.Find(filter).ToListAsync(); return results.OrderBy(x => x.Name).Select(x => x).ToList(); } /// public async Task> FindAllPoolsAsync() { List results = await _pools.Find(x => true).ToListAsync(); return results.OrderBy(x => x.Name).Select(x => x).ToList(); } /// public async Task GetPlatformAsync(DevicePlatformId platformId) { return await _platforms.Find(x => x.Id == platformId).FirstOrDefaultAsync(); } /// public async Task GetPoolAsync(DevicePoolId poolId) { return await _pools.Find(x => x.Id == poolId).FirstOrDefaultAsync(); } /// public async Task GetDeviceAsync(DeviceId deviceId) { return await _devices.Find(x => x.Id == deviceId).FirstOrDefaultAsync(); } /// public async Task GetDeviceByNameAsync(string deviceName) { return await _devices.Find(x => x.Name == deviceName).FirstOrDefaultAsync(); } /// public async Task CheckoutDeviceAsync(DeviceId deviceId, UserId? checkedOutByUserId) { UpdateDefinitionBuilder updateBuilder = Builders.Update; List> updates = new List>(); string? userId = checkedOutByUserId?.ToString(); updates.Add(updateBuilder.Set(x => x.CheckedOutByUser, String.IsNullOrEmpty(userId) ? null : userId)); if (checkedOutByUserId != null) { updates.Add(updateBuilder.Set(x => x.CheckOutTime, DateTime.UtcNow)); updates.Add(updateBuilder.Set(x => x.CheckoutExpiringNotificationSent, false)); } else { updates.Add(updateBuilder.Set(x => x.CheckoutExpiringNotificationSent, true)); } await _devices.FindOneAndUpdateAsync(x => x.Id == deviceId, updateBuilder.Combine(updates)); } /// public async Task UpdateDeviceAsync(DeviceId deviceId, DevicePoolId? newPoolId, string? newName, string? newAddress, string? newModelId, string? newNotes, bool? newEnabled, bool? newProblem, bool? newMaintenance, UserId? modifiedByUserId = null) { UpdateDefinitionBuilder updateBuilder = Builders.Update; List> updates = new List>(); DateTime utcNow = DateTime.UtcNow; if (modifiedByUserId != null) { updates.Add(updateBuilder.Set(x => x.ModifiedByUser, modifiedByUserId.ToString())); } if (newPoolId != null) { updates.Add(updateBuilder.Set(x => x.PoolId, newPoolId.Value)); } if (newName != null) { updates.Add(updateBuilder.Set(x => x.Name, newName)); } if (newEnabled != null) { updates.Add(updateBuilder.Set(x => x.Enabled, newEnabled)); } if (newAddress != null) { updates.Add(updateBuilder.Set(x => x.Address, newAddress)); } if (!String.IsNullOrEmpty(newModelId)) { if (newModelId == "Base") { updates.Add(updateBuilder.Set(x => x.ModelId, null)); } else { updates.Add(updateBuilder.Set(x => x.ModelId, newModelId)); } } if (newNotes != null) { updates.Add(updateBuilder.Set(x => x.Notes, newNotes)); } if (newProblem.HasValue) { DateTime? problemTime = null; if (newProblem.Value) { problemTime = utcNow; } updates.Add(updateBuilder.Set(x => x.ProblemTimeUtc, problemTime)); } if (newMaintenance.HasValue) { DateTime? maintenanceTime = null; if (newMaintenance.Value) { maintenanceTime = utcNow; } updates.Add(updateBuilder.Set(x => x.MaintenanceTimeUtc, maintenanceTime)); } if (updates.Count > 0) { await _devices.FindOneAndUpdateAsync(x => x.Id == deviceId, updateBuilder.Combine(updates)); } if (newProblem.HasValue && newProblem.Value) { IDeviceReservation? reservation = await TryGetDeviceReservationAsync(deviceId); if (reservation != null) { UpdateDefinition update = Builders.Update.Set(x => x.ProblemTimeUtc, utcNow); await _deviceTelemetry.FindOneAndUpdateAsync(t => t.ReservationId == reservation.Id && t.DeviceId == deviceId, update); } } } /// public async Task DeleteDeviceAsync(DeviceId deviceId) { FilterDefinition filter = Builders.Filter.Eq(x => x.Id, deviceId); DeleteResult result = await _devices.DeleteOneAsync(filter); return result.DeletedCount > 0; } /// public async Task> FindAllDeviceReservationsAsync(DevicePoolId? poolId = null) { return await _reservations.Find(a => poolId == null || a.PoolId == poolId).ToListAsync(); } /// public async Task TryAddReservationAsync(DevicePoolId poolId, List request, int problemCooldown, string? hostname, string? reservationDetails, string? streamId, string? jobId, string? stepId, string? jobName, string? stepName) { if (request.Count == 0) { return null; } DevicePoolDocument? pool = await _pools.Find(x => x.Id == poolId).FirstOrDefaultAsync(); if (pool == null || pool.PoolType != DevicePoolType.Automation) { return null; } HashSet allocated = new HashSet(); Dictionary platformRequestMap = new Dictionary(); List poolReservations = await _reservations.Find(x => x.PoolId == poolId).ToListAsync(); DateTime reservationTimeUtc = DateTime.UtcNow; // Get available devices List poolDevices = await _devices.Find(x => x.PoolId == poolId && x.Enabled && x.MaintenanceTimeUtc == null).ToListAsync(); // filter out problem devices poolDevices = poolDevices.FindAll(x => (x.ProblemTimeUtc == null || ((reservationTimeUtc - x.ProblemTimeUtc).Value.TotalMinutes > problemCooldown))); // filter out currently reserved devices poolDevices = poolDevices.FindAll(x => poolReservations.FirstOrDefault(p => p.Devices.Contains(x.Id)) == null); int availablePoolDevices = poolDevices.Count; // sort to use last reserved first to cycle devices poolDevices.Sort((a, b) => { DateTime? aTime = a.ReservationTimeUtc; DateTime? bTime = b.ReservationTimeUtc; if (aTime == bTime) { return 0; } if (aTime == null && bTime != null) { return -1; } if (aTime != null && bTime == null) { return 1; } return aTime < bTime ? -1 : 1; }); foreach (DeviceRequestData data in request) { DeviceDocument? device = poolDevices.FirstOrDefault(a => { if (allocated.Contains(a.Id) || a.PlatformId != data.PlatformId) { return false; } if (data.IncludeModels.Count > 0 && (a.ModelId == null || !data.IncludeModels.Contains(a.ModelId))) { return false; } if (data.ExcludeModels.Count > 0 && (a.ModelId != null && data.ExcludeModels.Contains(a.ModelId))) { return false; } return true; }); if (device == null) { // can't fulfill request return null; } allocated.Add(device.Id); platformRequestMap.Add(device.Id, data.RequestedPlatform); } // update reservation time and utilization for allocated devices foreach (DeviceId id in allocated) { DeviceDocument device = poolDevices.First((device) => device.Id == id); List? utilization = device.Utilization; utilization ??= new List(); // keep up to 100, maintaining order if (utilization.Count > 99) { utilization = utilization.GetRange(0, 99); } utilization.Insert(0, new DeviceUtilizationTelemetry(reservationTimeUtc) { JobId = jobId, StepId = stepId }); UpdateDefinitionBuilder deviceBuilder = Builders.Update; List> deviceUpdates = new List>(); deviceUpdates.Add(deviceBuilder.Set(x => x.ReservationTimeUtc, reservationTimeUtc)); deviceUpdates.Add(deviceBuilder.Set(x => x.Utilization, utilization)); await _devices.FindOneAndUpdateAsync(x => x.Id == id, deviceBuilder.Combine(deviceUpdates)); } List deviceIds = allocated.ToList(); List requestedPlatforms = deviceIds.Select(x => platformRequestMap[x]).ToList(); // Create new reservation DeviceReservationDocument newReservation = new DeviceReservationDocument(ObjectId.GenerateNewId(), poolId, deviceIds, requestedPlatforms, reservationTimeUtc, hostname, reservationDetails, streamId, jobId, stepId, jobName, stepName); await _reservations.InsertOneAsync(newReservation); // Create device telemetry data for reservation List telemetry = new List(); foreach (DeviceId deviceId in deviceIds) { telemetry.Add(new DeviceTelemetryDocument(deviceId, newReservation.Id, newReservation.CreateTimeUtc, streamId, jobId, stepId, jobName, stepName)); } if (telemetry.Count > 0) { await _deviceTelemetry.InsertManyAsync(telemetry); } return newReservation; } /// public async Task TryUpdateReservationAsync(ObjectId id) { UpdateResult result = await _reservations.UpdateOneAsync(x => x.Id == id, Builders.Update.Set(x => x.UpdateTimeUtc, DateTime.UtcNow)); return result.ModifiedCount == 1; } /// public async Task DeleteReservationAsync(ObjectId id) { FilterDefinition telemetryFilter = Builders.Filter.Eq(x => x.ReservationId, id); // update telemetry await _deviceTelemetry.UpdateManyAsync(x => x.ReservationId == id, Builders.Update.Set(x => x.ReservationFinishUtc, DateTime.UtcNow)); FilterDefinition filter = Builders.Filter.Eq(x => x.Id, id); DeleteResult result = await _reservations.DeleteOneAsync(filter); return result.DeletedCount > 0; } /// /// Deletes expired reservations /// public async Task ExpireReservationsAsync() { List reserves = await _reservations.Find(a => true).ToListAsync(); DateTime utcNow = DateTime.UtcNow; reserves = reserves.FindAll(r => (utcNow - r.UpdateTimeUtc).TotalMinutes > 10).ToList(); bool result = true; foreach (IDeviceReservation reservation in reserves) { if (!await DeleteReservationAsync(reservation.Id)) { result = false; } } return result; } /// /// Deletes expired user checkouts /// public async Task?> ExpireCheckedOutAsync(int checkoutDays) { FilterDefinition filter = Builders.Filter.Empty; filter &= Builders.Filter.Where(x => x.CheckedOutByUser != null && x.CheckOutTime != null); List checkedOutDevices = await _devices.Find(filter).ToListAsync(); DateTime utcNow = DateTime.UtcNow; List expiredDevices = checkedOutDevices.FindAll(x => (utcNow - x.CheckOutTime!.Value).TotalDays >= checkoutDays).ToList(); if (expiredDevices.Count > 0) { FilterDefinition updateFilter = Builders.Filter.In(x => x.Id, expiredDevices.Select(y => y.Id)); UpdateDefinitionBuilder deviceBuilder = Builders.Update; List> deviceUpdates = new List>(); deviceUpdates.Add(deviceBuilder.Set(x => x.CheckedOutByUser, null)); deviceUpdates.Add(deviceBuilder.Set(x => x.CheckOutTime, null)); deviceUpdates.Add(deviceBuilder.Set(x => x.CheckoutExpiringNotificationSent, true)); UpdateResult result = await _devices.UpdateManyAsync(Builders.Filter.In(x => x.Id, expiredDevices.Select(y => y.Id)), deviceBuilder.Combine(deviceUpdates)); if (result.ModifiedCount > 0) { return expiredDevices.Select(x => (UserId.Parse(x.CheckedOutByUser!), (IDevice)x)).ToList(); } } return null; } /// /// Gets a list of users to notify that their device is about to expire in the next 24 hours /// public async Task?> ExpireNotificatonsAsync(int checkoutDays) { FilterDefinition filter = Builders.Filter.Empty; filter &= Builders.Filter.Where(x => x.CheckedOutByUser != null && x.CheckoutExpiringNotificationSent != true); List checkedOutDevices = await _devices.Find(filter).ToListAsync(); DateTime utcNow = DateTime.UtcNow; List expiredDevices = checkedOutDevices.FindAll(x => (utcNow - x.CheckOutTime!.Value).TotalDays >= (checkoutDays - 1)).ToList(); if (expiredDevices.Count > 0) { FilterDefinition updateFilter = Builders.Filter.In(x => x.Id, expiredDevices.Select(y => y.Id)); UpdateDefinitionBuilder deviceBuilder = Builders.Update; List> deviceUpdates = new List>(); deviceUpdates.Add(deviceBuilder.Set(x => x.CheckoutExpiringNotificationSent, true)); UpdateResult result = await _devices.UpdateManyAsync(Builders.Filter.In(x => x.Id, expiredDevices.Select(y => y.Id)), deviceBuilder.Combine(deviceUpdates)); if (result.ModifiedCount > 0) { return expiredDevices.Select(x => (UserId.Parse(x.CheckedOutByUser!), (IDevice)x)).ToList(); } } return null; } /// public async Task> FindAllReservationsAsync() { List results = await _reservations.Find(x => true).ToListAsync(); return results.OrderBy(x => x.CreateTimeUtc.Ticks).Select(x => x).ToList(); } /// public async Task TryGetReservationFromLegacyGuidAsync(string legacyGuid) { return await _reservations.Find(r => r.LegacyGuid == legacyGuid).FirstOrDefaultAsync(); } /// public async Task TryGetDeviceReservationAsync(DeviceId id) { List results = await _reservations.Find(x => true).ToListAsync(); return results.FirstOrDefault(r => r.Devices.Contains(id)); } /// public async Task> FindDeviceTelemetryAsync(DeviceId[]? deviceIds = null, DateTimeOffset? minCreateTime = null, DateTimeOffset? maxCreateTime = null, int? index = null, int? count = null) { FilterDefinitionBuilder filterBuilder = Builders.Filter; FilterDefinition filter = filterBuilder.Empty; if (deviceIds != null && deviceIds.Length > 0) { filter &= filterBuilder.In(x => x.DeviceId, deviceIds); } if (minCreateTime != null) { filter &= filterBuilder.Gte(x => x.CreateTimeUtc!, minCreateTime.Value.UtcDateTime); } if (maxCreateTime != null) { filter &= filterBuilder.Lte(x => x.CreateTimeUtc!, maxCreateTime.Value.UtcDateTime); } List results = await _deviceTelemetry.Find(filter).Range(index, count).ToListAsync(); return results.ConvertAll(x => x); } struct DevicePoolTelemetryHelper { public List _available = new List(); public List _problem = new List(); public List _maintenance = new List(); public List _disabled = new List(); public Dictionary> _reserved = new Dictionary>(); public DevicePoolTelemetryHelper() { } } /// /// Create a device pool telemetry snapshot /// /// public async Task CreatePoolTelemetrySnapshot(int problemCooldown) { List devices = await FindAllDevicesAsync(); List pools = await FindAllPoolsAsync(); List reservations = await FindAllDeviceReservationsAsync(); // narrow to automation pools, may want to collect telemetry on other pools in the future pools = pools.Where(x => x.PoolType == DevicePoolType.Automation).ToList(); devices = devices.Where(x => pools.FirstOrDefault(p => p.Id == x.PoolId) != null).ToList(); if (devices.Count == 0 || pools.Count == 0) { return; } DateTime now = DateTime.UtcNow; List reservedDevices = devices.Where(x => reservations.FirstOrDefault(r => r.Devices.Contains(x.Id)) != null).ToList(); List maintenanceDevices = devices.Where(x => x.MaintenanceTimeUtc != null).ToList(); List disabledDevices = devices.Where(x => !x.Enabled).ToList(); List problemDevices = devices.Where(x => (x.ProblemTimeUtc != null && ((now - x.ProblemTimeUtc).Value.TotalMinutes < problemCooldown))).ToList(); Dictionary> poolTelemetry = new Dictionary>(); foreach (IDevicePool pool in pools) { List poolDevices = devices.Where(x => x.PoolId == pool.Id).ToList(); HashSet platforms = new HashSet(); poolDevices.ForEach(d => platforms.Add(d.PlatformId)); Dictionary helpers = new Dictionary(); foreach (DevicePlatformId platform in platforms) { helpers[platform] = new DevicePoolTelemetryHelper(); } foreach (IDevice device in poolDevices) { DevicePoolTelemetryHelper helper = helpers[device.PlatformId]; if (reservedDevices.Contains(device)) { IDeviceReservation? reservation = reservations.FirstOrDefault(x => x.Devices.Contains(device.Id)); if (reservation != null && reservation.StreamId != null) { StreamId streamId = new StreamId(reservation.StreamId); List? rdevices; if (!helper._reserved.TryGetValue(streamId, out rdevices)) { rdevices = new List(); helper._reserved[streamId] = rdevices; } rdevices.Add(new DeviceReservationPoolTelemetryDocument(device.Id, reservation.JobId, reservation.StepId, reservation.JobName, reservation.StepName)); } continue; } if (problemDevices.Contains(device)) { helper._problem.Add(device.Id); continue; } if (maintenanceDevices.Contains(device)) { helper._maintenance.Add(device.Id); continue; } if (disabledDevices.Contains(device)) { helper._disabled.Add(device.Id); continue; } helper._available.Add(device.Id); } List platformTelemtry = new List(); foreach (KeyValuePair platform in helpers) { DevicePoolTelemetryHelper helper = platform.Value; platformTelemtry.Add(new DevicePlatformTelemetryDocument(platform.Key, helper._available, helper._reserved, helper._maintenance, helper._problem, helper._disabled)); } poolTelemetry[pool.Id] = platformTelemtry; } await _poolTelemetry.InsertOneAsync(new DevicePoolTelemetryDocument(poolTelemetry)); } /// public async Task> FindPoolTelemetryAsync(DateTimeOffset? minCreateTime = null, DateTimeOffset? maxCreateTime = null, int? index = null, int? count = null) { FilterDefinitionBuilder filterBuilder = Builders.Filter; FilterDefinition filter = filterBuilder.Empty; if (minCreateTime != null) { filter &= filterBuilder.Gte(x => x.CreateTimeUtc!, minCreateTime.Value.UtcDateTime); } if (maxCreateTime != null) { filter &= filterBuilder.Lte(x => x.CreateTimeUtc!, maxCreateTime.Value.UtcDateTime); } List results = await _poolTelemetry.Find(filter).Range(index, count).ToListAsync(); return results.ConvertAll(x => x); } /// public async Task UpgradeAsync() { FilterDefinition filter = Builders.Filter.Empty; filter &= Builders.Filter.Where(x => x.Version == null || x.Version < DeviceDocument.CurrentVersion); List results = await _devices.Find(filter).ToListAsync(); if (results.Count > 0) { _logger.LogInformation("Found {Count} device documents to upgrade", results.Count); } foreach (DeviceDocument device in results) { // unversioned => 1 if (device.Version == null && DeviceDocument.CurrentVersion == 1) { UpdateDefinitionBuilder updateBuilder = Builders.Update; List> updates = new List>(); updates.Add(updateBuilder.Set(x => x.Version, DeviceDocument.CurrentVersion)); if (device.CheckOutTime != null) { DateTime now = DateTime.UtcNow; double days = (now - device.CheckOutTime!.Value).TotalDays; if (days > 3) { updates.Add(updateBuilder.Set(x => x.CheckOutTime, now)); updates.Add(updateBuilder.Set(x => x.CheckoutExpiringNotificationSent, null)); } } await _devices.FindOneAndUpdateAsync(x => x.Id == device.Id, updateBuilder.Combine(updates)); } } } } }