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


##########
io/registry.go:
##########
@@ -28,53 +29,121 @@ import (
        "github.com/apache/iceberg-go/internal/schemes"
 )
 
-type registry map[string]SchemeFactory
+// SchemeFactory creates an IO implementation for a URI and its properties.
+type SchemeFactory func(ctx context.Context, parsed *url.URL, props 
map[string]string) (IO, error)
 
 var (
-       regMutex        sync.RWMutex
-       defaultRegistry = registry{}
+       // ErrSchemeAlreadyRegistered indicates that a registry already has a 
factory for a scheme.
+       ErrSchemeAlreadyRegistered = errors.New("io scheme already registered")
+       // ErrInvalidSchemeFactory indicates that an attempt was made to 
register a nil factory.
+       ErrInvalidSchemeFactory = errors.New("io scheme factory is nil")
 )
 
-// SchemeFactory is a function that creates an IO implementation for a given 
URI and properties.
-type SchemeFactory func(ctx context.Context, parsed *url.URL, props 
map[string]string) (IO, error)
+// Registry manages FileIO factories for URI schemes. A registry may be used by
+// multiple goroutines. Factories run without the registry lock held, so they
+// may register other schemes or load additional FileIO instances.
+type Registry struct {
+       mu        sync.RWMutex
+       factories map[string]SchemeFactory
+}
+
+func builtInRegistry() *Registry {
+       localFSFactory := func(context.Context, *url.URL, map[string]string) 
(IO, error) {
+               return LocalFS{}, nil
+       }
+
+       return &Registry{factories: map[string]SchemeFactory{
+               "":     localFSFactory,
+               "file": localFSFactory,
+       }}
+}
+
+// defaultRegistry is populated by side-effect imports of the cloud providers.
+// Independent registries clone these registrations rather than sharing the 
map.
+var defaultRegistry = builtInRegistry()
 
-// Register adds a new scheme factory to the registry. If the scheme is 
already registered, it will panic.
-func Register(scheme string, factory SchemeFactory) {
+// NewRegistry creates an independent snapshot of the globally registered
+// schemes, including cloud providers already registered through side-effect
+// imports. Future registrations in either registry do not affect the other.

Review Comment:
   Should the doc warn that a package-level `NewRegistry()` var can miss 
backends? In a repro binary that blank-imports `io/gocloud/s3`, two packages 
declaring `var Reg = io.NewRegistry()` and importing only `io` both got `[ file 
mem]`, while the global registry had `s3`.
   
   ```suggestion
   // imports. Future registrations in either registry do not affect the other.
   // Called from a package-level var initializer, it can miss backends that the
   // calling package does not import.
   ```



##########
website/src/configuration.md:
##########
@@ -306,17 +306,29 @@ import (
 )
 
 func init() {
-    io.Register("myfs", func(ctx context.Context, parsed *url.URL, props 
map[string]string) (io.IO, error) {
+    io.MustRegister("myfs", func(ctx context.Context, parsed *url.URL, props 
map[string]string) (io.IO, error) {
         return newMyFS(parsed, props)
     })
 }
 ```
 
-`io.Register` panics on `nil` factory or duplicate scheme. Built-in schemes: 
`file`, `""` (the empty scheme).
+`io.MustRegister` panics for invalid or duplicate schemes, so it is suitable 
for provider `init` functions. `io.Register` now returns 
`ErrInvalidSchemeFactory` or `ErrSchemeAlreadyRegistered` instead of panicking. 
Built-in schemes: `file`, `""` (the empty scheme).
 Cloud schemes (`s3`, `gs`, `abfs`, etc.) are registered only when the matching 
backend package under `io/gocloud` is blank-imported.
 
 `io.GetRegisteredSchemes()` returns the current scheme list; 
`io.Unregister(scheme)` removes one.
 
+To register schemes at runtime without changing other users of the same 
process, create an independent registry:

Review Comment:
   Can this line say that catalogs and `view.CreateView` still resolve through 
the default registry? All 20 places in catalog/ and view/ that build a FileIO 
call the global `LoadFS`/`LoadFSFunc`, the same 20 as on `main` (for example 
[`view.CreateView`](https://github.com/apache/iceberg-go/blob/34a72c46fe8797aa824091713266fc9cad014a25/view/view.go#L219)).
   
   ```suggestion
   To register schemes at runtime without changing other users of the same 
process, create an independent registry (catalogs and `view.CreateView` still 
resolve through the default registry):
   ```



##########
io/registry.go:
##########
@@ -28,53 +29,121 @@ import (
        "github.com/apache/iceberg-go/internal/schemes"
 )
 
-type registry map[string]SchemeFactory
+// SchemeFactory creates an IO implementation for a URI and its properties.
+type SchemeFactory func(ctx context.Context, parsed *url.URL, props 
map[string]string) (IO, error)
 
 var (
-       regMutex        sync.RWMutex
-       defaultRegistry = registry{}
+       // ErrSchemeAlreadyRegistered indicates that a registry already has a 
factory for a scheme.
+       ErrSchemeAlreadyRegistered = errors.New("io scheme already registered")
+       // ErrInvalidSchemeFactory indicates that an attempt was made to 
register a nil factory.
+       ErrInvalidSchemeFactory = errors.New("io scheme factory is nil")
 )
 
-// SchemeFactory is a function that creates an IO implementation for a given 
URI and properties.
-type SchemeFactory func(ctx context.Context, parsed *url.URL, props 
map[string]string) (IO, error)
+// Registry manages FileIO factories for URI schemes. A registry may be used by
+// multiple goroutines. Factories run without the registry lock held, so they
+// may register other schemes or load additional FileIO instances.
+type Registry struct {
+       mu        sync.RWMutex
+       factories map[string]SchemeFactory
+}
+
+func builtInRegistry() *Registry {
+       localFSFactory := func(context.Context, *url.URL, map[string]string) 
(IO, error) {
+               return LocalFS{}, nil
+       }
+
+       return &Registry{factories: map[string]SchemeFactory{
+               "":     localFSFactory,
+               "file": localFSFactory,
+       }}
+}
+
+// defaultRegistry is populated by side-effect imports of the cloud providers.
+// Independent registries clone these registrations rather than sharing the 
map.
+var defaultRegistry = builtInRegistry()
 
-// Register adds a new scheme factory to the registry. If the scheme is 
already registered, it will panic.
-func Register(scheme string, factory SchemeFactory) {
+// NewRegistry creates an independent snapshot of the globally registered
+// schemes, including cloud providers already registered through side-effect
+// imports. Future registrations in either registry do not affect the other.
+func NewRegistry() *Registry {
+       return defaultRegistry.Clone()
+}
+
+// Clone returns an independent copy of a registry's factory registrations.
+// Factories themselves are shared, but the registration maps and locks are 
not.
+func (r *Registry) Clone() *Registry {
+       r.mu.RLock()
+       defer r.mu.RUnlock()
+
+       return &Registry{factories: maps.Clone(r.factories)}
+}
+
+// Register adds a scheme factory. Unlike MustRegister, it returns errors for
+// invalid or duplicate registrations rather than panicking.
+func (r *Registry) Register(scheme string, factory SchemeFactory) error {
        if factory == nil {
-               panic("io: Register factory is nil")
+               return ErrInvalidSchemeFactory
        }
 
-       regMutex.Lock()
-       defer regMutex.Unlock()
+       r.mu.Lock()
+       defer r.mu.Unlock()
+
+       if _, exists := r.factories[scheme]; exists {
+               return fmt.Errorf("%w: %q", ErrSchemeAlreadyRegistered, scheme)
+       }
 
-       if _, dup := defaultRegistry[scheme]; dup {
-               panic("io: Register called twice for scheme " + scheme)
+       if r.factories == nil {
+               r.factories = make(map[string]SchemeFactory)
        }
-       defaultRegistry[scheme] = factory
+       r.factories[scheme] = factory
+
+       return nil
 }
 
-// Unregister removes the requested scheme factory from the registry.
-func Unregister(scheme string) {
-       regMutex.Lock()
-       defer regMutex.Unlock()
-       delete(defaultRegistry, scheme)
+// MustRegister registers a scheme or panics. It is intended for provider init
+// functions where failing to register a required backend is unrecoverable.
+func (r *Registry) MustRegister(scheme string, factory SchemeFactory) {
+       if err := r.Register(scheme, factory); err != nil {
+               panic(err)
+       }
 }
 
-// GetRegisteredSchemes returns registered scheme names in sorted order.
-func GetRegisteredSchemes() []string {
-       regMutex.RLock()
-       defer regMutex.RUnlock()
+// Unregister removes a scheme from this registry only.
+func (r *Registry) Unregister(scheme string) {
+       r.mu.Lock()
+       defer r.mu.Unlock()
 
-       return slices.Sorted(maps.Keys(defaultRegistry))
+       delete(r.factories, scheme)
 }
 
-func init() {
-       // Register local filesystem schemes
-       localFSFactory := func(ctx context.Context, parsed *url.URL, props 
map[string]string) (IO, error) {
-               return LocalFS{}, nil
-       }
-       Register("file", localFSFactory)
-       Register("", localFSFactory)
+// RegisteredSchemes returns the scheme names in sorted order.
+func (r *Registry) RegisteredSchemes() []string {
+       r.mu.RLock()
+       defer r.mu.RUnlock()
+
+       return slices.Sorted(maps.Keys(r.factories))
+}
+
+// Register adds a scheme to the global registry and returns an error for nil
+// factories or duplicate registrations. Use MustRegister from init functions.
+func Register(scheme string, factory SchemeFactory) error {

Review Comment:
   Could `io.Register` keep `main`'s panic, or the error-returning version take 
a new name so existing calls stop compiling? With this change a statement call 
like `io.Register("myfs", f)` still compiles, but a duplicate keeps the first 
factory and a nil one is dropped, with no panic or log. A repro with a 
duplicate and a nil statement call logs `factory used=f1, fc-nil 
registered=false`; [`main` panics on 
both](https://github.com/apache/iceberg-go/blob/84c6fd693fe25bc9f6e3c9d99ef733930a6bc30f/io/registry.go#L41-L54).



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