Skip to content

Commit 5714498

Browse files
authored
feat(storage): add durable commit lifecycle (#3)
* feat(storage): add durable commit lifecycle * fix(deps): upgrade gorilla websocket
1 parent b3439ad commit 5714498

31 files changed

Lines changed: 3258 additions & 393 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ require (
3636
github.com/go-logr/stdr v1.2.2 // indirect
3737
github.com/go-ole/go-ole v1.3.0 // indirect
3838
github.com/google/uuid v1.6.0 // indirect
39-
github.com/gorilla/websocket v1.4.2 // indirect
39+
github.com/gorilla/websocket v1.5.3 // indirect
4040
github.com/holiman/uint256 v1.3.2 // indirect
4141
github.com/ipfs/go-block-format v0.2.0 // indirect
4242
github.com/ipfs/go-ipfs-util v0.0.3 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
109109
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
110110
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
111111
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
112-
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
113-
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
112+
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
113+
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
114114
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
115115
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
116116
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=

pdp/client.go

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net"
1313
"net/http"
1414
"net/url"
15+
"slices"
1516
"strings"
1617
"syscall"
1718
"time"
@@ -148,6 +149,7 @@ func (c *Client) doWithClient(client *http.Client, req *http.Request, expectStat
148149
}
149150
resp, err := client.Do(req)
150151
if err != nil {
152+
err = redactRequestError(err)
151153
return nil, nil, fmt.Errorf("pdp: %s %s: %w", req.Method, req.URL.Path, err)
152154
}
153155
defer func() { _ = resp.Body.Close() }()
@@ -170,18 +172,16 @@ func (c *Client) doWithClient(client *http.Client, req *http.Request, expectStat
170172
}
171173
return resp, body, nil
172174
}
173-
for _, s := range expectStatuses {
174-
if resp.StatusCode == s {
175-
return resp, body, nil
176-
}
175+
if slices.Contains(expectStatuses, resp.StatusCode) {
176+
return resp, body, nil
177177
}
178178
return resp, body, newHTTPError(req, resp, body)
179179
}
180180

181181
// isRetryable reports whether the error warrants a retry attempt.
182182
//
183183
// Non-retryable (permanent):
184-
// - context.Canceled / context.DeadlineExceeded
184+
// - caller context cancellation or deadline
185185
// - HTTP 4xx except 429
186186
// - HTTP 501 Not Implemented
187187
// - TLS alert errors (bad cert, expired cert, protocol violations)
@@ -196,11 +196,11 @@ func (c *Client) doWithClient(client *http.Client, req *http.Request, expectStat
196196
// Unknown error types are NOT retried. Older releases retried optimistically,
197197
// but that masked permanent misconfigurations (bad URL, invalid signer).
198198
// Callers that need broader retries should do so at the business layer.
199-
func isRetryable(err error) bool {
199+
func isRetryable(ctx context.Context, err error) bool {
200200
if err == nil {
201201
return false
202202
}
203-
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
203+
if ctx.Err() != nil || errors.Is(err, context.Canceled) {
204204
return false
205205
}
206206
if httpErr, ok := errors.AsType[*HTTPError](err); ok {
@@ -215,6 +215,14 @@ func isRetryable(err error) bool {
215215
if _, ok := errors.AsType[tls.AlertError](err); ok {
216216
return false
217217
}
218+
// http.Client and transport timeouts surface as url.Error. The caller's
219+
// context was checked above, so these are safe to retry for idempotent calls.
220+
if urlErr, ok := errors.AsType[*url.Error](err); ok && urlErr.Timeout() {
221+
return true
222+
}
223+
if errors.Is(err, context.DeadlineExceeded) {
224+
return false
225+
}
218226
// Connection-level failures are transient.
219227
if errors.Is(err, syscall.ECONNREFUSED) ||
220228
errors.Is(err, syscall.ECONNRESET) ||
@@ -239,14 +247,20 @@ func isRetryable(err error) bool {
239247
if dnsErr, ok := errors.AsType[*net.DNSError](err); ok {
240248
return dnsErr.IsTemporary || dnsErr.IsTimeout
241249
}
242-
// url.Error surfaces timeouts (request timeout, idle timeout).
243-
if urlErr, ok := errors.AsType[*url.Error](err); ok {
244-
return urlErr.Timeout()
245-
}
246250
// Unknown error type: do not retry. Safer than optimistic retry.
247251
return false
248252
}
249253

254+
func redactRequestError(err error) error {
255+
urlErr, ok := errors.AsType[*url.Error](err)
256+
if !ok {
257+
return err
258+
}
259+
redacted := *urlErr
260+
redacted.URL = redactURLString(urlErr.URL)
261+
return &redacted
262+
}
263+
250264
// httpRetryDelay returns the delay to wait before the next retry attempt.
251265
// For responses with a Retry-After header (429, 503) the server's value takes
252266
// precedence, capped at maxRetryDelay. Otherwise an exponential backoff
@@ -263,10 +277,7 @@ func httpRetryDelay(err error, attempt int) time.Duration {
263277
if attempt > maxShift {
264278
attempt = maxShift
265279
}
266-
d := time.Duration(1<<uint(attempt)) * time.Second
267-
if d > maxRetryDelay {
268-
d = maxRetryDelay
269-
}
280+
d := min(time.Duration(1<<uint(attempt))*time.Second, maxRetryDelay)
270281
return d
271282
}
272283

@@ -280,10 +291,11 @@ func httpRetryDelay(err error, attempt int) time.Duration {
280291
// mutate server state must not be retried here — see postJSON/deleteJSON.
281292
// Long-running and streaming calls should use c.do directly.
282293
func (c *Client) doRetryable(ctx context.Context, makeReq func() (*http.Request, error), expectStatuses ...int) (*http.Response, []byte, error) {
283-
maxRetries := c.maxRetries
284-
if maxRetries < 0 {
285-
maxRetries = 0
286-
}
294+
return c.doRetryableWithClient(ctx, c.httpClient, makeReq, expectStatuses...)
295+
}
296+
297+
func (c *Client) doRetryableWithClient(ctx context.Context, client *http.Client, makeReq func() (*http.Request, error), expectStatuses ...int) (*http.Response, []byte, error) {
298+
maxRetries := max(c.maxRetries, 0)
287299
for attempt := 0; attempt <= maxRetries; attempt++ {
288300
if err := ctx.Err(); err != nil {
289301
return nil, nil, err
@@ -292,11 +304,11 @@ func (c *Client) doRetryable(ctx context.Context, makeReq func() (*http.Request,
292304
if err != nil {
293305
return nil, nil, err
294306
}
295-
resp, body, err := c.do(req, expectStatuses...)
307+
resp, body, err := c.doWithClient(client, req, expectStatuses...)
296308
if err == nil {
297309
return resp, body, nil
298310
}
299-
if !isRetryable(err) || attempt == maxRetries {
311+
if !isRetryable(ctx, err) || attempt == maxRetries {
300312
return resp, body, err
301313
}
302314
if c.logger != nil {

pdp/client_test.go

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -551,10 +551,10 @@ func TestWaitForDataSetCreated(t *testing.T) {
551551
calls++
552552
w.Header().Set("Content-Type", "application/json")
553553
if calls == 1 {
554-
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x1","service":"svc","txStatus":"pending","dataSetCreated":false,"ok":null}`)
554+
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x0000000000000000000000000000000000000000000000000000000000000001","service":"svc","txStatus":"pending","dataSetCreated":false,"ok":null}`)
555555
return
556556
}
557-
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x1","service":"svc","txStatus":"confirmed","dataSetCreated":true,"ok":true,"dataSetId":42}`)
557+
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x0000000000000000000000000000000000000000000000000000000000000001","service":"svc","txStatus":"confirmed","dataSetCreated":true,"ok":true,"dataSetId":42}`)
558558
}))
559559
status, err := c.WaitForDataSetCreated(context.Background(), c.BaseURL().String()+"pdp/data-sets/created/0x1", 10*time.Millisecond)
560560
if err != nil {
@@ -569,7 +569,7 @@ func TestGetDataSetCreationStatus_Accepts202(t *testing.T) {
569569
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
570570
w.Header().Set("Content-Type", "application/json")
571571
w.WriteHeader(http.StatusAccepted)
572-
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x1","service":"svc","txStatus":"pending","dataSetCreated":false,"ok":null}`)
572+
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x0000000000000000000000000000000000000000000000000000000000000001","service":"svc","txStatus":"pending","dataSetCreated":false,"ok":null}`)
573573
}))
574574
status, err := c.GetDataSetCreationStatus(context.Background(), c.BaseURL().String()+"pdp/data-sets/created/0x1")
575575
if err != nil {
@@ -580,33 +580,30 @@ func TestGetDataSetCreationStatus_Accepts202(t *testing.T) {
580580
}
581581
}
582582

583-
func TestWaitForDataSetCreated_ConfirmedFalseStillPending(t *testing.T) {
583+
func TestWaitForDataSetCreated_ConfirmedWithoutResultStillPending(t *testing.T) {
584584
var calls int
585585
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
586586
calls++
587587
w.Header().Set("Content-Type", "application/json")
588588
if calls == 1 {
589-
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x1","service":"svc","txStatus":"confirmed","dataSetCreated":false,"ok":null}`)
589+
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x0000000000000000000000000000000000000000000000000000000000000001","service":"svc","txStatus":"confirmed","dataSetCreated":false,"ok":null}`)
590590
return
591591
}
592-
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x1","service":"svc","txStatus":"confirmed","dataSetCreated":true,"ok":true,"dataSetId":42}`)
592+
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x0000000000000000000000000000000000000000000000000000000000000001","service":"svc","txStatus":"confirmed","dataSetCreated":true,"ok":true,"dataSetId":42}`)
593593
}))
594-
status, err := c.WaitForDataSetCreated(context.Background(), c.BaseURL().String()+"pdp/data-sets/created/0x1", 10*time.Millisecond)
594+
status, err := c.WaitForDataSetCreated(context.Background(), c.BaseURL().String()+"pdp/data-sets/created/0x1", time.Millisecond)
595595
if err != nil {
596596
t.Fatal(err)
597597
}
598-
if calls < 2 {
599-
t.Fatalf("expected multiple polls, got %d", calls)
600-
}
601-
if status.DataSetID == nil || !status.DataSetID.Equal(types.NewBigInt(42)) {
602-
t.Fatalf("id=%v", status.DataSetID)
598+
if calls != 2 || status.DataSetID == nil || !status.DataSetID.Equal(types.NewBigInt(42)) {
599+
t.Fatalf("calls=%d status=%+v", calls, status)
603600
}
604601
}
605602

606603
func TestWaitForDataSetCreated_Rejected(t *testing.T) {
607604
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
608605
w.Header().Set("Content-Type", "application/json")
609-
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x1","service":"svc","txStatus":"rejected","dataSetCreated":false,"ok":false}`)
606+
_, _ = fmt.Fprint(w, `{"createMessageHash":"0x0000000000000000000000000000000000000000000000000000000000000001","service":"svc","txStatus":"rejected","dataSetCreated":false,"ok":false}`)
610607
}))
611608
_, err := c.WaitForDataSetCreated(context.Background(), c.BaseURL().String()+"pdp/data-sets/created/0x1", 10*time.Millisecond)
612609
if !errors.Is(err, ErrTxRejected) {
@@ -687,13 +684,13 @@ func TestAddPieces(t *testing.T) {
687684
}
688685

689686
func TestAddPieces_MaxBatchSizeAccepted(t *testing.T) {
690-
pcInfo, err := piece.CalculateFromBytes([]byte("hi"))
691-
if err != nil {
692-
t.Fatalf("CalculateFromBytes: %v", err)
693-
}
694687
pieces := make([]AddPieceInput, MaxAddPiecesBatchSize)
695688
for i := range pieces {
696-
pieces[i] = AddPieceInput{PieceCID: pcInfo.CIDv1}
689+
info, err := piece.CalculateFromBytes([]byte{byte(i), 0xa5})
690+
if err != nil {
691+
t.Fatalf("CalculateFromBytes(%d): %v", i, err)
692+
}
693+
pieces[i] = AddPieceInput{PieceCID: info.CIDv1}
697694
}
698695
var gotPieces int
699696
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -713,6 +710,42 @@ func TestAddPieces_MaxBatchSizeAccepted(t *testing.T) {
713710
}
714711
}
715712

713+
func TestAddPiecesRejectsDuplicateCanonicalCIDBeforeRequest(t *testing.T) {
714+
info := testPieceInfoV2(t)
715+
requests := 0
716+
c, _ := newTestClient(t, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
717+
requests++
718+
}))
719+
_, err := c.AddPieces(context.Background(), types.NewBigInt(5), []AddPieceInput{
720+
{PieceCID: info.CIDv1},
721+
{PieceCID: info.CIDv2},
722+
}, []byte{1})
723+
if err == nil || !strings.Contains(err.Error(), "duplicate pieceCID") {
724+
t.Fatalf("error=%v want duplicate pieceCID", err)
725+
}
726+
if requests != 0 {
727+
t.Fatalf("requests=%d want 0", requests)
728+
}
729+
}
730+
731+
func TestAddPiecesAllowsSameCIDAcrossRequests(t *testing.T) {
732+
info := testPieceInfoV2(t)
733+
requests := 0
734+
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
735+
requests++
736+
w.Header().Set("Location", "/pdp/data-sets/5/pieces/added/0xdead000000000000000000000000000000000000000000000000000000000000")
737+
w.WriteHeader(http.StatusCreated)
738+
}))
739+
for range 2 {
740+
if _, err := c.AddPieces(context.Background(), types.NewBigInt(5), []AddPieceInput{{PieceCID: info.CIDv2}}, []byte{1}); err != nil {
741+
t.Fatal(err)
742+
}
743+
}
744+
if requests != 2 {
745+
t.Fatalf("requests=%d want 2", requests)
746+
}
747+
}
748+
716749
func TestAddPieces_TooManyPieces(t *testing.T) {
717750
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
718751
t.Fatal("should not reach server")
@@ -756,10 +789,10 @@ func TestWaitForPiecesAdded(t *testing.T) {
756789
calls++
757790
w.Header().Set("Content-Type", "application/json")
758791
if calls == 1 {
759-
_, _ = fmt.Fprint(w, `{"txHash":"0x1","txStatus":"pending","dataSetId":5,"pieceCount":1,"addMessageOk":null,"piecesAdded":false}`)
792+
_, _ = fmt.Fprint(w, `{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000001","txStatus":"pending","dataSetId":5,"pieceCount":1,"addMessageOk":null,"piecesAdded":false}`)
760793
return
761794
}
762-
_, _ = fmt.Fprint(w, `{"txHash":"0x1","txStatus":"confirmed","dataSetId":5,"pieceCount":1,"addMessageOk":true,"piecesAdded":true,"confirmedPieceIds":[10,11]}`)
795+
_, _ = fmt.Fprint(w, `{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000001","txStatus":"confirmed","dataSetId":5,"pieceCount":1,"addMessageOk":true,"piecesAdded":true,"confirmedPieceIds":[10,11]}`)
763796
}))
764797
status, err := c.WaitForPiecesAdded(context.Background(), c.BaseURL().String()+"status", 10*time.Millisecond)
765798
if err != nil {
@@ -774,7 +807,7 @@ func TestGetAddPiecesStatus_Accepts202(t *testing.T) {
774807
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
775808
w.Header().Set("Content-Type", "application/json")
776809
w.WriteHeader(http.StatusAccepted)
777-
_, _ = fmt.Fprint(w, `{"txHash":"0x1","txStatus":"pending","dataSetId":5,"pieceCount":1,"addMessageOk":null,"piecesAdded":false}`)
810+
_, _ = fmt.Fprint(w, `{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000001","txStatus":"pending","dataSetId":5,"pieceCount":1,"addMessageOk":null,"piecesAdded":false}`)
778811
}))
779812
status, err := c.GetAddPiecesStatus(context.Background(), c.BaseURL().String()+"status")
780813
if err != nil {
@@ -785,26 +818,23 @@ func TestGetAddPiecesStatus_Accepts202(t *testing.T) {
785818
}
786819
}
787820

788-
func TestWaitForPiecesAdded_ConfirmedFalseStillPending(t *testing.T) {
821+
func TestWaitForPiecesAdded_ConfirmedWithoutResultStillPending(t *testing.T) {
789822
var calls int
790823
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
791824
calls++
792825
w.Header().Set("Content-Type", "application/json")
793826
if calls == 1 {
794-
_, _ = fmt.Fprint(w, `{"txHash":"0x1","txStatus":"confirmed","dataSetId":5,"pieceCount":1,"addMessageOk":null,"piecesAdded":false}`)
827+
_, _ = fmt.Fprint(w, `{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000001","txStatus":"confirmed","dataSetId":5,"pieceCount":1,"addMessageOk":null,"piecesAdded":false}`)
795828
return
796829
}
797-
_, _ = fmt.Fprint(w, `{"txHash":"0x1","txStatus":"confirmed","dataSetId":5,"pieceCount":1,"addMessageOk":true,"piecesAdded":true,"confirmedPieceIds":[10,11]}`)
830+
_, _ = fmt.Fprint(w, `{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000001","txStatus":"confirmed","dataSetId":5,"pieceCount":1,"addMessageOk":true,"piecesAdded":true,"confirmedPieceIds":[10]}`)
798831
}))
799-
status, err := c.WaitForPiecesAdded(context.Background(), c.BaseURL().String()+"status", 10*time.Millisecond)
832+
status, err := c.WaitForPiecesAdded(context.Background(), c.BaseURL().String()+"status", time.Millisecond)
800833
if err != nil {
801834
t.Fatal(err)
802835
}
803-
if calls < 2 {
804-
t.Fatalf("expected multiple polls, got %d", calls)
805-
}
806-
if len(status.ConfirmedPieceIDs) != 2 {
807-
t.Fatalf("len=%d", len(status.ConfirmedPieceIDs))
836+
if calls != 2 || len(status.ConfirmedPieceIDs) != 1 {
837+
t.Fatalf("calls=%d status=%+v", calls, status)
808838
}
809839
}
810840

@@ -825,7 +855,7 @@ func TestWaitForPiecesAdded_404ReturnsHTTPError(t *testing.T) {
825855
func TestGetAddPiecesStatus_LargeUint64DataSetID(t *testing.T) {
826856
c, _ := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
827857
w.Header().Set("Content-Type", "application/json")
828-
_, _ = fmt.Fprint(w, `{"txHash":"0x1","txStatus":"confirmed","dataSetId":9223372036854775808,"pieceCount":1,"addMessageOk":true,"piecesAdded":true,"confirmedPieceIds":[10]}`)
858+
_, _ = fmt.Fprint(w, `{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000001","txStatus":"confirmed","dataSetId":9223372036854775808,"pieceCount":1,"addMessageOk":true,"piecesAdded":true,"confirmedPieceIds":[10]}`)
829859
}))
830860
status, err := c.GetAddPiecesStatus(context.Background(), c.BaseURL().String()+"status")
831861
if err != nil {
@@ -923,7 +953,7 @@ func TestIsRetryable(t *testing.T) {
923953
}
924954
for _, tc := range tests {
925955
t.Run(tc.name, func(t *testing.T) {
926-
if got := isRetryable(tc.err); got != tc.want {
956+
if got := isRetryable(context.Background(), tc.err); got != tc.want {
927957
t.Errorf("isRetryable(%v) = %v, want %v", tc.err, got, tc.want)
928958
}
929959
})

0 commit comments

Comments
 (0)