This is an automated email from the ASF dual-hosted git repository.

laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new ec863888d refactor(encryption)!: replace global KMS registration with 
isolated registries (#2296)
ec863888d is described below

commit ec863888dccf543972e7bbfbf6b3fbbe8522a4db
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 27 15:40:22 2026 +0100

    refactor(encryption)!: replace global KMS registration with isolated 
registries (#2296)
---
 encryption/kms_registry.go      | 198 +++++++++++++++++++++--------
 encryption/kms_registry_test.go | 267 ++++++++++++++++++++++++++++++++++------
 website/src/releases.md         |   1 +
 3 files changed, 376 insertions(+), 90 deletions(-)

diff --git a/encryption/kms_registry.go b/encryption/kms_registry.go
index 6bbc29f62..188871e50 100644
--- a/encryption/kms_registry.go
+++ b/encryption/kms_registry.go
@@ -18,6 +18,7 @@
 package encryption
 
 import (
+       "context"
        "errors"
        "fmt"
        "maps"
@@ -26,86 +27,179 @@ import (
 )
 
 // KMSTypeKey is the catalog property that selects a [KeyManagementClient]
-// implementation registered via [RegisterKMS] (e.g. "memory"). It is a
-// registered short name, not a fully-qualified class name.
-//
-// KMSTypeKey ("kms-type") is an iceberg-go-specific mechanism for wiring up
-// a KMS client at runtime. It is not a standardized Iceberg spec property
-// and has no cross-implementation meaning. It will not be recognized by,
-// or portable to, other Iceberg implementations.
+// implementation registered with a [KMSRegistry] (e.g. "memory").
+// It is an iceberg-go-specific property and is not portable to other Iceberg
+// implementations.
 const KMSTypeKey = "kms-type"
 
-// ErrKMSTypeNotFound is returned by [LoadKeyManagementClient] when the
-// requested kmsType has not been registered via [RegisterKMS].
-var ErrKMSTypeNotFound = errors.New("encryption: kms type not found")
+var (
+       // ErrKMSTypeNotFound is returned by [KMSRegistry.Load] when the
+       // requested KMS has not been registered.
+       ErrKMSTypeNotFound = errors.New("encryption: kms type not found")
 
-// KMSFactory creates a [KeyManagementClient] from catalog/table properties.
-// props is the full property map (typically catalog properties); factories
-// should read whichever keys they need (e.g. key material, endpoints,
-// credentials) directly from it.
-type KMSFactory func(props map[string]string) (KeyManagementClient, error)
+       // ErrKMSAlreadyRegistered is returned when a factory name is already 
in use.
+       ErrKMSAlreadyRegistered = errors.New("encryption: kms factory already 
registered")
 
-var (
-       kmsRegMutex sync.RWMutex
-       kmsRegistry = map[string]KMSFactory{}
+       // ErrInvalidKMSRegistration is returned for an empty name or nil 
factory.
+       ErrInvalidKMSRegistration = errors.New("encryption: invalid kms 
registration")
 )
 
-// RegisterKMS adds a new named [KMSFactory] to the registry so it can later
-// be selected via the [KMSTypeKey] catalog property. It panics if factory is
-// nil or if name is already registered.
-func RegisterKMS(name string, factory KMSFactory) {
+// KMSFactory creates a [KeyManagementClient] from catalog/table properties.
+// Factories receive the caller's context and full property map.
+type KMSFactory func(ctx context.Context, props map[string]string) 
(KeyManagementClient, error)
+
+// KMSRegistry owns the named factories used to construct KMS clients.
+// A registry is safe for concurrent use and must not be copied after first 
use.
+// Its zero value is an empty, usable registry. Use [NewKMSRegistry] to clone
+// the global default, which includes the built-in "memory" factory (for 
testing).
+//
+// Separate registries let tests and catalog clients register the same name
+// without modifying each other's factories.
+type KMSRegistry struct {
+       mu        sync.RWMutex
+       factories map[string]KMSFactory
+}
+
+func builtInKMSRegistry() *KMSRegistry {
+       return &KMSRegistry{
+               factories: map[string]KMSFactory{
+                       "memory": func(_ context.Context, _ map[string]string) 
(KeyManagementClient, error) {
+                               return NewInMemoryKeyManagementClient(), nil
+                       },
+               },
+       }
+}
+
+// defaultKMSRegistry receives package-level registrations, including those
+// made by cloud KMS providers through side-effect imports.
+var defaultKMSRegistry = builtInKMSRegistry()
+
+// NewKMSRegistry returns an independent snapshot of the default registry.
+// Providers should be imported before calling it. Initializing a package-level
+// variable with NewKMSRegistry can miss registrations made by other packages.
+func NewKMSRegistry() *KMSRegistry {
+       return defaultKMSRegistry.Clone()
+}
+
+// Clone copies the registered factories into a separate registry. Factories
+// themselves are shared, but future registrations and removals are isolated.
+func (r *KMSRegistry) Clone() *KMSRegistry {
+       r.mu.RLock()
+       defer r.mu.RUnlock()
+
+       return &KMSRegistry{factories: maps.Clone(r.factories)}
+}
+
+// Register adds a named KMS factory. It returns an error for an empty name,
+// nil factory, or duplicate name. The registry is unchanged on error.
+func (r *KMSRegistry) Register(name string, factory KMSFactory) error {
+       if name == "" {
+               return fmt.Errorf("%w: name must not be empty", 
ErrInvalidKMSRegistration)
+       }
        if factory == nil {
-               panic("encryption: RegisterKMS factory is nil")
+               return fmt.Errorf("%w: factory for %q is nil", 
ErrInvalidKMSRegistration, name)
        }
 
-       kmsRegMutex.Lock()
-       defer kmsRegMutex.Unlock()
+       r.mu.Lock()
+       defer r.mu.Unlock()
 
-       if _, dup := kmsRegistry[name]; dup {
-               panic("encryption: RegisterKMS called twice for name " + name)
+       if _, exists := r.factories[name]; exists {
+               return fmt.Errorf("%w: %q", ErrKMSAlreadyRegistered, name)
+       }
+       if r.factories == nil {
+               r.factories = make(map[string]KMSFactory)
        }
-       kmsRegistry[name] = factory
+       r.factories[name] = factory
+
+       return nil
 }
 
-// UnregisterKMS removes the requested named factory from the registry.
-func UnregisterKMS(name string) {
-       kmsRegMutex.Lock()
-       defer kmsRegMutex.Unlock()
-       delete(kmsRegistry, name)
+// MustRegister registers a factory or panics. It is intended for init 
functions
+// in KMS provider packages where registration failure is unrecoverable.
+func (r *KMSRegistry) MustRegister(name string, factory KMSFactory) {
+       if err := r.Register(name, factory); err != nil {
+               panic(err)
+       }
 }
 
-// GetRegisteredKMSNames returns the names of all currently registered KMS
-// factories.
-func GetRegisteredKMSNames() []string {
-       kmsRegMutex.RLock()
-       defer kmsRegMutex.RUnlock()
+// Unregister removes a named factory. Removing an unknown name is a no-op.
+func (r *KMSRegistry) Unregister(name string) {
+       r.mu.Lock()
+       defer r.mu.Unlock()
+
+       delete(r.factories, name)
+}
+
+// Names returns the currently registered factory names in sorted order.
+// The returned slice is independent of the registry.
+func (r *KMSRegistry) Names() []string {
+       r.mu.RLock()
+       defer r.mu.RUnlock()
 
-       return slices.Collect(maps.Keys(kmsRegistry))
+       names := make([]string, 0, len(r.factories))
+       for name := range r.factories {
+               names = append(names, name)
+       }
+       slices.Sort(names)
+
+       return names
 }
 
-// LoadKeyManagementClient resolves and constructs a [KeyManagementClient]
-// from props. It looks up the KMS name under [KMSTypeKey]; it returns an
-// error wrapping [ErrKMSTypeNotFound] if the property is unset or the named
-// KMS has not been registered via [RegisterKMS].
-func LoadKeyManagementClient(props map[string]string) (KeyManagementClient, 
error) {
+// Load resolves and constructs a [KeyManagementClient] from props. It looks
+// up the name under [KMSTypeKey] and returns an error wrapping
+// [ErrKMSTypeNotFound] when the property is empty or not registered.
+// The factory is invoked outside the lock, so it may update this registry.
+func (r *KMSRegistry) Load(ctx context.Context, props map[string]string) 
(KeyManagementClient, error) {
        name := props[KMSTypeKey]
        if name == "" {
                return nil, fmt.Errorf("%w: %q is not set", ErrKMSTypeNotFound, 
KMSTypeKey)
        }
 
-       kmsRegMutex.RLock()
-       factory, ok := kmsRegistry[name]
-       kmsRegMutex.RUnlock()
+       r.mu.RLock()
+       factory, ok := r.factories[name]
+       r.mu.RUnlock()
 
        if !ok {
                return nil, fmt.Errorf("%w: %q", ErrKMSTypeNotFound, name)
        }
 
-       return factory(props)
+       return factory(ctx, props)
+}
+
+// RegisterKMS adds a factory to the process-wide default registry. As in
+// previous releases, invalid or duplicate registrations panic. Use a separate
+// KMSRegistry and its Register method to handle registration errors.
+func RegisterKMS(name string, factory KMSFactory) {
+       if factory == nil {
+               panic("encryption: RegisterKMS factory is nil")
+       }
+       if err := defaultKMSRegistry.Register(name, factory); err != nil {
+               if errors.Is(err, ErrKMSAlreadyRegistered) {
+                       panic("encryption: RegisterKMS called twice for name " 
+ name)
+               }
+               panic(err)
+       }
+}
+
+// MustRegisterKMS registers a global KMS factory or panics. It is intended
+// for use by KMS providers during package initialization.
+func MustRegisterKMS(name string, factory KMSFactory) {
+       RegisterKMS(name, factory)
+}
+
+// UnregisterKMS removes a factory from the default registry. Existing
+// independent registry snapshots are unaffected.
+func UnregisterKMS(name string) {
+       defaultKMSRegistry.Unregister(name)
+}
+
+// GetRegisteredKMSNames returns globally registered factory names, sorted.
+func GetRegisteredKMSNames() []string {
+       return defaultKMSRegistry.Names()
 }
 
-func init() {
-       RegisterKMS("memory", func(_ map[string]string) (KeyManagementClient, 
error) {
-               return NewInMemoryKeyManagementClient(), nil
-       })
+// LoadKeyManagementClient constructs a KMS client using the default registry.
+// Its context is forwarded to the selected factory.
+func LoadKeyManagementClient(ctx context.Context, props map[string]string) 
(KeyManagementClient, error) {
+       return defaultKMSRegistry.Load(ctx, props)
 }
diff --git a/encryption/kms_registry_test.go b/encryption/kms_registry_test.go
index 083d68717..358287d00 100644
--- a/encryption/kms_registry_test.go
+++ b/encryption/kms_registry_test.go
@@ -18,6 +18,9 @@
 package encryption_test
 
 import (
+       "context"
+       "fmt"
+       "sync"
        "testing"
 
        "github.com/apache/iceberg-go/encryption"
@@ -25,81 +28,269 @@ import (
        "github.com/stretchr/testify/require"
 )
 
-func TestLoadKeyManagementClient_Memory(t *testing.T) {
-       kms, err := encryption.LoadKeyManagementClient(map[string]string{
+func memoryKMSFactory(_ context.Context, _ map[string]string) 
(encryption.KeyManagementClient, error) {
+       return encryption.NewInMemoryKeyManagementClient(), nil
+}
+
+func TestKMSRegistry_LoadMemory(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       kms, err := registry.Load(t.Context(), map[string]string{
                encryption.KMSTypeKey: "memory",
        })
        require.NoError(t, err)
        assert.IsType(t, &encryption.InMemoryKeyManagementClient{}, kms)
 }
 
-func TestLoadKeyManagementClient_NoPropertiesSet(t *testing.T) {
-       _, err := encryption.LoadKeyManagementClient(map[string]string{})
+func TestKMSRegistry_NoPropertiesSet(t *testing.T) {
+       _, err := encryption.NewKMSRegistry().Load(t.Context(), 
map[string]string{})
        require.ErrorIs(t, err, encryption.ErrKMSTypeNotFound)
 }
 
-func TestLoadKeyManagementClient_UnregisteredName(t *testing.T) {
-       _, err := encryption.LoadKeyManagementClient(map[string]string{
+func TestKMSRegistry_UnregisteredName(t *testing.T) {
+       _, err := encryption.NewKMSRegistry().Load(t.Context(), 
map[string]string{
                encryption.KMSTypeKey: "does-not-exist",
        })
        require.ErrorIs(t, err, encryption.ErrKMSTypeNotFound)
 }
 
-func TestRegisterKMS_DuplicatePanics(t *testing.T) {
-       const name = "test-duplicate-kms"
-       factory := func(_ map[string]string) (encryption.KeyManagementClient, 
error) {
-               return encryption.NewInMemoryKeyManagementClient(), nil
-       }
+func TestKMSRegistry_DuplicateRegistration(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       require.NoError(t, registry.Register("custom", memoryKMSFactory))
+       err := registry.Register("custom", func(_ context.Context, _ 
map[string]string) (encryption.KeyManagementClient, error) {
+               return nil, assert.AnError
+       })
+       require.ErrorIs(t, err, encryption.ErrKMSAlreadyRegistered)
 
-       encryption.RegisterKMS(name, factory)
-       defer encryption.UnregisterKMS(name)
+       _, err = registry.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "custom"})
+       require.NoError(t, err)
+}
 
-       assert.Panics(t, func() {
-               encryption.RegisterKMS(name, factory)
-       })
+func TestKMSRegistry_InvalidRegistration(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       require.ErrorIs(t, registry.Register("invalid", nil), 
encryption.ErrInvalidKMSRegistration)
+       require.ErrorIs(t, registry.Register("", memoryKMSFactory), 
encryption.ErrInvalidKMSRegistration)
+       assert.Equal(t, []string{"memory"}, registry.Names())
 }
 
-func TestRegisterKMS_NilFactoryPanics(t *testing.T) {
-       assert.Panics(t, func() {
-               encryption.RegisterKMS("test-nil-factory-kms", nil)
-       })
+func TestKMSRegistry_IsolatedRegistries(t *testing.T) {
+       first := encryption.NewKMSRegistry()
+       second := encryption.NewKMSRegistry()
+       require.NoError(t, first.Register("shared", memoryKMSFactory))
+       require.NoError(t, second.Register("shared", memoryKMSFactory))
+
+       first.Unregister("shared")
+       _, err := first.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "shared"})
+       require.ErrorIs(t, err, encryption.ErrKMSTypeNotFound)
+
+       _, err = second.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "shared"})
+       require.NoError(t, err)
+       assert.Equal(t, []string{"memory", "shared"}, second.Names())
 }
 
-func TestGetRegisteredKMSNames_IncludesMemory(t *testing.T) {
-       names := encryption.GetRegisteredKMSNames()
-       assert.Contains(t, names, "memory")
+func TestKMSRegistry_NamesSortedAndIndependent(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       require.NoError(t, registry.Register("z", memoryKMSFactory))
+       require.NoError(t, registry.Register("a", memoryKMSFactory))
+
+       names := registry.Names()
+       assert.Equal(t, []string{"a", "memory", "z"}, names)
+
+       names[0] = "changed"
+       assert.Equal(t, []string{"a", "memory", "z"}, registry.Names())
 }
 
-func TestLoadKeyManagementClient_FactoryError(t *testing.T) {
-       const name = "test-error-kms"
+func TestKMSRegistry_ZeroValue(t *testing.T) {
+       var registry encryption.KMSRegistry
+       assert.Empty(t, registry.Names())
+       require.NoError(t, registry.Register("custom", memoryKMSFactory))
+
+       _, err := registry.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "custom"})
+       require.NoError(t, err)
+       registry.Unregister("custom")
+       assert.Empty(t, registry.Names())
+}
+
+func TestKMSRegistry_FactoryError(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
        wantErr := assert.AnError
-       encryption.RegisterKMS(name, func(_ map[string]string) 
(encryption.KeyManagementClient, error) {
+       require.NoError(t, registry.Register("error-kms", func(_ 
context.Context, _ map[string]string) (encryption.KeyManagementClient, error) {
                return nil, wantErr
-       })
-       defer encryption.UnregisterKMS(name)
+       }))
 
-       _, err := encryption.LoadKeyManagementClient(map[string]string{
-               encryption.KMSTypeKey: name,
-       })
+       _, err := registry.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "error-kms"})
        require.ErrorIs(t, err, wantErr)
 }
 
-func TestLoadKeyManagementClient_PassesPropsToFactory(t *testing.T) {
-       const name = "test-props-kms"
+func TestKMSRegistry_PassesContextToFactory(t *testing.T) {
+       type contextKey struct{}
+       ctx := context.WithValue(t.Context(), contextKey{}, "request")
+       registry := encryption.NewKMSRegistry()
+       called := false
+       require.NoError(t, registry.Register("ctx-kms", func(got 
context.Context, _ map[string]string) (encryption.KeyManagementClient, error) {
+               called = true
+               assert.Equal(t, "request", got.Value(contextKey{}))
+
+               return encryption.NewInMemoryKeyManagementClient(), nil
+       }))
+
+       _, err := registry.Load(ctx, map[string]string{encryption.KMSTypeKey: 
"ctx-kms"})
+       require.NoError(t, err)
+       assert.True(t, called, "factory must receive the caller context")
+}
+
+func TestKMSRegistry_PropagatesCanceledContext(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       require.NoError(t, registry.Register("context-kms", func(ctx 
context.Context, _ map[string]string) (encryption.KeyManagementClient, error) {
+               return nil, ctx.Err()
+       }))
+
+       ctx, cancel := context.WithCancel(t.Context())
+       cancel()
+
+       _, err := registry.Load(ctx, map[string]string{encryption.KMSTypeKey: 
"context-kms"})
+       require.ErrorIs(t, err, context.Canceled)
+}
+
+func TestKMSRegistry_PassesPropertiesToFactory(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
        wantProps := map[string]string{
-               encryption.KMSTypeKey:   name,
+               encryption.KMSTypeKey:   "props-kms",
                "encryption.kms.region": "us-east-1",
        }
 
        var gotProps map[string]string
-       encryption.RegisterKMS(name, func(props map[string]string) 
(encryption.KeyManagementClient, error) {
+       require.NoError(t, registry.Register("props-kms", func(_ 
context.Context, props map[string]string) (encryption.KeyManagementClient, 
error) {
                gotProps = props
 
                return encryption.NewInMemoryKeyManagementClient(), nil
+       }))
+
+       _, err := registry.Load(t.Context(), wantProps)
+       require.NoError(t, err)
+       assert.Equal(t, wantProps, gotProps)
+}
+
+func TestKMSRegistry_FactoryMayUpdateRegistry(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       require.NoError(t, registry.Register("nested", func(_ context.Context, 
_ map[string]string) (encryption.KeyManagementClient, error) {
+               if err := registry.Register("new-factory", memoryKMSFactory); 
err != nil {
+                       return nil, err
+               }
+
+               return encryption.NewInMemoryKeyManagementClient(), nil
+       }))
+
+       _, err := registry.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "nested"})
+       require.NoError(t, err)
+       _, err = registry.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "new-factory"})
+       require.NoError(t, err)
+}
+
+func TestKMSRegistry_ConcurrentUse(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       const workerCount = 32
+
+       var wg sync.WaitGroup
+       failures := make(chan error, workerCount)
+       for i := range workerCount {
+               wg.Go(func() {
+                       name := fmt.Sprintf("worker-%d", i)
+                       if err := registry.Register(name, memoryKMSFactory); 
err != nil {
+                               failures <- err
+
+                               return
+                       }
+                       if _, err := registry.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: name}); err != nil {
+                               failures <- err
+                       }
+                       _ = registry.Names()
+                       registry.Unregister(name)
+               })
+       }
+       wg.Wait()
+       close(failures)
+
+       for err := range failures {
+               require.NoError(t, err)
+       }
+}
+
+func TestKMSRegistry_DefaultRegistrySnapshot(t *testing.T) {
+       const name = "test-default-kms-registry-snapshot"
+       before := encryption.NewKMSRegistry()
+
+       // Mimic a provider registering through a side-effect import.
+       encryption.RegisterKMS(name, memoryKMSFactory)
+       t.Cleanup(func() { encryption.UnregisterKMS(name) })
+
+       after := encryption.NewKMSRegistry()
+       assert.Contains(t, encryption.GetRegisteredKMSNames(), name)
+
+       _, err := before.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: name})
+       require.ErrorIs(t, err, encryption.ErrKMSTypeNotFound)
+       _, err = after.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: name})
+       require.NoError(t, err)
+
+       // Removing a global registration must not mutate an existing snapshot.
+       encryption.UnregisterKMS(name)
+       _, err = after.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: name})
+       require.NoError(t, err)
+       _, err = encryption.NewKMSRegistry().Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: name})
+       require.ErrorIs(t, err, encryption.ErrKMSTypeNotFound)
+}
+
+func TestKMSRegistry_CloneIsIndependent(t *testing.T) {
+       registry := encryption.NewKMSRegistry()
+       clone := registry.Clone()
+       require.NoError(t, clone.Register("only-clone", memoryKMSFactory))
+
+       _, err := registry.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "only-clone"})
+       require.ErrorIs(t, err, encryption.ErrKMSTypeNotFound)
+       _, err = clone.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "only-clone"})
+       require.NoError(t, err)
+
+       registry.Unregister("memory")
+       _, err = clone.Load(t.Context(), 
map[string]string{encryption.KMSTypeKey: "memory"})
+       require.NoError(t, err)
+
+       // Cloning the zero-value registry must also create an independent 
instance.
+       var empty encryption.KMSRegistry
+       copied := empty.Clone()
+       require.NoError(t, copied.Register("custom", memoryKMSFactory))
+       assert.Empty(t, empty.Names())
+}
+
+func TestKMSRegistry_GlobalRegistrationCompatibility(t *testing.T) {
+       const name = "test-global-kms-registration"
+       encryption.MustRegisterKMS(name, memoryKMSFactory)
+       t.Cleanup(func() { encryption.UnregisterKMS(name) })
+
+       assert.Panics(t, func() {
+               encryption.RegisterKMS(name, func(_ context.Context, _ 
map[string]string) (encryption.KeyManagementClient, error) {
+                       return nil, assert.AnError
+               })
        })
-       defer encryption.UnregisterKMS(name)
+       assert.Panics(t, func() { encryption.RegisterKMS("test-nil-global-kms", 
nil) })
 
-       _, err := encryption.LoadKeyManagementClient(wantProps)
+       kms, err := encryption.LoadKeyManagementClient(t.Context(), 
map[string]string{encryption.KMSTypeKey: name})
        require.NoError(t, err)
-       assert.Equal(t, wantProps, gotProps, "the full props map given to 
LoadKeyManagementClient must reach the factory unmodified")
+       assert.IsType(t, &encryption.InMemoryKeyManagementClient{}, kms)
+
+       // Registering only on an instance must not change the process-wide 
default.
+       require.NoError(t, 
encryption.NewKMSRegistry().Register("instance-only", memoryKMSFactory))
+       _, err = encryption.LoadKeyManagementClient(t.Context(), 
map[string]string{encryption.KMSTypeKey: "instance-only"})
+       require.ErrorIs(t, err, encryption.ErrKMSTypeNotFound)
+}
+
+func TestKMSRegistry_GlobalLoaderPassesContext(t *testing.T) {
+       const name = "test-global-kms-context"
+       encryption.MustRegisterKMS(name, func(ctx context.Context, _ 
map[string]string) (encryption.KeyManagementClient, error) {
+               return nil, ctx.Err()
+       })
+       t.Cleanup(func() { encryption.UnregisterKMS(name) })
+
+       ctx, cancel := context.WithCancel(t.Context())
+       cancel()
+       _, err := encryption.LoadKeyManagementClient(ctx, 
map[string]string{encryption.KMSTypeKey: name})
+       require.ErrorIs(t, err, context.Canceled)
 }
diff --git a/website/src/releases.md b/website/src/releases.md
index 4cfc00695..c18fd3c6f 100644
--- a/website/src/releases.md
+++ b/website/src/releases.md
@@ -42,6 +42,7 @@ Per-release notes (highlights, breaking changes, 
contributors) are published on
 
 ### Unreleased compatibility notes
 
+- **KMS factory registration now takes a context.** Change 
`encryption.KMSFactory` implementations to accept `(context.Context, 
map[string]string)` instead of only properties, and call 
`encryption.LoadKeyManagementClient(ctx, props)` instead of 
`LoadKeyManagementClient(props)`. The existing global `RegisterKMS`, 
`UnregisterKMS`, and `GetRegisteredKMSNames` functions still use the default 
registry; `RegisterKMS` still panics for invalid registrations. Use 
`encryption.MustRegisterKMS` for  [...]
 - **Data file block size setter removed.** 
`iceberg.DataFileBuilder.BlockSizeInBytes` is removed. New v1 manifests use the 
Avro schema's 64 MiB default when the field is unset. V2/v3 omit this 
deprecated field, and legacy v1 values remain readable.
 - **View creation APIs take a named SQL representation.** 
`view.NewVersionFromSQL`, `view.CreateView`, `view.CreateViewWithIOProperties`, 
and `sql.Catalog.CreateView` now accept a `view.Representation{SQL: sql, 
Dialect: dialect}` instead of positional SQL/dialect strings. The dialect is 
required; Hive preserves the selected representation's dialect.
 

Reply via email to