laskoviymishka commented on code in PR #1594:
URL: https://github.com/apache/iceberg-go/pull/1594#discussion_r3681123568


##########
puffin/puffin_test.go:
##########
@@ -52,6 +72,62 @@ func defaultBlobInput() puffin.BlobMetadataInput {
        }
 }
 
+func TestWriterRejectsOperationsAfterPartialWriteFailure(t *testing.T) {
+       tests := []struct {
+               name     string
+               failCall int
+       }{
+               {name: "blob", failCall: 2},

Review Comment:
   These `failCall` numbers encode the global ordinal of each write across 
NewWriter, AddBlob, and Finish — 2 is the blob write, 3–5 the footer writes. 
Nothing ties them to the site they're meant to hit, though: add a checksum or a 
codec byte anywhere in AddBlob and every index below it shifts by one, the `if 
tt.failCall == 2` branch at line 94 silently misclassifies, and the suite stays 
green while intercepting the wrong call.
   
   I'd swap the raw number for something that names the intent — a 
`failsDuringAdd bool` on the table (true only for the blob case) driving that 
branch, plus a short comment enumerating which call each index is. I mostly 
want the drift to be loud instead of silent. wdyt?



##########
puffin/puffin_writer.go:
##########
@@ -53,6 +53,7 @@ type Writer struct {
        blobs     []BlobMetadata
        props     map[string]string
        done      bool
+       failed    error

Review Comment:
   This `failed` field is a real behavioral contract — once any physical write 
errors, every subsequent call is rejected — and it isn't written down anywhere. 
I think poisoning on failure is the right instinct (the stream's already 
half-written, the file is toast), but it's worth a line on the Writer type or 
the AddBlob/Finish godoc so callers know a failed writer is dead and not 
retryable.
   
   One thing to be deliberate about: AddProperties, ClearProperties, and 
SetCreatedBy get poisoned too, and none of them touch the stream. Blocking them 
is defensible — the writer's tainted either way — but it'll surprise someone 
who expects only the byte-writing methods to fail, and it diverges from Java's 
PuffinWriter, which has no failed state at all. I'd keep the poison broad and 
just document that it's intentional.



##########
puffin/puffin_test.go:
##########
@@ -52,6 +72,62 @@ func defaultBlobInput() puffin.BlobMetadataInput {
        }
 }
 
+func TestWriterRejectsOperationsAfterPartialWriteFailure(t *testing.T) {
+       tests := []struct {
+               name     string
+               failCall int
+       }{
+               {name: "blob", failCall: 2},
+               {name: "footer magic", failCall: 3},
+               {name: "footer payload", failCall: 4},
+               {name: "footer trailer", failCall: 5},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       writeErr := errors.New("injected partial write")
+                       output := &partialFailWriter{failCall: tt.failCall, 
err: writeErr}
+                       writer, err := puffin.NewWriter(output)
+                       require.NoError(t, err)
+
+                       _, err = writer.AddBlob(defaultBlobInput(), 
[]byte("first blob payload"))
+                       if tt.failCall == 2 {
+                               require.ErrorIs(t, err, writeErr)
+                       } else {
+                               require.NoError(t, err)
+                               require.ErrorIs(t, writer.Finish(), writeErr)
+                       }
+
+                       callsAfterFailure := output.calls
+                       bytesAfterFailure := output.Len()
+
+                       _, err = writer.AddBlob(defaultBlobInput(), 
[]byte("second blob payload"))
+                       assert.ErrorIs(t, err, writeErr)
+                       assert.ErrorIs(t, writer.Finish(), writeErr)
+                       assert.ErrorIs(t, 
writer.AddProperties(map[string]string{"key": "value"}), writeErr)
+                       assert.ErrorIs(t, writer.ClearProperties(), writeErr)
+                       assert.ErrorIs(t, writer.SetCreatedBy("test"), writeErr)
+                       assert.Equal(t, callsAfterFailure, output.calls)
+                       assert.Equal(t, bytesAfterFailure, output.Len())
+               })
+       }
+}
+
+func TestWriterRejectsOperationsAfterShortWrite(t *testing.T) {
+       output := &partialFailWriter{failCall: 2}
+       writer, err := puffin.NewWriter(output)
+       require.NoError(t, err)
+
+       _, err = writer.AddBlob(defaultBlobInput(), []byte("first blob 
payload"))
+       require.ErrorContains(t, err, "short write")
+
+       callsAfterFailure := output.calls
+       _, err = writer.AddBlob(defaultBlobInput(), []byte("second blob 
payload"))
+       assert.ErrorContains(t, err, "short write")
+       assert.ErrorContains(t, writer.Finish(), "short write")

Review Comment:
   This test only checks AddBlob and Finish get rejected, but the partial-write 
test above asserts all five methods including the three property setters. Same 
code path, so no real risk — just mirror the 
AddProperties/ClearProperties/SetCreatedBy asserts here so the two stay 
symmetric.



##########
puffin/puffin_writer.go:
##########
@@ -89,6 +90,9 @@ func NewWriter(w io.Writer) (*Writer, error) {
 // SetProperties merges the provided properties into the file-level properties
 // written to the footer. Can be called multiple times before Finish.
 func (w *Writer) AddProperties(props map[string]string) error {
+       if w.failed != nil {

Review Comment:
   Tiny thing that recurs in all five methods: `failed` is checked before 
`done`. They can't both be set, so there's no bug — but it reads as though 
`failed` outranks `finalized`, which is backwards. I'd check `done` first. 
Non-blocking.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to