Skip to content

Commit f592aaa

Browse files
authored
feat(tables): add run snapshot-expiry endpoint (#639)
1 parent b13e6bf commit f592aaa

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

tables-snapshot-expiry.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// Copyright (c) 2015-2026 MinIO, Inc.
3+
//
4+
// This file is part of MinIO Object Storage stack
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
//
19+
20+
package madmin
21+
22+
import (
23+
"context"
24+
"encoding/json"
25+
"net/http"
26+
"net/url"
27+
)
28+
29+
// RunTableSnapshotExpiryOptions carries optional overrides for an on-demand
30+
// snapshot-expiration run. A nil field inherits the server default.
31+
type RunTableSnapshotExpiryOptions struct {
32+
MaxSnapshotAgeHours *int `json:"maxSnapshotAgeHours,omitempty"`
33+
MinSnapshotsToKeep *int `json:"minSnapshotsToKeep,omitempty"`
34+
}
35+
36+
// RunTableSnapshotExpiryResult reports the outcome of RunTableSnapshotExpiry.
37+
type RunTableSnapshotExpiryResult struct {
38+
SnapshotsExpired int `json:"snapshotsExpired"`
39+
}
40+
41+
// RunTableSnapshotExpiry runs snapshot expiration for a single table
42+
// synchronously, regardless of the table's maintenance schedule or
43+
// enabled/disabled status. It expires snapshot references from table metadata
44+
// only; referenced files are reclaimed separately by unreferenced-file removal.
45+
// namespace uses dot ('.') as the level delimiter.
46+
func (adm *AdminClient) RunTableSnapshotExpiry(ctx context.Context, warehouse, namespace, table string, opts RunTableSnapshotExpiryOptions) (RunTableSnapshotExpiryResult, error) {
47+
var result RunTableSnapshotExpiryResult
48+
49+
body, err := json.Marshal(opts)
50+
if err != nil {
51+
return result, err
52+
}
53+
54+
values := make(url.Values)
55+
values.Set("warehouse", warehouse)
56+
values.Set("namespace", namespace)
57+
values.Set("table", table)
58+
59+
reqData := requestData{
60+
relPath: adminAPIPrefix + "/tables/snapshot-expiry/run",
61+
queryValues: values,
62+
content: body,
63+
}
64+
65+
resp, err := adm.executeMethod(ctx, http.MethodPost, reqData)
66+
defer closeResponse(resp)
67+
if err != nil {
68+
return result, err
69+
}
70+
71+
if resp.StatusCode != http.StatusOK {
72+
return result, httpRespToErrorResponse(resp)
73+
}
74+
75+
dec := json.NewDecoder(resp.Body)
76+
if err = dec.Decode(&result); err != nil {
77+
return result, err
78+
}
79+
80+
return result, nil
81+
}

tables-snapshot-expiry_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//
2+
// Copyright (c) 2015-2026 MinIO, Inc.
3+
//
4+
// This file is part of MinIO Object Storage stack
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
//
19+
20+
package madmin
21+
22+
import (
23+
"context"
24+
"encoding/json"
25+
"io"
26+
"net/http"
27+
"net/http/httptest"
28+
"net/url"
29+
"strings"
30+
"testing"
31+
)
32+
33+
func newRunSnapshotExpiryTestClient(t *testing.T, serverURL string) *AdminClient {
34+
t.Helper()
35+
client, err := New(mustParseHost(t, serverURL), "ak", "sk", false)
36+
if err != nil {
37+
t.Fatalf("New: %v", err)
38+
}
39+
return client
40+
}
41+
42+
// TestRunTableSnapshotExpiryRequest verifies the method, path, query parameters
43+
// and JSON body sent for an on-demand run with both overrides set.
44+
func TestRunTableSnapshotExpiryRequest(t *testing.T) {
45+
var (
46+
method string
47+
path string
48+
query url.Values
49+
body RunTableSnapshotExpiryOptions
50+
)
51+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52+
method, path, query = r.Method, r.URL.Path, r.URL.Query()
53+
b, _ := io.ReadAll(r.Body)
54+
_ = json.Unmarshal(b, &body)
55+
w.WriteHeader(http.StatusOK)
56+
_, _ = w.Write([]byte(`{"snapshotsExpired":0}`))
57+
}))
58+
defer server.Close()
59+
60+
maxAge, keep := 24, 3
61+
_, err := newRunSnapshotExpiryTestClient(t, server.URL).RunTableSnapshotExpiry(
62+
context.Background(), "wh", "ns.sub", "tbl",
63+
RunTableSnapshotExpiryOptions{MaxSnapshotAgeHours: &maxAge, MinSnapshotsToKeep: &keep})
64+
if err != nil {
65+
t.Fatalf("RunTableSnapshotExpiry: %v", err)
66+
}
67+
68+
if method != http.MethodPost {
69+
t.Errorf("method = %s, want POST", method)
70+
}
71+
if !strings.HasPrefix(path, "/minio/admin/") || !strings.HasSuffix(path, "/tables/snapshot-expiry/run") {
72+
t.Errorf("unexpected path: %s", path)
73+
}
74+
if got := query.Get("warehouse"); got != "wh" {
75+
t.Errorf("warehouse = %q, want wh", got)
76+
}
77+
if got := query.Get("namespace"); got != "ns.sub" {
78+
t.Errorf("namespace = %q, want ns.sub", got)
79+
}
80+
if got := query.Get("table"); got != "tbl" {
81+
t.Errorf("table = %q, want tbl", got)
82+
}
83+
if body.MaxSnapshotAgeHours == nil || *body.MaxSnapshotAgeHours != maxAge {
84+
t.Errorf("MaxSnapshotAgeHours = %v, want %d", body.MaxSnapshotAgeHours, maxAge)
85+
}
86+
if body.MinSnapshotsToKeep == nil || *body.MinSnapshotsToKeep != keep {
87+
t.Errorf("MinSnapshotsToKeep = %v, want %d", body.MinSnapshotsToKeep, keep)
88+
}
89+
}
90+
91+
// TestRunTableSnapshotExpiryOmitsUnsetOverrides verifies nil overrides are
92+
// omitted from the body so the server applies its defaults.
93+
func TestRunTableSnapshotExpiryOmitsUnsetOverrides(t *testing.T) {
94+
var rawBody string
95+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
96+
b, _ := io.ReadAll(r.Body)
97+
rawBody = string(b)
98+
w.WriteHeader(http.StatusOK)
99+
_, _ = w.Write([]byte(`{"snapshotsExpired":0}`))
100+
}))
101+
defer server.Close()
102+
103+
if _, err := newRunSnapshotExpiryTestClient(t, server.URL).RunTableSnapshotExpiry(
104+
context.Background(), "wh", "ns", "tbl", RunTableSnapshotExpiryOptions{}); err != nil {
105+
t.Fatalf("RunTableSnapshotExpiry: %v", err)
106+
}
107+
if rawBody != "{}" {
108+
t.Errorf("body = %q, want {}", rawBody)
109+
}
110+
}
111+
112+
// TestRunTableSnapshotExpiryDecode verifies the success response is decoded.
113+
func TestRunTableSnapshotExpiryDecode(t *testing.T) {
114+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
115+
w.WriteHeader(http.StatusOK)
116+
_, _ = w.Write([]byte(`{"snapshotsExpired":7}`))
117+
}))
118+
defer server.Close()
119+
120+
got, err := newRunSnapshotExpiryTestClient(t, server.URL).RunTableSnapshotExpiry(
121+
context.Background(), "wh", "ns", "tbl", RunTableSnapshotExpiryOptions{})
122+
if err != nil {
123+
t.Fatalf("RunTableSnapshotExpiry: %v", err)
124+
}
125+
if got.SnapshotsExpired != 7 {
126+
t.Errorf("SnapshotsExpired = %d, want 7", got.SnapshotsExpired)
127+
}
128+
}
129+
130+
// TestRunTableSnapshotExpiryError verifies a non-2xx response yields an error.
131+
func TestRunTableSnapshotExpiryError(t *testing.T) {
132+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
133+
w.WriteHeader(http.StatusForbidden)
134+
}))
135+
defer server.Close()
136+
137+
if _, err := newRunSnapshotExpiryTestClient(t, server.URL).RunTableSnapshotExpiry(
138+
context.Background(), "wh", "ns", "tbl", RunTableSnapshotExpiryOptions{}); err == nil {
139+
t.Fatal("expected error on non-OK response, got nil")
140+
}
141+
}

0 commit comments

Comments
 (0)