Sort only missing release assets once

This commit is contained in:
2026-11-01 19:29:50 +00:00
parent b72af69baf
commit eb61b40f08
2 changed files with 21 additions and 21 deletions

View File

@@ -300,7 +300,14 @@ avoidable asymptotic cost without claiming measured release-time savings.
## 7. Order the missing upload set once — P3 ## 7. Order the missing upload set once — P3
Status: open. Status: complete.
**Completed:** Declaration validation now receives an unsorted collection.
Classification collects only missing declarations, and that owned worklist is
sorted in place once after the no-upload exits. Scheduling now takes
O(`n + k log k`) metadata work while retaining filename order and the manifest
last. Existing delivery/artifact tests passed, including manifest matching,
upload ordering, conflict/unverifiable stops, and partial recovery.
**Evidence:** `orderedExpected`, `orderMissing`, and `uploadMissingAssets` in **Evidence:** `orderedExpected`, `orderMissing`, and `uploadMissingAssets` in
[shared asset delivery](../../internal/delivery/remote_assets.go). [shared asset delivery](../../internal/delivery/remote_assets.go).

View File

@@ -114,13 +114,17 @@ func remoteAssetDigest(ctx context.Context, backend forge.Backend, handle any) (
} }
func uploadMissingAssets(ctx context.Context, backend forge.Backend, releaseHandle any, inspection Inspection) (result UploadResult, err error) { func uploadMissingAssets(ctx context.Context, backend forge.Backend, releaseHandle any, inspection Inspection) (result UploadResult, err error) {
expected := orderedExpected(inspection.Assets) expected := make([]artifact.ExpectedAsset, len(inspection.Assets))
for index, observation := range inspection.Assets {
expected[index] = observation.Expected
}
if err := artifact.ValidateExpectedAssets(expected); err != nil { if err := artifact.ValidateExpectedAssets(expected); err != nil {
return UploadResult{State: Partial}, &forge.Failure{Kind: forge.FailureInvalid, Err: err} return UploadResult{State: Partial}, &forge.Failure{Kind: forge.FailureInvalid, Err: err}
} }
result = UploadResult{State: Partial} result = UploadResult{State: Partial}
preflightConflict := false preflightConflict := false
preflightUnverifiable := false preflightUnverifiable := false
var missing []artifact.ExpectedAsset
pending := make(map[string]struct{}, len(expected)) pending := make(map[string]struct{}, len(expected))
// Keep the initial observation order and compact pending identities once, // Keep the initial observation order and compact pending identities once,
// including on early returns after partial or unresolved uploads. // including on early returns after partial or unresolved uploads.
@@ -141,6 +145,7 @@ func uploadMissingAssets(ctx context.Context, backend forge.Backend, releaseHand
case AssetMissing: case AssetMissing:
result.Pending = append(result.Pending, item) result.Pending = append(result.Pending, item)
pending[observation.Expected.Filename] = struct{}{} pending[observation.Expected.Filename] = struct{}{}
missing = append(missing, observation.Expected)
case AssetConflicting: case AssetConflicting:
preflightConflict = true preflightConflict = true
result.Conflicting = append(result.Conflicting, item) result.Conflicting = append(result.Conflicting, item)
@@ -164,10 +169,8 @@ func uploadMissingAssets(ctx context.Context, backend forge.Backend, releaseHand
return result, nil return result, nil
} }
for _, item := range orderMissing(expected) { orderMissing(missing)
if _, needsUpload := pending[item.Filename]; !needsUpload { for _, item := range missing {
continue
}
content, err := assetContent(item) content, err := assetContent(item)
if err != nil { if err != nil {
return result, &forge.Failure{Kind: forge.FailureInvalid, Err: fmt.Errorf("open local release asset %q: %w", item.Filename, err)} return result, &forge.Failure{Kind: forge.FailureInvalid, Err: fmt.Errorf("open local release asset %q: %w", item.Filename, err)}
@@ -226,18 +229,16 @@ func reconcileAssetUpload(ctx context.Context, backend forge.Backend, releaseHan
} }
} }
func orderMissing(expected []artifact.ExpectedAsset) []artifact.ExpectedAsset { func orderMissing(missing []artifact.ExpectedAsset) {
result := append([]artifact.ExpectedAsset(nil), expected...) sort.Slice(missing, func(i, j int) bool {
sort.Slice(result, func(i, j int) bool { if missing[i].Filename == artifact.ManifestFilename {
if result[i].Filename == artifact.ManifestFilename {
return false return false
} }
if result[j].Filename == artifact.ManifestFilename { if missing[j].Filename == artifact.ManifestFilename {
return true return true
} }
return result[i].Filename < result[j].Filename return missing[i].Filename < missing[j].Filename
}) })
return result
} }
func assetContent(asset artifact.ExpectedAsset) (io.ReadCloser, error) { func assetContent(asset artifact.ExpectedAsset) (io.ReadCloser, error) {
@@ -247,14 +248,6 @@ func assetContent(asset artifact.ExpectedAsset) (io.ReadCloser, error) {
return os.Open(asset.Path) return os.Open(asset.Path)
} }
func orderedExpected(observations []AssetInspection) []artifact.ExpectedAsset {
result := make([]artifact.ExpectedAsset, 0, len(observations))
for _, observation := range observations {
result = append(result, observation.Expected)
}
return orderMissing(result)
}
func identity(asset artifact.ExpectedAsset) AssetIdentity { func identity(asset artifact.ExpectedAsset) AssetIdentity {
return AssetIdentity{Filename: asset.Filename, SHA256: asset.SHA256} return AssetIdentity{Filename: asset.Filename, SHA256: asset.SHA256}
} }