diff --git a/.gitignore b/.gitignore index a81cd81e..ea0d28af 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ docker-compose.yml golangci.yml cover*.out .vscode/settings.json +ginkgo.report \ No newline at end of file diff --git a/batch_command_delete.go b/batch_command_delete.go index da04409d..45a2d264 100644 --- a/batch_command_delete.go +++ b/batch_command_delete.go @@ -175,7 +175,7 @@ func (cmd *batchCommandDelete) commandType() commandType { } func (cmd *batchCommandDelete) executeSingle(client *Client) Error { - policy := cmd.batchDeletePolicy.toWritePolicy(cmd.policy) + policy := cmd.batchDeletePolicy.toWritePolicy(cmd.policy, client.dynConfig) for i, key := range cmd.keys { res, err := client.Operate(policy, key, DeleteOp()) cmd.records[i].setRecord(res) diff --git a/batch_command_operate.go b/batch_command_operate.go index 01f3d667..1494beb2 100644 --- a/batch_command_operate.go +++ b/batch_command_operate.go @@ -246,16 +246,16 @@ func (cmd *batchCommandOperate) executeSingle(client *Client) Error { } else if len(ops) == 0 { ops = append(ops, GetOp()) } - res, err = client.Operate(cmd.client.getUsableBatchReadPolicy(br.Policy).toWritePolicy(cmd.policy), br.Key, ops...) + res, err = client.Operate(cmd.client.getUsableBatchReadPolicy(br.Policy).ToWritePolicy(cmd.policy, client.dynConfig), br.Key, ops...) case *BatchWrite: - policy := cmd.client.getUsableBatchWritePolicy(br.Policy).toWritePolicy(cmd.policy) + policy := cmd.client.getUsableBatchWritePolicy(br.Policy).toWritePolicy(cmd.policy, client.dynConfig) policy.RespondPerEachOp = true res, err = client.Operate(policy, br.Key, br.Ops...) case *BatchDelete: - policy := cmd.client.getUsableBatchDeletePolicy(br.Policy).toWritePolicy(cmd.policy) + policy := cmd.client.getUsableBatchDeletePolicy(br.Policy).toWritePolicy(cmd.policy, client.dynConfig) res, err = client.Operate(policy, br.Key, DeleteOp()) case *BatchUDF: - policy := cmd.client.getUsableBatchUDFPolicy(br.Policy).toWritePolicy(cmd.policy) + policy := cmd.client.getUsableBatchUDFPolicy(br.Policy).toWritePolicy(cmd.policy, client.dynConfig) policy.RespondPerEachOp = true res, err = client.execute(policy, br.Key, br.PackageName, br.FunctionName, br.FunctionArgs...) } diff --git a/batch_command_udf.go b/batch_command_udf.go index 7e5a89c1..0d916edd 100644 --- a/batch_command_udf.go +++ b/batch_command_udf.go @@ -185,7 +185,7 @@ func (cmd *batchCommandUDF) isRead() bool { func (cmd *batchCommandUDF) executeSingle(client *Client) Error { for i, key := range cmd.keys { - policy := cmd.batchUDFPolicy.toWritePolicy(cmd.policy) + policy := cmd.batchUDFPolicy.toWritePolicy(cmd.policy, client.dynConfig) policy.RespondPerEachOp = true res, err := client.execute(policy, key, cmd.packageName, cmd.functionName, cmd.args...) cmd.records[i].setRecord(res) diff --git a/batch_delete_policy.go b/batch_delete_policy.go index 0e3d256e..bc5e2196 100644 --- a/batch_delete_policy.go +++ b/batch_delete_policy.go @@ -58,7 +58,15 @@ func NewBatchDeletePolicy() *BatchDeletePolicy { } } -func (bdp *BatchDeletePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { +func NewDynamicBatchDeletePolicy(dynConfig *DynConfig) *BatchDeletePolicy { + if dynConfig == nil { + return NewBatchDeletePolicy() + } + + return dynConfig.client.dynDefaultBatchDeletePolicy.Load() +} + +func (bdp *BatchDeletePolicy) toWritePolicy(bp *BatchPolicy, dynConfig *DynConfig) *WritePolicy { wp := bp.toWritePolicy() if bdp != nil { @@ -71,5 +79,70 @@ func (bdp *BatchDeletePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { wp.DurableDelete = bdp.DurableDelete wp.SendKey = bdp.SendKey } + + // In Case dynConfig is not initialized or running return the policy before + // merge + if dynConfig == nil { + return wp + } + + config := dynConfig.config + if config != nil && config.Dynamic.BatchDelete != nil { + if config.Dynamic.BatchDelete.DurableDelete != nil { + wp.DurableDelete = *config.Dynamic.BatchDelete.DurableDelete + } + if config.Dynamic.BatchDelete.SendKey != nil { + wp.SendKey = *config.Dynamic.BatchDelete.SendKey + } + } + return wp } + +// copy creates a new BasePolicy instance and copies the values from the source BatchDeletePolicy. +func (bd *BatchDeletePolicy) copy() *BatchDeletePolicy { + if bd == nil { + return nil + } + + response := *bd + return &response +} + +// patchDynamic applies the dynamic configuration and generates a new policy +func (bdp *BatchDeletePolicy) patchDynamic(dynConfig *DynConfig) *BatchDeletePolicy { + if dynConfig == nil { + return bdp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if bdp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultBatchDeletePolicy.Load() + } + if config != nil && config.Dynamic != nil && config.Dynamic.BatchDelete != nil { + // Dynamic configuration is exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + return bdp.copy().mapDynamic(dynConfig) + } else { + return bdp + } +} + +func (bdp *BatchDeletePolicy) mapDynamic(dynConfig *DynConfig) *BatchDeletePolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return bdp + } + + if dynConfig.config.Dynamic.BatchDelete != nil { + if dynConfig.config.Dynamic.BatchDelete.DurableDelete != nil { + bdp.DurableDelete = *dynConfig.config.Dynamic.BatchDelete.DurableDelete + } + if dynConfig.config.Dynamic.BatchDelete.SendKey != nil { + bdp.SendKey = *dynConfig.config.Dynamic.BatchDelete.SendKey + } + } + + return bdp +} diff --git a/batch_delete_policy_config_test.go b/batch_delete_policy_config_test.go new file mode 100644 index 00000000..8091cefa --- /dev/null +++ b/batch_delete_policy_config_test.go @@ -0,0 +1,101 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToBatchDeletePolicy", func() { + + Context("when applying full configuration to batch delete policy", func() { + It("should update the policy values based on the dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchDelete: &dynconfig.BatchDelete{ + DurableDelete: func() *bool { + r := true + return &r + }(), + SendKey: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial BatchReadPolicy. + policy := NewBatchDeletePolicy() + + // Verify defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.DurableDelete).To(BeFalse()) + Expect(policy.SendKey).To(BeFalse()) + + // Apply configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.DurableDelete).To(BeTrue()) + Expect(updatedPolicy.DurableDelete).To(BeTrue()) + }) + }) + + Context("when applying batch read config to a write policy", func() { + It("should update the write policy values based on the batch delete dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchDelete: &dynconfig.BatchDelete{ + DurableDelete: func() *bool { + r := true + return &r + }(), + SendKey: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial BatchPolicy (used for write operations). + batchPolicy := NewBatchPolicy() + + // Verify defaults. + Expect(batchPolicy).NotTo(BeNil()) + Expect(batchPolicy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(batchPolicy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(batchPolicy.ReadTouchTTLPercent).To(Equal(int32(0))) + + batchDeletePolicy := NewBatchDeletePolicy() + updatedWritePolicy := batchDeletePolicy.toWritePolicy(batchPolicy, config) + + // Validate applied configuration. + Expect(updatedWritePolicy).NotTo(BeNil()) + Expect(updatedWritePolicy.DurableDelete).To(BeTrue()) + Expect(updatedWritePolicy.SendKey).To(BeTrue()) + }) + }) +}) diff --git a/batch_policy.go b/batch_policy.go index d20d3b99..6abd8772 100644 --- a/batch_policy.go +++ b/batch_policy.go @@ -14,6 +14,10 @@ package aerospike +import ( + "time" +) + // BatchPolicy encapsulates parameters for policy attributes used in write operations. // This object is passed into methods where database writes can occur. type BatchPolicy struct { @@ -101,6 +105,14 @@ func NewBatchPolicy() *BatchPolicy { } } +func NewBatchPolicyOrDefaultFromCache(dynConfig *DynConfig) *BatchPolicy { + if dynConfig == nil { + return NewBatchPolicy() + } + + return dynConfig.client.dynDefaultBatchPolicy.Load() +} + // NewReadBatchPolicy initializes a new BatchPolicy instance for reads. func NewReadBatchPolicy() *BatchPolicy { return NewBatchPolicy() @@ -120,3 +132,75 @@ func (p *BatchPolicy) toWritePolicy() *WritePolicy { } return wp } + +// copyQueryPolicy creates a new BasePolicy instance and copies the values from the source BasePolicy. +func copyBatchPolicy(src *BatchPolicy) *BatchPolicy { + if src == nil { + return nil + } + + response := *src + return &response +} + +// applyConfigToQueryPolicy applies the dynamic configuration and generates a new policy +func applyConfigToBatchPolicy(policy *BatchPolicy, dynConfig *DynConfig) *BatchPolicy { + if dynConfig == nil { + return policy + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if policy == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultBatchPolicy.Load() + } else if config != nil && config.Dynamic != nil && config.Dynamic.BatchRead != nil { + // Dynamic configuration exists for policy in question. + var responsePolicy *BatchPolicy + // User has provided a custom policy. We need to apply the dynamic configuration. + responsePolicy = copyBatchPolicy(policy) + responsePolicy = mapDynamicBatchPolicy(responsePolicy, dynConfig) + + return responsePolicy + } else { + return policy + } +} + +func mapDynamicBatchPolicy(policy *BatchPolicy, dynConfig *DynConfig) *BatchPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return policy + } + + if dynConfig.config.Dynamic.BatchRead != nil { + if dynConfig.config.Dynamic.BatchRead.ReadModeAp != nil { + policy.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.BatchRead.ReadModeAp) + } + if dynConfig.config.Dynamic.BatchRead.ReadModeSc != nil { + policy.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.BatchRead.ReadModeSc) + } + if dynConfig.config.Dynamic.BatchRead.TotalTimeout != nil { + policy.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.BatchRead.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchRead.SocketTimeout != nil { + policy.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.BatchRead.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchRead.MaxRetries != nil { + policy.MaxRetries = *dynConfig.config.Dynamic.BatchRead.MaxRetries + } + if dynConfig.config.Dynamic.BatchRead.SleepBetweenRetries != nil { + policy.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.BatchRead.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchRead.AllowInline != nil { + policy.AllowInline = *dynConfig.config.Dynamic.BatchRead.AllowInline + } + if dynConfig.config.Dynamic.BatchRead.AllowInlineSSD != nil { + policy.AllowInlineSSD = *dynConfig.config.Dynamic.BatchRead.AllowInlineSSD + } + if dynConfig.config.Dynamic.BatchRead.RespondAllKeys != nil { + policy.RespondAllKeys = *dynConfig.config.Dynamic.BatchRead.RespondAllKeys + } + } + + return policy +} diff --git a/batch_read_policy.go b/batch_read_policy.go index a65b4b9a..7f15b9ba 100644 --- a/batch_read_policy.go +++ b/batch_read_policy.go @@ -14,6 +14,8 @@ package aerospike +import "time" + // BatchReadPolicy attributes used in batch read commands. type BatchReadPolicy struct { // FilterExpression is the optional expression filter. If FilterExpression exists and evaluates to false, the specific batch key @@ -54,9 +56,21 @@ func NewBatchReadPolicy() *BatchReadPolicy { } } -func (brp *BatchReadPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { +func NewDynamicBatchReadPolicy(dynConfig *DynConfig) *BatchReadPolicy { + if dynConfig == nil { + return NewBatchReadPolicy() + } + + return dynConfig.client.dynDefaultBatchReadPolicy.Load() +} + +func (brp *BatchReadPolicy) ToWritePolicy(bp *BatchPolicy, dynConfig *DynConfig) *WritePolicy { wp := bp.toWritePolicy() + if dynConfig != nil { + wp.BasePolicy = *dynConfig.client.dynDefaultBatchReadBasePolicy.Load() + } + if brp != nil { if brp.FilterExpression != nil { wp.FilterExpression = brp.FilterExpression @@ -66,5 +80,86 @@ func (brp *BatchReadPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { wp.ReadModeSC = brp.ReadModeSC wp.ReadTouchTTLPercent = brp.ReadTouchTTLPercent } + return wp } + +// copyBAtchReadPolicy creates a new BasePolicy instance and copies the values from the source BatchReadPolicy. +func (brp *BatchReadPolicy) copy() *BatchReadPolicy { + if brp == nil { + return nil + } + + response := *brp + return &response +} + +// patchDynamic applies the dynamic configuration and generates a new policy. +func (brp *BatchReadPolicy) patchDynamic(dynConfig *DynConfig) *BatchReadPolicy { + if dynConfig == nil { + return brp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if brp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultBatchReadPolicy.Load() + } else if config != nil && config.Dynamic != nil && config.Dynamic.BatchRead != nil { + // Dynamic configuration is exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + // Copy the existing write policy to preserve any custom settings. + return brp.copy().mapDynamic(dynConfig) + } else { + return brp + } +} + +func (brp *BatchReadPolicy) mapDynamic(dynConfig *DynConfig) *BatchReadPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return brp + } + + if dynConfig.config.Dynamic.BatchRead != nil { + if dynConfig.config.Dynamic.BatchRead.ReadModeAp != nil { + brp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.BatchRead.ReadModeAp) + } + if dynConfig.config.Dynamic.BatchRead.ReadModeSc != nil { + brp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.BatchRead.ReadModeSc) + } + } + + return brp +} + +func (brp *BasePolicy) mapConfigBatchReadToBasePolicy(dynConfig *DynConfig) *BasePolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return brp + } + + if dynConfig.config.Dynamic.BatchRead != nil { + if dynConfig.config.Dynamic.BatchRead.ReadModeAp != nil { + brp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.BatchRead.ReadModeAp) + } + if dynConfig.config.Dynamic.BatchRead.ReadModeSc != nil { + brp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.BatchRead.ReadModeSc) + } + if dynConfig.config.Dynamic.BatchRead.TotalTimeout != nil { + brp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.BatchRead.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchRead.SocketTimeout != nil { + brp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.BatchRead.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchRead.MaxRetries != nil { + brp.MaxRetries = *dynConfig.config.Dynamic.BatchRead.MaxRetries + } + if dynConfig.config.Dynamic.BatchRead.SleepBetweenRetries != nil { + brp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.BatchRead.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchRead.Replica != nil { + brp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.BatchRead.Replica) + } + } + + return brp +} diff --git a/batch_read_policy_config_test.go b/batch_read_policy_config_test.go new file mode 100644 index 00000000..e26e41d1 --- /dev/null +++ b/batch_read_policy_config_test.go @@ -0,0 +1,170 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToBatchReadPolicy", func() { + + Context("when applying full configuration to batch read policy", func() { + It("should update the policy values based on the dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchRead: &dynconfig.BatchRead{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ONE + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.ALLOW_UNAVAILABLE + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 15 + return &r + }(), + MaxRetries: func() *int { r := 5; return &r }(), + MaxConcurrentThread: func() *int { r := 5; return &r }(), + AllowInline: func() *bool { r := true; return &r }(), + RespondAllKeys: func() *bool { r := true; return &r }(), + }, + }, + }, + } + + // Create an initial BatchReadPolicy. + policy := NewBatchReadPolicy() + + // Verify defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.ReadTouchTTLPercent).To(Equal(int32(0))) + + // Apply configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(updatedPolicy.ReadModeSC).To(Equal(ReadModeSCAllowUnavailable)) + }) + }) + + Context("when applying batch read config to a write policy", func() { + It("should update the write policy values based on the batch read dynamic config", func() { + // Create the full configuration. + + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchRead: &dynconfig.BatchRead{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ALL + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.ALLOW_UNAVAILABLE + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 15 + return &r + }(), + MaxRetries: func() *int { r := 5; return &r }(), + MaxConcurrentThread: func() *int { r := 5; return &r }(), + AllowInline: func() *bool { r := true; return &r }(), + RespondAllKeys: func() *bool { r := true; return &r }(), + }, + }, + }, + } + + config.client = &Client{dynConfig: config} + config.updateCachedPolicies() + + // Create an initial BatchPolicy (used for write operations). + batchPolicy := NewBatchPolicy() + + // Verify defaults. + Expect(batchPolicy).NotTo(BeNil()) + Expect(batchPolicy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(batchPolicy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(batchPolicy.ReadTouchTTLPercent).To(Equal(int32(0))) + + // Apply configuration to BatchPolicy. + batchPolicy = config.client.dynDefaultBatchPolicy.Load() + + // Validate the loaded policy. + Expect(batchPolicy.ReadModeAP).To(Equal(ReadModeAPAll)) + Expect(batchPolicy.ReadModeSC).To(Equal(ReadModeSCAllowUnavailable)) + Expect(batchPolicy.TotalTimeout).To(Equal(15 * time.Millisecond)) + Expect(batchPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(batchPolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(batchPolicy.MaxRetries).To(Equal(5)) + Expect(batchPolicy.ReplicaPolicy).To(Equal(SEQUENCE)) + Expect(batchPolicy.SendKey).To(BeFalse()) + Expect(batchPolicy.UseCompression).To(BeFalse()) + Expect(batchPolicy.AllowInline).To(BeTrue()) + + // Apply the dynamic configuration to the BatchPolicy. + batchReadPolicy := config.client.dynDefaultBatchReadPolicy.Load() + updatedWritePolicy := batchReadPolicy.ToWritePolicy(batchPolicy, config) + + // Validate applied configuration. + Expect(updatedWritePolicy).NotTo(BeNil()) + Expect(updatedWritePolicy.ReadModeAP).To(Equal(ReadModeAPAll)) + Expect(updatedWritePolicy.ReadModeSC).To(Equal(ReadModeSCAllowUnavailable)) + Expect(updatedWritePolicy.ReplicaPolicy).To(Equal(MASTER)) + Expect(updatedWritePolicy.TotalTimeout).To(Equal(15 * time.Millisecond)) + Expect(updatedWritePolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedWritePolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(updatedWritePolicy.MaxRetries).To(Equal(5)) + Expect(updatedWritePolicy.SendKey).To(BeFalse()) + }) + }) +}) diff --git a/batch_udf_policy.go b/batch_udf_policy.go index 59b38026..a2f11248 100644 --- a/batch_udf_policy.go +++ b/batch_udf_policy.go @@ -66,7 +66,15 @@ func NewBatchUDFPolicy() *BatchUDFPolicy { } } -func (bup *BatchUDFPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { +func NewDynamicBatchUdfPolicy(dynConfig *DynConfig) *BatchUDFPolicy { + if dynConfig == nil { + return NewBatchUDFPolicy() + } + + return dynConfig.client.dynDefaultBatchUDFPolicy.Load() +} + +func (bup *BatchUDFPolicy) toWritePolicy(bp *BatchPolicy, dynConfig *DynConfig) *WritePolicy { wp := bp.toWritePolicy() if bup != nil { @@ -78,5 +86,70 @@ func (bup *BatchUDFPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { wp.DurableDelete = bup.DurableDelete wp.SendKey = bup.SendKey } + + // In Case dynConfig is not initialized or running return the policy before + // merge + if dynConfig == nil { + return wp + } + + config := dynConfig.config + if config != nil && config.Dynamic.BatchUdf != nil { + if config.Dynamic.BatchUdf.DurableDelete != nil { + wp.DurableDelete = *config.Dynamic.BatchUdf.DurableDelete + } + if config.Dynamic.BatchUdf.SendKey != nil { + wp.SendKey = *config.Dynamic.BatchUdf.SendKey + } + } + return wp } + +// copyBatchUDFPolicy creates a new BasePolicy instance and copies the values from the source BatchUDFPolicy. +func (bup *BatchUDFPolicy) copy() *BatchUDFPolicy { + if bup == nil { + return nil + } + + response := *bup + return &response +} + +// patchDynamic applies the dynamic configuration and generates a new policy +func (bup *BatchUDFPolicy) patchDynamic(dynConfig *DynConfig) *BatchUDFPolicy { + if dynConfig == nil { + return bup + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if bup == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultBatchUDFPolicy.Load() + } + if config != nil && config.Dynamic != nil && config.Dynamic.BatchUdf != nil { + // Dynamic configuration is exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + return bup.copy().mapDynamic(dynConfig) + } else { + return bup + } +} + +func (bup *BatchUDFPolicy) mapDynamic(dynConfig *DynConfig) *BatchUDFPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return bup + } + + if dynConfig.config.Dynamic.BatchUdf != nil { + if dynConfig.config.Dynamic.BatchUdf.DurableDelete != nil { + bup.DurableDelete = *dynConfig.config.Dynamic.BatchUdf.DurableDelete + } + if dynConfig.config.Dynamic.BatchUdf.SendKey != nil { + bup.SendKey = *dynConfig.config.Dynamic.BatchUdf.SendKey + } + } + + return bup +} diff --git a/batch_udf_policy_config_test.go b/batch_udf_policy_config_test.go new file mode 100644 index 00000000..f2ac0363 --- /dev/null +++ b/batch_udf_policy_config_test.go @@ -0,0 +1,101 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToBatchUDFPolicy", func() { + + Context("when applying full configuration to batch udf policy", func() { + It("should update the policy values based on the dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchUdf: &dynconfig.BatchUdf{ + DurableDelete: func() *bool { + r := true + return &r + }(), + SendKey: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial BatchReadPolicy. + policy := NewBatchUDFPolicy() + + // Verify defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.DurableDelete).To(BeFalse()) + Expect(policy.SendKey).To(BeFalse()) + + // Apply configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.DurableDelete).To(BeTrue()) + Expect(updatedPolicy.DurableDelete).To(BeTrue()) + }) + }) + + Context("when applying batch read config to a write policy", func() { + It("should update the write policy values based on the batch udf dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchUdf: &dynconfig.BatchUdf{ + DurableDelete: func() *bool { + r := true + return &r + }(), + SendKey: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial BatchPolicy (used for write operations). + batchPolicy := NewBatchPolicy() + + // Verify defaults. + Expect(batchPolicy).NotTo(BeNil()) + Expect(batchPolicy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(batchPolicy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(batchPolicy.ReadTouchTTLPercent).To(Equal(int32(0))) + + batchUdfPolicy := NewBatchUDFPolicy() + updatedWritePolicy := batchUdfPolicy.toWritePolicy(batchPolicy, config) + + // Validate applied configuration. + Expect(updatedWritePolicy).NotTo(BeNil()) + Expect(updatedWritePolicy.DurableDelete).To(BeTrue()) + Expect(updatedWritePolicy.SendKey).To(BeTrue()) + }) + }) +}) diff --git a/batch_write_policy.go b/batch_write_policy.go index 83ea260b..8b4a4fb1 100644 --- a/batch_write_policy.go +++ b/batch_write_policy.go @@ -14,6 +14,8 @@ package aerospike +import "time" + // BatchWritePolicy attributes used in batch write commands. type BatchWritePolicy struct { // FilterExpression is optional expression filter. If FilterExpression exists and evaluates to false, the specific batch key @@ -93,9 +95,21 @@ func NewBatchWritePolicy() *BatchWritePolicy { } } -func (bwp *BatchWritePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { +func NewDynamicBatchWritePolicy(dynConfig *DynConfig) *BatchWritePolicy { + if dynConfig == nil { + return NewBatchWritePolicy() + } + + return dynConfig.client.dynDefaultBatchWritePolicy.Load() +} + +func (bwp *BatchWritePolicy) toWritePolicy(bp *BatchPolicy, dynConfig *DynConfig) *WritePolicy { wp := bp.toWritePolicy() + if dynConfig != nil { + wp.BasePolicy = *dynConfig.client.dynDefaultBatchWriteBasePolicy.Load() + } + if bwp != nil { if bwp.FilterExpression != nil { wp.FilterExpression = bwp.FilterExpression @@ -111,3 +125,77 @@ func (bwp *BatchWritePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { return wp } + +// copyQueryPolicy creates a new BasePolicy instance and copies the values from the source BasePolicy. +func (bwp *BatchWritePolicy) copy() *BatchWritePolicy { + if bwp == nil { + return nil + } + + response := *bwp + return &response +} + +// applyConfigToQueryPolicy applies the dynamic configuration and generates a new policy +func (bwp *BatchWritePolicy) patchDynamic(dynConfig *DynConfig) *BatchWritePolicy { + if dynConfig == nil { + return bwp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if bwp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultBatchWritePolicy.Load() + } + if config != nil && config.Dynamic != nil && config.Dynamic.BatchWrite != nil { + // Dynamic configuration is exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + return bwp.copy().mapDynamic(dynConfig) + } else { + return bwp + } +} + +func (policy *BatchWritePolicy) mapDynamic(dynConfig *DynConfig) *BatchWritePolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return policy + } + + if dynConfig.config.Dynamic.BatchWrite != nil { + if dynConfig.config.Dynamic.BatchWrite.DurableDelete != nil { + policy.DurableDelete = *dynConfig.config.Dynamic.BatchWrite.DurableDelete + } + if dynConfig.config.Dynamic.BatchWrite.SendKey != nil { + policy.SendKey = *dynConfig.config.Dynamic.BatchWrite.SendKey + } + } + + return policy +} + +func (bp *BasePolicy) mapConfigBatchWriteToBasePolicy(dynConfig *DynConfig) *BasePolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return bp + } + + if dynConfig.config.Dynamic.BatchWrite != nil { + if dynConfig.config.Dynamic.BatchWrite.TotalTimeout != nil { + bp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.BatchWrite.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchWrite.SocketTimeout != nil { + bp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.BatchWrite.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchWrite.MaxRetries != nil { + bp.MaxRetries = *dynConfig.config.Dynamic.BatchWrite.MaxRetries + } + if dynConfig.config.Dynamic.BatchWrite.SleepBetweenRetries != nil { + bp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.BatchWrite.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.BatchWrite.Replica != nil { + bp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.BatchWrite.Replica) + } + } + + return bp +} diff --git a/batch_write_policy_config_test.go b/batch_write_policy_config_test.go new file mode 100644 index 00000000..55f6ec7b --- /dev/null +++ b/batch_write_policy_config_test.go @@ -0,0 +1,234 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToBatchWritePolicy", func() { + + Context("when applying full configuration to batch write policy", func() { + It("should update the policy values based on the dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchWrite: &dynconfig.BatchWrite{ + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 15 + return &r + }(), + MaxRetries: func() *int { + r := 5 + return &r + }(), + DurableDelete: func() *bool { + r := false + return &r + }(), + SendKey: func() *bool { + r := false + return &r + }(), + MaxConcurrentThread: func() *int { + r := 5 + return &r + }(), + AllowInline: func() *bool { + r := true + return &r + }(), + RespondAllKeys: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial BatchWritePolicy. + policy := NewBatchWritePolicy() + + // Check defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.RecordExistsAction).To(Equal(UPDATE)) + Expect(policy.GenerationPolicy).To(Equal(NONE)) + Expect(policy.CommitLevel).To(Equal(COMMIT_ALL)) + Expect(policy.Generation).To(Equal(uint32(0))) + Expect(policy.Expiration).To(Equal(uint32(0))) + Expect(policy.DurableDelete).To(BeFalse()) + Expect(policy.OnLockingOnly).To(BeFalse()) + Expect(policy.SendKey).To(BeFalse()) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate the applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.DurableDelete).To(BeFalse()) + Expect(updatedPolicy.SendKey).To(BeFalse()) + }) + }) + + Context("when applying batch write config to a write policy", func() { + It("should update the write policy values based on the batch write dynamic config", func() { + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + BatchWrite: &dynconfig.BatchWrite{ + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER_PROLES + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 15 + return &r + }(), + MaxRetries: func() *int { + r := 5 + return &r + }(), + DurableDelete: func() *bool { + r := false + return &r + }(), + SendKey: func() *bool { + r := true + return &r + }(), + MaxConcurrentThread: func() *int { + r := 5 + return &r + }(), + AllowInline: func() *bool { + r := true + return &r + }(), + RespondAllKeys: func() *bool { + r := true + return &r + }(), + }, + BatchRead: &dynconfig.BatchRead{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ALL + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.ALLOW_UNAVAILABLE + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 15 + return &r + }(), + MaxRetries: func() *int { r := 5; return &r }(), + MaxConcurrentThread: func() *int { r := 5; return &r }(), + AllowInline: func() *bool { r := true; return &r }(), + RespondAllKeys: func() *bool { r := true; return &r }(), + }, + }, + }, + } + + // Create the full configuration. + config.client = &Client{dynConfig: config} + config.updateCachedPolicies() + + // Check defaults for BatchPolicy (used for write operations). + batchPolicy := NewBatchPolicy() + + //Verify defaults + Expect(batchPolicy).NotTo(BeNil()) + Expect(batchPolicy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(batchPolicy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(batchPolicy.TotalTimeout).To(Equal(1 * time.Second)) + Expect(batchPolicy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(batchPolicy.MaxRetries).To(Equal(2)) + Expect(batchPolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(batchPolicy.ReplicaPolicy).To(Equal(SEQUENCE)) + Expect(batchPolicy.SendKey).To(BeFalse()) + Expect(batchPolicy.UseCompression).To(BeFalse()) + Expect(batchPolicy.AllowInline).To(BeTrue()) + + // Load the dynamic default batch policy. + batchPolicy = config.client.dynDefaultBatchPolicy.Load() + + // Validate the loaded policy. + Expect(batchPolicy.ReadModeAP).To(Equal(ReadModeAPAll)) + Expect(batchPolicy.ReadModeSC).To(Equal(ReadModeSCAllowUnavailable)) + Expect(batchPolicy.TotalTimeout).To(Equal(15 * time.Millisecond)) + Expect(batchPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(batchPolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(batchPolicy.MaxRetries).To(Equal(5)) + Expect(batchPolicy.ReplicaPolicy).To(Equal(SEQUENCE)) + Expect(batchPolicy.SendKey).To(BeFalse()) + Expect(batchPolicy.UseCompression).To(BeFalse()) + Expect(batchPolicy.AllowInline).To(BeTrue()) + + // Load the dynamic default batch write policy. + writePolicy := config.client.dynDefaultBatchWritePolicy.Load() + + // Apply configuration to convert to a write policy. + updatedWritePolicy := writePolicy.toWritePolicy(batchPolicy, config) + + Expect(updatedWritePolicy).NotTo(BeNil()) + Expect(updatedWritePolicy.ReplicaPolicy).To(Equal(MASTER_PROLES)) + Expect(updatedWritePolicy.TotalTimeout).To(Equal(15 * time.Millisecond)) + Expect(updatedWritePolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedWritePolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(updatedWritePolicy.MaxRetries).To(Equal(5)) + Expect(updatedWritePolicy.SendKey).To(BeTrue()) + }) + }) +}) diff --git a/bench_apply_configuration_test.go b/bench_apply_configuration_test.go new file mode 100644 index 00000000..9e6bad12 --- /dev/null +++ b/bench_apply_configuration_test.go @@ -0,0 +1,75 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "testing" + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" +) + +func BenchmarkApplyConfigToClientPolicy(b *testing.B) { + // Create a default client policy. + cp := NewClientPolicy() + + // Create a dummy dynamic configuration. Adjust the configuration fields as needed. + // In this example we pass nil for mappedPolicies and a minimal dynconfig.Config. + cfg := &dynconfig.Config{} + dynCfg := NewDynConfigForTest(cfg) + + // Ensure the function runs once before benchmarking. + _ = cp.patchDynamic(dynCfg) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = cp.patchDynamic(dynCfg) + } +} + +func BenchmarkApplyConfigToClientPolicyWithDynamicAndStaticConfig(b *testing.B) { + cp := NewClientPolicy() + + cfg := &dynconfig.Config{ + Static: &dynconfig.StaticConfig{ + Client: &dynconfig.Client{ + ConfigInterval: func() *int { i := int(1000 * time.Millisecond); return &i }(), + ConnectionQueueSize: func() *int { i := 100; return &i }(), + MinConnectionsPerNode: func() *int { i := 10; return &i }(), + }, + }, + Dynamic: &dynconfig.DynamicConfig{ + Client: &dynconfig.Client{ + Timeout: func() *int { i := int(1000 * time.Millisecond); return &i }(), + ErrorRateWindow: func() *int { i := 5; return &i }(), + MaxErrorRate: func() *int { i := 10; return &i }(), + LoginTimeout: func() *int { i := 1000; return &i }(), + RackAware: func() *bool { b := true; return &b }(), + RackIds: func() *[]int { i := []int{1, 2, 3}; return &i }(), + TendInterval: func() *int { i := 1000; return &i }(), + UseServiceAlternate: func() *bool { b := true; return &b }(), + }, + }, + } + + dynCfg := NewDynConfigForTest(cfg) + + _ = cp.patchDynamic(dynCfg) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = cp.patchDynamic(dynCfg) + } +} diff --git a/client.go b/client.go index 9e7f830e..fe9c8cbd 100644 --- a/client.go +++ b/client.go @@ -24,19 +24,28 @@ import ( "runtime" "strconv" "strings" + "sync/atomic" "time" + _ "github.com/aerospike/aerospike-client-go/v8/config/provider" "github.com/aerospike/aerospike-client-go/v8/logger" "github.com/aerospike/aerospike-client-go/v8/types" ) const unreachable = "UNREACHABLE" +// AEROSPIKE_CLIENT_CONFIG_URL is the environment variable that can be set to +// load the Aerospike client configuration from a URL. +var AEROSPIKE_CLIENT_CONFIG_URL = os.Getenv("AEROSPIKE_CLIENT_CONFIG_URL") + // Client encapsulates an Aerospike cluster. // All database operations are available against this object. type Client struct { cluster *Cluster + // Dynamic configuration + dynConfig *DynConfig + // DefaultPolicy is used for all read commands without a specific policy. DefaultPolicy *BasePolicy // DefaultBatchPolicy is the default parent policy used in batch read commands. Base policy fields @@ -66,6 +75,41 @@ type Client struct { // Default transaction policy when rolling the transaction records forward (commit) // or back (abort) in a batch. DefaultTxnRollPolicy *TxnRollPolicy + + // Policies used for dynamic configuration updates. + // ClientPolicy is used to update the client configuration. + dynDefaultClientPolicy atomic.Pointer[ClientPolicy] + // DefaultPolicy is used for all read commands without a specific policy. + dynDefaultPolicy atomic.Pointer[BasePolicy] + // DynamicScanPolicy is used for all scan commands without a specific policy. + dynDefaultScanPolicy atomic.Pointer[ScanPolicy] + // DynamicQueryPolicy is used for all query commands without a specific policy. + dynDefaultQueryPolicy atomic.Pointer[QueryPolicy] + // DynamicBatchPolicy is the default parent policy used in batch read commands. Base policy fields] + // include socketTimeout, totalTimeout, maxRetries, etc... + dynDefaultBatchPolicy atomic.Pointer[BatchPolicy] + // Write policy fields include generation, expiration, durableDelete, etc... + dynDefaultBatchWritePolicy atomic.Pointer[BatchWritePolicy] + // include socketTimeout, totalTimeout, maxRetries, etc... + // DynamicBatchReadPolicy is the default read policy used in batch operate commands. + dynDefaultBatchReadPolicy atomic.Pointer[BatchReadPolicy] + // DynamicBatchDeletePolicy is the default delete policy used in batch delete commands. + dynDefaultBatchDeletePolicy atomic.Pointer[BatchDeletePolicy] + // DynamicBatchUDFPolicy is the default user defined function policy used in batch UDF execute commands. + dynDefaultBatchUDFPolicy atomic.Pointer[BatchUDFPolicy] + // DynamicWritePolicy is used for all write commands without a specific policy. + dynDefaultWritePolicy atomic.Pointer[WritePolicy] + // Dynamic transaction policy when verifying record versions in a batch on a commit. + dynDefaultTxnVerifyPolicy atomic.Pointer[TxnVerifyPolicy] + // Dynamic transaction policy when rolling the transaction records forward (commit) + // or back (abort) in a batch. + dynDefaultTxnRollPolicy atomic.Pointer[TxnRollPolicy] + // DynamicMetricsPolicy is used for all metrics commands without a specific policy. + dynDefaultMetricsPolicy atomic.Pointer[MetricsPolicy] + // DynamicBasePolicy is used for all commands without a specific policy when running batch operations. + dynDefaultBatchReadBasePolicy atomic.Pointer[BasePolicy] + // DynamicBasePolicy is used for all commands without a specific policy when running batch operations. + dynDefaultBatchWriteBasePolicy atomic.Pointer[BasePolicy] } func clientFinalizer(f *Client) { @@ -106,18 +150,21 @@ func NewClientWithPolicy(policy *ClientPolicy, hostname string, port int) (*Clie // It is recommended to call the client.WarmUp() method right after connecting to the database // to fill up the connection pool to the required service level. func NewClientWithPolicyAndHost(policy *ClientPolicy, hosts ...*Host) (*Client, Error) { - if policy == nil { - policy = NewClientPolicy() - } + // Start dynamic configuration watcher + dynConfig := newDynConfigWithCallBack(policy, metricsSyncCallBack) - cluster, err := NewCluster(policy, hosts) - if err != nil && policy.FailIfNotConnected { + // Get updated client updatedPolicy with dynamic configuration + updatedPolicy := getUsableClientPolicy(policy, dynConfig) + + cluster, err := NewCluster(updatedPolicy, hosts) + if err != nil && updatedPolicy.FailIfNotConnected { logger.Logger.Debug("Failed to connect to host(s): %v; error: %s", hosts, err) return nil, err } client := &Client{ cluster: cluster, + dynConfig: dynConfig, DefaultPolicy: NewPolicy(), DefaultBatchPolicy: NewBatchPolicy(), DefaultBatchReadPolicy: NewBatchReadPolicy(), @@ -133,6 +180,17 @@ func NewClientWithPolicyAndHost(policy *ClientPolicy, hosts ...*Host) (*Client, DefaultTxnRollPolicy: NewTxnRollPolicy(), } + if dynConfig != nil { + // Running the callback function to load functionalities dependent on + // the instance of client. + dynConfig.lock.Lock() + defer dynConfig.lock.Unlock() + + dynConfig.client = client + dynConfig.updateCachedPolicies() + dynConfig.runCallBack() + } + runtime.SetFinalizer(client, clientFinalizer) return client, err } @@ -143,47 +201,92 @@ func NewClientWithPolicyAndHost(policy *ClientPolicy, hosts ...*Host) (*Client, // GetDefaultPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultPolicy() *BasePolicy { - return clnt.DefaultPolicy + if clnt.dynConfig == nil { + return clnt.DefaultPolicy + } else { + response := *clnt.dynDefaultPolicy.Load() + return &response + } } // GetDefaultBatchPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultBatchPolicy() *BatchPolicy { - return clnt.DefaultBatchPolicy + if clnt.dynConfig == nil { + return clnt.DefaultBatchPolicy + } else { + response := *clnt.dynDefaultBatchPolicy.Load() + return &response + } } // GetDefaultBatchWritePolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultBatchWritePolicy() *BatchWritePolicy { - return clnt.DefaultBatchWritePolicy + if clnt.dynConfig == nil { + return clnt.DefaultBatchWritePolicy + } else { + response := *clnt.dynDefaultBatchWritePolicy.Load() + return &response + } } // GetDefaultBatchReadPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultBatchReadPolicy() *BatchReadPolicy { - return clnt.DefaultBatchReadPolicy + if clnt.dynConfig == nil { + return clnt.DefaultBatchReadPolicy + } else { + response := *clnt.dynDefaultBatchReadPolicy.Load() + return &response + } } // GetDefaultBatchDeletePolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultBatchDeletePolicy() *BatchDeletePolicy { - return clnt.DefaultBatchDeletePolicy + if clnt.dynConfig == nil { + return clnt.DefaultBatchDeletePolicy + } else { + response := *clnt.dynDefaultBatchDeletePolicy.Load() + return &response + } } // GetDefaultBatchUDFPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultBatchUDFPolicy() *BatchUDFPolicy { - return clnt.DefaultBatchUDFPolicy + if clnt.dynConfig == nil { + return clnt.DefaultBatchUDFPolicy + } else { + response := *clnt.dynDefaultBatchUDFPolicy.Load() + return &response + } } // GetDefaultWritePolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultWritePolicy() *WritePolicy { - return clnt.DefaultWritePolicy + if clnt.dynConfig == nil { + return clnt.DefaultWritePolicy + } else { + response := *clnt.dynDefaultWritePolicy.Load() + return &response + } } // GetDefaultScanPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultScanPolicy() *ScanPolicy { - return clnt.DefaultScanPolicy + if clnt.dynConfig == nil { + return clnt.DefaultScanPolicy + } else { + response := *clnt.dynDefaultScanPolicy.Load() + return &response + } } // GetDefaultQueryPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultQueryPolicy() *QueryPolicy { - return clnt.DefaultQueryPolicy + if clnt.dynConfig == nil { + return clnt.DefaultQueryPolicy + } else { + response := *clnt.dynDefaultQueryPolicy.Load() + return &response + } } // GetDefaultAdminPolicy returns corresponding default policy from the client @@ -198,57 +301,67 @@ func (clnt *Client) GetDefaultInfoPolicy() *InfoPolicy { // GetDefaultTxnVerifyPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultTxnVerifyPolicy() *TxnVerifyPolicy { - return clnt.DefaultTxnVerifyPolicy + if clnt.dynConfig == nil { + return clnt.DefaultTxnVerifyPolicy + } else { + response := *clnt.dynDefaultTxnVerifyPolicy.Load() + return &response + } } // GetDefaultTxnRollPolicy returns corresponding default policy from the client func (clnt *Client) GetDefaultTxnRollPolicy() *TxnRollPolicy { - return clnt.DefaultTxnRollPolicy + if clnt.dynConfig == nil { + return clnt.DefaultTxnRollPolicy + } else { + response := *clnt.dynDefaultTxnRollPolicy.Load() + return &response + } } // SetDefaultPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultPolicy(policy *BasePolicy) { - clnt.DefaultPolicy = policy + clnt.DefaultPolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultBatchPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultBatchPolicy(policy *BatchPolicy) { - clnt.DefaultBatchPolicy = policy + clnt.DefaultBatchPolicy = applyConfigToBatchPolicy(policy, clnt.dynConfig) } // SetDefaultBatchWritePolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultBatchWritePolicy(policy *BatchWritePolicy) { - clnt.DefaultBatchWritePolicy = policy + clnt.DefaultBatchWritePolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultBatchReadPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultBatchReadPolicy(policy *BatchReadPolicy) { - clnt.DefaultBatchReadPolicy = policy + clnt.DefaultBatchReadPolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultBatchDeletePolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultBatchDeletePolicy(policy *BatchDeletePolicy) { - clnt.DefaultBatchDeletePolicy = policy + clnt.DefaultBatchDeletePolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultBatchUDFPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultBatchUDFPolicy(policy *BatchUDFPolicy) { - clnt.DefaultBatchUDFPolicy = policy + clnt.DefaultBatchUDFPolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultWritePolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultWritePolicy(policy *WritePolicy) { - clnt.DefaultWritePolicy = policy + clnt.DefaultWritePolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultScanPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultScanPolicy(policy *ScanPolicy) { - clnt.DefaultScanPolicy = policy + clnt.DefaultScanPolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultQueryPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultQueryPolicy(policy *QueryPolicy) { - clnt.DefaultQueryPolicy = policy + clnt.DefaultQueryPolicy = policy.pathDynamic(clnt.dynConfig) } // SetDefaultAdminPolicy sets corresponding default policy on the client @@ -263,12 +376,12 @@ func (clnt *Client) SetDefaultInfoPolicy(policy *InfoPolicy) { // SetDefaultTxnVerifyPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultTxnVerifyPolicy(policy *TxnVerifyPolicy) { - clnt.DefaultTxnVerifyPolicy = policy + clnt.DefaultTxnVerifyPolicy = policy.patchDynamic(clnt.dynConfig) } // SetDefaultTxnRollPolicy sets corresponding default policy on the client func (clnt *Client) SetDefaultTxnRollPolicy(policy *TxnRollPolicy) { - clnt.DefaultTxnRollPolicy = policy + clnt.DefaultTxnRollPolicy = policy.patchDynamic(clnt.dynConfig) } //------------------------------------------------------- @@ -1240,7 +1353,6 @@ func (clnt *Client) QueryExecute(policy *QueryPolicy, statement *Statement, ops ...*Operation, ) (*ExecuteTask, Error) { - if len(statement.BinNames) > 0 { return nil, ErrNoBinNamesAllowedInQueryExecute.err() } @@ -1366,8 +1478,8 @@ var infoErrRegexp = regexp.MustCompile(`(?i)(fail|error)((:|=)(?P[0-9]+))? func parseInfoErrorCode(response string) Error { match := infoErrRegexp.FindStringSubmatch(response) - var code = types.SERVER_ERROR - var message = response + code := types.SERVER_ERROR + message := response if len(match) > 0 { for i, name := range infoErrRegexp.SubexpNames() { @@ -1469,12 +1581,12 @@ func (clnt *Client) Commit(txn *Txn) (CommitStatus, Error) { default: fallthrough case TxnStateOpen: - if err := tr.Verify(&clnt.GetDefaultTxnVerifyPolicy().BatchPolicy, &clnt.GetDefaultTxnRollPolicy().BatchPolicy); err != nil { + if err := tr.Verify(&clnt.getUsableTxnVerifyPolicy(nil).BatchPolicy, &clnt.getUsableTxnRollPolicy(nil).BatchPolicy); err != nil { return CommitStatusUnverified, err } - return tr.Commit(&clnt.GetDefaultTxnRollPolicy().BatchPolicy) + return tr.Commit(&clnt.getUsableTxnRollPolicy(nil).BatchPolicy) case TxnStateVerified: - return tr.Commit(&clnt.GetDefaultTxnRollPolicy().BatchPolicy) + return tr.Commit(&clnt.getUsableTxnRollPolicy(nil).BatchPolicy) case TxnStateCommitted: return CommitStatusAlreadyCommitted, nil case TxnStateAborted: @@ -1493,7 +1605,7 @@ func (clnt *Client) Abort(txn *Txn) (AbortStatus, Error) { case TxnStateOpen: fallthrough case TxnStateVerified: - return tr.Abort(&clnt.GetDefaultTxnRollPolicy().BatchPolicy) + return tr.Abort(&clnt.getUsableTxnRollPolicy(nil).BatchPolicy) case TxnStateCommitted: return AbortStatusAlreadyCommitted, newError(types.TXN_ALREADY_COMMITTED, "Transaction already committed") case TxnStateAborted: @@ -1994,15 +2106,25 @@ func (clnt *Client) MetricsEnabled() bool { } // EnableMetrics enables the cluster command metrics gathering. -// If the parameters for the histogram in the policy are the different from the one already +// If the parameters for the histogram in the policy are different from the one already // on the cluster, the metrics will be reset. func (clnt *Client) EnableMetrics(policy *MetricsPolicy) { - clnt.cluster.EnableMetrics(policy) + if clnt.dynConfig == nil || + clnt.dynConfig.config == nil || + clnt.dynConfig.config.Dynamic == nil || + clnt.dynConfig.config.Dynamic.Metrics == nil { + + clnt.cluster.EnableMetrics(policy) + } } // DisableMetrics disables the cluster command metrics gathering. func (clnt *Client) DisableMetrics() { - clnt.cluster.DisableMetrics() + if clnt.dynConfig != nil { + logger.Logger.Warn("Dynamic configuration is enabled. Metrics cannot be disabled via the client API.") + } else { + clnt.cluster.DisableMetrics() + } } // Stats returns internal statistics regarding the inner state of the client and the cluster. @@ -2060,108 +2182,134 @@ func (clnt *Client) sendInfoCommand(timeout time.Duration, command string) (map[ return node.RequestInfo(&policy, command) } -//------------------------------------------------------- +// ------------------------------------------------------- // Policy Methods -//------------------------------------------------------- - +// ------------------------------------------------------- func (clnt *Client) getUsablePolicy(policy *BasePolicy) *BasePolicy { - if policy == nil { - if clnt.DefaultPolicy != nil { - return clnt.DefaultPolicy - } - return NewPolicy() + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultPolicy != nil { + return clnt.DefaultPolicy + } + return clnt.dynDefaultPolicy.Load() } func (clnt *Client) getUsableBatchPolicy(policy *BatchPolicy) *BatchPolicy { - if policy == nil { - if clnt.DefaultBatchPolicy != nil { - return clnt.DefaultBatchPolicy - } - return NewBatchPolicy() + if policy != nil { + // Merge policy with dynamic config + return applyConfigToBatchPolicy(policy, clnt.dynConfig) } - return policy -} - -func (clnt *Client) getUsableBaseBatchWritePolicy(policy *BatchPolicy) *BatchPolicy { - if policy == nil { - if clnt.DefaultBatchPolicy != nil { - return clnt.DefaultBatchPolicy - } - return NewBatchPolicy() + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultBatchPolicy != nil { + return clnt.DefaultBatchPolicy } - return policy + return clnt.dynDefaultBatchPolicy.Load() } func (clnt *Client) getUsableBatchReadPolicy(policy *BatchReadPolicy) *BatchReadPolicy { - if policy == nil { - if clnt.DefaultBatchReadPolicy != nil { - return clnt.DefaultBatchReadPolicy - } - return NewBatchReadPolicy() + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultBatchReadPolicy != nil { + return clnt.DefaultBatchReadPolicy + } + return clnt.dynDefaultBatchReadPolicy.Load() + } func (clnt *Client) getUsableBatchWritePolicy(policy *BatchWritePolicy) *BatchWritePolicy { - if policy == nil { - if clnt.DefaultBatchWritePolicy != nil { - return clnt.DefaultBatchWritePolicy - } - return NewBatchWritePolicy() + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultBatchWritePolicy != nil { + return clnt.DefaultBatchWritePolicy + } + return clnt.dynDefaultBatchWritePolicy.Load() } func (clnt *Client) getUsableBatchDeletePolicy(policy *BatchDeletePolicy) *BatchDeletePolicy { - if policy == nil { - if clnt.DefaultBatchDeletePolicy != nil { - return clnt.DefaultBatchDeletePolicy - } - return NewBatchDeletePolicy() + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultBatchDeletePolicy != nil { + return clnt.DefaultBatchDeletePolicy + } + return clnt.dynDefaultBatchDeletePolicy.Load() } func (clnt *Client) getUsableBatchUDFPolicy(policy *BatchUDFPolicy) *BatchUDFPolicy { - if policy == nil { - if clnt.DefaultBatchUDFPolicy != nil { - return clnt.DefaultBatchUDFPolicy - } - return NewBatchUDFPolicy() + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultBatchUDFPolicy != nil { + return clnt.DefaultBatchUDFPolicy + } + return clnt.dynDefaultBatchUDFPolicy.Load() } func (clnt *Client) getUsableWritePolicy(policy *WritePolicy) *WritePolicy { - if policy == nil { - if clnt.DefaultWritePolicy != nil { - return clnt.DefaultWritePolicy - } - return NewWritePolicy(0, 0) + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultWritePolicy != nil { + return clnt.DefaultWritePolicy + } + return clnt.dynDefaultWritePolicy.Load() } func (clnt *Client) getUsableScanPolicy(policy *ScanPolicy) *ScanPolicy { - if policy == nil { - if clnt.DefaultScanPolicy != nil { - return clnt.DefaultScanPolicy - } - return NewScanPolicy() + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultScanPolicy != nil { + return clnt.DefaultScanPolicy + } + return clnt.dynDefaultScanPolicy.Load() } func (clnt *Client) getUsableQueryPolicy(policy *QueryPolicy) *QueryPolicy { - if policy == nil { - if clnt.DefaultQueryPolicy != nil { - return clnt.DefaultQueryPolicy - } - return NewQueryPolicy() + if policy != nil { + // Merge policy with dynamic config + return policy.pathDynamic(clnt.dynConfig) } - return policy + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultQueryPolicy != nil { + return clnt.DefaultQueryPolicy + } + return clnt.dynDefaultQueryPolicy.Load() } func (clnt *Client) getUsableAdminPolicy(policy *AdminPolicy) *AdminPolicy { @@ -2184,6 +2332,43 @@ func (clnt *Client) getUsableInfoPolicy(policy *InfoPolicy) *InfoPolicy { return policy } +func (clnt *Client) getUsableTxnRollPolicy(policy *TxnRollPolicy) *TxnRollPolicy { + if policy != nil { + // Merge policy with dynamic config + return policy.patchDynamic(clnt.dynConfig) + } + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultTxnRollPolicy != nil { + return clnt.DefaultTxnRollPolicy + } + return clnt.dynDefaultTxnRollPolicy.Load() +} + +func (clnt *Client) getUsableTxnVerifyPolicy(policy *TxnVerifyPolicy) *TxnVerifyPolicy { + if policy != nil { + // Merge policy with dynamic config + + return policy.patchDynamic(clnt.dynConfig) + } + // Make sure to handle the case where the user is setting Default....Policy policy and + // dynConfig is nil. Essentially, we do not want to treat cache as default + // when dynConfig is nil. Separation of concerns. + if clnt.dynConfig == nil && clnt.DefaultTxnVerifyPolicy != nil { + return clnt.DefaultTxnVerifyPolicy + } + return clnt.dynDefaultTxnVerifyPolicy.Load() +} + +func getUsableClientPolicy(policy *ClientPolicy, dynConfig *DynConfig) *ClientPolicy { + if policy == nil { + return NewClientPolicy().patchDynamic(dynConfig) + } + + return policy.patchDynamic(dynConfig) +} + //------------------------------------------------------- // Utility Functions //------------------------------------------------------- diff --git a/client_config_test.go b/client_config_test.go new file mode 100644 index 00000000..13081329 --- /dev/null +++ b/client_config_test.go @@ -0,0 +1,214 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + gg "github.com/onsi/ginkgo/v2" + gm "github.com/onsi/gomega" +) + +var _ = gg.Describe("Default Policies", func() { + var client *Client + + gg.Context("when DynConfig is nil", func() { + gg.BeforeEach(func() { + client = &Client{ + dynConfig: nil, + DefaultPolicy: NewPolicy(), + DefaultBatchPolicy: NewBatchPolicy(), + DefaultBatchReadPolicy: NewBatchReadPolicy(), + DefaultBatchWritePolicy: NewBatchWritePolicy(), + DefaultBatchDeletePolicy: NewBatchDeletePolicy(), + DefaultBatchUDFPolicy: NewBatchUDFPolicy(), + DefaultWritePolicy: NewWritePolicy(0, 0), + DefaultScanPolicy: NewScanPolicy(), + DefaultQueryPolicy: NewQueryPolicy(), + DefaultTxnVerifyPolicy: NewTxnVerifyPolicy(), + DefaultTxnRollPolicy: NewTxnRollPolicy(), + } + }) + + gg.It("GetDefaultPolicy should load a default client policy", func() { + policy := client.GetDefaultPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchPolicy should load a default BatchPolicy", func() { + policy := client.GetDefaultBatchPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchReadPolicy should load a default BatchReadPolicy", func() { + policy := client.GetDefaultBatchReadPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchWritePolicy should load a default BatchWritePolicy", func() { + policy := client.GetDefaultBatchWritePolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchDeletePolicy should load a default BatchDeletePolicy", func() { + policy := client.GetDefaultBatchDeletePolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchUDFPolicy should load a default BatchUDFPolicy", func() { + policy := client.GetDefaultBatchUDFPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultWritePolicy should load a default WritePolicy", func() { + policy := client.GetDefaultWritePolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultScanPolicy should load a default ScanPolicy", func() { + policy := client.GetDefaultScanPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultQueryPolicy should load a default QueryPolicy", func() { + policy := client.GetDefaultQueryPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultTxnVerifyPolicy should load a default TxnVerifyPolicy", func() { + policy := client.GetDefaultTxnVerifyPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + + gg.It("GetDefaultTxnRollPolicy should load a default TxnRollPolicy", func() { + policy := client.GetDefaultTxnRollPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.dynConfig).To(gm.BeNil()) + }) + }) + + gg.Context("when DynConfig is not nil (policy cache is populated)", func() { + var dynCfg *DynConfig + gg.BeforeEach(func() { + config := &dynconfig.Config{} + dynCfg = NewDynConfigForTest(config) + dummyClientPolicy := NewClientPolicy() + dummyBatchPolicy := NewBatchPolicy() + dummyBatchReadPolicy := NewBatchReadPolicy() + dummyBatchWritePolicy := NewBatchWritePolicy() + dummyBatchDeletePolicy := NewBatchDeletePolicy() + dummyBatchUDFPolicy := NewBatchUDFPolicy() + dummyWritePolicy := NewWritePolicy(0, 0) + dummyScanPolicy := NewScanPolicy() + dummyQueryPolicy := NewQueryPolicy() + dummyTxnVerifyPolicy := NewTxnVerifyPolicy() + dummyTxnRollPolicy := NewTxnRollPolicy() + dummyBasePolicy := NewPolicy() + + client = &Client{ + dynConfig: dynCfg, + } + client.dynDefaultClientPolicy.Store(dummyClientPolicy) + client.dynDefaultBatchPolicy.Store(dummyBatchPolicy) + client.dynDefaultBatchReadPolicy.Store(dummyBatchReadPolicy) + client.dynDefaultBatchWritePolicy.Store(dummyBatchWritePolicy) + client.dynDefaultBatchDeletePolicy.Store(dummyBatchDeletePolicy) + client.dynDefaultBatchUDFPolicy.Store(dummyBatchUDFPolicy) + client.dynDefaultWritePolicy.Store(dummyWritePolicy) + client.dynDefaultScanPolicy.Store(dummyScanPolicy) + client.dynDefaultQueryPolicy.Store(dummyQueryPolicy) + client.dynDefaultTxnVerifyPolicy.Store(dummyTxnVerifyPolicy) + client.dynDefaultTxnRollPolicy.Store(dummyTxnRollPolicy) + client.dynDefaultPolicy.Store(dummyBasePolicy) + + dynCfg.client = client + }) + + gg.It("GetDefaultPolicy should fetch policy from cache", func() { + policy := client.GetDefaultPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultPolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchPolicy should fetch policy from cache", func() { + policy := client.GetDefaultBatchPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultBatchPolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchReadPolicy should fetch policy from cache", func() { + policy := client.GetDefaultBatchReadPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultBatchReadPolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchWritePolicy should fetch policy from cache", func() { + policy := client.GetDefaultBatchWritePolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultBatchWritePolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchDeletePolicy should fetch policy from cache", func() { + policy := client.GetDefaultBatchDeletePolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultBatchDeletePolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultBatchUDFPolicy should fetch policy from cache", func() { + policy := client.GetDefaultBatchUDFPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultBatchUDFPolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultWritePolicy should fetch policy from cache", func() { + policy := client.GetDefaultWritePolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultWritePolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultScanPolicy should fetch policy from cache", func() { + policy := client.GetDefaultScanPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultScanPolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultQueryPolicy should fetch policy from cache", func() { + policy := client.GetDefaultQueryPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultQueryPolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultTxnVerifyPolicy should fetch policy from cache", func() { + policy := client.GetDefaultTxnVerifyPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultTxnVerifyPolicy).To(gm.BeNil()) + }) + + gg.It("GetDefaultTxnRollPolicy should fetch policy from cache", func() { + policy := client.GetDefaultTxnRollPolicy() + gm.Expect(policy).ToNot(gm.BeNil()) + gm.Expect(client.DefaultTxnRollPolicy).To(gm.BeNil()) + }) + }) +}) diff --git a/client_policy.go b/client_policy.go index 1b4ad865..a0e3f174 100644 --- a/client_policy.go +++ b/client_policy.go @@ -161,6 +161,9 @@ type ClientPolicy struct { // Peers nodes for the cluster are not discovered and seed nodes are // retained despite connection failures. SeedOnlyCluster bool // = false + + // Determianes the interval for checking for configuration changes using configProvider. + ConfigInterval time.Duration // = 5 second } // NewClientPolicy generates a new ClientPolicy with default values. @@ -179,6 +182,7 @@ func NewClientPolicy() *ClientPolicy { MaxErrorRate: 100, ErrorRateWindow: 1, SeedOnlyCluster: false, + ConfigInterval: time.Second * 5, } } @@ -221,3 +225,92 @@ func (cp *ClientPolicy) peersString() string { } return "peers-clear-std" } + +// copyClientPolicy creates a new BasePolicy instance and copies the values from the source policy. +func (cp *ClientPolicy) copy() *ClientPolicy { + if cp == nil { + return nil + } + + response := *cp + return &response +} + +func (cp *ClientPolicy) mapDynamic(dynConfig *DynConfig) *ClientPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return cp + } + + if dynConfig.config.Dynamic.Client != nil { + if dynConfig.config.Dynamic.Client.IdleTimeout != nil { + cp.IdleTimeout = time.Duration(*dynConfig.config.Dynamic.Client.IdleTimeout) * time.Second + } + if dynConfig.config.Dynamic.Client.Timeout != nil { + cp.Timeout = time.Duration(*dynConfig.config.Dynamic.Client.Timeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Client.ErrorRateWindow != nil { + cp.ErrorRateWindow = *dynConfig.config.Dynamic.Client.ErrorRateWindow + } + if dynConfig.config.Dynamic.Client.MaxErrorRate != nil { + cp.MaxErrorRate = *dynConfig.config.Dynamic.Client.MaxErrorRate + } + if dynConfig.config.Dynamic.Client.LoginTimeout != nil { + cp.LoginTimeout = time.Duration(*dynConfig.config.Dynamic.Client.LoginTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Client.RackAware != nil { + cp.RackAware = *dynConfig.config.Dynamic.Client.RackAware + } + if dynConfig.config.Dynamic.Client.RackIds != nil { + cp.RackIds = *dynConfig.config.Dynamic.Client.RackIds + } + if dynConfig.config.Dynamic.Client.TendInterval != nil { + cp.TendInterval = time.Duration(*dynConfig.config.Dynamic.Client.TendInterval) * time.Millisecond + } + if dynConfig.config.Dynamic.Client.UseServiceAlternate != nil { + cp.UseServicesAlternate = *dynConfig.config.Dynamic.Client.UseServiceAlternate + } + } + + return cp +} + +func (cp *ClientPolicy) mapStatic(dynConfig *DynConfig) *ClientPolicy { + if dynConfig.config == nil || dynConfig.config.Static == nil { + return cp + } + + if dynConfig.config.Static.Client != nil { + if dynConfig.config.Static.Client.ConfigInterval != nil { + cp.ConfigInterval = time.Duration(*dynConfig.config.Static.Client.ConfigInterval) * time.Second + } + if dynConfig.config.Static.Client.ConnectionQueueSize != nil { + cp.ConnectionQueueSize = *dynConfig.config.Static.Client.ConnectionQueueSize + } + if dynConfig.config.Static.Client.MinConnectionsPerNode != nil { + cp.MinConnectionsPerNode = *dynConfig.config.Static.Client.MinConnectionsPerNode + } + } + + return cp +} + +// patchDynamic applies the dynamic configuration and generates a new policy. +func (policy *ClientPolicy) patchDynamic(dynConfig *DynConfig) *ClientPolicy { + if dynConfig == nil { + return policy + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if config != nil && ((config.Dynamic != nil && config.Dynamic.Client != nil) || (config.Static != nil && config.Static.Client != nil)) { + // User has provided a custom policy. We need to apply the dynamic configuration. + if policy != nil { + return policy.copy().mapStatic(dynConfig).mapDynamic(dynConfig) + } else { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultClientPolicy.Load() + } + } else { + return policy + } +} diff --git a/cluster.go b/cluster.go index 4c70a950..1ed5e526 100644 --- a/cluster.go +++ b/cluster.go @@ -57,7 +57,7 @@ type Cluster struct { // Hints for best node for a partition partitionWriteMap iatomic.TypedVal[partitionMap] //partitionMap - clientPolicy ClientPolicy + clientPolicy iatomic.SyncVal[*ClientPolicy] infoPolicy InfoPolicy connectionThreshold iatomic.Int // number of parallel opening connections @@ -115,9 +115,8 @@ func NewCluster(policy *ClientPolicy, hosts []*Host) (*Cluster, Error) { } newCluster := &Cluster{ - clientPolicy: clientPolicy, - infoPolicy: InfoPolicy{Timeout: policy.Timeout}, - tendChannel: make(chan struct{}), + infoPolicy: InfoPolicy{Timeout: policy.Timeout}, + tendChannel: make(chan struct{}), seeds: *iatomic.NewSyncVal(hosts), aliases: *sm.New[Host, *Node](16), @@ -129,6 +128,7 @@ func NewCluster(policy *ClientPolicy, hosts []*Host) (*Cluster, Error) { supportsPartitionQuery: *iatomic.NewBool(false), } + newCluster.clientPolicy.Set(&clientPolicy) newCluster.partitionWriteMap.Set(make(partitionMap)) @@ -159,7 +159,7 @@ func NewCluster(policy *ClientPolicy, hosts []*Host) (*Cluster, Error) { // start up cluster maintenance go routine newCluster.wgTend.Add(1) - go newCluster.clusterBoss(&newCluster.clientPolicy) + go newCluster.clusterBoss(newCluster.clientPolicy.Get()) if err == nil { logger.Logger.Debug("New cluster initialized and ready to be used...") @@ -183,7 +183,7 @@ func (clstr *Cluster) clusterBoss(policy *ClientPolicy) { defer func() { if r := recover(); r != nil { logger.Logger.Error("Cluster tend goroutine crashed: %s", debug.Stack()) - go clstr.clusterBoss(&clstr.clientPolicy) + go clstr.clusterBoss(clstr.clientPolicy.Get()) } }() @@ -207,10 +207,11 @@ Loop: logger.Logger.Warn(err.Error()) } + tendInterval := clstr.clientPolicy.Get().TendInterval // Tending took longer than requested tend interval. // Tending is too slow for the cluster, and may be falling behind schedule. - if tendDuration := time.Since(tm); tendDuration > clstr.clientPolicy.TendInterval { - logger.Logger.Warn("Tending took %s, while your requested ClientPolicy.TendInterval is %s. Tends are slower than the interval, and may be falling behind the changes in the cluster.", tendDuration, clstr.clientPolicy.TendInterval) + if tendDuration := time.Since(tm); tendDuration > tendInterval { + logger.Logger.Warn("Tending took %s, while your requested ClientPolicy.TendInterval is %s. Tends are slower than the interval, and may be falling behind the changes in the cluster.", tendDuration, tendInterval) } } } @@ -249,7 +250,7 @@ func (clstr *Cluster) tend() Error { // All node additions/deletions are performed in tend goroutine. // If active nodes don't exist, seed cluster. - if len(nodes) == 0 || (clstr.clientPolicy.SeedOnlyCluster && len(nodes) < clstr.GetSeedCount()) { + if len(nodes) == 0 || (clstr.clientPolicy.Get().SeedOnlyCluster && len(nodes) < clstr.GetSeedCount()) { logger.Logger.Info("No nodes available; seeding...") if newNodesFound, err := clstr.seedNodes(); !newNodesFound { return err @@ -288,7 +289,7 @@ func (clstr *Cluster) tend() Error { seq.Do(_peer.hosts, func(host *Host) error { // attempt connection to the host - nv := nodeValidator{seedOnlyCluster: clstr.clientPolicy.SeedOnlyCluster} + nv := nodeValidator{seedOnlyCluster: clstr.clientPolicy.Get().SeedOnlyCluster} if err := nv.validateNode(clstr, host); err != nil { logger.Logger.Warn("Add node `%s` failed: `%s`", host, err) return nil @@ -361,8 +362,9 @@ func (clstr *Cluster) tend() Error { clstr.aggregateNodeStats(clstr.GetNodes()) + clusterClientPolicy := *clstr.clientPolicy.Get() // Reset connection error window for all nodes every connErrorWindow tend iterations. - if clstr.clientPolicy.MaxErrorRate > 0 && clstr.tendCount%clstr.clientPolicy.ErrorRateWindow == 0 { + if clusterClientPolicy.MaxErrorRate > 0 && clstr.tendCount%clusterClientPolicy.ErrorRateWindow == 0 { for _, node := range clstr.GetNodes() { node.resetErrorCount() } @@ -469,14 +471,15 @@ func (clstr *Cluster) waitTillStabilized() Error { doneCh <- err }() + clusterClientPolicy := *clstr.clientPolicy.Get() select { - case <-time.After(clstr.clientPolicy.Timeout): - if clstr.clientPolicy.FailIfNotConnected { + case <-time.After(clusterClientPolicy.Timeout): + if clusterClientPolicy.FailIfNotConnected { clstr.Close() } return ErrTimeout.err() case err := <-doneCh: - if err != nil && clstr.clientPolicy.FailIfNotConnected { + if err != nil && clusterClientPolicy.FailIfNotConnected { clstr.Close() } return err @@ -536,11 +539,12 @@ func (clstr *Cluster) seedNodes() (newSeedsFound bool, errChain Error) { logger.Logger.Info("Seeding the cluster. Seeds count: %d", len(seedArray)) + clusterClientPolicy := *clstr.clientPolicy.Get() // Add all nodes at once to avoid copying entire array multiple times. for i, seed := range seedArray { go func(index int, seed *Host) { nodesToAdd := make(nodesToAddT, 128) - nv := nodeValidator{seedOnlyCluster: clstr.clientPolicy.SeedOnlyCluster} + nv := nodeValidator{seedOnlyCluster: clusterClientPolicy.SeedOnlyCluster} err := nv.seedNodes(clstr, seed, nodesToAdd) if err != nil { logger.Logger.Warn("Seed %s failed: %s", seed.String(), err.Error()) @@ -568,7 +572,7 @@ L: if seedCount <= 0 { break L } - case <-time.After(clstr.clientPolicy.Timeout): + case <-time.After(clusterClientPolicy.Timeout): // time is up, no seeds found break L } @@ -604,7 +608,7 @@ func (clstr *Cluster) addAlias(host *Host, node *Node) { func (clstr *Cluster) findNodesToRemove(refreshCount int) []*Node { removeList := []*Node{} - if clstr.clientPolicy.SeedOnlyCluster { + if clstr.clientPolicy.Get().SeedOnlyCluster { // Don't remove any node even if its bad or inactive. return removeList } @@ -684,7 +688,7 @@ func (clstr *Cluster) addNodes(nodesToAdd map[string]*Node) { defer clstr.updateClusterFeatures() clstr.nodes.Update(func(nodes []*Node) ([]*Node, error) { - if clstr.clientPolicy.SeedOnlyCluster && clstr.GetSeedCount() == len(nodes) { + if clstr.clientPolicy.Get().SeedOnlyCluster && clstr.GetSeedCount() == len(nodes) { // Don't add new nodes. return nodes, nil } @@ -934,14 +938,15 @@ func (clstr *Cluster) Password() (res []byte) { func (clstr *Cluster) changePassword(user string, password string, hash []byte) { // change password ONLY if the user is the same if clstr.user == user { - clstr.clientPolicy.Password = password + clstr.clientPolicy.Get().Password = password clstr.password.Set(hash) } } // ClientPolicy returns the client policy that is currently used with the cluster. func (clstr *Cluster) ClientPolicy() (res ClientPolicy) { - return clstr.clientPolicy + returnPolicy := *clstr.clientPolicy.Get() + return returnPolicy } // WarmUp fills the connection pool with connections for all nodes. diff --git a/config.go b/config.go new file mode 100644 index 00000000..dc569096 --- /dev/null +++ b/config.go @@ -0,0 +1,532 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "encoding/json" + "fmt" + "regexp" + "runtime/debug" + "strings" + "sync" + "sync/atomic" + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + registry "github.com/aerospike/aerospike-client-go/v8/config/registry" + "github.com/aerospike/aerospike-client-go/v8/logger" +) + +type DynConfig struct { + lock sync.RWMutex + + config *dynconfig.Config + wgConfig sync.WaitGroup + + configInitialized *atomic.Bool + client *Client // Reference to the client to use for callbacks and cached policies. + configProvider dynconfig.ConfigProvider + configWatchChannel chan struct{} + + metricsCallback func(config *dynconfig.Config, client *Client) + + scheme string + dsn string +} + +func parseDsn(inputDsn string) map[string]string { + // ^\s* skip any leading spaces or tabs + // ([A-Za-z][A-Za-z0-9+.-]*://)? optionally capture scheme:// (RFC-style) + // (.*) capture the rest of the line verbatim + re := regexp.MustCompile(registry.DSN_REGEX_PATTERN) + match := re.FindStringSubmatch(inputDsn) + if match == nil { + return nil + } + + result := make(map[string]string) + for i, name := range re.SubexpNames() { + if i > 0 && name != "" { + result[name] = match[i] + } + } + return result +} + +func newDynConfigWithCallBack(policy *ClientPolicy, fn func(config *dynconfig.Config, client *Client)) *DynConfig { + // Dynamic configuration is not enabled if the config URL is empty. + if strings.TrimSpace(AEROSPIKE_CLIENT_CONFIG_URL) == "" { + return nil + } + if policy == nil { + policy = NewClientPolicy() + } + + parts := parseDsn(AEROSPIKE_CLIENT_CONFIG_URL) + if len(parts) < 2 { + logger.Logger.Error("Invalid config URL %s. Expected format: [scheme://dsn] | [file path]", AEROSPIKE_CLIENT_CONFIG_URL) + return nil + } + + var schema string + if parts[registry.DSN_SCHEME] == "" { + schema = registry.DEFAULT_SCHEME + } else { + schema = parts[registry.DSN_SCHEME] + } + urlPath := parts[registry.DSN_PATH] + + // At this point in time we should have at least one configuration provider in the registry. + provider, _ := registry.Get(schema) + if provider == nil { + logger.Logger.Error("No configuration provider found for scheme %s.", schema) + return nil + } + + dynConfig := &DynConfig{ + configWatchChannel: make(chan struct{}), + configInitialized: &atomic.Bool{}, + metricsCallback: fn, + scheme: schema, + dsn: urlPath, + configProvider: provider, + } + dynConfig.initConfig() + + dynConfig.wgConfig.Add(1) + go dynConfig.watchConfig(policy.ConfigInterval) + + return dynConfig +} + +// ---------------------------------------------------------------- +// Functions used to manage the configuration state +// ---------------------------------------------------------------- + +func (dc *DynConfig) loadConfig() { + dc.lock.Lock() + defer dc.lock.Unlock() + + if !dc.configInitialized.Load() && dc.configProvider != nil { + dc.initConfig() + } else { + dc.providerLoadConfig() + } + + // Invoke the callback if it is set. + dc.runCallBack() +} + +func (dc *DynConfig) runCallBack() { + if dc.metricsCallback != nil && dc.config != nil && dc.config.Dynamic != nil && dc.config.Dynamic.Metrics != nil { + dc.metricsCallback(dc.config, dc.client) + } +} + +// providerLoadConfig loads the config from the provider and hydrates +// the dynamic policies. It also clears the cache for dynamic configuration to ensure that the new +// config is used. +func (dc *DynConfig) providerLoadConfig() { + loadedConfig := dc.configProvider.LoadConfig(dc.dsn) + if loadedConfig != nil { + if dc.config.Dynamic == nil { + logger.Logger.Warn("Dynamic configuration is enabled and configuration is empty. Configuration will load default policy values.") + } + + dc.config.Dynamic = loadedConfig.Dynamic // This is updating the entire dynamic config object + + dc.hydrateDynamicPolicyFromConfig() + logger.Logger.Debug("Dynamic configuration updated internal state from provider.") + if logger.Logger.IsLogLevelEnabled(logger.DEBUG) { + // Log the configuration in debug mode + configJSON, err := json.Marshal(dc.config) + if err != nil { + logger.Logger.Error("Failed to marshal config to JSON: %v", err) + } else { + logger.Logger.Debug("Updated dynamic configuration: '%s'", string(configJSON)) + } + } + } +} + +// initConfig is called only once on startup. It loads the config and +// hydrates the static and dynamic policies. It also clears the cache to ensure that the new config is used. +func (dc *DynConfig) initConfig() { + loadedConfig := dc.configProvider.LoadConfig(dc.dsn) + if loadedConfig != nil { + dc.config = loadedConfig // This is updating the entire config object + + if dc.client != nil { + dc.hydrateStaticPolicyFromConfig() + dc.hydrateDynamicPolicyFromConfig() + } + + dc.configInitialized.Store(true) + logger.Logger.Debug("Dynamic configuration initialized...") + if logger.Logger.IsLogLevelEnabled(logger.DEBUG) { + // Log the configuration in debug mode + configJSON, err := json.Marshal(dc.config) + if err != nil { + logger.Logger.Error("Failed to marshal config to JSON: %v", err) + } else { + logger.Logger.Debug("Updated dynamic configuration: '%s'", string(configJSON)) + } + } + } +} + +func (dc *DynConfig) updateCachedPolicies() { + // This function is called to update the cached policies in the client. + // It is used to ensure that the policies are updated when the config changes. + + if dc.client != nil { + dc.hydrateStaticPolicyFromConfig() + dc.hydrateDynamicPolicyFromConfig() + } else { + panic(fmt.Errorf("Client is not set in DynConfig, cannot update cached policies")) + } +} + +func (dc *DynConfig) hydrateStaticPolicyFromConfig() { + dc.client.dynDefaultClientPolicy.Store(dc.generateStaticClientPolicy()) +} + +func (dc *DynConfig) hydrateDynamicPolicyFromConfig() { + dc.client.dynDefaultClientPolicy.Store(dc.generateDynamicClientPolicy()) + dc.client.dynDefaultPolicy.Store(dc.generateDynamicReadPolicy()) + dc.client.dynDefaultWritePolicy.Store(dc.generateDynamicWritePolicy()) + dc.client.dynDefaultQueryPolicy.Store(dc.generateDynamicQueryPolicy()) + dc.client.dynDefaultScanPolicy.Store(dc.generateDynamicScanPolicy()) + dc.client.dynDefaultBatchPolicy.Store(dc.generateDynamicBatchPolicy()) + dc.client.dynDefaultBatchReadPolicy.Store(dc.generateDynamicBatchReadPolicy()) + dc.client.dynDefaultBatchWritePolicy.Store(dc.generateDynamicBatchWritePolicy()) + dc.client.dynDefaultBatchUDFPolicy.Store(dc.generateDynamicBatchUdfPolicy()) + dc.client.dynDefaultBatchDeletePolicy.Store(dc.generateDynamicBatchDeletePolicy()) + dc.client.dynDefaultTxnRollPolicy.Store(dc.generateDynamicTxnRollPolicy()) + dc.client.dynDefaultTxnVerifyPolicy.Store(dc.generateDynamicTxnVerifyPolicy()) + dc.client.dynDefaultMetricsPolicy.Store(dc.generateDynamicMetricsPolicy()) + dc.client.dynDefaultBatchReadBasePolicy.Store(dc.generateDynamicBatchReadBasePolicy()) + dc.client.dynDefaultBatchWriteBasePolicy.Store(dc.generateDynamicBatchWriteBasePolicy()) +} + +func (dc *DynConfig) generateStaticClientPolicy() *ClientPolicy { + policy := NewClientPolicy() + + policy = policy.mapStatic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicClientPolicy() *ClientPolicy { + // Loading current client policy since static fields are set at init time + // We need to merge and preserve static and dynamic values. + policy := dc.client.dynDefaultClientPolicy.Load() + if policy == nil { + policy = NewClientPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicWritePolicy() *WritePolicy { + var policy *WritePolicy + if dc.client != nil && dc.client.DefaultWritePolicy != nil { + // Not going go make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultWritePolicy.copy() + } else { + policy = NewWritePolicy(0, 0) + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicBatchReadBasePolicy() *BasePolicy { + var policy *BasePolicy + if dc.client != nil && dc.client.DefaultBatchReadPolicy != nil { + // Not going go make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultBatchPolicy.BasePolicy.copy() + } else { + policy = NewPolicy() + } + + policy = policy.mapConfigBatchReadToBasePolicy(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicBatchWriteBasePolicy() *BasePolicy { + var policy *BasePolicy + if dc.client != nil && dc.client.DefaultBatchWritePolicy != nil { + // Not going go make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultBatchPolicy.BasePolicy.copy() + } else { + policy = NewPolicy() + } + + policy = policy.mapConfigBatchWriteToBasePolicy(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicReadPolicy() *BasePolicy { + var policy *BasePolicy + if dc.client != nil && dc.client.DefaultPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultPolicy.copy() + } else { + policy = NewPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicQueryPolicy() *QueryPolicy { + var policy *QueryPolicy + if dc.client != nil && dc.client.DefaultQueryPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultQueryPolicy.copy() + } else { + // If no default query policy is set, create a new one. + policy = NewQueryPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicScanPolicy() *ScanPolicy { + var policy *ScanPolicy + if dc.client != nil && dc.client.DefaultScanPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultScanPolicy.copy() + } else { + // If no default scan policy is set, create a new one. + policy = NewScanPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicBatchWritePolicy() *BatchWritePolicy { + var policy *BatchWritePolicy + if dc.client != nil && dc.client.DefaultBatchWritePolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultBatchWritePolicy.copy() + } else { + // If no default batch write policy is set, create a new one. + policy = NewBatchWritePolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicBatchReadPolicy() *BatchReadPolicy { + var policy *BatchReadPolicy + if dc.client != nil && dc.client.DefaultBatchReadPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultBatchReadPolicy.copy() + } else { + // If no default batch read policy is set, create a new one. + policy = NewBatchReadPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicTxnRollPolicy() *TxnRollPolicy { + var policy *TxnRollPolicy + if dc.client != nil && dc.client.DefaultTxnRollPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultTxnRollPolicy.copy() + } else { + // If no default txn roll policy is set, create a new one. + policy = NewTxnRollPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicTxnVerifyPolicy() *TxnVerifyPolicy { + var policy *TxnVerifyPolicy + if dc.client != nil && dc.client.DefaultTxnVerifyPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultTxnVerifyPolicy.copy() + } else { + // If no default txn verify policy is set, create a new one. + policy = NewTxnVerifyPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicBatchDeletePolicy() *BatchDeletePolicy { + var policy *BatchDeletePolicy + if dc.client != nil && dc.client.DefaultBatchDeletePolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultBatchDeletePolicy.copy() + } else { + // If no default batch delete policy is set, create a new one. + policy = NewBatchDeletePolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicBatchUdfPolicy() *BatchUDFPolicy { + var policy *BatchUDFPolicy + if dc.client != nil && dc.client.DefaultBatchUDFPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = dc.client.DefaultBatchUDFPolicy.copy() + } else { + // If no default batch udf policy is set, create a new one. + policy = NewBatchUDFPolicy() + } + + policy = policy.mapDynamic(dc) + + return policy +} + +func (dc *DynConfig) generateDynamicBatchPolicy() *BatchPolicy { + var policy *BatchPolicy + if dc.client != nil && dc.client.DefaultBatchPolicy != nil { + // Not going to make changes to policy user has set but will create a copy of it + // and apply dynamic configuration to it. The copy of the merged policy will be returned + policy = copyBatchPolicy(dc.client.DefaultBatchPolicy) + } else { + // If no default batch policy is set, create a new one. + policy = NewBatchPolicy() + } + + policy = mapDynamicBatchPolicy(policy, dc) + + return policy +} + +func (dc *DynConfig) generateDynamicMetricsPolicy() *MetricsPolicy { + policy := DefaultMetricsPolicy() + + policy = policy.mapDynamic(dc) + + return policy +} + +// ---------------------------------------------------------------- +// Main watch goroutine for the config provider +// ---------------------------------------------------------------- +func (dc *DynConfig) watchConfig(interval time.Duration) { + logger.Logger.Info("Starting the config watch goroutine...") + + // If the config is not loaded, we will use the default interval. + // If the config is loaded, we will use the interval from the config. + // This allows the config to be updated dynamically without restarting the client. + dc.lock.RLock() + var mergedConfigInterval time.Duration + // Handle the condition where dynamic config is eneabled but config was not loaded becuase + // the file could not be found or the url is not valid. In that case we will use the interval passed + // in or use the default interval of 1 second. + if dc.config == nil { + mergedConfigInterval = interval + } else { + // If the config is already loaded, use the interval from the config. + if dc.config.Static != nil && dc.config.Static.Client != nil && dc.config.Static.Client.ConfigInterval != nil { + mergedConfigInterval = time.Duration(*dc.config.Static.Client.ConfigInterval) * time.Millisecond + } else { + mergedConfigInterval = interval + } + } + dc.lock.RUnlock() + + defer func() { + // TODO: Add exponential backoff here to resource starvation + if r := recover(); r != nil { + logger.Logger.Error("Watch config goroutine crashed: %s", debug.Stack()) + go dc.watchConfig(mergedConfigInterval) + } + }() + defer dc.wgConfig.Done() + + configInterval := max(mergedConfigInterval, 1*time.Second) +Loop: + for { + select { + case <-dc.configWatchChannel: + logger.Logger.Debug("Watch config channel closed. Stopping watch goroutine.") + break Loop + case <-time.After(configInterval): + tm := time.Now() + fmt.Printf("Config interval set to: %s\n", configInterval.String()) + dc.loadConfig() + if configDuration := time.Since(tm); configDuration > interval { + logger.Logger.Warn("Watching took %s.", configDuration) + } + } + } +} + +// getConfigIfNotLoadedOrInitialized is used to get the config if it is not initialized yet. +func (dc *DynConfig) getConfigIfNotLoadedOrInitialized() *dynconfig.Config { + config := dc.config + + if config == nil && !dc.configInitialized.Load() { + // On initial load it is possible that the config is not yet loaded. This will kick things off to make sure + // config is loaded. + dc.loadConfig() + config = dc.config + } + + return config +} + +// ---------------------------------------------------------------- +// Testing functions +// ---------------------------------------------------------------- + +func NewDynConfigForTest(config *dynconfig.Config) *DynConfig { + return &DynConfig{ + config: config, + } +} diff --git a/config/dynconfig.go b/config/dynconfig.go new file mode 100644 index 00000000..e13134a4 --- /dev/null +++ b/config/dynconfig.go @@ -0,0 +1,329 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dynconfig + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +// Package dynconfig provides a configuration provider interface and structures +// for loading and managing dynamic configurations in a system. +// It includes static and dynamic configurations for various components such as +// client, read, write, query, scan, batch operations, transactions, and metrics. +// The configurations are defined using YAML tags for easy serialization and +// deserialization. +type ConfigProvider interface { + LoadConfig(dsn string) *Config +} + +// ---------------------------------------------------------------- +// Structures used to serialize and deserialize the configuration +// ---------------------------------------------------------------- +type Config struct { + Static *StaticConfig `yaml:"static"` + Dynamic *DynamicConfig `yaml:"dynamic"` +} + +type StaticConfig struct { + Client *Client `yaml:"client"` +} + +type DynamicConfig struct { + Client *Client `yaml:"client"` + Read *Read `yaml:"read"` + Write *Write `yaml:"write"` + Query *Query `yaml:"query"` + Scan *Scan `yaml:"scan"` + BatchRead *BatchRead `yaml:"batch_read"` + BatchWrite *BatchWrite `yaml:"batch_write"` + BatchUdf *BatchUdf `yaml:"batch_udf"` + BatchDelete *BatchDelete `yaml:"batch_delete"` + TxnRoll *TxnRoll `yaml:"txn_roll"` + TxnVerify *TxnVerify `yaml:"txn_verify"` + Metrics *Metrics `yaml:"metrics"` +} + +type Client struct { + // static config + ConfigInterval *int `yaml:"config_interval"` + ConnectionQueueSize *int `yaml:"max_connections_per_node"` + MinConnectionsPerNode *int `yaml:"min_connections_per_node"` + + // dynamic config + IdleTimeout *int `yaml:"max_socket_idle"` + Timeout *int `yaml:"timeout"` + ErrorRateWindow *int `yaml:"error_rate_window"` + MaxErrorRate *int `yaml:"max_error_rate"` + LoginTimeout *int `yaml:"login_timeout"` + RackAware *bool `yaml:"rack_aware"` + RackIds *[]int `yaml:"rack_ids"` + TendInterval *int `yaml:"tend_interval"` + UseServiceAlternate *bool `yaml:"use_service_alternate"` +} + +type Read struct { + ReadModeAp *ReadModeAp `yaml:"read_mode_ap"` + ReadModeSc *ReadModeSc `yaml:"read_mode_sc"` + Replica *Replica `yaml:"replica"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` +} + +type Write struct { + Replica *Replica `yaml:"replica"` + SendKey *bool `yaml:"send_key"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` + DurableDelete *bool `yaml:"durable_delete"` +} + +type Query struct { + ReadModeAp *ReadModeAp `yaml:"read_mode_ap"` + ReadModeSc *ReadModeSc `yaml:"read_mode_sc"` + Replica *Replica `yaml:"replica"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` + IncludeBinData *bool `yaml:"include_bin_data"` + RecordQueueSize *int `yaml:"record_queue_size"` + ExpectedDuration *QueryDuration `yaml:"expected_duration"` +} + +type Scan struct { + ReadModeAp *ReadModeAp `yaml:"read_mode_ap"` + ReadModeSc *ReadModeSc `yaml:"read_mode_sc"` + Replica *Replica `yaml:"replica"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TimeoutDelay *int `yaml:"timeout_delay"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` + MaxConcurrentNodes *int `yaml:"max_concurrent_nodes"` +} + +type BatchRead struct { + ReadModeAp *ReadModeAp `yaml:"read_mode_ap"` + ReadModeSc *ReadModeSc `yaml:"read_mode_sc"` + Replica *Replica `yaml:"replica"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` + MaxConcurrentThread *int `yaml:"max_concurrent_thread"` + AllowInline *bool `yaml:"allow_inline"` + AllowInlineSSD *bool `yaml:"allow_inline_ssd"` + RespondAllKeys *bool `yaml:"respond_all_keys"` +} + +type BatchWrite struct { + Replica *Replica `yaml:"replica"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` + DurableDelete *bool `yaml:"durable_delete"` + SendKey *bool `yaml:"send_key"` + MaxConcurrentThread *int `yaml:"max_concurrent_thread"` + AllowInline *bool `yaml:"allow_inline"` + AllowInlineSSD *bool `yaml:"allow_inline_ssd"` + RespondAllKeys *bool `yaml:"respond_all_keys"` +} + +type BatchUdf struct { + DurableDelete *bool `yaml:"durable_delete"` + SendKey *bool `yaml:"send_key"` +} + +type BatchDelete struct { + DurableDelete *bool `yaml:"durable_delete"` + SendKey *bool `yaml:"send_key"` +} + +type TxnRoll struct { + ReadModeAp *ReadModeAp `yaml:"read_mode_ap"` + ReadModeSc *ReadModeSc `yaml:"read_mode_sc"` + Replica *Replica `yaml:"replica"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` + AllowInline *bool `yaml:"allow_inline"` + AllowInlineSSD *bool `yaml:"allow_inline_ssd"` + RespondAllKeys *bool `yaml:"respond_all_keys"` +} + +type TxnVerify struct { + ReadModeAp *ReadModeAp `yaml:"read_mode_ap"` + ReadModeSc *ReadModeSc `yaml:"read_mode_sc"` + Replica *Replica `yaml:"replica"` + SleepBetweenRetries *int `yaml:"sleep_between_retries"` + SocketTimeout *int `yaml:"socket_timeout"` + TotalTimeout *int `yaml:"total_timeout"` + MaxRetries *int `yaml:"max_retries"` + AllowInline *bool `yaml:"allow_inline"` + AllowInlineSSD *bool `yaml:"allow_inline_ssd"` + RespondAllKeys *bool `yaml:"respond_all_keys"` +} + +type Metrics struct { + Enable *bool `yaml:"enable"` + LatencyColumns *int `yaml:"latency_columns"` + LatencyBase *int `yaml:"latency_base"` +} + +// ---------------------------------------------------------------- +// Enum types +// ---------------------------------------------------------------- + +// TODO(Khosrow): Deal with the circular dependencies to remove the redefinition of these types. +// The subtypes can also be their own types instead of map to validate their own input + +type ReadModeAp int + +const ( + ONE ReadModeAp = iota + ALL +) + +var ReadModeApYaml = map[ReadModeAp]string{ + ONE: "ONE", + ALL: "ALL", +} + +type ReadModeSc int + +const ( + SESSION ReadModeSc = iota + LINEARIZE + ALLOW_REPLICA + ALLOW_UNAVAILABLE +) + +var ReadModeScYaml = map[ReadModeSc]string{ + SESSION: "SESSION", + LINEARIZE: "LINEARIZE", + ALLOW_REPLICA: "ALLOW_REPLICA", + ALLOW_UNAVAILABLE: "ALLOW_UNAVAILABLE", +} + +type Replica int + +const ( + MASTER Replica = iota + MASTER_PROLES + SEQUENCE + PREFER_RACK +) + +var ReplicaYaml = map[Replica]string{ + MASTER: "MASTER", + MASTER_PROLES: "MASTER_PROLES", + SEQUENCE: "SEQUENCE", + PREFER_RACK: "PREFER_RACK", +} + +type QueryDuration int + +const ( + LONG QueryDuration = iota + SHORT + LONG_RELAX_AP +) + +// ---------------------------------------------------------------- +// UnmarshalYAML methods for enum types +// ---------------------------------------------------------------- + +func (r *ReadModeAp) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + return err + } + switch strings.ToUpper(s) { + case "ONE": + *r = ONE + case "ALL": + *r = ALL + default: + return fmt.Errorf("invalid ReadModeAp value: %s", s) + } + return nil +} + +func (r *ReadModeSc) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + return err + } + switch strings.ToUpper(s) { + case "SESSION": + *r = SESSION + case "LINEARIZE": + *r = LINEARIZE + case "ALLOW_REPLICA": + *r = ALLOW_REPLICA + case "ALLOW_UNAVAILABLE": + *r = ALLOW_UNAVAILABLE + default: + return fmt.Errorf("invalid ReadModeSc value: %s", s) + } + return nil +} + +func (r *Replica) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + return err + } + switch strings.ToUpper(s) { + case "MASTER": + *r = MASTER + case "MASTER_PROLES": + *r = MASTER_PROLES + case "SEQUENCE": + *r = SEQUENCE + case "PREFER_RACK": + *r = PREFER_RACK + default: + return fmt.Errorf("invalid Replica value: %s", s) + } + return nil +} + +func (r *QueryDuration) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + return err + } + switch strings.ToUpper(s) { + case "LONG": + *r = LONG + case "SHORT": + *r = SHORT + case "LONG_RELAX_AP": + *r = LONG_RELAX_AP + default: + return fmt.Errorf("invalid QueryDuration value: %s", s) + } + return nil +} diff --git a/config/provider/yaml-config-provider.go b/config/provider/yaml-config-provider.go new file mode 100644 index 00000000..2ee9cc5f --- /dev/null +++ b/config/provider/yaml-config-provider.go @@ -0,0 +1,79 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package provider + +import ( + "os" + "strings" + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + registry "github.com/aerospike/aerospike-client-go/v8/config/registry" + "github.com/aerospike/aerospike-client-go/v8/logger" + "gopkg.in/yaml.v3" +) + +const driverName = "file://" + +type YamlConfigProvider struct { + oldModTime time.Time +} + +// Register the YamlConfigProvider with the configuration provider registry +func init() { + registry.Register(driverName, NewYamlConfigProvider()) +} + +func NewYamlConfigProvider() dynconfig.ConfigProvider { + return &YamlConfigProvider{oldModTime: time.Time{}} +} + +func NewYamlConfigProviderWithPath(configFilePath string) dynconfig.ConfigProvider { + return &YamlConfigProvider{ + oldModTime: time.Time{}, + } +} + +// LoadConfig loads the configuration from a YAML file specified by the DSN. +func (yc *YamlConfigProvider) LoadConfig(filePath string) *dynconfig.Config { + // Get the file info + info, err := os.Stat(filePath) + if err != nil { + logger.Logger.Error("File %s could not be found. Error: %v", filePath, err) + return nil + } + + modTime := info.ModTime() + // Compare to previously stored modTime + if modTime.After(yc.oldModTime) { + yc.oldModTime = modTime + data, err := os.ReadFile(filePath) + if err != nil { + logger.Logger.Error("Failed to read file %s. Error: %v", filePath, err) + return nil + } + + var config dynconfig.Config + if err := yaml.Unmarshal(data, &config); err != nil { + logger.Logger.Error("Failed to serialize file %s to object. Error: %s", + filePath, strings.ReplaceAll(err.Error(), "\n", " ")) + return nil + } else { + return &config + } + } + + return nil +} diff --git a/config/registry/registry.go b/config/registry/registry.go new file mode 100644 index 00000000..31c301d8 --- /dev/null +++ b/config/registry/registry.go @@ -0,0 +1,57 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package configregistry + +import ( + "sync" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" +) + +const ( + DSN_REGEX_PATTERN = `^\s*(?P[A-Za-z][A-Za-z0-9+.-]*://)?(?P.*)$` + DEFAULT_SCHEME = "file://" + DSN_SCHEME = "scheme" + DSN_PATH = "path" +) + +var ( + ConfigProvidersMu sync.RWMutex + ConfigProviders = make(map[string]dynconfig.ConfigProvider) +) + +// Register registers a config provider by name. +func Register(driverType string, provider dynconfig.ConfigProvider) { + if provider == nil { + panic("Config provider cannot be nil") + } + + ConfigProvidersMu.Lock() + defer ConfigProvidersMu.Unlock() + + if _, found := ConfigProviders[driverType]; found { + panic("Config provider " + driverType + " is already registered") + } + ConfigProviders[driverType] = provider +} + +// Get retrieves a config provider by name. +func Get(name string) (dynconfig.ConfigProvider, bool) { + ConfigProvidersMu.RLock() + defer ConfigProvidersMu.RUnlock() + provider, ok := ConfigProviders[name] + + return provider, ok +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 00000000..3bb44e4e --- /dev/null +++ b/config_test.go @@ -0,0 +1,229 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "sync/atomic" + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + gg "github.com/onsi/ginkgo/v2" + gm "github.com/onsi/gomega" +) + +// fakeConfigProvider implements the dynconfig.ConfigProvider interface. +type fakeConfigProvider struct { + config *dynconfig.Config +} + +func (f *fakeConfigProvider) LoadConfig(dsn string) *dynconfig.Config { + return f.config +} + +var _ = gg.Describe("DynConfig - initConfig and providerLoadConfig", func() { + var ( + dc *DynConfig + dsn = "dummy" + ) + + gg.Context("initConfig()", func() { + gg.Context("when loadedConfig is nil", func() { + gg.BeforeEach(func() { + fakeProvider := &fakeConfigProvider{config: nil} + + dc = &DynConfig{ + configProvider: fakeProvider, + configInitialized: &atomic.Bool{}, + config: &dynconfig.Config{}, + } + dc.client = &Client{ + dynConfig: dc, + } + dc.initConfig() + dc.updateCachedPolicies() + }) + + gg.It("should update dc.config.Dynamic with Defaults", func() { + defaultDynamic := dc.client.dynDefaultPolicy.Load() + gm.Expect(dc.client.dynDefaultClientPolicy.Load()).ToNot(gm.BeNil()) + gm.Expect(defaultDynamic).ToNot(gm.BeNil()) + + // Making sure config has not been updated if LoadConfig() invoked from configProvider is nil. + // In those case we should load/update default policies into the cache. + gm.Expect(defaultDynamic.TotalTimeout).To(gm.Equal(time.Duration(1000 * time.Millisecond))) + }) + }) + + gg.Context("when loadedConfig is not nil", func() { + var newTimeout int + gg.BeforeEach(func() { + newTimeout = 5000 + // Create a dummy loaded dynamic configuration. + dummyDyn := &dynconfig.DynamicConfig{ + Read: &dynconfig.Read{ + TotalTimeout: func() *int { + d := newTimeout + return &d + }(), + }, + } + dummyCfg := &dynconfig.Config{ + Dynamic: dummyDyn, + } + fakeProvider := &fakeConfigProvider{config: dummyCfg} + + // Initialize dc.config with an old dynamic configuration. + oldDyn := &dynconfig.DynamicConfig{ + Read: &dynconfig.Read{ + TotalTimeout: func() *int { + d := 1 + return &d + }(), + }, + } + + dc = &DynConfig{ + configProvider: fakeProvider, + configInitialized: &atomic.Bool{}, + dsn: dsn, + config: &dynconfig.Config{ + Dynamic: oldDyn, + }, + } + + dc.client = &Client{ + dynConfig: dc, + } + dc.client.dynDefaultPolicy.Store(&BasePolicy{TotalTimeout: 1 * time.Second}) + + // Call initConfig to update dc.config and rehydrate dynamic cache. + dc.initConfig() + dc.updateCachedPolicies() + }) + + gg.It("should clear the cache and update dc.config.Dynamic based on loaded config", func() { + // dc.config.Dynamic should be updated. + gm.Expect(dc.config.Dynamic).ToNot(gm.BeNil()) + readCfg := dc.config.Dynamic.Read + gm.Expect(readCfg).ToNot(gm.BeNil()) + // Ensuring the new value is set in the config as well. + gm.Expect(*readCfg.TotalTimeout).To(gm.Equal(int(newTimeout))) + + policy := dc.client.dynDefaultPolicy.Load() + // At this point the dynamic cache should be updated with the new value. + gm.Expect(policy.TotalTimeout).To(gm.Equal(time.Duration(newTimeout) * time.Millisecond)) + }) + }) + }) + + gg.Context("providerLoadConfig()", func() { + gg.Context("when loadedConfig is nil", func() { + gg.BeforeEach(func() { + // Fake provider that returns nil. + fakeProvider := &fakeConfigProvider{config: nil} + + // Prepopulate previous dynamic configuration. + prevDyn := &dynconfig.DynamicConfig{ + Read: &dynconfig.Read{ + TotalTimeout: func() *int { + d := int(2 * time.Second) + return &d + }(), + }, + } + + dc = &DynConfig{ + configProvider: fakeProvider, + configInitialized: &atomic.Bool{}, + dsn: dsn, + config: &dynconfig.Config{ + Dynamic: prevDyn, + }, + } + dc.client = &Client{ + dynConfig: dc, + } + dc.client.dynDefaultPolicy.Store(&BasePolicy{TotalTimeout: 1 * time.Second}) + + // Call providerLoadConfig which should do nothing as loadedConfig is nil. + dc.providerLoadConfig() + }) + + gg.It("should NOT update dc.config.Dynamic nor the dynamic cache", func() { + gm.Expect(dc.config.Dynamic).ToNot(gm.BeNil()) + expected := int(2 * time.Second) + gm.Expect(*dc.config.Dynamic.Read.TotalTimeout).To(gm.Equal(expected)) + + policy := dc.client.dynDefaultPolicy.Load() + // The cached policy remains with its old value. + gm.Expect(policy.TotalTimeout).To(gm.Equal(1 * time.Second)) + }) + }) + + gg.Context("when loadedConfig is not nil", func() { + var newTimeout time.Duration + gg.BeforeEach(func() { + newTimeout = 5000 * time.Millisecond + // New loaded dynamic configuration. + dummyDyn := &dynconfig.DynamicConfig{ + Read: &dynconfig.Read{ + TotalTimeout: func() *int { + d := int(newTimeout) + return &d + }(), + }, + } + dummyCfg := &dynconfig.Config{ + Dynamic: dummyDyn, + } + fakeProvider := &fakeConfigProvider{config: dummyCfg} + + dc = &DynConfig{ + configProvider: fakeProvider, + configInitialized: &atomic.Bool{}, + dsn: dsn, + // Start with an old dynamic configuration. + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Read: &dynconfig.Read{ + TotalTimeout: func() *int { + d := int(1 * time.Second) + return &d + }(), + }, + }, + }, + } + + dc.client = &Client{ + dynConfig: dc, + } + dc.client.dynDefaultPolicy.Store(&BasePolicy{TotalTimeout: 1 * time.Second}) + + dc.providerLoadConfig() + }) + + gg.It("should update dc.config.Dynamic and rehydrate the dynamic cache", func() { + gm.Expect(dc.config.Dynamic).ToNot(gm.BeNil()) + readCfg := dc.config.Dynamic.Read + gm.Expect(readCfg).ToNot(gm.BeNil()) + gm.Expect(*readCfg.TotalTimeout).To(gm.Equal(int(time.Duration(newTimeout)))) + + policy := dc.client.dynDefaultPolicy.Load() + gm.Expect(policy.TotalTimeout).To(gm.Equal(time.Duration(newTimeout) * time.Millisecond)) + }) + }) + }) +}) diff --git a/dynconfig_serialze_test.go b/dynconfig_serialze_test.go new file mode 100644 index 00000000..5bfb7b1b --- /dev/null +++ b/dynconfig_serialze_test.go @@ -0,0 +1,120 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike_test + +import ( + "fmt" + + "gopkg.in/yaml.v3" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("YAML Unmarshal for Enum Types", func() { + DescribeTable("ReadModeAp", + func(input string, expected dynconfig.ReadModeAp, expectErr bool) { + var wrapper struct { + Val dynconfig.ReadModeAp `yaml:"val"` + } + err := yaml.Unmarshal([]byte(fmt.Sprintf("val: \"%s\"", input)), &wrapper) + if expectErr { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Val).To(Equal(expected)) + } + }, + Entry("ONE", "ONE", dynconfig.ONE, false), + Entry("ALL", "ALL", dynconfig.ALL, false), + Entry("lowercase", "one", dynconfig.ONE, false), + Entry("invalid", "foo", nil, true), + Entry("empty", "", nil, true), + Entry("quoted invalid", `"badval"`, nil, true), + Entry("number", "123", nil, true), + ) + + DescribeTable("ReadModeSc", + func(input string, expected dynconfig.ReadModeSc, expectErr bool) { + var wrapper struct { + Val dynconfig.ReadModeSc `yaml:"val"` + } + err := yaml.Unmarshal([]byte(fmt.Sprintf("val: \"%s\"", input)), &wrapper) + if expectErr { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Val).To(Equal(expected)) + } + }, + Entry("SESSION", "SESSION", dynconfig.SESSION, false), + Entry("LINEARIZE", "LINEARIZE", dynconfig.LINEARIZE, false), + Entry("ALLOW_REPLICA", "ALLOW_REPLICA", dynconfig.ALLOW_REPLICA, false), + Entry("ALLOW_UNAVAILABLE", "ALLOW_UNAVAILABLE", dynconfig.ALLOW_UNAVAILABLE, false), + Entry("lowercase", "session", dynconfig.SESSION, false), + Entry("invalid", "foo", nil, true), + Entry("empty", "", nil, true), + Entry("quoted invalid", `"badval"`, nil, true), + Entry("number", "123", nil, true), + ) + + DescribeTable("Replica", + func(input string, expected dynconfig.Replica, expectErr bool) { + var wrapper struct { + Val dynconfig.Replica `yaml:"val"` + } + err := yaml.Unmarshal([]byte(fmt.Sprintf("val: \"%s\"", input)), &wrapper) + if expectErr { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Val).To(Equal(expected)) + } + }, + Entry("MASTER", "MASTER", dynconfig.MASTER, false), + Entry("MASTER_PROLES", "MASTER_PROLES", dynconfig.MASTER_PROLES, false), + Entry("SEQUENCE", "SEQUENCE", dynconfig.SEQUENCE, false), + Entry("PREFER_RACK", "PREFER_RACK", dynconfig.PREFER_RACK, false), + Entry("lowercase", "master", dynconfig.MASTER, false), + Entry("invalid", "foo", nil, true), + Entry("empty", "", nil, true), + Entry("quoted invalid", `"badval"`, nil, true), + Entry("number", "123", nil, true), + ) + + DescribeTable("QueryDuration", + func(input string, expected dynconfig.QueryDuration, expectErr bool) { + var wrapper struct { + Val dynconfig.QueryDuration `yaml:"val"` + } + err := yaml.Unmarshal([]byte(fmt.Sprintf("val: \"%s\"", input)), &wrapper) + if expectErr { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(wrapper.Val).To(Equal(expected)) + } + }, + Entry("LONG", "LONG", dynconfig.LONG, false), + Entry("SHORT", "SHORT", dynconfig.SHORT, false), + Entry("LONG_RELAX_AP", "LONG_RELAX_AP", dynconfig.LONG_RELAX_AP, false), + Entry("lowercase", "long", dynconfig.LONG, false), + Entry("invalid", "foo", nil, true), + Entry("empty", "", nil, true), + Entry("quoted invalid", `"badval"`, nil, true), + Entry("number", "123", nil, true), + ) +}) diff --git a/go.mod b/go.mod index 1fec62d6..cc0e37c5 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,24 @@ go 1.23.0 require ( github.com/onsi/ginkgo/v2 v2.22.2 github.com/onsi/gomega v1.36.2 + github.com/stretchr/testify v1.10.0 github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad github.com/yuin/gopher-lua v1.1.1 golang.org/x/sync v0.12.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/stretchr/testify v1.10.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/tools v0.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/logger/logger.go b/logger/logger.go index 028fe5e6..bb66aa34 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -57,6 +57,10 @@ func newLogger() *logger { } } +func (lgr *logger) IsLogLevelEnabled(level LogPriority) bool { + return lgr.level == level +} + // SetLogger sets the *log.Logger object where log messages should be sent to. // This method is not goroutine-safe, and is not designed to be accessed // from multiple goroutines. diff --git a/metric_policy_config_test.go b/metric_policy_config_test.go new file mode 100644 index 00000000..6137ae91 --- /dev/null +++ b/metric_policy_config_test.go @@ -0,0 +1,88 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToMetricsPolicy", func() { + + Context("when applying full configuration", func() { + It("updates the policy values based on the dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Metrics: &dynconfig.Metrics{ + Enable: func() *bool { r := true; return &r }(), + LatencyBase: func() *int { r := 3; return &r }(), + LatencyColumns: func() *int { r := 3; return &r }(), + }, + }, + }, + } + + // Create an initial TxnVerifyPolicy. + policy := DefaultMetricsPolicy() + + // Check defaults. + Expect(policy).NotTo(BeNil()) + Expect(int(policy.LatencyBase)).To(Equal(int(2))) + Expect(int(policy.LatencyColumns)).To(Equal(int(24))) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate the applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(int(updatedPolicy.LatencyBase)).To(Equal(int(3))) + Expect(int(updatedPolicy.LatencyColumns)).To(Equal(int(3))) + }) + }) + + Context("when applying configuration with select fields", func() { + It("updates only the specified fields and leaves others unchanged", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Metrics: &dynconfig.Metrics{ + Enable: func() *bool { r := true; return &r }(), + LatencyColumns: func() *int { r := 3; return &r }(), + }, + }, + }, + } + + // Create an initial TxnVerifyPolicy. + policy := DefaultMetricsPolicy() + + // Check defaults. + Expect(policy).NotTo(BeNil()) + Expect(int(policy.LatencyBase)).To(Equal(int(2))) + Expect(int(policy.LatencyColumns)).To(Equal(int(24))) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate the applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(int(updatedPolicy.LatencyColumns)).To(Equal(int(3))) + }) + }) +}) diff --git a/metrics_policy.go b/metrics_policy.go index a55f20cf..dfffb1fe 100644 --- a/metrics_policy.go +++ b/metrics_policy.go @@ -15,6 +15,7 @@ package aerospike import ( + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" "github.com/aerospike/aerospike-client-go/v8/types/histogram" ) @@ -59,3 +60,63 @@ func DefaultMetricsPolicy() *MetricsPolicy { LatencyBase: 2, } } + +// copyMetricsPolicy creates a new BasePolicy instance and copies the values from the source policy. +func (mp *MetricsPolicy) copy() *MetricsPolicy { + if mp == nil { + return nil + } + + response := *mp + return &response +} + +// metricsSyncCallBack is a callback function that is called when the dynamic configuration changes. +// Changes will only be made if the there is a discrepancy between the current configuration and the new configuration. +func metricsSyncCallBack(config *dynconfig.Config, client *Client) { + // Metrics are not enabled but configuration is set to enable metrics + if client != nil && !client.MetricsEnabled() && config != nil && config.Dynamic != nil && config.Dynamic.Metrics != nil && *config.Dynamic.Metrics.Enable { + client.cluster.EnableMetrics(client.dynDefaultMetricsPolicy.Load()) + } else if client != nil && client.MetricsEnabled() && config != nil && config.Dynamic != nil && config.Dynamic.Metrics != nil && !*config.Dynamic.Metrics.Enable { + // Metrics are enabled but configuration is set to disable metrics + client.cluster.DisableMetrics() + } +} + +// patchDynamic implements the configuration logic without locking. +func (mp *MetricsPolicy) patchDynamic(dynConfig *DynConfig) *MetricsPolicy { + if dynConfig == nil { + return mp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if config != nil && config.Dynamic != nil && config.Dynamic.Metrics != nil { + if mp != nil { + // Copy the existing policy to preserve custom settings. + return mp.copy().mapDynamic(dynConfig) + } else { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultMetricsPolicy.Load() + } + } else { + return mp + } +} + +func (mp *MetricsPolicy) mapDynamic(dynConfig *DynConfig) *MetricsPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return mp + } + + if dynConfig.config.Dynamic.Metrics != nil { + if dynConfig.config.Dynamic.Metrics.LatencyColumns != nil { + mp.LatencyColumns = *dynConfig.config.Dynamic.Metrics.LatencyColumns + } + if dynConfig.config.Dynamic.Metrics.LatencyBase != nil { + mp.LatencyBase = *dynConfig.config.Dynamic.Metrics.LatencyBase + } + } + + return mp +} diff --git a/node.go b/node.go index 8cf9bdea..69592df1 100644 --- a/node.go +++ b/node.go @@ -74,6 +74,7 @@ type Node struct { // NewNode initializes a server node with connection parameters. func newNode(cluster *Cluster, nv *nodeValidator) *Node { + clusterClientPolicy := *cluster.clientPolicy.Get() newNode := &Node{ cluster: cluster, name: nv.name, @@ -85,7 +86,7 @@ func newNode(cluster *Cluster, nv *nodeValidator) *Node { // Assign host to first IP alias because the server identifies nodes // by IP address (not hostname). - connections: *newConnectionHeap(cluster.clientPolicy.MinConnectionsPerNode, cluster.clientPolicy.ConnectionQueueSize), + connections: *newConnectionHeap(clusterClientPolicy.MinConnectionsPerNode, clusterClientPolicy.ConnectionQueueSize), connectionCount: *iatomic.NewInt(0), peersGeneration: *iatomic.NewInt(-1), partitionGeneration: *iatomic.NewInt(-2), @@ -140,7 +141,7 @@ func (nd *Node) Refresh(peers *peers) Error { var infoMap map[string]string commands := []string{"node", "peers-generation", "partition-generation"} - if nd.cluster.clientPolicy.RackAware { + if nd.cluster.clientPolicy.Get().RackAware { commands = append(commands, "rack-ids") } @@ -194,7 +195,8 @@ func (nd *Node) Refresh(peers *peers) Error { // refreshSessionToken refreshes the session token if it has been expired func (nd *Node) refreshSessionToken() (err Error) { // no session token to refresh - if !nd.cluster.clientPolicy.RequiresAuthentication() { + clusterClientPolicy := *nd.cluster.clientPolicy.Get() + if !clusterClientPolicy.RequiresAuthentication() { return nil } @@ -202,13 +204,13 @@ func (nd *Node) refreshSessionToken() (err Error) { // Consider when the next tend will be in this calculation. If the next tend will be too late, // refresh the sessionInfo now. - if st.expiration.IsZero() || time.Now().Before(st.expiration.Add(-nd.cluster.clientPolicy.TendInterval)) { + if st.expiration.IsZero() || time.Now().Before(st.expiration.Add(-clusterClientPolicy.TendInterval)) { return nil } - nd.usingTendConn(nd.cluster.clientPolicy.LoginTimeout, func(conn *Connection) { + nd.usingTendConn(clusterClientPolicy.LoginTimeout, func(conn *Connection) { command := newLoginCommand(conn.dataBuffer) - if err = command.login(&nd.cluster.clientPolicy, conn, nd.cluster.Password()); err != nil { + if err = command.login(&clusterClientPolicy, conn, nd.cluster.Password()); err != nil { // force new connections to use default creds until a new valid session token is acquired nd.resetSessionInfo() // Socket not authenticated. Do not put back into pool. @@ -222,7 +224,7 @@ func (nd *Node) refreshSessionToken() (err Error) { } func (nd *Node) updateRackInfo(infoMap map[string]string) Error { - if !nd.cluster.clientPolicy.RackAware { + if !nd.cluster.clientPolicy.Get().RackAware { return nil } @@ -354,7 +356,7 @@ func (nd *Node) refreshFailed(e Error) { nd.peersGeneration.Set(-1) nd.partitionGeneration.Set(-1) - if nd.cluster.clientPolicy.RackAware { + if nd.cluster.clientPolicy.Get().RackAware { nd.rebalanceGeneration.Set(-1) } @@ -372,7 +374,7 @@ func (nd *Node) refreshFailed(e Error) { // a fresh connection or exhaust the queue. func (nd *Node) dropIdleConnections() { if nd.cluster != nil { - nd.connections.DropIdle(nd.cluster.clientPolicy.TendInterval) + nd.connections.DropIdle(nd.cluster.clientPolicy.Get().TendInterval) } } @@ -421,19 +423,19 @@ func (nd *Node) newConnectionAllowed() Error { if !nd.active.Get() { return ErrServerNotAvailable.err() } - + clusterClientPolicy := *nd.cluster.clientPolicy.Get() // if connection count is limited and enough connections are already created, don't create a new one cc := nd.connectionCount.IncrementAndGet() defer nd.connectionCount.DecrementAndGet() - if nd.cluster.clientPolicy.LimitConnectionsToQueueSize && cc > nd.cluster.clientPolicy.ConnectionQueueSize { + if clusterClientPolicy.LimitConnectionsToQueueSize && cc > clusterClientPolicy.ConnectionQueueSize { return ErrTooManyConnectionsForNode.err() } // Check for opening connection threshold - if nd.cluster.clientPolicy.OpeningConnectionThreshold > 0 { + if clusterClientPolicy.OpeningConnectionThreshold > 0 { ct := nd.cluster.connectionThreshold.IncrementAndGet() defer nd.cluster.connectionThreshold.DecrementAndGet() - if ct > nd.cluster.clientPolicy.OpeningConnectionThreshold { + if ct > clusterClientPolicy.OpeningConnectionThreshold { return ErrTooManyOpeningConnections.err() } } @@ -447,9 +449,10 @@ func (nd *Node) newConnection(overrideThreshold bool) (*Connection, Error) { return nil, ErrServerNotAvailable.err() } + clusterClientPolicy := *nd.cluster.clientPolicy.Get() // if connection count is limited and enough connections are already created, don't create a new one cc := nd.connectionCount.IncrementAndGet() - if nd.cluster.clientPolicy.LimitConnectionsToQueueSize && cc > nd.cluster.clientPolicy.ConnectionQueueSize { + if clusterClientPolicy.LimitConnectionsToQueueSize && cc > clusterClientPolicy.ConnectionQueueSize { nd.connectionCount.DecrementAndGet() nd.stats.ConnectionsPoolEmpty.IncrementAndGet() @@ -457,9 +460,9 @@ func (nd *Node) newConnection(overrideThreshold bool) (*Connection, Error) { } // Check for opening connection threshold - if !overrideThreshold && nd.cluster.clientPolicy.OpeningConnectionThreshold > 0 { + if !overrideThreshold && clusterClientPolicy.OpeningConnectionThreshold > 0 { ct := nd.cluster.connectionThreshold.IncrementAndGet() - if ct > nd.cluster.clientPolicy.OpeningConnectionThreshold { + if ct > clusterClientPolicy.OpeningConnectionThreshold { nd.cluster.connectionThreshold.DecrementAndGet() nd.connectionCount.DecrementAndGet() @@ -470,7 +473,7 @@ func (nd *Node) newConnection(overrideThreshold bool) (*Connection, Error) { } nd.stats.ConnectionsAttempts.IncrementAndGet() - conn, err := NewConnection(&nd.cluster.clientPolicy, nd.host) + conn, err := NewConnection(&clusterClientPolicy, nd.host) if err != nil { nd.incrErrorCount() nd.connectionCount.DecrementAndGet() @@ -481,7 +484,7 @@ func (nd *Node) newConnection(overrideThreshold bool) (*Connection, Error) { sessionInfo := nd.sessionInfo.Get() // need to authenticate - if err = conn.login(&nd.cluster.clientPolicy, nd.cluster.Password(), sessionInfo); err != nil { + if err = conn.login(&clusterClientPolicy, nd.cluster.Password(), sessionInfo); err != nil { // increment node errors if authentication hit a network error if networkError(err) { nd.incrErrorCount() @@ -494,7 +497,7 @@ func (nd *Node) newConnection(overrideThreshold bool) (*Connection, Error) { } nd.stats.ConnectionsSuccessful.IncrementAndGet() - conn.setIdleTimeout(nd.cluster.clientPolicy.IdleTimeout) + conn.setIdleTimeout(clusterClientPolicy.IdleTimeout) return conn, nil } @@ -884,8 +887,9 @@ func (nd *Node) WarmUp(count int) (int, Error) { // fillMinCounts will fill the connection pool to the minimum required // by the ClientPolicy.MinConnectionsPerNode func (nd *Node) fillMinConns() (int, Error) { - if nd.cluster.clientPolicy.MinConnectionsPerNode > 0 { - toFill := nd.cluster.clientPolicy.MinConnectionsPerNode - nd.connectionCount.Get() + clusterClientPolicy := *nd.cluster.clientPolicy.Get() + if clusterClientPolicy.MinConnectionsPerNode > 0 { + toFill := clusterClientPolicy.MinConnectionsPerNode - nd.connectionCount.Get() if toFill > 0 { return nd.WarmUp(toFill) } @@ -896,7 +900,7 @@ func (nd *Node) fillMinConns() (int, Error) { // Increments error count for the node. If errorCount goes above the threshold, // the node will not accept any more requests until the next window. func (nd *Node) incrErrorCount() { - if nd.cluster.clientPolicy.MaxErrorRate > 0 { + if nd.cluster.clientPolicy.Get().MaxErrorRate > 0 { nd.errorCount.GetAndIncrement() } } @@ -908,7 +912,8 @@ func (nd *Node) resetErrorCount() { // checks if the errorCount is within set limits func (nd *Node) errorCountWithinLimit() bool { - return nd.cluster.clientPolicy.MaxErrorRate <= 0 || nd.errorCount.Get() <= nd.cluster.clientPolicy.MaxErrorRate + clusterClientPolicy := *nd.cluster.clientPolicy.Get() + return clusterClientPolicy.MaxErrorRate <= 0 || nd.errorCount.Get() <= clusterClientPolicy.MaxErrorRate } // returns error if errorCount has gone above the threshold set in the policy diff --git a/node_validator.go b/node_validator.go index 0bf27f6f..e3a1e1ff 100644 --- a/node_validator.go +++ b/node_validator.go @@ -75,7 +75,7 @@ func (ndv *nodeValidator) seedNodes(cluster *Cluster, host *Host, nodesToAdd nod } func (ndv *nodeValidator) validateNode(cluster *Cluster, host *Host) Error { - if clusterNodes := cluster.GetNodes(); cluster.clientPolicy.IgnoreOtherSubnetAliases && len(clusterNodes) > 0 { + if clusterNodes := cluster.GetNodes(); cluster.clientPolicy.Get().IgnoreOtherSubnetAliases && len(clusterNodes) > 0 { masterHostname := clusterNodes[0].host.Name ip, ipnet, err := net.ParseCIDR(masterHostname + "/24") if err != nil { @@ -143,7 +143,7 @@ func (ndv *nodeValidator) setAliases(host *Host) Error { } func (ndv *nodeValidator) validateAlias(cluster *Cluster, alias *Host) Error { - clientPolicy := cluster.clientPolicy + clientPolicy := *cluster.clientPolicy.Get() clientPolicy.Timeout /= 2 conn, err := NewConnection(&clientPolicy, alias) diff --git a/partition.go b/partition.go index 1121dc7c..57dfd06f 100644 --- a/partition.go +++ b/partition.go @@ -250,7 +250,7 @@ func (ptn *Partition) getRackNode(cluster *Cluster) (*Node, Error) { replicas := ptn.partitions.Replicas // Try to find a node on the same rack first: - for _, rackId := range cluster.clientPolicy.RackIds { + for _, rackId := range cluster.clientPolicy.Get().RackIds { seq := ptn.sequence for range replicas { index := seq % len(replicas) diff --git a/peers_parser.go b/peers_parser.go index 3a644dc7..3836e9aa 100644 --- a/peers_parser.go +++ b/peers_parser.go @@ -26,7 +26,7 @@ import ( var aeroerr = newError(types.PARSE_ERROR, "Error parsing peers list.") func parsePeers(cluster *Cluster, node *Node) (*peerListParser, Error) { - cmd := cluster.clientPolicy.peersString() + cmd := cluster.clientPolicy.Get().peersString() info, err := node.RequestInfo(&cluster.infoPolicy, cmd) if err != nil { diff --git a/policy.go b/policy.go index 455bd2bf..fd1982eb 100644 --- a/policy.go +++ b/policy.go @@ -188,6 +188,14 @@ func NewPolicy() *BasePolicy { } } +func NewDynamicPolicy(dynConfig *DynConfig) *BasePolicy { + if dynConfig == nil { + return NewPolicy() + } + + return dynConfig.client.dynDefaultPolicy.Load() +} + var _ Policy = &BasePolicy{} // GetBasePolicy returns embedded BasePolicy in all types that embed this struct. @@ -205,3 +213,65 @@ func (p *BasePolicy) deadline() time.Time { func (p *BasePolicy) compress() bool { return p.UseCompression } + +// copyBasePolicy creates a new BasePolicy instance and copies the values from the source BasePolicy. +func (bp *BasePolicy) copy() *BasePolicy { + if bp == nil { + return nil + } + + response := *bp + return &response +} + +// patchDynamic applies the dynamic configuration and generates a new policy +func (bp *BasePolicy) patchDynamic(dynConfig *DynConfig) *BasePolicy { + if dynConfig == nil { + return bp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if bp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultPolicy.Load() + } else if config != nil && config.Dynamic != nil && config.Dynamic.Read != nil { + // Dynamic configuration exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + return bp.copy().mapDynamic(dynConfig) + } else { + return bp + } +} + +func (bp *BasePolicy) mapDynamic(dynConfig *DynConfig) *BasePolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return bp + } + + if dynConfig.config.Dynamic.Read != nil { + if dynConfig.config.Dynamic.Read.ReadModeAp != nil { + bp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.Read.ReadModeAp) + } + if dynConfig.config.Dynamic.Read.ReadModeSc != nil { + bp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.Read.ReadModeSc) + } + if dynConfig.config.Dynamic.Read.TotalTimeout != nil { + bp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.Read.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Read.SocketTimeout != nil { + bp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.Read.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Read.MaxRetries != nil { + bp.MaxRetries = *dynConfig.config.Dynamic.Read.MaxRetries + } + if dynConfig.config.Dynamic.Read.SleepBetweenRetries != nil { + bp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.Read.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.Read.Replica != nil { + bp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.Read.Replica) + } + } + + return bp +} diff --git a/policy_config_test.go b/policy_config_test.go new file mode 100644 index 00000000..d75f2b8b --- /dev/null +++ b/policy_config_test.go @@ -0,0 +1,152 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToBasePolicy", func() { + + Context("when applying full configuration", func() { + It("should update all policy values based on the dynamic config", func() { + // Create a dummy configuration + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Read: &dynconfig.Read{ + ReadModeAp: func() *dynconfig.ReadModeAp { + d := dynconfig.ALL + return &d + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + d := dynconfig.LINEARIZE + return &d + }(), + TotalTimeout: func() *int { + d := 5 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + MaxRetries: func() *int { + d := 3 + return &d + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + Replica: func() *dynconfig.Replica { + d := dynconfig.PREFER_RACK + return &d + }(), + }, + }, + }, + } + + // Create an initial base policy. + policy := NewPolicy() + + // Verify defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.TotalTimeout).To(Equal(1_000 * time.Millisecond)) + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.MaxRetries).To(Equal(2)) + Expect(policy.SendKey).To(BeFalse()) + Expect(policy.ReplicaPolicy).To(Equal(SEQUENCE)) + Expect(policy.UseCompression).To(BeFalse()) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate the applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.ReadModeAP).To(Equal(ReadModeAPAll)) + Expect(updatedPolicy.ReadModeSC).To(Equal(ReadModeSCLinearize)) + Expect(updatedPolicy.TotalTimeout).To(Equal(5 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(3)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.UseCompression).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(PREFER_RACK)) + }) + }) + + Context("when applying configuration with select fields", func() { + It("should update only the specified configuration fields and leave the rest unchanged", func() { + // Create a dummy configuration with only a subset of fields. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Read: &dynconfig.Read{ + SocketTimeout: func() *int { + d := 3 + return &d + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + Replica: func() *dynconfig.Replica { + d := dynconfig.PREFER_RACK + return &d + }(), + }, + }, + }, + } + + // Create an initial base policy. + policy := NewPolicy() + + // Verify defaults. + Expect(mapReadModeAPToReadModeAP(dynconfig.ONE)).To(Equal(ReadModeAPOne)) + Expect(mapReadModeSCToReadModeSC(dynconfig.LINEARIZE)).To(Equal(ReadModeSCLinearize)) + Expect(policy.TotalTimeout).To(Equal(1_000 * time.Millisecond)) + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.MaxRetries).To(Equal(2)) + Expect(policy.SendKey).To(BeFalse()) + Expect(policy.ReplicaPolicy).To(Equal(SEQUENCE)) + Expect(policy.UseCompression).To(BeFalse()) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate that the selected fields were updated. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.TotalTimeout).To(Equal(1_000 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + // MaxRetries should remain at default since it wasn't set in the config. + Expect(updatedPolicy.MaxRetries).To(Equal(2)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.UseCompression).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(PREFER_RACK)) + }) + }) +}) diff --git a/query_duration.go b/query_duration.go index 0b4dc8a6..250e1d7a 100644 --- a/query_duration.go +++ b/query_duration.go @@ -17,6 +17,12 @@ package aerospike +import ( + "fmt" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" +) + // QueryDuration defines the expected query duration. The server treats the query in different ways depending on the expected duration. // This enum is ignored for aggregation queries, background queries and server versions < 6.0. type QueryDuration int @@ -46,3 +52,16 @@ const ( // This value is treated exactly like LONG for server versions < 7.1. LONG_RELAX_AP ) + +func mapQueryDuration(expectedDuration dynconfig.QueryDuration) QueryDuration { + switch expectedDuration { + case dynconfig.SHORT: + return SHORT + case dynconfig.LONG: + return LONG + case dynconfig.LONG_RELAX_AP: + return LONG_RELAX_AP + default: + panic(fmt.Sprintf("Unknown QueryDuration: %v", expectedDuration)) + } +} diff --git a/query_policy.go b/query_policy.go index 4309bd7b..91e88f71 100644 --- a/query_policy.go +++ b/query_policy.go @@ -14,6 +14,10 @@ package aerospike +import ( + "time" +) + // QueryPolicy encapsulates parameters for policy attributes used in query operations. // // Inherited Policy fields Policy.Txn are ignored in query commands. @@ -56,3 +60,80 @@ func NewQueryPolicy() *QueryPolicy { MultiPolicy: *NewMultiPolicy(), } } + +func NewDynamicQueryPolicy(dynConfig *DynConfig) *QueryPolicy { + if dynConfig == nil { + return NewQueryPolicy() + } + + return dynConfig.client.dynDefaultQueryPolicy.Load() +} + +// copyQueryPolicy creates a new BasePolicy instance and copies the values from the source BasePolicy. +func (qp *QueryPolicy) copy() *QueryPolicy { + if qp == nil { + return nil + } + + response := *qp + return &response +} + +// applyConfigToQueryPolicy applies the dynamic configuration and generates a new policy. +func (qp *QueryPolicy) pathDynamic(dynConfig *DynConfig) *QueryPolicy { + if dynConfig == nil { + return qp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if qp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultQueryPolicy.Load() + + } else if config != nil && config.Dynamic != nil && config.Dynamic.Query != nil { + // Dynamic configuration is exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + return qp.copy().mapDynamic(dynConfig) + } else { + return qp + } +} + +func (qp *QueryPolicy) mapDynamic(dynConfig *DynConfig) *QueryPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return qp + } + + if dynConfig.config.Dynamic.Query != nil { + if dynConfig.config.Dynamic.Query.ReadModeAp != nil { + qp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.Query.ReadModeAp) + } + if dynConfig.config.Dynamic.Query.ReadModeSc != nil { + qp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.Query.ReadModeSc) + } + if dynConfig.config.Dynamic.Query.TotalTimeout != nil { + qp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.Query.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Query.SocketTimeout != nil { + qp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.Query.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Query.MaxRetries != nil { + qp.MaxRetries = *dynConfig.config.Dynamic.Query.MaxRetries + } + if dynConfig.config.Dynamic.Query.SleepBetweenRetries != nil { + qp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.Query.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.Query.Replica != nil { + qp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.Query.Replica) + } + if dynConfig.config.Dynamic.Query.IncludeBinData != nil { + qp.IncludeBinData = *dynConfig.config.Dynamic.Query.IncludeBinData + } + if dynConfig.config.Dynamic.Query.ExpectedDuration != nil { + qp.ExpectedDuration = mapQueryDuration(*dynConfig.config.Dynamic.Query.ExpectedDuration) + } + } + + return qp +} diff --git a/query_policy_config_test.go b/query_policy_config_test.go new file mode 100644 index 00000000..2855fd21 --- /dev/null +++ b/query_policy_config_test.go @@ -0,0 +1,179 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToQueryPolicy", func() { + + Context("when applying full configuration", func() { + It("should update all policy values based on the dynamic config", func() { + // Create a dummy configuration in dynconfig. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Query: &dynconfig.Query{ + ReadModeAp: func() *dynconfig.ReadModeAp { + d := dynconfig.ALL + return &d + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + d := dynconfig.LINEARIZE + return &d + }(), + TotalTimeout: func() *int { + d := 3000 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + MaxRetries: func() *int { + d := 3 + return &d + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + Replica: func() *dynconfig.Replica { + d := dynconfig.PREFER_RACK + return &d + }(), + IncludeBinData: func() *bool { + d := false + return &d + }(), + RecordQueueSize: func() *int { + d := 50 + return &d + }(), + ExpectedDuration: func() *dynconfig.QueryDuration { + d := dynconfig.SHORT + return &d + }(), + }, + }, + }, + } + + // Create an initial QueryPolicy. + policy := NewQueryPolicy() + + // Check defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.TotalTimeout).To(Equal(0 * time.Millisecond)) + // SocketTimeout is in seconds. + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.SendKey).To(BeFalse()) + Expect(policy.ReplicaPolicy).To(Equal(SEQUENCE)) + Expect(policy.UseCompression).To(BeFalse()) + // ExpectedDuration check for default value. + // (Assuming default ExpectedDuration conversion to int yields LONG.) + Expect(int(policy.ExpectedDuration)).To(Equal(LONG)) + + // Apply the configuration. + updatedPolicy := policy.pathDynamic(config) + + // Validate the applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.ReadModeAP).To(Equal(ReadModeAPAll)) + Expect(updatedPolicy.ReadModeSC).To(Equal(ReadModeSCLinearize)) + Expect(updatedPolicy.TotalTimeout).To(Equal(3000 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + // Note: Some tests change MaxRetries; full config changes it to 3. + Expect(updatedPolicy.MaxRetries).To(Equal(3)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.UseCompression).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(PREFER_RACK)) + Expect(updatedPolicy.IncludeBinData).To(BeFalse()) + Expect(int(updatedPolicy.ExpectedDuration)).To(Equal(SHORT)) + }) + }) + + Context("when applying configuration with select fields", func() { + It("should update only the specified configuration fields and leave the remainder unchanged", func() { + // Create a dummy configuration in dynconfig with only a subset of fields. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Query: &dynconfig.Query{ + TotalTimeout: func() *int { + d := 5 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.PREFER_RACK + return &r + }(), + }, + }, + }, + } + + // Create an initial QueryPolicy. + policy := NewQueryPolicy() + + // Check defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.TotalTimeout).To(Equal(0 * time.Second)) + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.SendKey).To(BeFalse()) + Expect(policy.ReplicaPolicy).To(Equal(SEQUENCE)) + Expect(policy.UseCompression).To(BeFalse()) + Expect(int(policy.ExpectedDuration)).To(Equal(LONG)) + + // Apply the configuration. + updatedPolicy := policy.pathDynamic(config) + + // Validate that the specified fields were updated. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.TotalTimeout).To(Equal(5 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + // MaxRetries should remain unchanged (default = 5) since it was not set. + Expect(updatedPolicy.MaxRetries).To(Equal(5)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.UseCompression).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(PREFER_RACK)) + Expect(policy.UseCompression).To(BeFalse()) + Expect(int(policy.ExpectedDuration)).To(Equal(LONG)) + }) + }) +}) diff --git a/read_mode_ap.go b/read_mode_ap.go index ca5af369..a5ac3fdf 100644 --- a/read_mode_ap.go +++ b/read_mode_ap.go @@ -17,6 +17,12 @@ package aerospike +import ( + "fmt" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" +) + // ReadModeAP is the read policy in AP (availability) mode namespaces. // It indicates how duplicates should be consulted in a read operation. // Only makes a difference during migrations and only applicable in AP mode. @@ -30,3 +36,14 @@ const ( // the read operation. ReadModeAPAll ) + +func mapReadModeAPToReadModeAP(readModeAP dynconfig.ReadModeAp) ReadModeAP { + switch readModeAP { + case dynconfig.ONE: + return ReadModeAPOne + case dynconfig.ALL: + return ReadModeAPAll + default: + panic(fmt.Sprintf("Unknown ReadModeAP value: %v", readModeAP)) + } +} diff --git a/read_mode_sc.go b/read_mode_sc.go index 38ea6c6e..79bf0122 100644 --- a/read_mode_sc.go +++ b/read_mode_sc.go @@ -17,6 +17,12 @@ package aerospike +import ( + "strconv" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" +) + // ReadModeSC is the read policy in SC (strong consistency) mode namespaces. // Determines SC read consistency options. type ReadModeSC int @@ -38,3 +44,18 @@ const ( // partitions. Increasing sequence of record versions is not guaranteed. ReadModeSCAllowUnavailable ) + +func mapReadModeSCToReadModeSC(readModeSC dynconfig.ReadModeSc) ReadModeSC { + switch readModeSC { + case dynconfig.SESSION: + return ReadModeSCSession + case dynconfig.LINEARIZE: + return ReadModeSCLinearize + case dynconfig.ALLOW_REPLICA: + return ReadModeSCAllowReplica + case dynconfig.ALLOW_UNAVAILABLE: + return ReadModeSCAllowUnavailable + default: + panic("unknown ReadModeC value: " + strconv.Itoa(int(readModeSC))) + } +} diff --git a/replica_policy.go b/replica_policy.go index 766d844c..007fa7f6 100644 --- a/replica_policy.go +++ b/replica_policy.go @@ -17,6 +17,12 @@ package aerospike +import ( + "fmt" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" +) + // ReplicaPolicy defines type of node partition targeted by read commands. type ReplicaPolicy int @@ -46,3 +52,18 @@ const ( // in order to function properly. PREFER_RACK ) + +func mapReplicaToReplicaPolicy(replica dynconfig.Replica) ReplicaPolicy { + switch replica { + case dynconfig.MASTER: + return MASTER + case dynconfig.MASTER_PROLES: + return MASTER_PROLES + case dynconfig.SEQUENCE: + return SEQUENCE + case dynconfig.PREFER_RACK: + return PREFER_RACK + default: + panic(fmt.Sprintf("Unknown ReplicaPolicy: %v", replica)) + } +} diff --git a/scan_policy.go b/scan_policy.go index cbb1f267..2d81974e 100644 --- a/scan_policy.go +++ b/scan_policy.go @@ -14,6 +14,10 @@ package aerospike +import ( + "time" +) + // ScanPolicy encapsulates parameters used in scan operations. // // Inherited Policy fields Policy.Txn are ignored in scan commands. @@ -42,3 +46,82 @@ func NewScanPolicy() *ScanPolicy { MultiPolicy: mp, } } + +func NewDynamicScanPolicy(dynConfig *DynConfig) *ScanPolicy { + if dynConfig == nil { + return NewScanPolicy() + } + + return dynConfig.client.dynDefaultScanPolicy.Load() +} + +// copyQueryPolicy creates a new BasePolicy instance and copies the values from the source BasePolicy. +func (sp *ScanPolicy) copy() *ScanPolicy { + if sp == nil { + return nil + } + + response := *sp + return &response +} + +// applyConfigToQueryPolicy applies the dynamic configuration and generates a new policy. +func (sp *ScanPolicy) patchDynamic(dynConfig *DynConfig) *ScanPolicy { + if dynConfig == nil { + return sp + } + + config := dynConfig.config + + if config == nil && !dynConfig.configInitialized.Load() { + // On initial load it is possible that the config is not yet loaded. This will kick things off to make sure + // config is loaded. + dynConfig.loadConfig() + config = dynConfig.config + } + + if sp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultScanPolicy.Load() + } else if config != nil && config.Dynamic != nil && config.Dynamic.Scan != nil { + // Dynamic configuration is exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + return sp.copy().mapDynamic(dynConfig) + } else { + return sp + } +} + +func (sp *ScanPolicy) mapDynamic(dynConfig *DynConfig) *ScanPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return sp + } + + if dynConfig.config.Dynamic.Scan != nil { + if dynConfig.config.Dynamic.Scan.ReadModeAp != nil { + sp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.Scan.ReadModeAp) + } + if dynConfig.config.Dynamic.Scan.ReadModeSc != nil { + sp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.Scan.ReadModeSc) + } + if dynConfig.config.Dynamic.Scan.TotalTimeout != nil { + sp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.Scan.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Scan.SocketTimeout != nil { + sp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.Scan.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Scan.MaxRetries != nil { + sp.MaxRetries = *dynConfig.config.Dynamic.Scan.MaxRetries + } + if dynConfig.config.Dynamic.Scan.SleepBetweenRetries != nil { + sp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.Scan.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.Scan.Replica != nil { + sp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.Scan.Replica) + } + if dynConfig.config.Dynamic.Scan.MaxConcurrentNodes != nil { + sp.MaxConcurrentNodes = *dynConfig.config.Dynamic.Scan.MaxConcurrentNodes + } + } + return sp +} diff --git a/scan_policy_config_test.go b/scan_policy_config_test.go new file mode 100644 index 00000000..63306ff8 --- /dev/null +++ b/scan_policy_config_test.go @@ -0,0 +1,169 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToScanPolicy", func() { + + Context("when applying full configuration", func() { + It("should update all policy values based on the dynamic config", func() { + // Create the full configuration for scan policies. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Scan: &dynconfig.Scan{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ONE + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.SESSION + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.PREFER_RACK + return &r + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 5000 + return &r + }(), + MaxRetries: func() *int { r := 3; return &r }(), + MaxConcurrentNodes: func() *int { r := 5; return &r }(), + }, + }, + }, + } + + // Create an initial ScanPolicy. + policy := NewScanPolicy() + + // Validate default values. + Expect(policy).NotTo(BeNil()) + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.TotalTimeout).To(Equal(0 * time.Second)) + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.SleepMultiplier).To(Equal(1.0)) + Expect(policy.IncludeBinData).To(BeTrue()) + Expect(policy.SendKey).To(BeFalse()) + Expect(policy.UseCompression).To(BeFalse()) + Expect(policy.MaxConcurrentNodes).To(Equal(0)) + Expect(policy.RecordQueueSize).To(Equal(50)) + Expect(policy.RecordsPerSecond).To(Equal(0)) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(updatedPolicy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(updatedPolicy.TotalTimeout).To(Equal(5000 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(3)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + Expect(updatedPolicy.SleepMultiplier).To(Equal(1.0)) + Expect(updatedPolicy.IncludeBinData).To(BeTrue()) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.UseCompression).To(BeFalse()) + Expect(updatedPolicy.MaxConcurrentNodes).To(Equal(5)) + Expect(updatedPolicy.RecordQueueSize).To(Equal(50)) + Expect(updatedPolicy.RecordsPerSecond).To(Equal(0)) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(PREFER_RACK)) + }) + }) + + Context("when applying configuration with select fields", func() { + It("should update only the specified configuration fields and leave the rest unchanged", func() { + // Create a configuration with only a subset of scan fields. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Scan: &dynconfig.Scan{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ALL + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.ALLOW_UNAVAILABLE + return &r + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + TotalTimeout: func() *int { + r := 5000 + return &r + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + MaxRetries: func() *int { r := 3; return &r }(), + MaxConcurrentNodes: func() *int { r := 5; return &r }(), + }, + }, + }, + } + + // Create an initial ScanPolicy. + policy := NewScanPolicy() + + // Validate default values. + Expect(policy).NotTo(BeNil()) + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.TotalTimeout).To(Equal(0 * time.Second)) + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.SleepMultiplier).To(Equal(1.0)) + Expect(policy.IncludeBinData).To(BeTrue()) + Expect(policy.SendKey).To(BeFalse()) + Expect(policy.UseCompression).To(BeFalse()) + Expect(policy.MaxConcurrentNodes).To(Equal(0)) + Expect(policy.RecordQueueSize).To(Equal(50)) + Expect(policy.RecordsPerSecond).To(Equal(0)) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.TotalTimeout).To(Equal(5000 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(3)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + // Even if only select fields are configured, SendKey gets overridden. + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(SEQUENCE)) + }) + }) +}) diff --git a/txn_roll_policy.go b/txn_roll_policy.go index 70a4529d..a6ef293b 100644 --- a/txn_roll_policy.go +++ b/txn_roll_policy.go @@ -14,7 +14,9 @@ package aerospike -import "time" +import ( + "time" +) // Transaction policy fields used to batch roll forward/backward records on // commit or abort. Used a placeholder for now as there are no additional fields beyond BatchPolicy. @@ -35,3 +37,79 @@ func NewTxnRollPolicy() *TxnRollPolicy { BatchPolicy: mp, } } + +func NewDynamicTxnRollPolicy(dynConfig *DynConfig) *TxnRollPolicy { + if dynConfig == nil { + return NewTxnRollPolicy() + } + + return dynConfig.client.dynDefaultTxnRollPolicy.Load() +} + +func (trp *TxnRollPolicy) copy() *TxnRollPolicy { + if trp == nil { + return nil + } + + response := *trp + return &response +} + +// patchDynamic applies the dynamic configuration and generates a new policy. +func (trp *TxnRollPolicy) patchDynamic(dynConfig *DynConfig) *TxnRollPolicy { + if dynConfig == nil { + return trp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if trp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultTxnRollPolicy.Load() + } else if config != nil && config.Dynamic != nil && config.Dynamic.TxnRoll != nil { + // Dynamic configuration is exists for policy in question. + var responseTxnRollPolicy *TxnRollPolicy + // User has provided a custom policy. We need to apply the dynamic configuration. + responseTxnRollPolicy = trp.copy() + responseTxnRollPolicy = responseTxnRollPolicy.mapDynamic(dynConfig) + + return responseTxnRollPolicy + } else { + return trp + } +} + +func (trp *TxnRollPolicy) mapDynamic(dynConfig *DynConfig) *TxnRollPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return trp + } + + if dynConfig.config.Dynamic.TxnRoll != nil { + if dynConfig.config.Dynamic.TxnRoll.ReadModeAp != nil { + trp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.TxnRoll.ReadModeAp) + } + if dynConfig.config.Dynamic.TxnRoll.ReadModeSc != nil { + trp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.TxnRoll.ReadModeSc) + } + if dynConfig.config.Dynamic.TxnRoll.Replica != nil { + trp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.TxnRoll.Replica) + } + if dynConfig.config.Dynamic.TxnRoll.SleepBetweenRetries != nil { + trp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.TxnRoll.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.TxnRoll.SocketTimeout != nil { + trp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.TxnRoll.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.TxnRoll.TotalTimeout != nil { + trp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.TxnRoll.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.TxnRoll.MaxRetries != nil { + trp.MaxRetries = *dynConfig.config.Dynamic.TxnRoll.MaxRetries + } + if dynConfig.config.Dynamic.TxnRoll.RespondAllKeys != nil { + trp.RespondAllKeys = *dynConfig.config.Dynamic.TxnRoll.RespondAllKeys + } + } + + return trp +} diff --git a/txn_roll_policy_config_test.go b/txn_roll_policy_config_test.go new file mode 100644 index 00000000..9e7645ec --- /dev/null +++ b/txn_roll_policy_config_test.go @@ -0,0 +1,184 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToTxnRollPolicy", func() { + Context("when applying full configuration", func() { + It("should update the policy values based on the dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + TxnRoll: &dynconfig.TxnRoll{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ALL + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.LINEARIZE + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER_PROLES + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 15 + return &r + }(), + MaxRetries: func() *int { + r := 5 + return &r + }(), + AllowInline: func() *bool { + r := true + return &r + }(), + AllowInlineSSD: func() *bool { + r := true + return &r + }(), + RespondAllKeys: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial TxnRollPolicy. + policy := NewTxnRollPolicy() + + // Validate defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.ReplicaPolicy).To(Equal(MASTER)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Second)) + Expect(policy.SocketTimeout).To(Equal(3 * time.Second)) + Expect(policy.TotalTimeout).To(Equal(10 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.AllowInline).To(BeTrue()) + Expect(policy.RespondAllKeys).To(BeTrue()) + + updatedPolicy := policy.patchDynamic(config) + + // Validate applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.ReadModeAP).To(Equal(ReadModeAPAll)) + Expect(updatedPolicy.ReadModeSC).To(Equal(ReadModeSCLinearize)) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(MASTER_PROLES)) + Expect(updatedPolicy.TotalTimeout).To(Equal(15 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(5)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + }) + }) + + Context("when applying configuration with select fields", func() { + It("should update only the specified configuration fields and leave the rest unchanged", func() { + // Create a configuration with all fields as in full config. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + TxnRoll: &dynconfig.TxnRoll{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ALL + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.ALLOW_UNAVAILABLE + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER_PROLES + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + MaxRetries: func() *int { + r := 5 + return &r + }(), + AllowInline: func() *bool { + r := true + return &r + }(), + AllowInlineSSD: func() *bool { + r := true + return &r + }(), + RespondAllKeys: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial TxnRollPolicy. + policy := NewTxnRollPolicy() + + // Validate defaults. + Expect(policy.ReadModeAP).To(Equal(ReadModeAPOne)) + Expect(policy.ReadModeSC).To(Equal(ReadModeSCSession)) + Expect(policy.ReplicaPolicy).To(Equal(MASTER)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Second)) + Expect(policy.SocketTimeout).To(Equal(3 * time.Second)) + Expect(policy.TotalTimeout).To(Equal(10 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.AllowInline).To(BeTrue()) + Expect(policy.RespondAllKeys).To(BeTrue()) + + // Apply configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.ReadModeSC).To(Equal(ReadModeSCAllowUnavailable)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(updatedPolicy.TotalTimeout).To(Equal(10 * time.Second)) + Expect(updatedPolicy.MaxRetries).To(Equal(5)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(MASTER_PROLES)) + }) + }) +}) diff --git a/txn_verify_policy.go b/txn_verify_policy.go index 5b6159c5..3211787e 100644 --- a/txn_verify_policy.go +++ b/txn_verify_policy.go @@ -14,7 +14,9 @@ package aerospike -import "time" +import ( + "time" +) // Transaction policy fields used to batch verify record versions on commit. // Used a placeholder for now as there are no additional fields beyond BatchPolicy. @@ -36,3 +38,79 @@ func NewTxnVerifyPolicy() *TxnVerifyPolicy { BatchPolicy: mp, } } + +func NewDynamicTxnVerifyPolicy(dynConfig *DynConfig) *TxnVerifyPolicy { + if dynConfig == nil { + return NewTxnVerifyPolicy() + } + + return dynConfig.client.dynDefaultTxnVerifyPolicy.Load() +} + +func (tvp *TxnVerifyPolicy) copy() *TxnVerifyPolicy { + if tvp == nil { + return nil + } + + response := *tvp + return &response +} + +// applyConfigToTxnRollPolicy applies the dynamic configuration and generates a new policy. +func (tvp *TxnVerifyPolicy) patchDynamic(dynConfig *DynConfig) *TxnVerifyPolicy { + if dynConfig == nil { + return tvp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + if tvp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultTxnVerifyPolicy.Load() + } else if config != nil && config.Dynamic != nil && config.Dynamic.TxnVerify != nil { + // Dynamic configuration is exists for policy in question. + var responsePolicy *TxnVerifyPolicy + // User has provided a custom policy. We need to apply the dynamic configuration. + responsePolicy = tvp.copy() + responsePolicy = responsePolicy.mapDynamic(dynConfig) + + return responsePolicy + } else { + return tvp + } +} + +func (tvp *TxnVerifyPolicy) mapDynamic(dynConfig *DynConfig) *TxnVerifyPolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return tvp + } + + if dynConfig.config.Dynamic.TxnVerify != nil { + if dynConfig.config.Dynamic.TxnVerify.ReadModeAp != nil { + tvp.ReadModeAP = mapReadModeAPToReadModeAP(*dynConfig.config.Dynamic.TxnVerify.ReadModeAp) + } + if dynConfig.config.Dynamic.TxnVerify.ReadModeSc != nil { + tvp.ReadModeSC = mapReadModeSCToReadModeSC(*dynConfig.config.Dynamic.TxnVerify.ReadModeSc) + } + if dynConfig.config.Dynamic.TxnVerify.TotalTimeout != nil { + tvp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.TxnVerify.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.TxnVerify.SocketTimeout != nil { + tvp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.TxnVerify.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.TxnVerify.MaxRetries != nil { + tvp.MaxRetries = *dynConfig.config.Dynamic.TxnVerify.MaxRetries + } + if dynConfig.config.Dynamic.TxnVerify.SleepBetweenRetries != nil { + tvp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.TxnVerify.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.TxnVerify.Replica != nil { + tvp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.TxnVerify.Replica) + } + if dynConfig.config.Dynamic.TxnVerify.MaxRetries != nil { + tvp.MaxRetries = *dynConfig.config.Dynamic.TxnVerify.MaxRetries + } + } + + return tvp +} diff --git a/txn_verify_policy_config_test.go b/txn_verify_policy_config_test.go new file mode 100644 index 00000000..1f0ac7ba --- /dev/null +++ b/txn_verify_policy_config_test.go @@ -0,0 +1,164 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ApplyConfigToTxnVerifyPolicy", func() { + + Context("when applying full configuration", func() { + It("updates the policy values based on the dynamic config", func() { + // Create the full configuration. + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + TxnVerify: &dynconfig.TxnVerify{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ALL + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.LINEARIZE + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1 + return &d + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + TotalTimeout: func() *int { + r := 20 + return &r + }(), + MaxRetries: func() *int { r := 5; return &r }(), + AllowInline: func() *bool { r := true; return &r }(), + AllowInlineSSD: func() *bool { r := true; return &r }(), + RespondAllKeys: func() *bool { r := true; return &r }(), + }, + }, + }, + } + + // Create an initial TxnVerifyPolicy. + policy := NewTxnVerifyPolicy() + + // Check defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.TotalTimeout).To(Equal(10 * time.Second)) + Expect(policy.SocketTimeout).To(Equal(3 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Second)) + Expect(policy.SendKey).To(BeFalse()) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate the applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.ReadModeAP).To(Equal(ReadModeAPAll)) + Expect(updatedPolicy.ReadModeSC).To(Equal(ReadModeSCLinearize)) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(MASTER)) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.TotalTimeout).To(Equal(20 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(5)) + Expect(updatedPolicy.AllowInline).To(BeTrue()) + Expect(updatedPolicy.RespondAllKeys).To(BeTrue()) + }) + }) + + Context("when applying configuration with select fields", func() { + It("updates only the specified fields and leaves others unchanged", func() { + // Create a configuration with select fields (omitting some values like MaxRetries). + config := &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + TxnVerify: &dynconfig.TxnVerify{ + ReadModeAp: func() *dynconfig.ReadModeAp { + r := dynconfig.ALL + return &r + }(), + ReadModeSc: func() *dynconfig.ReadModeSc { + r := dynconfig.LINEARIZE + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.MASTER + return &r + }(), + SleepBetweenRetries: func() *int { + d := 1_000 + return &d + }(), + SocketTimeout: func() *int { + d := 3_000 + return &d + }(), + TotalTimeout: func() *int { + r := 20_000 + return &r + }(), + // Intentionally leave out MaxRetries and AllowInline. + AllowInlineSSD: func() *bool { + r := true + return &r + }(), + RespondAllKeys: func() *bool { + r := true + return &r + }(), + }, + }, + }, + } + + // Create an initial TxnVerifyPolicy. + policy := NewTxnVerifyPolicy() + + // Check defaults. + Expect(policy).NotTo(BeNil()) + Expect(policy.TotalTimeout).To(Equal(10 * time.Second)) + Expect(policy.SocketTimeout).To(Equal(3 * time.Second)) + Expect(policy.MaxRetries).To(Equal(5)) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Second)) + Expect(policy.SendKey).To(BeFalse()) + + // Apply the configuration. + updatedPolicy := policy.patchDynamic(config) + + // Validate the applied configuration. + Expect(updatedPolicy).NotTo(BeNil()) + Expect(updatedPolicy.SocketTimeout).To(Equal(3_000 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(5)) // unchanged + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(1_000 * time.Millisecond)) + Expect(updatedPolicy.TotalTimeout).To(Equal(20_000 * time.Millisecond)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(MASTER)) + }) + }) +}) diff --git a/write_policy.go b/write_policy.go index 7eea7481..a8f3b3ed 100644 --- a/write_policy.go +++ b/write_policy.go @@ -16,6 +16,7 @@ package aerospike import ( "math" + "time" ) const ( @@ -103,3 +104,75 @@ func NewWritePolicy(generation, expiration uint32) *WritePolicy { return res } + +func NewDynamicWritePolicy(dynConfig *DynConfig) *WritePolicy { + if dynConfig == nil { + return NewWritePolicy(0, 0) + } + + return dynConfig.client.dynDefaultWritePolicy.Load() +} + +// copyWritePolicy creates a new WritePolicy instance and copies the values from the source WritePolicy. +func (wp *WritePolicy) copy() *WritePolicy { + if wp == nil { + return nil + } + + response := *wp + return &response +} + +// patchDynamic applies the dynamic configuration and generates a new policy +func (wp *WritePolicy) patchDynamic(dynConfig *DynConfig) *WritePolicy { + // If dynamic config is not set, return the policy as is. + if dynConfig == nil { + return wp + } + + config := dynConfig.getConfigIfNotLoadedOrInitialized() + + // If no policy is passed in, we don't need to map. Just returned what is in mapped cache already. + if wp == nil { + // Passed in policy is nil, fetch mapped default policy from cache. + return dynConfig.client.dynDefaultWritePolicy.Load() + } else if config != nil && config.Dynamic != nil && config.Dynamic.Write != nil { + // Dynamic configuration is exists for policy in question. + // User has provided a custom policy. We need to apply the dynamic configuration. + return wp.copy().mapDynamic(dynConfig) + } else { + return wp + } +} + +func (wp *WritePolicy) mapDynamic(dynConfig *DynConfig) *WritePolicy { + if dynConfig.config == nil || dynConfig.config.Dynamic == nil { + return wp + } + + if dynConfig.config.Dynamic.Write != nil { + if dynConfig.config.Dynamic.Write.Replica != nil { + wp.ReplicaPolicy = mapReplicaToReplicaPolicy(*dynConfig.config.Dynamic.Write.Replica) + } + if dynConfig.config.Dynamic.Write.SendKey != nil { + wp.SendKey = *dynConfig.config.Dynamic.Write.SendKey + } + if dynConfig.config.Dynamic.Write.SleepBetweenRetries != nil { + wp.SleepBetweenRetries = time.Duration(*dynConfig.config.Dynamic.Write.SleepBetweenRetries) * time.Millisecond + } + if dynConfig.config.Dynamic.Write.SocketTimeout != nil { + wp.SocketTimeout = time.Duration(*dynConfig.config.Dynamic.Write.SocketTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Write.TotalTimeout != nil { + wp.TotalTimeout = time.Duration(*dynConfig.config.Dynamic.Write.TotalTimeout) * time.Millisecond + } + if dynConfig.config.Dynamic.Write.MaxRetries != nil { + wp.MaxRetries = *dynConfig.config.Dynamic.Write.MaxRetries + } + if dynConfig.config.Dynamic.Write.DurableDelete != nil { + wp.DurableDelete = *dynConfig.config.Dynamic.Write.DurableDelete + } + } + + return wp +} diff --git a/write_policy_config_test.go b/write_policy_config_test.go new file mode 100644 index 00000000..0b323f8e --- /dev/null +++ b/write_policy_config_test.go @@ -0,0 +1,162 @@ +// Copyright 2014-2022 Aerospike, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aerospike + +import ( + "time" + + dynconfig "github.com/aerospike/aerospike-client-go/v8/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("WritePolicy Config", func() { + var ( + config *DynConfig + policy *WritePolicy + ) + + Context("when applying complete write configuration", func() { + BeforeEach(func() { + // Create the full config. + config = &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Write: &dynconfig.Write{ + TotalTimeout: func() *int { + r := 5000 + return &r + }(), + SocketTimeout: func() *int { + d := 3 + return &d + }(), + MaxRetries: func() *int { + r := 3 + return &r + }(), + DurableDelete: func() *bool { + r := true + return &r + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + SendKey: func() *bool { + r := true + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.PREFER_RACK + return &r + }(), + }, + }, + }, + } + + // Create an initial WritePolicy. + policy = NewWritePolicy(0, 0) + }) + + It("should update all fields from the configuration", func() { + // Check default values of initial policy. + Expect(policy).ToNot(BeNil()) + Expect(policy.TotalTimeout).To(Equal(1_000 * time.Millisecond)) + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.MaxRetries).To(Equal(0)) + Expect(policy.DurableDelete).To(BeFalse()) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.SendKey).To(BeFalse()) + + updatedPolicy := policy.patchDynamic(config) + + // Validate the updated policy. + Expect(updatedPolicy).ToNot(BeNil()) + Expect(updatedPolicy.TotalTimeout).To(Equal(5000 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(3)) + Expect(updatedPolicy.DurableDelete).To(BeTrue()) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + Expect(updatedPolicy.SendKey).To(BeTrue()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(PREFER_RACK)) + }) + }) + + Context("when applying configuration with select fields", func() { + BeforeEach(func() { + config = &DynConfig{ + config: &dynconfig.Config{ + Dynamic: &dynconfig.DynamicConfig{ + Write: &dynconfig.Write{ + SocketTimeout: func() *int { + d := 3 + return &d + }(), + MaxRetries: func() *int { + r := 3 + return &r + }(), + DurableDelete: func() *bool { + r := true + return &r + }(), + SleepBetweenRetries: func() *int { + d := 2 + return &d + }(), + SendKey: func() *bool { + r := false + return &r + }(), + Replica: func() *dynconfig.Replica { + r := dynconfig.PREFER_RACK + return &r + }(), + }, + }, + }, + } + + policy = NewWritePolicy(0, 0) + }) + + It("should update only select fields while leaving defaults intact", func() { + // Check default values of initial policy. + Expect(policy).ToNot(BeNil()) + Expect(policy.TotalTimeout).To(Equal(1_000 * time.Millisecond)) + Expect(policy.SocketTimeout).To(Equal(30 * time.Second)) + Expect(policy.MaxRetries).To(Equal(0)) + Expect(policy.DurableDelete).To(BeFalse()) + Expect(policy.SleepBetweenRetries).To(Equal(1 * time.Millisecond)) + Expect(policy.SendKey).To(BeFalse()) + + updatedPolicy := policy.patchDynamic(config) + + // Validate the updated policy. + Expect(updatedPolicy).ToNot(BeNil()) + // TotalTimeout remains unchanged + Expect(updatedPolicy.TotalTimeout).To(Equal(1_000 * time.Millisecond)) + Expect(updatedPolicy.SocketTimeout).To(Equal(3 * time.Millisecond)) + Expect(updatedPolicy.MaxRetries).To(Equal(3)) + Expect(updatedPolicy.DurableDelete).To(BeTrue()) + Expect(updatedPolicy.SleepBetweenRetries).To(Equal(2 * time.Millisecond)) + Expect(updatedPolicy.SendKey).To(BeFalse()) + Expect(updatedPolicy.ReplicaPolicy).To(Equal(PREFER_RACK)) + }) + }) +})