Skip to content

Commit a2e1e65

Browse files
authored
Merge pull request #1286 from slskd/search
Fix 'stuck' searches, including those that contain a single letter
2 parents f3b79d3 + 32e0b71 commit a2e1e65

File tree

3 files changed

+77
-32
lines changed

3 files changed

+77
-32
lines changed

src/slskd/Search/API/Controllers/SearchesController.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace slskd.Search.API
2424
using Asp.Versioning;
2525
using Microsoft.AspNetCore.Authorization;
2626
using Microsoft.AspNetCore.Mvc;
27+
using Serilog;
2728
using SearchQuery = Soulseek.SearchQuery;
2829
using SearchScope = Soulseek.SearchScope;
2930

@@ -50,6 +51,7 @@ public SearchesController(ISearchService searchService, IOptionsSnapshot<Options
5051

5152
private ISearchService Searches { get; }
5253
private IOptionsSnapshot<Options> OptionsSnapshot { get; }
54+
private ILogger Log { get; set; } = Serilog.Log.ForContext<SearchesController>();
5355

5456
/// <summary>
5557
/// Performs a search for the specified <paramref name="request"/>.
@@ -83,12 +85,19 @@ public async Task<IActionResult> Post([FromBody] SearchRequest request)
8385
}
8486
catch (Exception ex) when (ex is ArgumentException || ex is Soulseek.DuplicateTokenException)
8587
{
88+
Log.Error(ex, "Failed to execute search {Search}: {Message}", request, ex.Message);
8689
return BadRequest(ex.Message);
8790
}
8891
catch (InvalidOperationException ex)
8992
{
93+
Log.Error(ex, "Failed to execute search {Search}: {Message}", request, ex.Message);
9094
return Conflict(ex.Message);
9195
}
96+
catch (Exception ex)
97+
{
98+
Log.Error(ex, "Failed to execute search {Search}: {Message}", request, ex.Message);
99+
return StatusCode(500, ex.Message);
100+
}
92101

93102
return Ok(search);
94103
}

src/slskd/Search/SearchService.cs

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ public async Task<Search> StartAsync(Guid id, SearchQuery query, SearchScope sco
228228

229229
var rateLimiter = new RateLimiter(250);
230230

231+
// initialize the search record, save it to the database, and broadcast the creation
232+
// we do this so the UI has some feedback to show to the user that we've gotten their request
231233
var search = new Search()
232234
{
233235
SearchText = query.SearchText,
@@ -241,6 +243,10 @@ public async Task<Search> StartAsync(Guid id, SearchQuery query, SearchScope sco
241243
context.Add(search);
242244
context.SaveChanges();
243245

246+
await SearchHub.BroadcastCreateAsync(search);
247+
248+
// initialize the list of responses that we'll use to accumulate them
249+
// populated by the responseHandler we pass to SearchAsync
244250
List<SearchResponse> responses = new();
245251

246252
options ??= new SearchOptions();
@@ -266,47 +272,75 @@ public async Task<Search> StartAsync(Guid id, SearchQuery query, SearchScope sco
266272
Update(search);
267273
}));
268274

269-
var soulseekSearchTask = Client.SearchAsync(
270-
query,
271-
responseHandler: (response) => responses.Add(response),
272-
scope,
273-
token,
274-
options,
275-
cancellationToken: cancellationTokenSource.Token);
276-
277-
_ = Task.Run(async () =>
275+
try
278276
{
279-
try
280-
{
281-
var soulseekSearch = await soulseekSearchTask;
282-
search = search.WithSoulseekSearch(soulseekSearch);
283-
}
284-
finally
277+
// initiate the search. this can throw at invocation if there's a problem with
278+
// the client state (e.g. disconnected) or a problem with the search (e.g. no terms)
279+
var soulseekSearchTask = Client.SearchAsync(
280+
query,
281+
responseHandler: (response) => responses.Add(response),
282+
scope,
283+
token,
284+
options,
285+
cancellationToken: cancellationTokenSource.Token);
286+
287+
// seach looks ok so far; let the rest of the logic run asynchronously
288+
// on a background thread. this logic needs to clean up after itself and
289+
// update the search record to accurately reflect the final state
290+
_ = Task.Run(async () =>
285291
{
286-
rateLimiter.Dispose();
287-
CancellationTokens.TryRemove(id, out _);
288-
289292
try
290293
{
291-
search.EndedAt = DateTime.UtcNow;
292-
search.Responses = responses.Select(r => Response.FromSoulseekSearchResponse(r));
293-
294-
Update(search);
295-
296-
// zero responses before broadcasting, as we don't want to blast this
297-
// data out over the SignalR socket
298-
await SearchHub.BroadcastUpdateAsync(search with { Responses = [] });
294+
var soulseekSearch = await soulseekSearchTask;
295+
search = search.WithSoulseekSearch(soulseekSearch);
299296
}
300297
catch (Exception ex)
301298
{
302-
Log.Error(ex, "Failed to persist search for {SearchQuery} ({Id})", query, id);
299+
Log.Error(ex, "Failed to execute search {Search}: {Message}", new { query, scope, options }, ex.Message);
300+
search.State = SearchStates.Completed | SearchStates.Errored;
303301
}
304-
}
305-
});
302+
finally
303+
{
304+
rateLimiter.Dispose();
305+
CancellationTokens.TryRemove(id, out _);
306+
307+
try
308+
{
309+
search.EndedAt = DateTime.UtcNow;
310+
search.Responses = responses.Select(r => Response.FromSoulseekSearchResponse(r));
311+
312+
Update(search);
313+
314+
// zero responses before broadcasting, as we don't want to blast this
315+
// data out over the SignalR socket
316+
await SearchHub.BroadcastUpdateAsync(search with { Responses = [] });
317+
}
318+
catch (Exception ex)
319+
{
320+
// record may be left 'hanging' and will need to be cleaned up at the next boot
321+
Log.Error(ex, "Failed to persist search for {SearchQuery} ({Id})", query, id);
322+
}
323+
}
324+
});
306325

307-
await SearchHub.BroadcastCreateAsync(search);
326+
await SearchHub.BroadcastUpdateAsync(search);
327+
328+
return search;
329+
}
330+
catch (Exception ex)
331+
{
332+
// we'll end up here if the initial call throws for an ArgumentException, InvalidOperationException if
333+
// the app isn't connected, and a few other straightforward issues that arise before even requesting the search
334+
Log.Error(ex, "Failed to execute search {Search}: {Message}", new { query, scope, options }, ex.Message);
335+
336+
search.State = SearchStates.Completed | SearchStates.Errored;
337+
search.EndedAt = search.StartedAt;
338+
Update(search);
339+
340+
await SearchHub.BroadcastUpdateAsync(search with { Responses = [] });
308341

309-
return search;
342+
throw;
343+
}
310344
}
311345

312346
/// <summary>

src/web/src/components/Search/Searches.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ const Searches = ({ server } = {}) => {
7272
});
7373
});
7474

75-
searchHub.on('create', () => {});
75+
searchHub.on('create', (search) => {
76+
onUpdate((old) => ({ ...old, [search.id]: search }));
77+
});
7678

7779
searchHub.onreconnecting((connectionError) =>
7880
onConnectionError(connectionError?.message ?? 'Disconnected'),

0 commit comments

Comments
 (0)