Skip to content

Commit dd22b91

Browse files
TT-18143: fix zero pruned size in retention plans (#544)
TT-18143: fix zero pruned size in retention plans (#544)
1 parent ab1dd1f commit dd22b91

4 files changed

Lines changed: 109 additions & 12 deletions

File tree

.github/workflows/retention-plan.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ jobs:
7171
# shellcheck disable=SC2086
7272
go run . pkgs plan --grace-days "$GRACE_DAYS" $REPOS > plan.txt
7373
74-
# Newly eligible = checksums in this plan that were not in the
75-
# previous committed plan. Only these trigger the Slack notice.
74+
# One released file is indexed once per distro version on
75+
# Packagecloud, so entry counts run ~10x higher than file counts;
76+
# checksums identify the underlying files. Newly eligible =
77+
# checksums in this plan that were not in the previous committed
78+
# plan. Only these trigger the Slack notice.
7679
- name: Diff against previous plan
7780
id: diff
7881
run: |
@@ -85,6 +88,7 @@ jobs:
8588
fi
8689
comm -23 current.shas previous.shas > new.shas
8790
echo "new_count=$(wc -l < new.shas | tr -d ' ')" >> "$GITHUB_OUTPUT"
91+
echo "total_files=$(wc -l < current.shas | tr -d ' ')" >> "$GITHUB_OUTPUT"
8892
echo "total_pruned=$(jq '[.[].pruned] | add' plan.json)" >> "$GITHUB_OUTPUT"
8993
echo "total_gib=$(jq '[.[].pruned_bytes] | add / 1073741824 * 10 | round / 10' plan.json)" >> "$GITHUB_OUTPUT"
9094
echo "not_before=$(jq -r '.[0].not_before[:10]' plan.json)" >> "$GITHUB_OUTPUT"
@@ -121,7 +125,7 @@ jobs:
121125
echo "|------|----------|----------|--------|------------|------------|"
122126
jq -r '.[] | "| \(.repo) | \(.retained + .pruned) | \(.retained) | \(.pruned) | \(.pruned_bytes / 1073741824 * 10 | round / 10) | \(.not_before[:10]) |"' plan.json
123127
echo ""
124-
echo "$NEW_COUNT newly eligible packages since the previous plan."
128+
echo "$NEW_COUNT files newly eligible since the previous plan."
125129
echo "Committed as [\`$COMMIT\`](https://github.com/TykTechnologies/artifact-retention-plans/commit/$COMMIT)."
126130
echo ""
127131
echo "Nothing was deleted by this workflow."
@@ -141,7 +145,7 @@ jobs:
141145
"type": "section",
142146
"text": {
143147
"type": "mrkdwn",
144-
"text": "*Artifact retention plan published* — @support-team\n${{ steps.diff.outputs.new_count }} packages are newly eligible for pruning (${{ steps.diff.outputs.total_pruned }} total eligible, ${{ steps.diff.outputs.total_gib }} GiB). Nothing will be deleted before *${{ steps.diff.outputs.not_before }}*.\n<https://github.com/TykTechnologies/artifact-retention-plans/commit/${{ steps.commit.outputs.sha }}|Review the committed plan>"
148+
"text": "*Artifact retention plan published* — @support-team\n${{ steps.diff.outputs.new_count }} files are newly eligible for pruning since the last plan. In total ${{ steps.diff.outputs.total_files }} files are eligible, which appear as ${{ steps.diff.outputs.total_pruned }} package listings on Packagecloud (one per distro version, ${{ steps.diff.outputs.total_gib }} GiB). Nothing will be deleted before *${{ steps.diff.outputs.not_before }}*.\n<https://github.com/TykTechnologies/artifact-retention-plans/commit/${{ steps.commit.outputs.sha }}|Review the committed plan>"
145149
}
146150
}
147151
]

cmd/pkgs.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ checksums, which later execution stages verify before deleting.`,
119119
RunE: func(cmd *cobra.Command, args []string) error {
120120
asJSON, _ := cmd.Flags().GetBool("json")
121121
graceDays, _ := cmd.Flags().GetInt("grace-days")
122+
concurrency, _ := cmd.Flags().GetInt("concurrency")
122123
grace := time.Duration(graceDays) * 24 * time.Hour
123124
tracks, err := pkgs.LoadTracks()
124125
if err != nil {
@@ -138,6 +139,7 @@ checksums, which later execution stages verify before deleting.`,
138139
if err != nil {
139140
return fmt.Errorf("planning %s: %w", repoName, err)
140141
}
142+
pkgClient.FillPrunedBytes(&plan, items, concurrency)
141143
plans = append(plans, plan)
142144
if !asJSON {
143145
fmt.Fprint(cmd.OutOrStdout(), plan.Render())
@@ -229,6 +231,7 @@ func init() {
229231

230232
planSubCmd.Flags().Bool("json", false, "Emit the plan as JSON, including the prune-eligible package list")
231233
planSubCmd.Flags().Int("grace-days", 30, "Days until the plan's not_before deadline; override for the 90-day launch notice")
234+
planSubCmd.Flags().Int("concurrency", 8, "Concurrent size lookups, bounded overall by rps/burst")
232235

233236
mirrorSubCmd.Flags().String("plan", "", "Plan file from 'pkgs plan --json'")
234237
mirrorSubCmd.MarkFlagRequired("plan")

pkgs/plan.go

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package pkgs
33
import (
44
"encoding/json"
55
"fmt"
6+
"net/http"
67
"sort"
7-
"strconv"
88
"strings"
9+
"sync/atomic"
910
"time"
1011

12+
"github.com/rs/zerolog/log"
1113
pc "github.com/tyklabs/packagecloud/api/v1"
1214
"golang.org/x/mod/semver"
15+
"golang.org/x/sync/errgroup"
1316
)
1417

1518
// Plan is a dry-run report of what the retention policy would prune
@@ -29,8 +32,9 @@ type Plan struct {
2932
Cutoff string `json:"cutoff,omitempty"`
3033
Series []string `json:"series"`
3134

32-
Retained int `json:"retained"`
33-
Pruned int `json:"pruned"`
35+
Retained int `json:"retained"`
36+
Pruned int `json:"pruned"`
37+
// PrunedBytes counts each unique file once
3438
PrunedBytes int64 `json:"pruned_bytes"`
3539

3640
PrunedSeries map[string]int `json:"pruned_series,omitempty"`
@@ -150,9 +154,6 @@ func BuildPlan(repoName string, cfg pkgConfig, tracks Tracks, items []pc.Package
150154
if prune {
151155
p.Pruned++
152156
p.PrunedSeries[semver.MajorMinor(v)]++
153-
if sz, err := strconv.ParseInt(item.Size, 10, 64); err == nil {
154-
p.PrunedBytes += sz
155-
}
156157
p.Packages = append(p.Packages, PlanPackage{
157158
Name: item.Name,
158159
Version: item.Version,
@@ -169,6 +170,66 @@ func BuildPlan(repoName string, cfg pkgConfig, tracks Tracks, items []pc.Package
169170
return p, nil
170171
}
171172

173+
// FillPrunedBytes sets p.PrunedBytes from the Content-Length of each
174+
// unique prune-eligible file; the listing API does not return sizes.
175+
// The count is advisory, so failures are logged and skipped.
176+
func (c *Client) FillPrunedBytes(p *Plan, items []pc.PackageDetail, concurrency int) {
177+
urlBySha := make(map[string]string, len(items))
178+
for _, item := range items {
179+
urlBySha[item.Sha256Sum] = item.DownloadURL
180+
}
181+
seen := make(map[string]bool, len(p.Packages))
182+
var total atomic.Int64
183+
g := new(errgroup.Group)
184+
g.SetLimit(concurrency)
185+
for _, pp := range p.Packages {
186+
if seen[pp.Sha256Sum] {
187+
continue
188+
}
189+
seen[pp.Sha256Sum] = true
190+
url, found := urlBySha[pp.Sha256Sum]
191+
if !found {
192+
log.Warn().Str("sha256", pp.Sha256Sum).Msgf("sizing %s: not in the repo listing", pp.Filename)
193+
continue
194+
}
195+
g.Go(func() error {
196+
size, err := c.headSize(url)
197+
if err != nil {
198+
log.Warn().Err(err).Msgf("sizing %s", pp.Filename)
199+
return nil
200+
}
201+
total.Add(size)
202+
return nil
203+
})
204+
}
205+
_ = g.Wait()
206+
p.PrunedBytes = total.Load()
207+
}
208+
209+
// headSize returns the Content-Length of a download URL
210+
func (c *Client) headSize(url string) (int64, error) {
211+
req, err := http.NewRequestWithContext(c.ctx, "HEAD", url, nil)
212+
if err != nil {
213+
return 0, err
214+
}
215+
req.SetBasicAuth(c.token, "")
216+
if err := c.limiter.Wait(c.ctx); err != nil {
217+
return 0, err
218+
}
219+
resp, err := http.DefaultClient.Do(req)
220+
if err != nil {
221+
return 0, err
222+
}
223+
resp.Body.Close()
224+
if resp.StatusCode != http.StatusOK {
225+
return 0, fmt.Errorf("%s: %s", url, resp.Status)
226+
}
227+
if resp.ContentLength < 0 {
228+
return 0, fmt.Errorf("%s: no content length", url)
229+
}
230+
return resp.ContentLength, nil
231+
}
232+
172233
// Render returns a human-readable summary of the plan
173234
func (p Plan) Render() string {
174235
var b strings.Builder

pkgs/plan_test.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package pkgs
22

33
import (
4+
"net/http"
5+
"net/http/httptest"
46
"testing"
57
"time"
68

@@ -20,7 +22,6 @@ func pkg(version string, age time.Duration) pc.PackageDetail {
2022
DistroVersion: "ubuntu/jammy",
2123
Filename: "tyk-test_" + version + "_amd64.deb",
2224
Sha256Sum: "sha-" + version,
23-
Size: "1000",
2425
CreateTime: planNow.Add(-age),
2526
}
2627
}
@@ -60,7 +61,6 @@ func TestBuildPlanTrackDriven(t *testing.T) {
6061
assert.Equal(t, 1, plan.NonSemver)
6162
assert.Equal(t, map[string]int{"v3.0.9": 1}, plan.Protected)
6263
assert.Equal(t, map[string]int{"v2.8": 1, "v2.9": 1}, plan.PrunedSeries)
63-
assert.Equal(t, int64(2000), plan.PrunedBytes)
6464

6565
// the prune list carries the tamper-evident identity
6666
require.Len(t, plan.Packages, 2)
@@ -94,6 +94,35 @@ func TestBuildPlanStatic(t *testing.T) {
9494
assert.Equal(t, map[string]int{"v1.8.2": 1}, plan.Protected)
9595
}
9696

97+
func TestFillPrunedBytes(t *testing.T) {
98+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99+
assert.Equal(t, http.MethodHead, r.Method)
100+
w.Header().Set("Content-Length", "1000")
101+
}))
102+
defer srv.Close()
103+
104+
items := []pc.PackageDetail{
105+
pkg("1.6.9", 0),
106+
pkg("1.6.10", 0),
107+
pkg("2.0.0", 0), // retained: must not be sized
108+
}
109+
// same file under a second distro version: counted once
110+
dup := pkg("1.6.9", 0)
111+
dup.DistroVersion = "debian/bookworm"
112+
items = append(items, dup)
113+
for i := range items {
114+
items[i].DownloadURL = srv.URL + "/" + items[i].Filename
115+
}
116+
117+
plan, err := BuildPlan("tyk-test", pkgConfig{VersionCutoff: "v1.7"}, testTracks, items, planNow, planGrace)
118+
require.NoError(t, err)
119+
require.Equal(t, 3, plan.Pruned)
120+
121+
c := NewClient("test-token", "tyk", 100, 100)
122+
c.FillPrunedBytes(&plan, items, 4)
123+
assert.Equal(t, int64(2000), plan.PrunedBytes)
124+
}
125+
97126
func TestBuildPlanBadTrack(t *testing.T) {
98127
_, err := BuildPlan("x", pkgConfig{Track: "nonesuch", Editions: []string{"ce"}}, testTracks, nil, planNow, planGrace)
99128
require.Error(t, err)

0 commit comments

Comments
 (0)