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
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
[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) {
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 {
return UploadResult{State: Partial}, &forge.Failure{Kind: forge.FailureInvalid, Err: err}
}
result = UploadResult{State: Partial}
preflightConflict := false
preflightUnverifiable := false
var missing []artifact.ExpectedAsset
pending := make(map[string]struct{}, len(expected))
// Keep the initial observation order and compact pending identities once,
// including on early returns after partial or unresolved uploads.
@@ -141,6 +145,7 @@ func uploadMissingAssets(ctx context.Context, backend forge.Backend, releaseHand
case AssetMissing:
result.Pending = append(result.Pending, item)
pending[observation.Expected.Filename] = struct{}{}
missing = append(missing, observation.Expected)
case AssetConflicting:
preflightConflict = true
result.Conflicting = append(result.Conflicting, item)
@@ -164,10 +169,8 @@ func uploadMissingAssets(ctx context.Context, backend forge.Backend, releaseHand
return result, nil
}
for _, item := range orderMissing(expected) {
if _, needsUpload := pending[item.Filename]; !needsUpload {
continue
}
orderMissing(missing)
for _, item := range missing {
content, err := assetContent(item)
if err != nil {
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 {
result := append([]artifact.ExpectedAsset(nil), expected...)
sort.Slice(result, func(i, j int) bool {
if result[i].Filename == artifact.ManifestFilename {
func orderMissing(missing []artifact.ExpectedAsset) {
sort.Slice(missing, func(i, j int) bool {
if missing[i].Filename == artifact.ManifestFilename {
return false
}
if result[j].Filename == artifact.ManifestFilename {
if missing[j].Filename == artifact.ManifestFilename {
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) {
@@ -247,14 +248,6 @@ func assetContent(asset artifact.ExpectedAsset) (io.ReadCloser, error) {
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 {
return AssetIdentity{Filename: asset.Filename, SHA256: asset.SHA256}
}