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


##########
catalog/hive/hive.go:
##########
@@ -150,7 +150,13 @@ func (c *Catalog) ListTables(ctx context.Context, 
namespace table.Identifier) it
                for _, tableName := range tableNames {
                        tbl, err := c.client.GetTable(ctx, database, tableName)
                        if err != nil {
-                               continue
+                               if isNoSuchObjectError(err) {
+                                       continue
+                               }
+
+                               yield(nil, fmt.Errorf("failed to load table 
%s.%s while listing: %w", database, tableName, err))

Review Comment:
   Small consistency thing: Glue and REST both yield `table.Identifier{}` on 
the error path and here we yield `nil`. Callers shouldn't be dereferencing the 
identifier after a non-nil error anyway so it's harmless, but I'd match the 
other catalogs.



##########
catalog/hive/hive.go:
##########
@@ -150,7 +150,13 @@ func (c *Catalog) ListTables(ctx context.Context, 
namespace table.Identifier) it
                for _, tableName := range tableNames {
                        tbl, err := c.client.GetTable(ctx, database, tableName)
                        if err != nil {
-                               continue
+                               if isNoSuchObjectError(err) {

Review Comment:
   `isNoSuchObjectError` is now the whole ballgame here: it's the only thing 
standing between "skip a concurrently deleted table" and "abort the listing 
with a real error." The problem is it matches on the substrings `"not found"` 
and `"does not exist"`, and plenty of operational failures carry those (a DNS 
miss like `"metastore host not found"`, an auth `"credentials not found"`, or 
anything wrapping our own `catalog.ErrNoSuchTable`, whose message is `"table 
does not exist"`). Each of those gets classified as a concurrent deletion and 
silently skipped, which is the exact bug this PR is trying to kill.
   
   In the `GetTable` path gohive only ever returns typed Thrift exceptions, so 
I'd lead with a type check and let that carry the decision:
   
   ```go
   var nsoe *hive_metastore.NoSuchObjectException
   if errors.As(err, &nsoe) {
       continue
   }
   ```
   
   If we want to keep a string fallback for other callers I'd narrow it to just 
`"NoSuchObjectException"` and drop the two broad ones. Same gate applies on the 
`ListViews` side. wdyt?



##########
catalog/hive/hive_test.go:
##########
@@ -280,6 +280,39 @@ func TestHiveListTablesEmpty(t *testing.T) {
        mockClient.AssertExpectations(t)
 }
 
+func TestHiveListTablesHandlesObjectLoadingErrors(t *testing.T) {

Review Comment:
   The two subtests are each single-purpose: one only skips, one only 
propagates. The case I'd actually worry about is the mixed one, since that's 
where the new branch ordering can go wrong: a listing where a NoSuchObject 
entry and an operational-failure entry both appear, plus a good one after. I'd 
add a subtest with `[deleted (skip), broken (propagate), good]` and assert the 
broken error surfaces while the good one's `GetTable` is never reached. A 
cancelled-context case that propagates would be a nice one to have too. (Same 
goes for the `ListViews` test.)



##########
catalog/hive/hive_test.go:
##########
@@ -280,6 +280,39 @@ func TestHiveListTablesEmpty(t *testing.T) {
        mockClient.AssertExpectations(t)
 }
 
+func TestHiveListTablesHandlesObjectLoadingErrors(t *testing.T) {
+       t.Run("skips concurrently deleted tables", func(t *testing.T) {
+               mockClient := &mockHiveClient{}
+               mockClient.On("GetTables", mock.Anything, "test_database", 
"*").Return([]string{"deleted", "test_table"}, nil).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"deleted").Return(nil, errNoSuchObject).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"test_table").Return(testIcebergHiveTable1, nil).Once()
+
+               var tables []table.Identifier
+               for identifier, err := range NewCatalogWithClient(mockClient, 
nil).ListTables(context.Background(), DatabaseIdentifier("test_database")) {
+                       require.NoError(t, err)
+                       tables = append(tables, identifier)
+               }
+               require.Equal(t, 
[]table.Identifier{TableIdentifier("test_database", "test_table")}, tables)
+               mockClient.AssertExpectations(t)
+       })
+
+       t.Run("propagates operational failures", func(t *testing.T) {
+               loadErr := errors.New("metastore unavailable")
+               mockClient := &mockHiveClient{}
+               mockClient.On("GetTables", mock.Anything, "test_database", 
"*").Return([]string{"broken", "test_table"}, nil).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"broken").Return(nil, loadErr).Once()
+
+               var gotErr error
+               for _, err := range NewCatalogWithClient(mockClient, 
nil).ListTables(context.Background(), DatabaseIdentifier("test_database")) {
+                       gotErr = err
+               }
+               require.ErrorIs(t, gotErr, loadErr)
+               require.ErrorContains(t, gotErr, "failed to load table 
test_database.broken while listing")
+               mockClient.AssertNotCalled(t, "GetTable", mock.Anything, 
"test_database", "test_table")

Review Comment:
   `AssertNotCalled` with those exact args doesn't really check what we want: 
there's no `On` registered for `"test_table"`, so any call would already blow 
up as unexpected. It also doesn't prove iteration actually stopped. I'd swap it 
for `mockClient.AssertNumberOfCalls(t, "GetTable", 1)`, which pins that we 
stopped after the broken entry.



##########
catalog/hive/hive_test.go:
##########
@@ -280,6 +280,39 @@ func TestHiveListTablesEmpty(t *testing.T) {
        mockClient.AssertExpectations(t)
 }
 
+func TestHiveListTablesHandlesObjectLoadingErrors(t *testing.T) {
+       t.Run("skips concurrently deleted tables", func(t *testing.T) {
+               mockClient := &mockHiveClient{}
+               mockClient.On("GetTables", mock.Anything, "test_database", 
"*").Return([]string{"deleted", "test_table"}, nil).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"deleted").Return(nil, errNoSuchObject).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"test_table").Return(testIcebergHiveTable1, nil).Once()
+
+               var tables []table.Identifier
+               for identifier, err := range NewCatalogWithClient(mockClient, 
nil).ListTables(context.Background(), DatabaseIdentifier("test_database")) {

Review Comment:
   Every other test in this file passes `iceberg.Properties{}` here; these new 
ones pass `nil`. If `ApplyProperties` ever iterates the map without a nil guard 
that's a setup panic masking the thing under test. I'd pass 
`iceberg.Properties{}` to stay consistent.



##########
catalog/hive/hive_test.go:
##########
@@ -280,6 +280,39 @@ func TestHiveListTablesEmpty(t *testing.T) {
        mockClient.AssertExpectations(t)
 }
 
+func TestHiveListTablesHandlesObjectLoadingErrors(t *testing.T) {
+       t.Run("skips concurrently deleted tables", func(t *testing.T) {
+               mockClient := &mockHiveClient{}
+               mockClient.On("GetTables", mock.Anything, "test_database", 
"*").Return([]string{"deleted", "test_table"}, nil).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"deleted").Return(nil, errNoSuchObject).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"test_table").Return(testIcebergHiveTable1, nil).Once()
+
+               var tables []table.Identifier
+               for identifier, err := range NewCatalogWithClient(mockClient, 
nil).ListTables(context.Background(), DatabaseIdentifier("test_database")) {
+                       require.NoError(t, err)
+                       tables = append(tables, identifier)
+               }
+               require.Equal(t, 
[]table.Identifier{TableIdentifier("test_database", "test_table")}, tables)
+               mockClient.AssertExpectations(t)
+       })
+
+       t.Run("propagates operational failures", func(t *testing.T) {
+               loadErr := errors.New("metastore unavailable")
+               mockClient := &mockHiveClient{}
+               mockClient.On("GetTables", mock.Anything, "test_database", 
"*").Return([]string{"broken", "test_table"}, nil).Once()
+               mockClient.On("GetTable", mock.Anything, "test_database", 
"broken").Return(nil, loadErr).Once()
+
+               var gotErr error
+               for _, err := range NewCatalogWithClient(mockClient, 
nil).ListTables(context.Background(), DatabaseIdentifier("test_database")) {
+                       gotErr = err

Review Comment:
   This loop keeps going after the error and only holds the last one, so it 
leans on the iterator stopping itself rather than checking that it does. I'd 
break on the first error like a real consumer would, and while we're unpacking 
both values, pin that the identifier is nil on the error yield:
   
   ```go
   for ident, err := range NewCatalogWithClient(...).ListTables(...) {
       if err != nil {
           require.Nil(t, ident)
           gotErr = err
           break
       }
   }
   ```



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