zeroshade commented on code in PR #1610:
URL: https://github.com/apache/iceberg-go/pull/1610#discussion_r3732158566


##########
manifest.go:
##########
@@ -1358,16 +1359,30 @@ func NewManifestWriter(version int, out io.Writer, spec 
PartitionSpec, schema *S
 
 func (w *ManifestWriter) Close() error {
        if w.closed {
-               return nil
+               return w.closeErr
        }
+       w.closed = true
 
+       var emptyErr error
        if w.addedFiles+w.existingFiles+w.deletedFiles == 0 {
-               return errors.New("empty manifest file has been written")
+               emptyErr = errors.New("empty manifest file has been written")

Review Comment:
   Consider promoting this to an exported sentinel — `var ErrEmptyManifest = 
errors.New("empty manifest file has been written")` — and wrapping it here. As 
an inline `errors.New`, callers have no way to `errors.Is` the condition, and 
the new tests have to match on the message text. Given that this PR is 
precisely about making the empty-manifest error stable and observable across 
repeat closes, a sentinel feels like the natural companion. Non-blocking.



##########
manifest_test.go:
##########
@@ -2091,6 +2108,117 @@ func (m *ManifestTestSuite) TestManifestWriterMeta() {
        m.Equal("[]", string(md["partition-spec"]))
 }
 
+func (m *ManifestTestSuite) TestEmptyManifestWriterCloseIsTerminal() {

Review Comment:
   The central question this change settles — that the file left on the output 
is now a readable Avro file containing zero entries — isn't asserted anywhere. 
Worth extending this case to decode `out.Bytes()` back and assert an empty 
entry list. That readability property is exactly what distinguishes the new 
behavior from the pre-PR truncated output, so it seems worth pinning. 
Suggestion only; the terminal-close assertions here are already solid.



##########
manifest.go:
##########
@@ -1753,7 +1768,11 @@ func WriteManifest(
        if err != nil {
                return nil, err
        }
-       defer internal.CheckedClose(w, &err)
+       defer func() {

Review Comment:
   Non-blocking, for consistency: this `if !w.closed` guard (and its twin in 
`WriteManifestV3`) is the right pattern, but it is applied only at these two 
call sites. The `defer internal.CheckedClose(wr, &retErr)` plus explicit 
`wr.Close()` pairs in `table/snapshot_producers.go` (`:223`, `:378`, `:837`, 
`:907`) will now join the cached error with itself and surface a doubled 
message. Worth a follow-up that either repeats the guard there or moves the 
once-only behavior into a helper on the writer, so callers don't each have to 
know about it.



##########
manifest_test.go:
##########
@@ -2091,6 +2108,117 @@ func (m *ManifestTestSuite) TestManifestWriterMeta() {
        m.Equal("[]", string(md["partition-spec"]))
 }
 
+func (m *ManifestTestSuite) TestEmptyManifestWriterCloseIsTerminal() {
+       var out bytes.Buffer
+       writer, err := NewManifestWriter(2, &out, *UnpartitionedSpec, 
testSchema, snapshotID)
+       m.Require().NoError(err)
+
+       firstErr := writer.Close()
+       m.Require().EqualError(firstErr, "empty manifest file has been written")
+       m.ErrorContains(writer.Add(manifestEntryV2Records[0]), "closed manifest 
writer")
+       m.Equal(firstErr, writer.Close())
+
+       _, err = writer.ToManifestFile("manifest.avro", int64(out.Len()))
+       m.Equal(firstErr, err)
+}
+
+func (m *ManifestTestSuite) TestManifestWriterSuccessfulCloseIsTerminal() {
+       var out bytes.Buffer
+       writer, err := NewManifestWriter(2, &out, *UnpartitionedSpec, 
testSchema, snapshotID)
+       m.Require().NoError(err)
+       m.Require().NoError(writer.Add(manifestEntryV2Records[0]))
+
+       m.NoError(writer.Close())
+       m.NoError(writer.Close())
+}
+
+func (m *ManifestTestSuite) 
TestEmptyManifestWriterCloseAfterConstructionFailure() {

Review Comment:
   Consider a companion case: `Close()` after an `addEntry` failure with 
entries already written. That exercises the branch where `emptyErr` is nil but 
`writerErr` is not, and confirms the cached result stays stable across repeat 
closes on that path too. Non-blocking.



##########
manifest_test.go:
##########
@@ -2091,6 +2108,117 @@ func (m *ManifestTestSuite) TestManifestWriterMeta() {
        m.Equal("[]", string(md["partition-spec"]))
 }
 
+func (m *ManifestTestSuite) TestEmptyManifestWriterCloseIsTerminal() {
+       var out bytes.Buffer
+       writer, err := NewManifestWriter(2, &out, *UnpartitionedSpec, 
testSchema, snapshotID)
+       m.Require().NoError(err)
+
+       firstErr := writer.Close()
+       m.Require().EqualError(firstErr, "empty manifest file has been written")
+       m.ErrorContains(writer.Add(manifestEntryV2Records[0]), "closed manifest 
writer")
+       m.Equal(firstErr, writer.Close())
+
+       _, err = writer.ToManifestFile("manifest.avro", int64(out.Len()))
+       m.Equal(firstErr, err)
+}
+
+func (m *ManifestTestSuite) TestManifestWriterSuccessfulCloseIsTerminal() {
+       var out bytes.Buffer
+       writer, err := NewManifestWriter(2, &out, *UnpartitionedSpec, 
testSchema, snapshotID)
+       m.Require().NoError(err)
+       m.Require().NoError(writer.Add(manifestEntryV2Records[0]))
+
+       m.NoError(writer.Close())
+       m.NoError(writer.Close())
+}
+
+func (m *ManifestTestSuite) 
TestEmptyManifestWriterCloseAfterConstructionFailure() {
+       writeErr := errors.New("write failed")
+
+       writer, err := NewManifestWriter(
+               2,
+               manifestFailingWriter{err: writeErr},
+               *UnpartitionedSpec,
+               testSchema,
+               snapshotID,
+       )
+       m.Require().ErrorIs(err, writeErr)
+       m.Require().NotNil(writer)
+
+       var closeErr error
+       m.NotPanics(func() {
+               closeErr = writer.Close()
+       })
+       m.EqualError(closeErr, "empty manifest file has been written")
+
+       // Close returns the cached result after becoming terminal.
+       m.Equal(closeErr, writer.Close())
+}
+
+func (m *ManifestTestSuite) TestEmptyManifestWriterCloseJoinsUnderlyingError() 
{
+       writer, err := NewManifestWriter(
+               2,
+               io.Discard,
+               *UnpartitionedSpec,
+               testSchema,
+               snapshotID,
+       )
+       m.Require().NoError(err)
+
+       avroSchema := writer.writer.Schema()
+       m.Require().NoError(writer.writer.Close())
+
+       underlyingErr := errors.New("underlying close failed")
+       writer.writer, err = ocf.NewWriter(
+               io.Discard,
+               avroSchema,
+               ocf.WithCodec(manifestCloseErrorCodec{
+                       Codec: ocf.DeflateCodec(flate.DefaultCompression),
+                       err:   underlyingErr,
+               }),
+       )
+       m.Require().NoError(err)
+
+       firstErr := writer.Close()
+       m.Require().Error(firstErr)
+       m.ErrorIs(firstErr, underlyingErr)
+       m.EqualError(
+               firstErr,
+               "empty manifest file has been written\nunderlying close failed",
+       )
+
+       // Close returns the cached combined result after becoming terminal.
+       m.Equal(firstErr, writer.Close())
+}
+
+func (m *ManifestTestSuite) TestWriteManifestEmptyErrorIsNotDuplicated() {

Review Comment:
   Non-blocking coverage note: all the new cases construct v2 writers. Since 
`Close()` is shared across versions and `WriteManifestV3` received the same 
guard, running these table-driven over v1/v2/v3 would be cheap insurance 
against a version-specific regression.



-- 
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