Skip to content

Commit 8ad630a

Browse files
fix(catalog/glue): don't roll back S3 Tables table on reload failure
The final LoadTable ran inside the create's rollback scope, so a transient read failure after a successful commit deleted an already-created table (and reclaimed its storage). Move the reload out of that scope: only allocate/write/commit failures roll back now, which also matches the generic create path. Document that S3 Tables keeps its own Glue TableType on the commit update. Signed-off-by: iremcaginyurtturk <cagin.yurtturk@getbruin.com>
1 parent b723237 commit 8ad630a

2 files changed

Lines changed: 55 additions & 13 deletions

File tree

catalog/glue/glue.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,7 @@ func (c *Catalog) createS3TablesTable(ctx context.Context, database, tableName s
372372
return nil, fmt.Errorf("failed to allocate S3 Tables storage for %s.%s: %w", database, tableName, err)
373373
}
374374

375-
tbl, err := c.commitS3TablesTable(ctx, database, tableName, identifier, schema, opts...)
376-
if err != nil {
375+
if err := c.commitS3TablesTable(ctx, database, tableName, identifier, schema, opts...); err != nil {
377376
if _, delErr := c.glueSvc.DeleteTable(ctx, &glue.DeleteTableInput{
378377
CatalogId: c.catalogId,
379378
DatabaseName: aws.String(database),
@@ -385,29 +384,33 @@ func (c *Catalog) createS3TablesTable(ctx context.Context, database, tableName s
385384
return nil, err
386385
}
387386

388-
return tbl, nil
387+
// The table is committed; load it outside the rollback scope so a transient
388+
// read failure does not delete an already-created table.
389+
return c.LoadTable(ctx, identifier)
389390
}
390391

391392
// commitS3TablesTable reads the service-assigned location, writes the Iceberg
392-
// metadata to it, and points the Glue entry at that metadata.
393-
func (c *Catalog) commitS3TablesTable(ctx context.Context, database, tableName string, identifier table.Identifier, schema *iceberg.Schema, opts ...catalog.CreateTableOpt) (*table.Table, error) {
393+
// metadata to it, and points the Glue entry at that metadata. It does not reload
394+
// the table: the caller does that only after a successful commit, so a read
395+
// failure never triggers a rollback of an already-created table.
396+
func (c *Catalog) commitS3TablesTable(ctx context.Context, database, tableName string, identifier table.Identifier, schema *iceberg.Schema, opts ...catalog.CreateTableOpt) error {
394397
allocated, err := c.glueSvc.GetTable(ctx, &glue.GetTableInput{
395398
CatalogId: c.catalogId,
396399
DatabaseName: aws.String(database),
397400
Name: aws.String(tableName),
398401
})
399402
if err != nil {
400-
return nil, fmt.Errorf("failed to load allocated S3 Tables table %s.%s: %w", database, tableName, err)
403+
return fmt.Errorf("failed to load allocated S3 Tables table %s.%s: %w", database, tableName, err)
401404
}
402405
if allocated == nil || allocated.Table == nil || allocated.Table.StorageDescriptor == nil {
403-
return nil, fmt.Errorf("S3 Tables did not return a storage descriptor for %s.%s", database, tableName)
406+
return fmt.Errorf("S3 Tables did not return a storage descriptor for %s.%s", database, tableName)
404407
}
405408
managedLocation := aws.ToString(allocated.Table.StorageDescriptor.Location)
406409
if managedLocation == "" {
407-
return nil, fmt.Errorf("S3 Tables did not assign a storage location for %s.%s", database, tableName)
410+
return fmt.Errorf("S3 Tables did not assign a storage location for %s.%s", database, tableName)
408411
}
409412
if allocated.Table.VersionId == nil {
410-
return nil, fmt.Errorf("cannot commit table %s.%s: because Glue table version id is missing", database, tableName)
413+
return fmt.Errorf("cannot commit table %s.%s: because Glue table version id is missing", database, tableName)
411414
}
412415

413416
// Copy rather than append onto the caller's opts, whose backing array may
@@ -418,13 +421,15 @@ func (c *Catalog) commitS3TablesTable(ctx context.Context, database, tableName s
418421

419422
staged, err := internal.CreateStagedTable(ctx, c.props, c.LoadNamespaceProperties, identifier, schema, stagedOpts...)
420423
if err != nil {
421-
return nil, err
424+
return err
422425
}
423426

424427
if err := internal.WriteMetadata(ctx, staged.Table); err != nil {
425-
return nil, err
428+
return err
426429
}
427430

431+
// constructTableInput sends TableType=EXTERNAL_TABLE; S3 Tables keeps its own
432+
// service type (e.g. "customer") on read, which getRawTable accepts.
428433
_, err = c.glueSvc.UpdateTable(ctx, &glue.UpdateTableInput{
429434
CatalogId: c.catalogId,
430435
DatabaseName: aws.String(database),
@@ -433,10 +438,10 @@ func (c *Catalog) commitS3TablesTable(ctx context.Context, database, tableName s
433438
SkipArchive: aws.Bool(c.props.GetBool(SkipArchive, SkipArchiveDefault)),
434439
})
435440
if err != nil {
436-
return nil, fmt.Errorf("failed to commit S3 Tables table %s.%s: %w", database, tableName, err)
441+
return fmt.Errorf("failed to commit S3 Tables table %s.%s: %w", database, tableName, err)
437442
}
438443

439-
return c.LoadTable(ctx, identifier)
444+
return nil
440445
}
441446

442447
// RegisterTable registers a new table using existing metadata.

catalog/glue/glue_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2895,3 +2895,40 @@ func TestGlueCreateTableS3TablesRollbackOnUpdateFailure(t *testing.T) {
28952895
require.ErrorContains(t, err, "failed to commit S3 Tables table")
28962896
mockGlueSvc.AssertExpectations(t)
28972897
}
2898+
2899+
// TestGlueCreateTableS3TablesNoRollbackOnLoadFailure verifies that a transient
2900+
// failure of the final reload does not delete an already-committed table.
2901+
func TestGlueCreateTableS3TablesNoRollbackOnLoadFailure(t *testing.T) {
2902+
ctx := context.Background()
2903+
managedLocation := "file://" + t.TempDir()
2904+
schema := s3TablesTestSchema()
2905+
2906+
mockGlueSvc := &mockGlueClient{}
2907+
mockGlueSvc.On("GetDatabase", mock.Anything, &glue.GetDatabaseInput{
2908+
Name: aws.String("test_database"),
2909+
}, mock.Anything).Return(federatedDatabaseOutput("aws:s3tables"), nil).Once()
2910+
mockGlueSvc.On("CreateTable", mock.Anything, mock.Anything, mock.Anything).
2911+
Return(&glue.CreateTableOutput{}, nil).Once()
2912+
mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
2913+
DatabaseName: aws.String("test_database"),
2914+
Name: aws.String("test_table"),
2915+
}, mock.Anything).Return(&glue.GetTableOutput{Table: &types.Table{
2916+
Name: aws.String("test_table"),
2917+
DatabaseName: aws.String("test_database"),
2918+
VersionId: aws.String("1"),
2919+
StorageDescriptor: &types.StorageDescriptor{Location: aws.String(managedLocation)},
2920+
}}, nil).Once()
2921+
mockGlueSvc.On("UpdateTable", mock.Anything, mock.Anything, mock.Anything).
2922+
Return(&glue.UpdateTableOutput{}, nil).Once()
2923+
// The trailing reload (LoadTable -> GetTable) fails transiently.
2924+
mockGlueSvc.On("GetTable", mock.Anything, &glue.GetTableInput{
2925+
DatabaseName: aws.String("test_database"),
2926+
Name: aws.String("test_table"),
2927+
}, mock.Anything).Return((*glue.GetTableOutput)(nil), errors.New("load boom")).Once()
2928+
2929+
cat := &Catalog{glueSvc: mockGlueSvc, awsCfg: &aws.Config{}}
2930+
_, err := cat.CreateTable(ctx, TableIdentifier("test_database", "test_table"), schema)
2931+
require.ErrorContains(t, err, "load boom")
2932+
mockGlueSvc.AssertNotCalled(t, "DeleteTable", mock.Anything, mock.Anything, mock.Anything)
2933+
mockGlueSvc.AssertExpectations(t)
2934+
}

0 commit comments

Comments
 (0)