Repository navigation
feat: retry a selection past 500 in rounds instead of refusing it #388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| import { useCallback, useEffect, useMemo, useState } from "react"; | ||
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| import { Link, useSearchParams } from "react-router"; | ||
| import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||
| import { ArrowUp, Download, Pause, Play, Plus, RefreshCw, RotateCcw, TriangleAlert, X } from "lucide-react"; | ||
| import { | ||
| api, | ||
| type BulkRetryPlan, | ||
| type BulkRetrySelection, | ||
| type ExchangeStatus, | ||
| type ExportFilesCheck, | ||
|
|
@@ -304,20 +305,74 @@ export function ExchangesPage() { | |
| staleTime: 30_000, | ||
| }); | ||
|
|
||
| /** How far a retry in rounds has got. */ | ||
| const [bulkProgress, setBulkProgress] = useState<{ done: number; total: number } | null>(null); | ||
| /** Set when the confirm is closed mid-way: the rounds stop after the one under way. */ | ||
| const stopRounds = useRef(false); | ||
| /** | ||
| * The run the open confirm belongs to. Closing it moves this on, so a run that ends afterwards | ||
| * reports what it did but can't close a confirm opened since, or clear a selection made since. | ||
| */ | ||
| const bulkRun = useRef(0); | ||
|
|
||
| /** | ||
| * A selection past what one request carries out: the preview listed every attempt that will run, | ||
| * and they go back as ids, a round at a time. No request runs long, and a round that fails leaves | ||
| * the ones before it done. The attempts, not the selection, so a round can't reach past them to an | ||
| * attempt an earlier round just made. | ||
| */ | ||
| async function retryInRounds(planned: BulkRetryPlan, reset: boolean) { | ||
| const done = { ...planned, willRetry: 0, skipped: [...planned.skipped], stoppedAt: null as number | null }; | ||
| const total = planned.targets.length; | ||
| stopRounds.current = false; | ||
| setBulkProgress({ done: 0, total }); | ||
| for (let at = 0; at < total; at += planned.limit) { | ||
| if (stopRounds.current) { | ||
| done.stoppedAt = at; | ||
| break; | ||
| } | ||
| let round: BulkRetryPlan; | ||
| try { | ||
| round = await api.bulkRetryExchanges({ ids: planned.targets.slice(at, at + planned.limit) }, { reset }); | ||
| } catch (e) { | ||
| throw new Error( | ||
| `${done.willRetry.toLocaleString()} of ${total.toLocaleString()} were retried, then a round failed` + | ||
| ` (${e instanceof Error ? e.message : String(e)}). The rest were not retried.`, | ||
| ); | ||
| } | ||
| done.willRetry += round.willRetry; | ||
| done.skipped.push(...round.skipped); | ||
| setBulkProgress({ done: Math.min(at + planned.limit, total), total }); | ||
| } | ||
| return done; | ||
| } | ||
|
|
||
| const bulkRetry = useMutation({ | ||
| mutationFn: (reset: boolean) => api.bulkRetryExchanges(selection, { reset }), | ||
| onSuccess: (done) => { | ||
| setBulkConfirm(false); | ||
| setBulkReset(false); | ||
| clearSelection(); | ||
| mutationFn: (reset: boolean) => | ||
| plan && plan.selected > plan.limit | ||
| ? retryInRounds(plan, reset) | ||
| : api.bulkRetryExchanges(selection, { reset }).then((done) => ({ ...done, stoppedAt: null })), | ||
| onMutate: () => ({ run: ++bulkRun.current }), | ||
| onSuccess: (done, _reset, started) => { | ||
| setBulkProgress(null); | ||
| if (started?.run === bulkRun.current) { | ||
| setBulkConfirm(false); | ||
| setBulkReset(false); | ||
| clearSelection(); | ||
| } | ||
| setBulkResult( | ||
|
Comment on lines
350
to
363
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '305,380p' SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx
rg -n 'clearSelection|setSelection|bulkConfirm|bulkRetry' SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsxRepository: simplify9/Bitween-api Length of output: 4678 The selection and dialog state can be overwritten by a stale round completion. The close handler stops later rounds but does not cancel the pending mutation or clear the selection. If the user reopens the dialog while the request is pending, the old Guard completion with the operation’s dialog/selection identity, or invalidate the operation when the dialog closes. Do not run 🤖 Prompt for AI Agents |
||
| `${done.willRetry.toLocaleString()} retr${done.willRetry === 1 ? "y" : "ies"} started` + | ||
| (done.substituted.length > 0 ? `, ${done.substituted.length} continuing an existing chain` : "") + | ||
| (done.skipped.length > 0 ? `, ${done.skipped.length} skipped` : "") + | ||
| (done.stoppedAt != null | ||
| ? `. Stopped after ${done.stoppedAt.toLocaleString()} of ${done.targets.length.toLocaleString()}; the rest were not retried` | ||
| : "") + | ||
| ".", | ||
| ); | ||
| void queryClient.invalidateQueries({ queryKey: keys.exchanges.all }); | ||
| }, | ||
| // The list changed as far as the rounds got. | ||
| onError: () => void queryClient.invalidateQueries({ queryKey: keys.exchanges.all }), | ||
| }); | ||
|
|
||
| // Asked first, so someone hears that some exchanges are past the files storage keeps before choosing to | ||
|
|
@@ -654,10 +709,18 @@ export function ExchangesPage() { | |
| plan={plan ?? null} | ||
| planLoading={planLoading} | ||
| busy={bulkRetry.isPending} | ||
| progress={bulkProgress} | ||
| error={bulkRetry.error?.message ?? null} | ||
| onConfirm={(reset) => bulkRetry.mutate(reset)} | ||
| onResetChange={setBulkReset} | ||
| onClose={() => { | ||
| stopRounds.current = true; | ||
| bulkRun.current++; | ||
| setBulkConfirm(false); | ||
| setBulkProgress(null); | ||
| // Not while rounds are still under way: Retry would be offered again, and a second run | ||
| // beside the first would retry what the first just did. | ||
| if (bulkRetry.isError) bulkRetry.reset(); | ||
| // The dialog starts unticked each time it opens, so the mirrored copy has to as well. | ||
| setBulkReset(false); | ||
| }} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '45,140p' SW.Bitween.Api/Resources/Xchanges/BulkRetryPlanner.csRepository: simplify9/Bitween-api
Length of output: 4297
🏁 Script executed:
Repository: simplify9/Bitween-api
Length of output: 16242
🏁 Script executed:
Repository: simplify9/Bitween-api
Length of output: 16869
🏁 Script executed:
Repository: simplify9/Bitween-api
Length of output: 25957
Use a set for target membership checks.
BulkRetryPreviewcan process up to 5,000 resolved IDs. When those IDs map to distinct eligible targets,targets.Contains(targetId)performs up to about 12.5 million equality checks. Keep the list for insertion order and use aHashSet<string>for membership.Suggested fix
var plan = new XchangeBulkRetryPlan { Selected = selected.Count, Limit = Limit, MaxSelection = MaxSelection }; var targets = new List<string>(); + var targetSet = new HashSet<string>(); foreach (var selectedId in selected) { var targetId = newestAttempt[selectedId]; @@ - if (!targets.Contains(targetId)) + if (targetSet.Add(targetId)) targets.Add(targetId);🤖 Prompt for AI Agents