-
Notifications
You must be signed in to change notification settings - Fork 9
perf(storage): bound multipart migration and upload planning #617
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
Merged
AptS-1738
merged 16 commits into
master
from
perf/storage-migration-multipart-heap-budget
Sep 13, 2026
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
eda68d8
perf(storage): bound multipart migration heap usage
AptS-1738 5cfd8a7
feat(remote): expose target connector runtime capabilities
AptS-1738 2b223b1
feat(remote): proxy target connector capabilities
AptS-1738 6aabdb3
fix(qiniu): report official multipart limits
AptS-1738 2423c16
fix(storage): apply COS and OBS multipart limits
AptS-1738 ceba1f6
feat(upload): plan multipart sessions from driver capabilities
AptS-1738 5139086
test(upload): cover capability-aware multipart initialization
AptS-1738 efe230d
test(upload): support multipart driver fixtures
AptS-1738 225f631
fix(ci): cover multipart planning edge cases
AptS-1738 207bcb0
Merge remote-tracking branch 'origin/master' into perf/storage-migrat…
AptS-1738 98df01d
fix(review): tighten multipart capability decisions
AptS-1738 c609f73
test(frontend): cover migration multipart states
AptS-1738 36bd4bd
test(frontend): cover migration capacity detail
AptS-1738 9ce692a
test(frontend): cover absent multipart plan
AptS-1738 bb79f1d
test(coverage): exercise multipart capability adapters
AptS-1738 bb35a45
test(review): cover connector capability mappings
AptS-1738 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
...tend-panel/src/components/admin/admin-policies-page/StoragePolicyMigrationDialog.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import type { ReactNode } from "react"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import { StoragePolicyMigrationDialog } from "@/components/admin/admin-policies-page/StoragePolicyMigrationDialog"; | ||
| import type { StoragePolicy, StoragePolicyMigrationDryRun } from "@/types/api"; | ||
|
|
||
| vi.mock("react-i18next", () => ({ | ||
| useTranslation: () => ({ | ||
| t: (key: string, values?: Record<string, unknown>) => | ||
| values ? `${key} ${Object.values(values).join(" ")}` : key, | ||
| }), | ||
| })); | ||
| vi.mock("@/components/ui/dialog", () => ({ | ||
| Dialog: ({ children, open }: { children: ReactNode; open: boolean }) => | ||
| open ? <div>{children}</div> : null, | ||
| DialogContent: ({ children }: { children: ReactNode }) => ( | ||
| <div>{children}</div> | ||
| ), | ||
| DialogDescription: ({ children }: { children: ReactNode }) => ( | ||
| <p>{children}</p> | ||
| ), | ||
| DialogFooter: ({ children }: { children: ReactNode }) => ( | ||
| <footer>{children}</footer> | ||
| ), | ||
| DialogHeader: ({ children }: { children: ReactNode }) => ( | ||
| <header>{children}</header> | ||
| ), | ||
| DialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>, | ||
| })); | ||
| vi.mock("@/components/ui/button", () => ({ | ||
| Button: ({ | ||
| children, | ||
| ...props | ||
| }: { | ||
| children?: ReactNode; | ||
| [key: string]: unknown; | ||
| }) => <button {...props}>{children}</button>, | ||
| })); | ||
| vi.mock("@/components/ui/icon", () => ({ | ||
| Icon: ({ name }: { name: string }) => <i>{name}</i>, | ||
| })); | ||
| vi.mock("@/components/ui/label", () => ({ | ||
| Label: ({ children }: { children: ReactNode }) => <span>{children}</span>, | ||
| })); | ||
| vi.mock("@/components/ui/select", () => ({ | ||
| Select: ({ children }: { children: ReactNode }) => <div>{children}</div>, | ||
| SelectContent: ({ children }: { children: ReactNode }) => ( | ||
| <div>{children}</div> | ||
| ), | ||
| SelectItem: ({ children }: { children: ReactNode }) => ( | ||
| <span>{children}</span> | ||
| ), | ||
| SelectTrigger: ({ children }: { children: ReactNode }) => ( | ||
| <div>{children}</div> | ||
| ), | ||
| SelectValue: ({ children }: { children?: ReactNode }) => ( | ||
| <span>{children}</span> | ||
| ), | ||
| })); | ||
|
|
||
| const policies = [ | ||
| { id: 1, name: "Source" }, | ||
| { id: 2, name: "Target" }, | ||
| ] as StoragePolicy[]; | ||
|
|
||
| function dryRun( | ||
| reason: "provider_limits" | "buffered_heap_budget" | null, | ||
| uploadMode: "native_streaming" | "buffered" = "native_streaming", | ||
| ) { | ||
| return { | ||
| can_start: reason === null, | ||
| content_sha256_blob_count: 0, | ||
| estimated_copy_blob_count: 1, | ||
| opaque_blob_count: 0, | ||
| opaque_key_conflict_count: 0, | ||
| source_blob_count: 1, | ||
| source_policy_id: 1, | ||
| source_total_bytes: 1024, | ||
| target_capacity: null, | ||
| target_capacity_check: "sufficient", | ||
| target_connection_ok: true, | ||
| target_matching_blob_count: 0, | ||
| target_policy_id: 2, | ||
| target_supports_stream_upload: true, | ||
| warnings: [], | ||
| multipart_plan: { | ||
| blob_size: 1024, | ||
| can_start: reason === null, | ||
| heap_budget: 64 * 1024 * 1024, | ||
| part_count: 1, | ||
| part_size: 1024, | ||
| provider_max_parts: 10_000, | ||
| provider_max_part_size: null, | ||
| reason, | ||
| upload_mode: uploadMode, | ||
| }, | ||
| } as StoragePolicyMigrationDryRun; | ||
| } | ||
|
|
||
| function renderDialog(dryRunValue: StoragePolicyMigrationDryRun) { | ||
| return render( | ||
| <StoragePolicyMigrationDialog | ||
| dryRun={dryRunValue} | ||
| dryRunLoading={false} | ||
| open | ||
| policies={policies} | ||
| sourcePolicyId="1" | ||
| submitting={false} | ||
| targetPolicyId="2" | ||
| onOpenChange={vi.fn()} | ||
| onDryRun={vi.fn()} | ||
| onSourcePolicyChange={vi.fn()} | ||
| onSubmit={vi.fn()} | ||
| onTargetPolicyChange={vi.fn()} | ||
| />, | ||
| ); | ||
| } | ||
|
|
||
| function renderWithoutDryRun() { | ||
| return render( | ||
| <StoragePolicyMigrationDialog | ||
| dryRun={null} | ||
| dryRunLoading={false} | ||
| open | ||
| policies={policies} | ||
| sourcePolicyId="1" | ||
| submitting={false} | ||
| targetPolicyId="2" | ||
| onOpenChange={vi.fn()} | ||
| onDryRun={vi.fn()} | ||
| onSourcePolicyChange={vi.fn()} | ||
| onSubmit={vi.fn()} | ||
| onTargetPolicyChange={vi.fn()} | ||
| />, | ||
| ); | ||
| } | ||
|
|
||
| describe("StoragePolicyMigrationDialog", () => { | ||
| it("renders native streaming multipart plan details", () => { | ||
| renderDialog(dryRun(null)); | ||
| expect( | ||
| screen.getByText("policy_migration_multipart_plan"), | ||
| ).toBeInTheDocument(); | ||
| expect( | ||
| screen.getByText("policy_migration_multipart_mode_native_streaming"), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders the translated multipart block reason", () => { | ||
| renderDialog(dryRun("buffered_heap_budget")); | ||
| expect( | ||
| screen.getByText( | ||
| "policy_migration_multipart_reason_buffered_heap_budget", | ||
| ), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders the buffered multipart mode", () => { | ||
| renderDialog(dryRun("provider_limits", "buffered")); | ||
| expect( | ||
| screen.getByText("policy_migration_multipart_mode_buffered"), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders a verified target capacity detail", () => { | ||
| const value = dryRun(null); | ||
| value.target_capacity = { | ||
| status: "supported", | ||
| total_bytes: 100, | ||
| available_bytes: 40, | ||
| used_bytes: 60, | ||
| source: "test", | ||
| observed_at: "2026-01-01T00:00:00Z", | ||
| }; | ||
| renderDialog(value); | ||
| expect( | ||
| screen.getByText(/policy_migration_capacity_available_of_total.*40.*100/), | ||
| ).toBeInTheDocument(); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| it("renders the dialog before a dry run exists", () => { | ||
| renderWithoutDryRun(); | ||
| expect(screen.getByText("policy_migration_dry_run")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders a dry run without a multipart plan", () => { | ||
| const value = dryRun(null); | ||
| value.multipart_plan = null; | ||
| renderDialog(value); | ||
| expect(screen.queryByText("policy_migration_multipart_plan")).toBeNull(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.