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


##########
catalog/sql/sql_test.go:
##########
@@ -1911,6 +1914,58 @@ func (s *SqliteCatalogTestSuite) 
TestLoadEmptyNamespaceProperties() {
        }
 }
 
+func (s *SqliteCatalogTestSuite) TestCreateNamespaceConcurrent() {
+       // Two callers creating the same namespace: exactly one succeeds and 
the other
+       // gets ErrNamespaceAlreadyExists, not the driver's duplicate-key error.
+       const writers = 8
+
+       // A busy timeout so the writers queue on the sqlite lock instead of 
failing
+       // with SQLITE_BUSY, which is a different contention problem to this 
one.
+       loaded, err := catalog.Load(context.Background(), "default", 
iceberg.Properties{
+               "uri":             s.catalogUri() + "?_pragma=" + 
url.QueryEscape("busy_timeout(10000)"),
+               sqlcat.DriverKey:  sqliteshim.ShimName,
+               sqlcat.DialectKey: string(sqlcat.SQLite),
+               "type":            "sql",
+               "warehouse":       "file://" + s.warehouse,
+       })
+       s.Require().NoError(err)
+
+       cat := loaded.(*sqlcat.Catalog)
+       ctx := context.Background()
+       namespace := table.Identifier{databaseName()}
+
+       start := make(chan struct{})
+       errs := make(chan error, writers)
+
+       var wg sync.WaitGroup
+       for range writers {
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       <-start
+                       errs <- cat.CreateNamespace(ctx, namespace, nil)
+               }()
+       }
+       close(start)
+       wg.Wait()
+       close(errs)
+
+       created, alreadyExists := 0, 0
+       for err := range errs {
+               switch {
+               case err == nil:
+                       created++
+               case errors.Is(err, catalog.ErrNamespaceAlreadyExists):
+                       alreadyExists++
+               default:
+                       s.Failf("unexpected error", "want nil or 
ErrNamespaceAlreadyExists, got %v", err)

Review Comment:
   `s.Failf` marks the test failed but doesn't stop the loop, so one unexpected 
error leaves `created` and `alreadyExists` short and you get three failures 
instead of one, with the message that actually matters buried under two count 
mismatches.
   
   I'd collect and assert after the drain:
   
   ```go
   var unexpected []error
   // ... default: unexpected = append(unexpected, err)
   s.Empty(unexpected, "want nil or ErrNamespaceAlreadyExists")
   s.Equal(1, created)
   s.Equal(writers-1, alreadyExists)
   ```



##########
catalog/sql/sql_test.go:
##########
@@ -1911,6 +1914,58 @@ func (s *SqliteCatalogTestSuite) 
TestLoadEmptyNamespaceProperties() {
        }
 }
 
+func (s *SqliteCatalogTestSuite) TestCreateNamespaceConcurrent() {
+       // Two callers creating the same namespace: exactly one succeeds and 
the other
+       // gets ErrNamespaceAlreadyExists, not the driver's duplicate-key error.
+       const writers = 8
+
+       // A busy timeout so the writers queue on the sqlite lock instead of 
failing
+       // with SQLITE_BUSY, which is a different contention problem to this 
one.
+       loaded, err := catalog.Load(context.Background(), "default", 
iceberg.Properties{
+               "uri":             s.catalogUri() + "?_pragma=" + 
url.QueryEscape("busy_timeout(10000)"),
+               sqlcat.DriverKey:  sqliteshim.ShimName,
+               sqlcat.DialectKey: string(sqlcat.SQLite),
+               "type":            "sql",
+               "warehouse":       "file://" + s.warehouse,
+       })
+       s.Require().NoError(err)
+
+       cat := loaded.(*sqlcat.Catalog)
+       ctx := context.Background()
+       namespace := table.Identifier{databaseName()}
+
+       start := make(chan struct{})
+       errs := make(chan error, writers)
+
+       var wg sync.WaitGroup
+       for range writers {
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       <-start
+                       errs <- cat.CreateNamespace(ctx, namespace, nil)
+               }()
+       }
+       close(start)
+       wg.Wait()
+       close(errs)
+
+       created, alreadyExists := 0, 0
+       for err := range errs {
+               switch {
+               case err == nil:
+                       created++
+               case errors.Is(err, catalog.ErrNamespaceAlreadyExists):
+                       alreadyExists++
+               default:
+                       s.Failf("unexpected error", "want nil or 
ErrNamespaceAlreadyExists, got %v", err)
+               }
+       }
+
+       s.Equal(1, created)
+       s.Equal(writers-1, alreadyExists)

Review Comment:
   This assertion can't tell whether the new branch ran.
   
   The pre-check at line 1246 sits outside `withWriteTx`, which doesn't open 
until 1261. So a loser has two ways to get here: it sees `exists == true` at 
1251 and returns before touching the insert, or it passes the pre-check, loses 
the write-lock race, and reaches the new code at 1276. Both return 
`fmt.Errorf("%w: %s", catalog.ErrNamespaceAlreadyExists, 
strings.Join(namespace, "."))`, byte for byte, so `alreadyExists == 7` holds 
either way and nothing in the drain loop can distinguish them. Which path a 
given goroutine takes is down to scheduling.
   
   I'd add a deterministic test next to this one that forces the insert to 
fail: create the namespace first, then drive the insert path with the row 
already present, so 1276-1278 are actually covered. Keeping this one as a 
concurrency smoke test is fine, I'd just not lean on it as the regression test 
for the fix.



##########
catalog/sql/sql.go:
##########
@@ -1271,6 +1271,12 @@ func (c *Catalog) CreateNamespace(ctx context.Context, 
namespace table.Identifie
 
                _, err := tx.NewInsert().Model(&toInsert).Exec(ctx)
                if err != nil {
+                       // A concurrent writer can insert between the check 
above and here, and
+                       // the driver's duplicate-key error is not what callers 
match on.
+                       if _, exists, checkErr := c.resolveNamespaceKey(ctx, 
namespace); checkErr == nil && exists {

Review Comment:
   non-blocking: the comment covers the race but not the `checkErr == nil` half 
of the guard. I'd add a clause saying that if the re-check itself fails we fall 
through and return the original insert error, since that's the non-obvious 
part. wdyt?



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