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


##########
catalog/rest/vended_creds.go:
##########
@@ -230,3 +271,181 @@ func (v *vendedCredentialRefresher) close() error {
 
        return nil
 }
+
+// prefixScopedIO selects a plan credential using the actual object location
+// passed to Open/Remove. A single plan may cover metadata, data, and delete
+// files in different storage prefixes, so resolving credentials once at plan
+// creation would be incorrect.
+type prefixScopedIO struct {
+       ctx         context.Context
+       baseProps   iceberg.Properties
+       credentials []StorageCredential
+
+       mu          sync.Mutex
+       filesystems map[string]iceio.IO
+       closed      bool
+       nowFunc     func() time.Time
+}
+
+func newPrefixScopedIO(ctx context.Context, baseProps iceberg.Properties, 
credentials []StorageCredential) *prefixScopedIO {
+       return &prefixScopedIO{
+               ctx:         ctx,
+               baseProps:   maps.Clone(baseProps),
+               credentials: slices.Clone(credentials),
+               filesystems: make(map[string]iceio.IO),
+       }
+}
+
+func (p *prefixScopedIO) Open(name string) (iceio.File, error) {
+       fs, err := p.filesystemFor(name)
+       if err != nil {
+               return nil, err
+       }
+
+       return fs.Open(name)
+}
+
+func (p *prefixScopedIO) Remove(name string) error {
+       fs, err := p.filesystemFor(name)
+       if err != nil {
+               return err
+       }
+
+       return fs.Remove(name)
+}
+
+func (p *prefixScopedIO) filesystemFor(name string) (iceio.IO, error) {
+       credentialIndex := matchingStorageCredentialIndex(p.credentials, name)
+       key := scopedFilesystemKey(credentialIndex, name)
+
+       p.mu.Lock()
+       if p.closed {
+               p.mu.Unlock()
+
+               return nil, errors.New("prefix-scoped IO is closed")
+       }
+       if credentialIndex >= 0 {
+               if expiresAt, ok := 
parseCredentialExpiry(p.credentials[credentialIndex].Config); ok &&
+                       p.now().After(expiresAt) {
+                       p.mu.Unlock()
+
+                       return nil, fmt.Errorf("%w: %s expired at %s",
+                               ErrVendedCredentialsExpired, name, 
expiresAt.Format(time.RFC3339))
+               }
+       }
+       if fs, ok := p.filesystems[key]; ok {
+               p.mu.Unlock()
+
+               return fs, nil
+       }
+       p.mu.Unlock()
+
+       props := p.propertiesForLocation(name)
+
+       fs, err := iceio.LoadFS(p.ctx, props, name)
+       if err != nil {
+               return nil, err
+       }
+
+       p.mu.Lock()
+       if p.closed {
+               p.mu.Unlock()
+
+               return nil, errors.New("prefix-scoped IO is closed")
+       }
+       if existing, ok := p.filesystems[key]; ok {

Review Comment:
   Loading outside the mutex avoids serialization, but this double-check 
discards the newly created `fs` without closing it when another goroutine wins 
the cache race. The `p.closed` branch above has the same leak. Cloud 
implementations can own closable clients—for example, `BlobFileIO` promotes 
`blob.Bucket.Close`.
   
   I reproduced this 20/20 times with two concurrent blocked `LoadFS` calls: 
expected the losing filesystem to be closed once; the close count remained 
zero. Please close the newly loaded filesystem before returning from either 
discard branch and add regressions for both concurrent insertion and 
close-during-load.



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