/// <summary> /// Enqueues an upload. /// </summary> /// <param name="username">The username of the remote user.</param> /// <param name="filename">The filename to enqueue.</param> public void Enqueue(string username, string filename) { SyncRoot.Wait(); try { var upload = new Upload() { Username = username, Filename = filename }; Uploads.AddOrUpdate( key: username, addValue: new List <Upload>(new[] { upload }), updateValueFactory: (key, list) => { list.Add(upload); return(list); }); Log.Debug("Enqueued: {File} for {User} at {Time}", Path.GetFileName(upload.Filename), upload.Username, upload.Enqueued); } finally { SyncRoot.Release(); Process(); } }
/// <summary> /// Awaits the start of an upload. /// </summary> /// <param name="username">The username of the remote user.</param> /// <param name="filename">The filename for which to await the start.</param> /// <returns>The operation context.</returns> public Task AwaitStartAsync(string username, string filename) { SyncRoot.Wait(); try { if (!Uploads.TryGetValue(username, out var list)) { throw new SlskdException($"No enqueued uploads for user {username}"); } var upload = list.FirstOrDefault(e => e.Filename == filename); if (upload == default) { throw new SlskdException($"File {filename} is not enqueued for user {username}"); } upload.Ready = DateTime.UtcNow; Log.Debug("Ready: {File} for {User} at {Time}", Path.GetFileName(upload.Filename), upload.Username, upload.Enqueued); return(upload.TaskCompletionSource.Task); } finally { SyncRoot.Release(); Process(); } }
/// <summary> /// Signals the completion of an upload. /// </summary> /// <param name="username">The username of the remote user.</param> /// <param name="filename">The completed filename.</param> public void Complete(string username, string filename) { SyncRoot.Wait(); try { if (!Uploads.TryGetValue(username, out var list)) { throw new SlskdException($"No enqueued uploads for user {username}"); } var upload = list.FirstOrDefault(e => e.Filename == filename); if (upload == default) { throw new SlskdException($"File {filename} is not enqueued for user {username}"); } list.Remove(upload); Log.Debug("Complete: {File} for {User} at {Time}", Path.GetFileName(upload.Filename), upload.Username, upload.Enqueued); // ensure the slot is returned to the group from which it was acquired the group may have been removed during the // transfer. if so, do nothing. if (Groups.ContainsKey(upload.Group ?? string.Empty)) { var group = Groups[upload.Group]; group.UsedSlots = Math.Max(0, group.UsedSlots - 1); Log.Debug("Group {Group} slots: {Used}/{Available}", group.Name, group.UsedSlots, group.Slots); } if (!list.Any() && Uploads.TryRemove(username, out _)) { Log.Debug("Cleaned up tracking list for {User}; no more queued uploads to track", username); } } finally { SyncRoot.Release(); Process(); } }
private Upload Process() { SyncRoot.Wait(); try { if (Groups.Values.Sum(g => g.UsedSlots) >= MaxSlots) { return(null); } // flip the uploads dictionary so that it is keyed by group instead of user. wait until just before we process the // queue to do this, and fetch each user's group as we do, to allow users to move between groups at run time. we // delay "pinning" an upload to a group (via UsedSlots, below) for the same reason. var readyUploadsByGroup = Uploads.Aggregate( seed: new ConcurrentDictionary <string, List <Upload> >(), func: (groups, user) => { var ready = user.Value.Where(u => u.Ready.HasValue && !u.Started.HasValue); if (ready.Any()) { var group = Users.GetGroup(user.Key); groups.AddOrUpdate( key: group, addValue: new List <Upload>(ready), updateValueFactory: (group, list) => { list.AddRange(ready); return(list); }); } return(groups); }); // process each group in ascending order of priority, and stop after the first ready upload is released. foreach (var group in Groups.Values.OrderBy(g => g.Priority).ThenBy(g => g.Name)) { if (group.UsedSlots >= group.Slots || !readyUploadsByGroup.TryGetValue(group.Name, out var uploads) || !uploads.Any()) { continue; } var upload = uploads .OrderBy(u => group.Strategy == QueueStrategy.FirstInFirstOut ? u.Enqueued : u.Ready) .First(); // mark the upload as started, and "pin" it to the group from which the slot is obtained, so the slot can be // returned to the proper place upon completion upload.Started = DateTime.UtcNow; upload.Group = group.Name; group.UsedSlots++; // release the upload upload.TaskCompletionSource.SetResult(); Log.Debug("Started: {File} for {User} at {Time}", Path.GetFileName(upload.Filename), upload.Username, upload.Enqueued); Log.Debug("Group {Group} slots: {Used}/{Available}", group.Name, group.UsedSlots, group.Slots); return(upload); } return(null); } finally { SyncRoot.Release(); } }
private void Configure(Options options) { int GetExistingUsedSlotsOrDefault(string group) => Groups.ContainsKey(group) ? Groups[group].UsedSlots : 0; SyncRoot.Wait(); try { var optionsHash = Compute.Sha1Hash(options.Groups.ToJson()); if (optionsHash == LastOptionsHash && options.Global.Upload.Slots == LastGlobalSlots) { return; } MaxSlots = options.Global.Upload.Slots; // statically add built-in groups var groups = new List <UploadGroup>() { // the priority group is hard-coded with priority 0, slot count equivalent to the overall max, and a FIFO // strategy. all other groups have a minimum priority of 1 (enforced by options validation) to ensure that // privileged users always take priority, regardless of user configuration. the strategy is fixed to FIFO // because that gives privileged users the closest experience to the official client, as well as the // appearance of fairness once the first upload begins. new UploadGroup() { Name = Application.PrivilegedGroup, Priority = 0, Slots = MaxSlots, UsedSlots = GetExistingUsedSlotsOrDefault(Application.PrivilegedGroup), Strategy = QueueStrategy.FirstInFirstOut, }, new UploadGroup() { Name = Application.DefaultGroup, Priority = options.Groups.Default.Upload.Priority, Slots = options.Groups.Default.Upload.Slots, UsedSlots = GetExistingUsedSlotsOrDefault(Application.DefaultGroup), Strategy = (QueueStrategy)Enum.Parse(typeof(QueueStrategy), options.Groups.Default.Upload.Strategy, true), }, new UploadGroup() { Name = Application.LeecherGroup, Priority = options.Groups.Leechers.Upload.Priority, Slots = options.Groups.Leechers.Upload.Slots, UsedSlots = GetExistingUsedSlotsOrDefault(Application.LeecherGroup), Strategy = (QueueStrategy)Enum.Parse(typeof(QueueStrategy), options.Groups.Leechers.Upload.Strategy, true), }, }; // dynamically add user-defined groups groups.AddRange(options.Groups.UserDefined.Select(kvp => new UploadGroup() { Name = kvp.Key, Priority = kvp.Value.Upload.Priority, Slots = kvp.Value.Upload.Slots, UsedSlots = GetExistingUsedSlotsOrDefault(kvp.Key), Strategy = (QueueStrategy)Enum.Parse(typeof(QueueStrategy), kvp.Value.Upload.Strategy, true), })); Groups = groups.ToDictionary(g => g.Name); LastGlobalSlots = options.Global.Upload.Slots; LastOptionsHash = optionsHash; } finally { SyncRoot.Release(); Process(); } }