private async Task <(IEnumerable <Match> matches, bool moreMatchesAvailable)> GetMatchesInCommonPageAsync(string guid, string guidInCommon, int pageNumber, bool throwException, Throttle throttle)
        {
            if (guid == guidInCommon)
            {
                return(Enumerable.Empty <Match>(), false);
            }

            var nameUnavailableCount = 0;
            var nameUnavailableMax   = 60;
            var retryCount           = 0;
            var retryMax             = 5;

            while (true)
            {
                await throttle.WaitAsync();

                var throttleReleased = false;

                try
                {
                    using (var testsResponse = await _ancestryLoginHelper.AncestryClient.GetAsync($"discoveryui-matchesservice/api/samples/{guid}/matches/list?page={pageNumber}&relationguid={guidInCommon}&bookmarkdata={{\"moreMatchesAvailable\":true,\"lastMatchesServicePageIdx\":{pageNumber - 1}}}"))
                    {
                        throttle.Release();
                        throttleReleased = true;

                        if (testsResponse.StatusCode == System.Net.HttpStatusCode.Gone)
                        {
                            return(Enumerable.Empty <Match>(), false);
                        }
                        if (testsResponse.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
                        {
                            await Task.Delay(120000);

                            continue;
                        }
                        testsResponse.EnsureSuccessStatusCode();
                        var matches = await testsResponse.Content.ReadAsAsync <MatchesV2>();

                        var matchesInCommon = matches.MatchGroups.SelectMany(matchGroup => matchGroup.Matches)
                                              .Select(match => new Match
                        {
                            MatchTestAdminDisplayName = match.AdminDisplayName,
                            MatchTestDisplayName      = match.DisplayName,
                            TestGuid           = match.TestGuid,
                            SharedCentimorgans = match.Relationship?.SharedCentimorgans ?? 0,
                            SharedSegments     = match.Relationship?.SharedSegments ?? 0,
                            Starred            = match.Starred,
                            Note = match.Note,
                        })
                                              .ToList();

                        if (matchesInCommon.Any(match => match.Name == "name unavailable") && ++nameUnavailableCount < nameUnavailableMax)
                        {
                            await Task.Delay(3000);

                            continue;
                        }

                        return(matchesInCommon, matches.BookmarkData.MoreMatchesAvailable);
                    }
                }
                catch (Exception ex)
                {
                    if (++retryCount >= retryMax)
                    {
                        FileUtils.LogException(ex, true);
                        await Task.Delay(ex is UnsupportedMediaTypeException? 30000 : 3000);

                        if (throwException)
                        {
                            throw;
                        }
                        return(Enumerable.Empty <Match>(), false);
                    }
                    await Task.Delay(ex is UnsupportedMediaTypeException? 30000 : 3000);
                }
                finally
                {
                    if (!throttleReleased)
                    {
                        throttle.Release();
                    }
                }
            }
        }
        public async Task <IEnumerable <Match> > GetMatchesPageAsync(string guid, HashSet <int> tagIds, int pageNumber, bool includeAdditionalInfo, Throttle throttle, ProgressData progressData)
        {
            var nameUnavailableCount = 0;
            var nameUnavailableMax   = 60;
            var retryCount           = 0;
            var retryMax             = 5;

            while (true)
            {
                await throttle.WaitAsync();

                var throttleReleased = false;

                try
                {
                    using (var testsResponse = await _ancestryLoginHelper.AncestryClient.GetAsync($"discoveryui-matchesservice/api/samples/{guid}/matches/list?page={pageNumber}&bookmarkdata={{\"moreMatchesAvailable\":true,\"lastMatchesServicePageIdx\":{pageNumber - 1}}}"))
                    {
                        throttle.Release();
                        throttleReleased = true;

                        testsResponse.EnsureSuccessStatusCode();
                        var matches = await testsResponse.Content.ReadAsAsync <MatchesV2>();

                        var result = matches.MatchGroups.SelectMany(matchGroup => matchGroup.Matches)
                                     .Select(match => ConvertMatch(match, tagIds))
                                     .ToList();

                        // Sometimes Ancestry returns matches with partial data.
                        // If that happens, retry and hope to get full data the next time.
                        if (result.Any(match => match.Name == "name unavailable") && ++nameUnavailableCount < nameUnavailableMax)
                        {
                            await Task.Delay(3000);

                            continue;
                        }

                        progressData.Increment();
                        if (includeAdditionalInfo)
                        {
                            try
                            {
                                await GetAdditionalInfoAsync(guid, result, throttle);
                            }
                            catch
                            {
                                // non-fatal if unable to download trees
                            }

                            if (pageNumber == 1)
                            {
                                try
                                {
                                    await GetParentsAsync(guid, result, throttle);
                                }
                                catch
                                {
                                    // non-fatal if unable to download parents
                                }
                            }
                        }

                        progressData.Increment();
                        return(result);
                    }
                }
                catch (Exception ex)
                {
                    if (++retryCount >= retryMax)
                    {
                        FileUtils.LogException(ex, true);
                        await Task.Delay(ex is UnsupportedMediaTypeException? 30000 : 3000);

                        return(Enumerable.Empty <Match>());
                    }
                    await Task.Delay(ex is UnsupportedMediaTypeException? 30000 : 3000);
                }
                finally
                {
                    if (!throttleReleased)
                    {
                        throttle.Release();
                    }
                }
            }
        }