From c7301af28431df4f7c9446c3c149a977f706ba06 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Tue, 16 Apr 2024 14:59:08 +0200 Subject: [PATCH 01/28] [CLIENT-2889] Increase MaxRecvMsgSize to handle big records --- proxy_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy_client.go b/proxy_client.go index 418ed888..f96101c5 100644 --- a/proxy_client.go +++ b/proxy_client.go @@ -260,7 +260,7 @@ func (clnt *ProxyClient) returnGrpcConnToPool(conn *grpc.ClientConn) { func (clnt *ProxyClient) createGrpcConn(noInterceptor bool) (*grpc.ClientConn, Error) { // make a new connection // Implement TLS and auth - dialOptions := []grpc.DialOption{} + dialOptions := []grpc.DialOption{grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(MaxBufferSize)), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(MaxBufferSize))} if clnt.clientPolicy.TlsConfig != nil { dialOptions = append(dialOptions, grpc.WithTransportCredentials(credentials.NewTLS(clnt.clientPolicy.TlsConfig))) } else { From 906d50af07e84eb729c1ea63923ef5dc2b4e76e4 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Tue, 16 Apr 2024 17:28:50 +0200 Subject: [PATCH 02/28] [CLIENT-2890] Support []MapPair return in reflection This fix supports unmarshalling ordered maps into hash maps map[K]V and []MapPair in the structs. --- batch_test.go | 28 ++++++--- client_object_test.go | 57 ++++++++++++++++++ read_command_reflect.go | 124 ++++++++++++++++++++++++---------------- 3 files changed, 151 insertions(+), 58 deletions(-) diff --git a/batch_test.go b/batch_test.go index 3e0cb97a..c13e2ffc 100644 --- a/batch_test.go +++ b/batch_test.go @@ -223,7 +223,7 @@ var _ = gg.Describe("Aerospike", func() { op2 := as.NewBatchDelete(bdPolicy, key1) op3 := as.NewBatchRead(nil, key1, []string{"bin2"}) - brecs := []as.BatchRecordIfc{op1, op2, op3} + brecs := []as.BatchRecordIfc{op1, op3} err := client.BatchOperate(bpolicy, brecs) gm.Expect(err).ToNot(gm.HaveOccurred()) @@ -232,18 +232,30 @@ var _ = gg.Describe("Aerospike", func() { gm.Expect(op1.BatchRec().Record.Bins).To(gm.Equal(as.BinMap{"bin1": nil, "bin2": nil})) gm.Expect(op1.BatchRec().InDoubt).To(gm.BeFalse()) + // There is no guarantee for the order of execution for different commands + gm.Expect(op3.BatchRec().Err).ToNot(gm.HaveOccurred()) + gm.Expect(op3.BatchRec().Record).ToNot(gm.BeNil()) + gm.Expect(op3.BatchRec().Record.Bins).To(gm.Equal(as.BinMap{"bin2": "b"})) + + exists, err := client.Exists(nil, key1) + gm.Expect(err).ToNot(gm.HaveOccurred()) + gm.Expect(exists).To(gm.BeTrue()) + + brecs = []as.BatchRecordIfc{op1, op2} + err = client.BatchOperate(bpolicy, brecs) + gm.Expect(err).ToNot(gm.HaveOccurred()) + + gm.Expect(op1.BatchRec().Err).ToNot(gm.HaveOccurred()) + gm.Expect(op1.BatchRec().ResultCode).To(gm.Equal(types.OK)) + gm.Expect(op1.BatchRec().Record.Bins).To(gm.Equal(as.BinMap{"bin1": nil, "bin2": nil})) + gm.Expect(op1.BatchRec().InDoubt).To(gm.BeFalse()) + gm.Expect(op2.BatchRec().Err).ToNot(gm.HaveOccurred()) gm.Expect(op2.BatchRec().ResultCode).To(gm.Equal(types.OK)) gm.Expect(op2.BatchRec().Record.Bins).To(gm.Equal(as.BinMap{})) gm.Expect(op2.BatchRec().InDoubt).To(gm.BeFalse()) - // There is guarantee for the order of execution for different commands - // gm.Expect(op3.BatchRec().Err).To(gm.HaveOccurred()) - // gm.Expect(op3.BatchRec().ResultCode).To(gm.Equal(types.KEY_NOT_FOUND_ERROR)) - // gm.Expect(op3.BatchRec().Record).To(gm.BeNil()) - // gm.Expect(op3.BatchRec().InDoubt).To(gm.BeFalse()) - - exists, err := client.Exists(nil, key1) + exists, err = client.Exists(nil, key1) gm.Expect(err).ToNot(gm.HaveOccurred()) gm.Expect(exists).To(gm.BeFalse()) } diff --git a/client_object_test.go b/client_object_test.go index 6a0eb8e3..05a0f7be 100644 --- a/client_object_test.go +++ b/client_object_test.go @@ -127,6 +127,7 @@ var _ = gg.Describe("Aerospike", func() { InterfacePP *interface{} ByteArray []byte + ArrByteArray [][]byte Array [3]interface{} SliceString []string SliceFloat64 []float64 @@ -255,6 +256,7 @@ var _ = gg.Describe("Aerospike", func() { InterfacePP *interface{} `as:"interfacepp"` ByteArray []byte `as:"bytearray"` + ArrByteArray [][]byte `as:"arrbytearray"` Array [3]interface{} `as:"array"` SliceString []string `as:"slicestring"` SliceFloat64 []float64 `as:"slicefloat64"` @@ -414,6 +416,7 @@ var _ = gg.Describe("Aerospike", func() { InterfacePP: &iface, ByteArray: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + ArrByteArray: [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}, []byte{7, 8, 9}}, Array: [3]interface{}{1, "string", nil}, SliceString: []string{"string1", "string2", "string3"}, SliceFloat64: []float64{1.1, 2.2, 3.3, 4.4}, @@ -563,6 +566,7 @@ var _ = gg.Describe("Aerospike", func() { InterfacePP: &iface, ByteArray: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + ArrByteArray: [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}, []byte{7, 8, 9}}, Array: [3]interface{}{1, "string", nil}, SliceString: []string{"string1", "string2", "string3"}, SliceFloat64: []float64{1.1, 2.2, 3.3, 4.4}, @@ -1015,6 +1019,59 @@ var _ = gg.Describe("Aerospike", func() { gm.Expect(err).To(gm.HaveOccurred()) }) + gg.It("XXXshould create a valid Sorted CDT Map and then Get Correct Values back in both map and []MapPair fields", func() { + + cdtBinName := "orderedMap" + + items := map[interface{}]interface{}{ + "Charlie": 55, + "Jim": 98, + "John": 76, + "Harry": 82, + } + + // Write values to empty map. + _, err := client.Operate(nil, key, + as.MapPutItemsOp(as.NewMapPolicy(as.MapOrder.KEY_ORDERED, as.MapWriteMode.UPDATE), cdtBinName, items), + ) + + gm.Expect(err).ToNot(gm.HaveOccurred()) + + type testObjectTagged struct { + TTL uint32 `asm:"ttl"` + Gen uint32 `asm:"gen"` + + M map[string]int `as:"orderedMap"` + } + + instance := testObjectTagged{} + err = client.GetObject(nil, key, &instance) + gm.Expect(err).ToNot(gm.HaveOccurred()) + gm.Expect(instance.M).To(gm.Equal(map[string]int{ + "Charlie": 55, + "Jim": 98, + "John": 76, + "Harry": 82, + })) + + type testObjectTaggedSorted struct { + TTL uint32 `asm:"ttl"` + Gen uint32 `asm:"gen"` + + M []as.MapPair `as:"orderedMap"` + } + + instanceSorted := testObjectTaggedSorted{} + err = client.GetObject(nil, key, &instanceSorted) + gm.Expect(err).ToNot(gm.HaveOccurred()) + gm.Expect(instanceSorted.M).To(gm.Equal([]as.MapPair{ + {"Charlie", 55}, + {"Harry", 82}, + {"Jim", 98}, + {"John", 76}, + })) + }) + gg.It("must get all objects with the most complex structure possible", func() { found, err := client.BatchGetObjects(nil, keys, resObjects) gm.Expect(err).ToNot(gm.HaveOccurred()) diff --git a/read_command_reflect.go b/read_command_reflect.go index 1e79d4d6..210c11d1 100644 --- a/read_command_reflect.go +++ b/read_command_reflect.go @@ -136,6 +136,57 @@ func setObjectField(mappings map[string][]int, obj reflect.Value, fieldName stri return setValue(f, value) } +func fillMap(f, newMap, emptyStruct reflect.Value, key, elem, value interface{}, fieldKind reflect.Kind) Error { + var newKey, newVal reflect.Value + fKeyType := f.Type().Key() + if key != nil { + newKey = reflect.ValueOf(key) + } else { + newKey = reflect.Zero(fKeyType) + } + + if newKey.Type() != fKeyType { + if !newKey.CanConvert(fKeyType) { + return newError(types.PARSE_ERROR, fmt.Sprintf("Invalid key `%#v` for %s field", value, fieldKind)) + } + newKey = newKey.Convert(fKeyType) + } + + fElemType := f.Type().Elem() + if elem != nil { + newVal = reflect.ValueOf(elem) + } else { + newVal = reflect.Zero(fElemType) + } + + if newVal.Type() != fElemType { + switch newVal.Kind() { + case reflect.Map, reflect.Slice, reflect.Array: + newVal = reflect.New(fElemType) + if err := setValue(newVal.Elem(), elem); err != nil { + return err + } + newVal = reflect.Indirect(newVal) + default: + if !newVal.CanConvert(fElemType) { + return newError(types.PARSE_ERROR, fmt.Sprintf("Invalid value `%#v` for %s field", value, fieldKind)) + } + newVal = newVal.Convert(fElemType) + } + } + + if newVal.Kind() == reflect.Map && newVal.Len() == 0 && newMap.Type().Elem().Kind() == emptyStruct.Type().Kind() { + if newMap.Type().Elem().NumField() == 0 { + newMap.SetMapIndex(newKey, emptyStruct) + } else { + return newError(types.PARSE_ERROR, "Map value type is struct{}, but data returned from database is a non-empty map[interface{}]interface{}") + } + } else { + newMap.SetMapIndex(newKey, newVal) + } + return nil +} + func setValue(f reflect.Value, value interface{}) Error { // find the name based on tag mapping if f.CanSet() { @@ -332,65 +383,38 @@ func setValue(f reflect.Value, value interface{}) Error { } case reflect.Map: emptyStruct := reflect.ValueOf(struct{}{}) - theMap, ok := value.(map[interface{}]interface{}) - if !ok { - return newError(types.PARSE_ERROR, fmt.Sprintf("Invalid value `%#v` for %s field", value, fieldKind)) - } - if theMap != nil { + if theMap, ok := value.(map[interface{}]interface{}); ok { newMap := reflect.MakeMap(f.Type()) - var newKey, newVal reflect.Value for key, elem := range theMap { - fKeyType := f.Type().Key() - if key != nil { - newKey = reflect.ValueOf(key) - } else { - newKey = reflect.Zero(fKeyType) - } - - if newKey.Type() != fKeyType { - if !newKey.CanConvert(fKeyType) { - return newError(types.PARSE_ERROR, fmt.Sprintf("Invalid key `%#v` for %s field", value, fieldKind)) - } - newKey = newKey.Convert(fKeyType) - } - - fElemType := f.Type().Elem() - if elem != nil { - newVal = reflect.ValueOf(elem) - } else { - newVal = reflect.Zero(fElemType) - } - - if newVal.Type() != fElemType { - switch newVal.Kind() { - case reflect.Map, reflect.Slice, reflect.Array: - newVal = reflect.New(fElemType) - if err := setValue(newVal.Elem(), elem); err != nil { - return err - } - newVal = reflect.Indirect(newVal) - default: - if !newVal.CanConvert(fElemType) { - return newError(types.PARSE_ERROR, fmt.Sprintf("Invalid value `%#v` for %s field", value, fieldKind)) - } - newVal = newVal.Convert(fElemType) - } + if err := fillMap(f, newMap, emptyStruct, key, elem, value, fieldKind); err != nil { + return err } - - if newVal.Kind() == reflect.Map && newVal.Len() == 0 && newMap.Type().Elem().Kind() == emptyStruct.Type().Kind() { - if newMap.Type().Elem().NumField() == 0 { - newMap.SetMapIndex(newKey, emptyStruct) - } else { - return newError(types.PARSE_ERROR, "Map value type is struct{}, but data returned from database is a non-empty map[interface{}]interface{}") - } - } else { - newMap.SetMapIndex(newKey, newVal) + } + f.Set(newMap) + } else if theMap, ok := value.([]MapPair); ok { + newMap := reflect.MakeMap(f.Type()) + for _, mp := range theMap { + key, elem := mp.Key, mp.Value + if err := fillMap(f, newMap, emptyStruct, key, elem, value, fieldKind); err != nil { + return err } } f.Set(newMap) + } else { + return newError(types.PARSE_ERROR, fmt.Sprintf("Invalid value `%#v` for %s field", value, fieldKind)) } case reflect.Struct: + // support MapPair + if f.Type().Name() == "MapPair" { + v, ok := value.(MapPair) + if !ok { + return newError(types.PARSE_ERROR, fmt.Sprintf("Invalid value `%#v` for MapPair %s field", value, fieldKind)) + } + f.Set(reflect.ValueOf(v)) + break + } + // support time.Time if f.Type().PkgPath() == "time" && f.Type().Name() == "Time" { v, ok := value.(int) From 409eecaa01573371e0cdc0d6fde285d72358d332 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Thu, 13 Apr 2023 09:55:23 +0200 Subject: [PATCH 03/28] [CLIENT-2274] Use constant sized connection buffers The client would use a single buffer on the connection and would grow it per demand in case it needed a bigger buffer, but would not shrink it. This helped with avoiding using buffer pools and the associated synchrinization, but resulted in hugew memory use in case there were a few large records in the results, even if they were infrequent. This changeset does two things: 1. Will use a memory pool for large records only. Large records are defined as records bigger than aerospike.DefaultBufferSize. This is a tiered pool with different buffer sizes. The pool uses sync.Pool under the cover, releasing unused buffers back to the runtime. 2. By using bigger aerospike.DefaultBufferSize values, the user can immitate the old behavior, so no memory pool is used. This change should result in much lower memory use by the client. --- buffered_connection.go | 2 +- command.go | 20 ++++-- connection.go | 13 ++++ tools/asinfo/asinfo.go | 7 ++- types/buffer_pool.go | 124 ++++++++++++++++++++++++++------------ types/buffer_pool_test.go | 65 ++++++++++++++++++++ 6 files changed, 182 insertions(+), 49 deletions(-) create mode 100644 types/buffer_pool_test.go diff --git a/buffered_connection.go b/buffered_connection.go index 681bcfe3..944a79d1 100644 --- a/buffered_connection.go +++ b/buffered_connection.go @@ -56,7 +56,7 @@ func (bc *bufferedConn) buf() []byte { func (bc *bufferedConn) shiftContentToHead(length int) { // shift data to the head of the byte slice if length > bc.emptyCap() { - buf := make([]byte, bc.len()+length) + buf := bigBuffPool.Get(bc.len() + length) copy(buf, bc.buf()[bc.head:bc.tail]) bc.conn.dataBuffer = buf diff --git a/command.go b/command.go index 3ef4ee32..878b5309 100644 --- a/command.go +++ b/command.go @@ -120,6 +120,10 @@ const ( _AS_MSG_TYPE_COMPRESSED int64 = 4 ) +var ( + bigBuffPool = types.NewBufferPool() +) + // command interface describes all commands available type command interface { getPolicy(ifc command) Policy @@ -2430,7 +2434,7 @@ func (cmd *baseCommand) sizeBufferSz(size int, willCompress bool) Error { cmd.dataBuffer = cmd.dataBuffer[:size] } else { // not enough space - cmd.dataBuffer = make([]byte, size) + cmd.dataBuffer = bigBuffPool.Get(size) } // The trick here to keep a ref to the buffer, and set the buffer itself @@ -2776,13 +2780,17 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time return errChain.setInDoubt(ifc.isRead(), cmd.commandWasSent) } - // in case it has grown and re-allocated - if len(cmd.dataBufferCompress) > len(cmd.dataBuffer) { - cmd.conn.dataBuffer = cmd.dataBufferCompress - } else { - cmd.conn.dataBuffer = cmd.dataBuffer + // in case it has grown and re-allocated, put it back in the pool + if len(cmd.dataBufferCompress) > DefaultBufferSize && &cmd.dataBufferCompress != &cmd.conn.origDataBuffer { + bigBuffPool.Put(cmd.dataBufferCompress) + } else if len(cmd.dataBuffer) > DefaultBufferSize && &cmd.dataBuffer != &cmd.conn.origDataBuffer { + bigBuffPool.Put(cmd.dataBuffer) } + cmd.dataBuffer = nil + cmd.dataBufferCompress = nil + cmd.conn.dataBuffer = cmd.conn.origDataBuffer + // Put connection back in pool. ifc.putConnection(cmd.conn) diff --git a/connection.go b/connection.go index 42997f40..2c7b00fe 100644 --- a/connection.go +++ b/connection.go @@ -32,6 +32,9 @@ import ( // DefaultBufferSize specifies the initial size of the connection buffer when it is created. // If not big enough (as big as the average record), it will be reallocated to size again // which will be more expensive. +// This value should not be changed after connecting to a database, otherwise there is a chance +// of the original connection buffers ending up in the buffer pool and bveing simultaniously +// used both from the pool and on the original connection. var DefaultBufferSize = 64 * 1024 // 64 KiB // bufPool reuses the data buffers to remove pressure from @@ -60,6 +63,12 @@ type Connection struct { // to avoid having a buffer pool and contention dataBuffer []byte + // This is a reference to the original data buffer. + // After a big buffer is used temporarily, we will use + // this field to reset the dataBuffer field to the original + // smaller buffer. + origDataBuffer []byte + compressed bool inflater io.ReadCloser // inflater may consume more bytes than required. @@ -125,6 +134,8 @@ func newGrpcFakeConnection(payload []byte, callback func() ([]byte, Error)) *Con // an error will be returned func newConnection(address string, timeout time.Duration) (*Connection, Error) { newConn := &Connection{dataBuffer: bufPool.Get().([]byte)} + newConn.origDataBuffer = newConn.dataBuffer + runtime.SetFinalizer(newConn, connectionFinalizer) // don't wait indefinitely @@ -413,6 +424,7 @@ func (ctn *Connection) Close() { } ctn.dataBuffer = nil + ctn.origDataBuffer = nil ctn.node = nil } }) @@ -506,6 +518,7 @@ func (ctn *Connection) refresh() { } ctn.compressed = false ctn.inflater = nil + ctn.dataBuffer = ctn.origDataBuffer } // initInflater sets up the zlib inflater to read compressed data from the connection diff --git a/tools/asinfo/asinfo.go b/tools/asinfo/asinfo.go index 943b1c2b..3288cbcb 100644 --- a/tools/asinfo/asinfo.go +++ b/tools/asinfo/asinfo.go @@ -46,13 +46,14 @@ func main() { *value = strings.Trim(*value, " ") // connect to the host - client, err := as.NewClientWithPolicy(clientPolicy, *host, *port) + conn, err := as.NewConnection(clientPolicy, as.NewHost(*host, *port)) dieIfError(err) - node := client.Cluster().GetNodes()[0] - infoMap, err := node.RequestInfo(as.NewInfoPolicy(), *value) + infoMap, err := conn.RequestInfo(*value) dieIfError(err) + defer conn.Close() + if len(infoMap) == 0 { log.Printf("Query successful, no information for -v \"%s\"\n\n", *value) return diff --git a/types/buffer_pool.go b/types/buffer_pool.go index 622d6eb6..6fd3a979 100644 --- a/types/buffer_pool.go +++ b/types/buffer_pool.go @@ -1,4 +1,4 @@ -// Copyright 2014-2022 Aerospike, Inc. +// Copyright 2014-2023 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,21 +14,14 @@ package types -import "sync" +import ( + "sync" +) -// BufferPool implements a specialized buffer pool. -// Pool size will be limited, and each buffer size will be -// constrained to the init and max buffer sizes. -type BufferPool struct { - pool [][]byte - poolSize int - - pos int64 - - maxBufSize int - initBufSize int +const maxBufSize = 16 * 1024 * 1024 // 2 * 8 MiB, server block size as of v6.3 - mutex sync.Mutex +type BufferPool struct { + pools []sync.Pool } // NewBufferPool creates a new buffer pool. @@ -36,40 +29,93 @@ type BufferPool struct { // If cap(buffer) is larger than maxBufferSize when it is put back in the buffer, // it will be thrown away. This will prevent unwanted memory bloat and // set a deterministic maximum-size for the pool which will not be exceeded. -func NewBufferPool(poolSize, initBufferSize, maxBufferSize int) *BufferPool { - return &BufferPool{ - pool: make([][]byte, poolSize), - pos: -1, - poolSize: poolSize, - maxBufSize: maxBufferSize, - initBufSize: initBufferSize, +func NewBufferPool() *BufferPool { + p := &BufferPool{} + + max := fastlog2(uint64(maxBufSize)) + for i := 1; i <= max; i++ { + blockSize := 1 << i + p.pools = append(p.pools, + sync.Pool{ + New: func() interface{} { + // The Pool's New function should generally only return pointer + // types, since a pointer can be put into the return interface + // value without an allocation: + return make([]byte, blockSize, blockSize) + }, + }) + } + + return p +} + +// powerOf2 returns true if a number is an EXACT power of 2. +func powerOf2(sz int) bool { + return sz > 0 && (sz&(sz-1)) == 0 +} + +// Returns the pool index based on the size of the buffer. +// Will return -1 if the value falls outside of the pool range. +func (bp *BufferPool) poolIndex(sz int) int { + factor := fastlog2(uint64(sz)) + szl := factor - 1 + if !powerOf2(sz) { + szl++ + } + if szl >= 0 && szl < len(bp.pools) { + return szl } + return -1 } -// Get returns a buffer from the pool. If pool is empty, a new buffer of -// size initBufSize will be created and returned. -func (bp *BufferPool) Get() (res []byte) { - bp.mutex.Lock() - if bp.pos >= 0 { - res = bp.pool[bp.pos] - bp.pos-- - } else { - res = make([]byte, bp.initBufSize) +// Get returns a buffer from the pool. If sz is bigger than maxBufferSize, +// a fresh buffer will be created and not taken from the pool. +func (bp *BufferPool) Get(sz int) []byte { + // Short circuit + if sz > maxBufSize { + return make([]byte, sz, sz) } - bp.mutex.Unlock() - return res + if szl := bp.poolIndex(sz); szl >= 0 { + res := bp.pools[szl].Get().([]byte) + origLen := 1 << (szl + 1) + return res[:origLen] // return the slice to its max capacity + } + + // this line will never be reached, but Go would complain if omitted + return make([]byte, sz, sz) } // Put will put the buffer back in the pool, unless cap(buf) is bigger than -// initBufSize, in which case it will be thrown away +// maxBufSize, in which case it will be thrown away func (bp *BufferPool) Put(buf []byte) { - if len(buf) <= bp.maxBufSize { - bp.mutex.Lock() - if bp.pos < int64(bp.poolSize-1) { - bp.pos++ - bp.pool[bp.pos] = buf + sz := cap(buf) + // throw away random non-power of 2 buffer sizes + if powerOf2(sz) { + if szl := bp.poolIndex(sz); szl >= 0 { + bp.pools[szl].Put(buf) + return } - bp.mutex.Unlock() } } + +/////////////////////////////////////////////////////////////////// + +var log2tab64 = [64]int8{ + 0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3, 61, + 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22, 4, 62, + 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21, 56, + 45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5, 63, +} + +// fastlog2 implements the fastlog2 function for uint64 values. +func fastlog2(value uint64) int { + value |= value >> 1 + value |= value >> 2 + value |= value >> 4 + value |= value >> 8 + value |= value >> 16 + value |= value >> 32 + + return int(log2tab64[(value*0x03f6eaf2cd271461)>>58]) +} diff --git a/types/buffer_pool_test.go b/types/buffer_pool_test.go new file mode 100644 index 00000000..9b762cf0 --- /dev/null +++ b/types/buffer_pool_test.go @@ -0,0 +1,65 @@ +// Copyright 2014-2021 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 types + +import ( + "math/rand" + + gg "github.com/onsi/ginkgo" + gm "github.com/onsi/gomega" +) + +var _ = gg.Describe("BufferPool Test", func() { + + gg.Context("Any size Buffer Pool", func() { + var bp *BufferPool + check := func(sz int) { + buf := bp.Get(sz) + + gm.Expect(len(buf)).To(gm.BeNumerically(">=", sz)) + if sz <= maxBufSize { + if powerOf2(sz) { + gm.Expect(len(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))))) + gm.Expect(cap(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))))) + } else { + gm.Expect(len(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))+1))) + gm.Expect(cap(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))+1))) + } + } else { + gm.Expect(len(buf)).To(gm.BeNumerically("==", sz)) + } + bp.Put(buf) + } + + gg.It("should return a buffer with correct size", func() { + bp = NewBufferPool() + + for i := 1; i < 24; i++ { + check(1< Date: Wed, 17 Apr 2024 19:08:36 +0000 Subject: [PATCH 04/28] [CLIENT-2891] Export various batch struct fields --- batch_delete.go | 18 +++++++++--------- batch_udf.go | 50 ++++++++++++++++++++++++------------------------- batch_write.go | 24 ++++++++++++------------ command.go | 20 ++++++++++---------- 4 files changed, 56 insertions(+), 56 deletions(-) diff --git a/batch_delete.go b/batch_delete.go index 4533923d..6886d129 100644 --- a/batch_delete.go +++ b/batch_delete.go @@ -20,8 +20,8 @@ var _ BatchRecordIfc = &BatchDelete{} type BatchDelete struct { BatchRecord - // policy os the optional write policy. - policy *BatchDeletePolicy + // Policy os the optional write Policy. + Policy *BatchDeletePolicy } func (bd *BatchDelete) hasWrite() bool { @@ -36,7 +36,7 @@ func (bd *BatchDelete) key() *Key { func NewBatchDelete(policy *BatchDeletePolicy, key *Key) *BatchDelete { return &BatchDelete{ BatchRecord: *newSimpleBatchRecord(key, true), - policy: policy, + Policy: policy, } } @@ -44,7 +44,7 @@ func NewBatchDelete(policy *BatchDeletePolicy, key *Key) *BatchDelete { func newBatchDelete(policy *BatchDeletePolicy, key *Key) (*BatchDelete, *BatchRecord) { bd := &BatchDelete{ BatchRecord: *newSimpleBatchRecord(key, true), - policy: policy, + Policy: policy, } return bd, &bd.BatchRecord } @@ -62,23 +62,23 @@ func (bd *BatchDelete) equals(obj BatchRecordIfc) bool { return false } - return bd.policy == other.policy + return bd.Policy == other.Policy } // Return wire protocol size. For internal use only. func (bd *BatchDelete) size(parentPolicy *BasePolicy) (int, Error) { size := 2 // gen(2) = 2 - if bd.policy != nil { - if bd.policy.FilterExpression != nil { - if sz, err := bd.policy.FilterExpression.size(); err != nil { + if bd.Policy != nil { + if bd.Policy.FilterExpression != nil { + if sz, err := bd.Policy.FilterExpression.size(); err != nil { return -1, err } else { size += sz + int(_FIELD_HEADER_SIZE) } } - if bd.policy.SendKey || parentPolicy.SendKey { + if bd.Policy.SendKey || parentPolicy.SendKey { if sz, err := bd.Key.userKey.EstimateSize(); err != nil { return -1, err } else { diff --git a/batch_udf.go b/batch_udf.go index 3305e734..6eced59e 100644 --- a/batch_udf.go +++ b/batch_udf.go @@ -20,17 +20,17 @@ var _ BatchRecordIfc = &BatchUDF{} type BatchUDF struct { BatchRecord - // Optional UDF policy. - policy *BatchUDFPolicy + // Policy is the optional UDF Policy. + Policy *BatchUDFPolicy - // Package or lua module name. - packageName string + // PackageName specify the lua module name. + PackageName string - // Lua function name. - functionName string + // FunctionName specify Lua function name. + FunctionName string - // Optional arguments to lua function. - functionArgs []Value + // FunctionArgs specify optional arguments to lua function. + FunctionArgs []Value // Wire protocol bytes for function args. For internal use only. argBytes []byte @@ -40,10 +40,10 @@ type BatchUDF struct { func NewBatchUDF(policy *BatchUDFPolicy, key *Key, packageName, functionName string, functionArgs ...Value) *BatchUDF { return &BatchUDF{ BatchRecord: *newSimpleBatchRecord(key, true), - policy: policy, - packageName: packageName, - functionName: functionName, - functionArgs: functionArgs, + Policy: policy, + PackageName: packageName, + FunctionName: functionName, + FunctionArgs: functionArgs, } } @@ -51,10 +51,10 @@ func NewBatchUDF(policy *BatchUDFPolicy, key *Key, packageName, functionName str func newBatchUDF(policy *BatchUDFPolicy, key *Key, packageName, functionName string, functionArgs ...Value) (*BatchUDF, *BatchRecord) { res := &BatchUDF{ BatchRecord: *newSimpleBatchRecord(key, true), - policy: policy, - packageName: packageName, - functionName: functionName, - functionArgs: functionArgs, + Policy: policy, + PackageName: packageName, + FunctionName: functionName, + FunctionArgs: functionArgs, } return res, &res.BatchRecord } @@ -78,8 +78,8 @@ func (bu *BatchUDF) equals(obj BatchRecordIfc) bool { if other, ok := obj.(*BatchUDF); !ok { return false } else { - return bu.functionName == other.functionName && &bu.functionArgs == &other.functionArgs && - bu.packageName == other.packageName && bu.policy == other.policy + return bu.FunctionName == other.FunctionName && &bu.FunctionArgs == &other.FunctionArgs && + bu.PackageName == other.PackageName && bu.Policy == other.Policy } } @@ -87,16 +87,16 @@ func (bu *BatchUDF) equals(obj BatchRecordIfc) bool { func (bu *BatchUDF) size(parentPolicy *BasePolicy) (int, Error) { size := 2 // gen(2) = 2 - if bu.policy != nil { - if bu.policy.FilterExpression != nil { - sz, err := bu.policy.FilterExpression.size() + if bu.Policy != nil { + if bu.Policy.FilterExpression != nil { + sz, err := bu.Policy.FilterExpression.size() if err != nil { return -1, err } size += sz + int(_FIELD_HEADER_SIZE) } - if bu.policy.SendKey || parentPolicy.SendKey { + if bu.Policy.SendKey || parentPolicy.SendKey { if sz, err := bu.Key.userKey.EstimateSize(); err != nil { return -1, err } else { @@ -111,11 +111,11 @@ func (bu *BatchUDF) size(parentPolicy *BasePolicy) (int, Error) { size += sz + int(_FIELD_HEADER_SIZE) + 1 } - size += len(bu.packageName) + int(_FIELD_HEADER_SIZE) - size += len(bu.functionName) + int(_FIELD_HEADER_SIZE) + size += len(bu.PackageName) + int(_FIELD_HEADER_SIZE) + size += len(bu.FunctionName) + int(_FIELD_HEADER_SIZE) packer := newPacker() - sz, err := packValueArray(packer, bu.functionArgs) + sz, err := packValueArray(packer, bu.FunctionArgs) if err != nil { return -1, err } diff --git a/batch_write.go b/batch_write.go index 8d57572d..cac051f9 100644 --- a/batch_write.go +++ b/batch_write.go @@ -22,11 +22,11 @@ var _ BatchRecordIfc = &BatchWrite{} type BatchWrite struct { BatchRecord - // Optional write policy. - policy *BatchWritePolicy + // Policy is an optional write Policy. + Policy *BatchWritePolicy - // Required operations for this key. - ops []*Operation + // Ops specify required operations for this key. + Ops []*Operation } // NewBatchWrite initializesa policy, batch key and read/write operations. @@ -36,8 +36,8 @@ type BatchWrite struct { func NewBatchWrite(policy *BatchWritePolicy, key *Key, ops ...*Operation) *BatchWrite { return &BatchWrite{ BatchRecord: *newSimpleBatchRecord(key, true), - ops: ops, - policy: policy, + Ops: ops, + Policy: policy, } } @@ -62,23 +62,23 @@ func (bw *BatchWrite) equals(obj BatchRecordIfc) bool { return false } - return &bw.ops == &other.ops && bw.policy == other.policy && (bw.policy == nil || !bw.policy.SendKey) + return &bw.Ops == &other.Ops && bw.Policy == other.Policy && (bw.Policy == nil || !bw.Policy.SendKey) } // Return wire protocol size. For internal use only. func (bw *BatchWrite) size(parentPolicy *BasePolicy) (int, Error) { size := 2 // gen(2) = 2 - if bw.policy != nil { - if bw.policy.FilterExpression != nil { - if sz, err := bw.policy.FilterExpression.size(); err != nil { + if bw.Policy != nil { + if bw.Policy.FilterExpression != nil { + if sz, err := bw.Policy.FilterExpression.size(); err != nil { return -1, err } else { size += sz + int(_FIELD_HEADER_SIZE) } } - if bw.policy.SendKey || parentPolicy.SendKey { + if bw.Policy.SendKey || parentPolicy.SendKey { if sz, err := bw.Key.userKey.EstimateSize(); err != nil { return -1, err } else { @@ -95,7 +95,7 @@ func (bw *BatchWrite) size(parentPolicy *BasePolicy) (int, Error) { hasWrite := false - for _, op := range bw.ops { + for _, op := range bw.Ops { if op.opType.isWrite { hasWrite = true } diff --git a/command.go b/command.go index 3ef4ee32..967d1060 100644 --- a/command.go +++ b/command.go @@ -701,32 +701,32 @@ func (cmd *baseCommand) setBatchOperateIfc(policy *BatchPolicy, records []BatchR case _BRT_BATCH_WRITE: bw := record.(*BatchWrite) - if bw.policy != nil { - attr.setBatchWrite(bw.policy) + if bw.Policy != nil { + attr.setBatchWrite(bw.Policy) } else { attr.setWrite(&policy.BasePolicy) } - attr.adjustWrite(bw.ops) - cmd.writeBatchOperations(key, bw.ops, attr, attr.filterExp) + attr.adjustWrite(bw.Ops) + cmd.writeBatchOperations(key, bw.Ops, attr, attr.filterExp) case _BRT_BATCH_UDF: bu := record.(*BatchUDF) - if bu.policy != nil { - attr.setBatchUDF(bu.policy) + if bu.Policy != nil { + attr.setBatchUDF(bu.Policy) } else { attr.setUDF(&policy.BasePolicy) } cmd.writeBatchWrite(key, attr, attr.filterExp, 3, 0) - cmd.writeFieldString(bu.packageName, UDF_PACKAGE_NAME) - cmd.writeFieldString(bu.functionName, UDF_FUNCTION) + cmd.writeFieldString(bu.PackageName, UDF_PACKAGE_NAME) + cmd.writeFieldString(bu.FunctionName, UDF_FUNCTION) cmd.writeFieldBytes(bu.argBytes, UDF_ARGLIST) case _BRT_BATCH_DELETE: bd := record.(*BatchDelete) - if bd.policy != nil { - attr.setBatchDelete(bd.policy) + if bd.Policy != nil { + attr.setBatchDelete(bd.Policy) } else { attr.setDelete(&policy.BasePolicy) } From 81322a821a369472ee0086b39695f9e3509fa144 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Sun, 21 Apr 2024 22:27:27 +0000 Subject: [PATCH 05/28] Fix typo --- execute_task.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/execute_task.go b/execute_task.go index caab2f6a..15fbb2cd 100644 --- a/execute_task.go +++ b/execute_task.go @@ -35,7 +35,7 @@ type ExecuteTask struct { clnt *ProxyClient // The following map keeps an account of what nodes were ever observed with the job registered on them. - // If the job was ever observed, the task will return true for it is not found anymore (purged from task queue after completetion) + // If the job was ever observed, the task will return true for it is not found anymore (purged from task queue after completion) observed map[string]struct{} } @@ -107,7 +107,7 @@ func (etsk *ExecuteTask) IsDone() (bool, Error) { // it means that it is completed. if !node.SupportsPartitionQuery() { // Task not found. On server prior to v6, this could mean task was already completed or not started yet. - // If the job was not observed before, its completetion is in doubt. + // If the job was not observed before, its completion is in doubt. // Otherwise it means it was completed. if _, existed := etsk.observed[node.GetName()]; !existed && etsk.retries.Get() < 20 { // If the job was not found in some nodes, it may mean that the job was not started yet. From 0dcf8f01282ae908032fc519ce89104ca115f15c Mon Sep 17 00:00:00 2001 From: Swarit Pandey Date: Tue, 23 Apr 2024 07:28:10 +0530 Subject: [PATCH 06/28] chore: deprecate io/ioutil Signed-off-by: Swarit Pandey --- aerospike_suite_test.go | 3 +-- client.go | 4 ++-- examples/tls_secure_connection/tls_secure_connection.go | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/aerospike_suite_test.go b/aerospike_suite_test.go index d9be6a30..497ae1f3 100644 --- a/aerospike_suite_test.go +++ b/aerospike_suite_test.go @@ -21,7 +21,6 @@ import ( "encoding/pem" "flag" "fmt" - "io/ioutil" "log" "math/rand" "os" @@ -380,7 +379,7 @@ func initTLS() *tls.Config { // Read content from file func readFromFile(filePath string) ([]byte, error) { - dataBytes, err := ioutil.ReadFile(filePath) + dataBytes, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("Failed to read from file `%s`: `%v`", filePath, err) } diff --git a/client.go b/client.go index 64b77372..e048e499 100644 --- a/client.go +++ b/client.go @@ -19,7 +19,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "os" "runtime" "strconv" "strings" @@ -786,7 +786,7 @@ func (clnt *Client) ScanNode(apolicy *ScanPolicy, node *Node, namespace string, // If the policy is nil, the default relevant policy will be used. func (clnt *Client) RegisterUDFFromFile(policy *WritePolicy, clientPath string, serverPath string, language Language) (*RegisterTask, Error) { policy = clnt.getUsableWritePolicy(policy) - udfBody, err := ioutil.ReadFile(clientPath) + udfBody, err := os.ReadFile(clientPath) if err != nil { return nil, newCommonError(err) } diff --git a/examples/tls_secure_connection/tls_secure_connection.go b/examples/tls_secure_connection/tls_secure_connection.go index 5b3f1710..4cdbb4c6 100644 --- a/examples/tls_secure_connection/tls_secure_connection.go +++ b/examples/tls_secure_connection/tls_secure_connection.go @@ -21,7 +21,6 @@ import ( "crypto/tls" "crypto/x509" "flag" - "io/ioutil" "log" "os" "path/filepath" @@ -102,7 +101,7 @@ func readCertificates(serverCertDir string, clientCertFile, clientKeyFile string // Adding server certificates to the pool. // These certificates are used to verify the identity of the server nodes to the client. for _, caFile := range serverCerts { - caCert, err := ioutil.ReadFile(caFile) + caCert, err := os.ReadFile(caFile) if err != nil { log.Fatalf("FAILED: Adding server certificate %s to the pool failed: %s", caFile, err) } From 82e04b5aceba490595bcc37bf0cb773b2fc7d3f5 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Tue, 23 Apr 2024 18:22:13 +0000 Subject: [PATCH 07/28] Minor docs cleanup and gofmt -s --- README.md | 27 +++++++++++++++------------ cdt_bitwise_test.go | 16 ++++++++-------- cdt_map_test.go | 2 +- client_object_test.go | 4 ++-- internal/lua/resources/aerospike.go | 1 + internal/lua/resources/stream_ops.go | 1 + pkg/bcrypt/bcrypt.go | 4 ++-- 7 files changed, 30 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 9e4c2810..654b1075 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,20 @@ You can refer to the test files for idiomatic use cases. Please refer to [`CHANGELOG.md`](CHANGELOG.md) for release notes, or if you encounter breaking changes. -- [Usage](#usage) -- [Prerequisites](#Prerequisites) -- [Installation](#Installation) -- [Tweaking Performance](#Performance) -- [Benchmarks](#Benchmarks) -- [API Documentation](#API-Documentation) -- [Google App Engine](#App-Engine) -- [Reflection](#Reflection) -- [Tests](#Tests) -- [Examples](#Examples) - - [Tools](#Tools) +- [Aerospike Go Client v7](#aerospike-go-client-v7) + - [Usage](#usage) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Some Hints:](#some-hints) + - [Performance Tweaking](#performance-tweaking) + - [Tests](#tests) + - [Examples](#examples) + - [Tools](#tools) + - [Benchmarks](#benchmarks) + - [API Documentation](#api-documentation) + - [Google App Engine](#google-app-engine) + - [Reflection, and Object API](#reflection-and-object-api) + - [License](#license) ## Usage @@ -155,7 +158,7 @@ See the [`tools/benchmark/README.md`](tools/benchmark/README.md) for details. ## API Documentation -A simple API documentation is available in the [`docs`](docs/README.md) directory. The latest up-to-date docs can be found in [![Godoc](https://godoc.org/github.com/aerospike/aerospike-client-go?status.svg)](https://pkg.go.dev/github.com/aerospike/aerospike-client-go). +A simple API documentation is available in the [`docs`](docs/README.md) directory. The latest up-to-date docs can be found in [![Godoc](https://godoc.org/github.com/aerospike/aerospike-client-go?status.svg)](https://pkg.go.dev/github.com/aerospike/aerospike-client-go/v7). ## Google App Engine diff --git a/cdt_bitwise_test.go b/cdt_bitwise_test.go index 43d06e72..9c4ac6c0 100644 --- a/cdt_bitwise_test.go +++ b/cdt_bitwise_test.go @@ -561,14 +561,14 @@ var _ = gg.Describe("CDT Bitwise Test", func() { gm.Expect(record).NotTo(gm.BeNil()) expected := [][]byte{ - []byte{0x80}, - []byte{0x80}, - []byte{0x80}, - []byte{0xC1}, - - []byte{0xAA, 0xAA}, - []byte{0x55, 0x54}, - []byte{0x55, 0x54}, + {0x80}, + {0x80}, + {0x80}, + {0xC1}, + + {0xAA, 0xAA}, + {0x55, 0x54}, + {0x55, 0x54}, } // assertRecordFound(key, record) diff --git a/cdt_map_test.go b/cdt_map_test.go index 70761e22..9c61ce06 100644 --- a/cdt_map_test.go +++ b/cdt_map_test.go @@ -178,7 +178,7 @@ var _ = gg.Describe("CDT Map Test", func() { rec, err = client.Get(nil, key) gm.Expect(err).ToNot(gm.HaveOccurred()) - gm.Expect(rec.Bins).To(gm.Equal(as.BinMap{"bin": []as.MapPair{as.MapPair{Key: "mk1", Value: []interface{}{"v1.0", "v1.1"}}, as.MapPair{Key: "mk2", Value: []interface{}{"v2.0", "v2.1"}}}})) + gm.Expect(rec.Bins).To(gm.Equal(as.BinMap{"bin": []as.MapPair{{Key: "mk1", Value: []interface{}{"v1.0", "v1.1"}}, {Key: "mk2", Value: []interface{}{"v2.0", "v2.1"}}}})) }) gg.It("should create a valid CDT Map using MapPutOp", func() { diff --git a/client_object_test.go b/client_object_test.go index 05a0f7be..5e7b74e7 100644 --- a/client_object_test.go +++ b/client_object_test.go @@ -416,7 +416,7 @@ var _ = gg.Describe("Aerospike", func() { InterfacePP: &iface, ByteArray: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, - ArrByteArray: [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}, []byte{7, 8, 9}}, + ArrByteArray: [][]byte{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}, Array: [3]interface{}{1, "string", nil}, SliceString: []string{"string1", "string2", "string3"}, SliceFloat64: []float64{1.1, 2.2, 3.3, 4.4}, @@ -566,7 +566,7 @@ var _ = gg.Describe("Aerospike", func() { InterfacePP: &iface, ByteArray: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, - ArrByteArray: [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6}, []byte{7, 8, 9}}, + ArrByteArray: [][]byte{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}, Array: [3]interface{}{1, "string", nil}, SliceString: []string{"string1", "string2", "string3"}, SliceFloat64: []float64{1.1, 2.2, 3.3, 4.4}, diff --git a/internal/lua/resources/aerospike.go b/internal/lua/resources/aerospike.go index 3b22279c..0440db57 100644 --- a/internal/lua/resources/aerospike.go +++ b/internal/lua/resources/aerospike.go @@ -1,3 +1,4 @@ +//go:build !app_engine // +build !app_engine package luaLib diff --git a/internal/lua/resources/stream_ops.go b/internal/lua/resources/stream_ops.go index a6cfb9df..c7d447a3 100644 --- a/internal/lua/resources/stream_ops.go +++ b/internal/lua/resources/stream_ops.go @@ -1,3 +1,4 @@ +//go:build !app_engine // +build !app_engine package luaLib diff --git a/pkg/bcrypt/bcrypt.go b/pkg/bcrypt/bcrypt.go index fe84921a..f544ecd3 100644 --- a/pkg/bcrypt/bcrypt.go +++ b/pkg/bcrypt/bcrypt.go @@ -27,8 +27,8 @@ var enc = base64.NewEncoding("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv // Helper function to build the bcrypt hash string // payload takes : -// * []byte -> which it base64 encodes it (trims padding "=") and writes it to the buffer -// * string -> which it writes straight to the buffer +// - []byte -> which it base64 encodes it (trims padding "=") and writes it to the buffer +// - string -> which it writes straight to the buffer func build_bcrypt_str(minor byte, rounds uint, payload ...interface{}) []byte { rs := bytes.NewBuffer(make([]byte, 0, 61)) rs.WriteString("$2") From cf681c4a4bd82de80e87e50af6bdb1175dbcdcb3 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Wed, 24 Apr 2024 21:47:40 +0200 Subject: [PATCH 08/28] Resize and adjust connection buffer over time Add a histogram and use the median value in intervals to readjust the connection buffer down or upwards to optimize memory use. --- batch_command_operate.go | 1 - buffered_connection.go | 2 +- command.go | 33 ++-- connection.go | 85 +++++++--- delete_command.go | 1 - execute_command.go | 1 - exists_command.go | 1 - key.go | 4 +- operate_command.go | 1 - proxy_query_partition_command.go | 1 - proxy_scan_command.go | 1 - read_command.go | 1 - read_header_command.go | 1 - server_command.go | 1 - touch_command.go | 1 - types/histogram/bench_histogram_test.go | 83 ++++++++++ types/histogram/histogram.go | 126 +++++++++++++++ types/histogram/histogram_test.go | 150 ++++++++++++++++++ types/histogram/log2hist.go | 124 +++++++++++++++ .../{buffer_pool.go => pool/tiered_buffer.go} | 74 ++++++--- utils/buffer/buffer.go | 24 +-- value.go | 6 +- write_command.go | 1 - 23 files changed, 625 insertions(+), 98 deletions(-) create mode 100644 types/histogram/bench_histogram_test.go create mode 100644 types/histogram/histogram.go create mode 100644 types/histogram/histogram_test.go create mode 100644 types/histogram/log2hist.go rename types/{buffer_pool.go => pool/tiered_buffer.go} (66%) diff --git a/batch_command_operate.go b/batch_command_operate.go index 0708fe25..b05dd342 100644 --- a/batch_command_operate.go +++ b/batch_command_operate.go @@ -234,7 +234,6 @@ func (cmd *batchCommandOperate) generateBatchNodes(cluster *Cluster) ([]*batchNo } func (cmd *batchCommandOperate) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/buffered_connection.go b/buffered_connection.go index 944a79d1..ff4dfd85 100644 --- a/buffered_connection.go +++ b/buffered_connection.go @@ -56,7 +56,7 @@ func (bc *bufferedConn) buf() []byte { func (bc *bufferedConn) shiftContentToHead(length int) { // shift data to the head of the byte slice if length > bc.emptyCap() { - buf := bigBuffPool.Get(bc.len() + length) + buf := buffPool.Get(bc.len() + length) copy(buf, bc.buf()[bc.head:bc.tail]) bc.conn.dataBuffer = buf diff --git a/command.go b/command.go index 878b5309..ee5b959d 100644 --- a/command.go +++ b/command.go @@ -24,6 +24,7 @@ import ( "github.com/aerospike/aerospike-client-go/v7/logger" "github.com/aerospike/aerospike-client-go/v7/types" + "github.com/aerospike/aerospike-client-go/v7/types/pool" ParticleType "github.com/aerospike/aerospike-client-go/v7/types/particle_type" Buffer "github.com/aerospike/aerospike-client-go/v7/utils/buffer" @@ -121,7 +122,7 @@ const ( ) var ( - bigBuffPool = types.NewBufferPool() + buffPool = pool.NewTieredBufferPool(MinBufferSize, PoolCutOffBufferSize) ) // command interface describes all commands available @@ -2401,13 +2402,6 @@ func (cmd *baseCommand) validateHeader(header int64) Error { return nil } -var ( - // MaxBufferSize protects against allocating massive memory blocks - // for buffers. Tweak this number if you are returning a lot of - // LDT elements in your queries. - MaxBufferSize = 1024 * 1024 * 120 // 120 MB -) - const ( msgHeaderPad = 16 zlibHeaderPad = 2 @@ -2426,6 +2420,10 @@ func (cmd *baseCommand) sizeBufferSz(size int, willCompress bool) Error { return newCustomNodeError(cmd.node, types.PARSE_ERROR, fmt.Sprintf("Invalid size for buffer: %d", size)) } + if cmd.conn != nil && cmd.conn.buffHist != nil { + cmd.conn.buffHist.Add(uint64(size)) + } + if size <= len(cmd.dataBuffer) { // don't touch the buffer // this is a noop, here to silence the linters @@ -2434,7 +2432,7 @@ func (cmd *baseCommand) sizeBufferSz(size int, willCompress bool) Error { cmd.dataBuffer = cmd.dataBuffer[:size] } else { // not enough space - cmd.dataBuffer = bigBuffPool.Get(size) + cmd.dataBuffer = buffPool.Get(size) } // The trick here to keep a ref to the buffer, and set the buffer itself @@ -2497,7 +2495,7 @@ func (cmd *baseCommand) compress() Error { // If not possible to reuse it, reallocate a buffer. if compressedSz+msgHeaderPad > len(cmd.dataBufferCompress) { // compression added to the size of the message - buf := make([]byte, compressedSz+msgHeaderPad) + buf := buffPool.Get(compressedSz + msgHeaderPad) if n := copy(buf[msgHeaderPad:], b.Bytes()); n < compressedSz { return newError(types.SERIALIZE_ERROR) } @@ -2544,9 +2542,7 @@ func (cmd *baseCommand) isRead() bool { // This function should only be called from grpc commands. func (cmd *baseCommand) grpcPutBufferBack() { // put the data buffer back in the pool in case it gets used again - if len(cmd.dataBuffer) >= DefaultBufferSize && len(cmd.dataBuffer) <= MaxBufferSize { - bufPool.Put(cmd.dataBuffer) - } + buffPool.Put(cmd.dataBuffer) cmd.dataBuffer = nil } @@ -2780,11 +2776,12 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time return errChain.setInDoubt(ifc.isRead(), cmd.commandWasSent) } - // in case it has grown and re-allocated, put it back in the pool - if len(cmd.dataBufferCompress) > DefaultBufferSize && &cmd.dataBufferCompress != &cmd.conn.origDataBuffer { - bigBuffPool.Put(cmd.dataBufferCompress) - } else if len(cmd.dataBuffer) > DefaultBufferSize && &cmd.dataBuffer != &cmd.conn.origDataBuffer { - bigBuffPool.Put(cmd.dataBuffer) + // in case it has grown and re-allocated, it means + // it was borrowed from the pool, sp put it back. + if &cmd.dataBufferCompress != &cmd.conn.origDataBuffer { + buffPool.Put(cmd.dataBufferCompress) + } else if &cmd.dataBuffer != &cmd.conn.origDataBuffer { + buffPool.Put(cmd.dataBuffer) } cmd.dataBuffer = nil diff --git a/connection.go b/connection.go index 2c7b00fe..fed97fc7 100644 --- a/connection.go +++ b/connection.go @@ -27,25 +27,39 @@ import ( "github.com/aerospike/aerospike-client-go/v7/logger" "github.com/aerospike/aerospike-client-go/v7/types" + "github.com/aerospike/aerospike-client-go/v7/types/histogram" ) -// DefaultBufferSize specifies the initial size of the connection buffer when it is created. -// If not big enough (as big as the average record), it will be reallocated to size again -// which will be more expensive. -// This value should not be changed after connecting to a database, otherwise there is a chance -// of the original connection buffers ending up in the buffer pool and bveing simultaniously -// used both from the pool and on the original connection. -var DefaultBufferSize = 64 * 1024 // 64 KiB - -// bufPool reuses the data buffers to remove pressure from -// the allocator and the GC during connection churns. -var bufPool = sync.Pool{ - New: func() interface{} { - return make([]byte, DefaultBufferSize) - }, -} +const _BUFF_ADJUST_INTERVAL = 5 * time.Second + +var ( + // DefaultBufferSize specifies the initial size of the connection buffer when it is created. + // If not big enough (as big as the average record), it will be reallocated to size again + // which will be more expensive. + DefaultBufferSize = 64 * 1024 // 64 KiB + + // MaxBufferSize protects against allocating massive memory blocks + // for buffers. Tweak this number if you are returning a lot of + // large records in your requests. + MaxBufferSize = 1024 * 1024 * 120 // 120 MiB + + // PoolCutOffBufferSize specifies the largest buffer size that will be pooled. Anything larger will be + // allocated per request and thrown away afterwards to avoid allocating very big buffers. + PoolCutOffBufferSize = 1024 * 1024 // 1MiB + + // MinBufferSize specifies the smallest buffer size that would be allocated for connections. Smaller buffer + // requests will allocate at least this amount of memory. This protects against allocating too many small + // buffers that would require reallocation and putting pressure on the GC. + MinBufferSize = 8 * 1024 // 16 KiB +) // Connection represents a connection with a timeout. +// Connections maintain a buffer to minimize requesting buffers from the pool. +// If a returned record requires a bigger buffer, the connection will borrow a larger +// buffer from the pool and temporarily use it, returning it after the request. +// A histogram keeps track of the sizes of buffers used for the connection, and the median +// value is used to resize the connection buffer on intervals to optimize memory usage and +// minimize GC pressure. type Connection struct { node *Node @@ -60,6 +74,10 @@ type Connection struct { // connection object conn net.Conn + // histogram to adjust the buff size to optimal value over time + buffHist *histogram.Log2 + bufferAdjustDeadline time.Time + // to avoid having a buffer pool and contention dataBuffer []byte @@ -133,7 +151,9 @@ func newGrpcFakeConnection(payload []byte, callback func() ([]byte, Error)) *Con // If the connection is not established in the specified timeout, // an error will be returned func newConnection(address string, timeout time.Duration) (*Connection, Error) { - newConn := &Connection{dataBuffer: bufPool.Get().([]byte)} + newConn := &Connection{dataBuffer: buffPool.Get(DefaultBufferSize)} + newConn.buffHist = histogram.NewLog2(32) + newConn.bufferAdjustDeadline = time.Now().Add(_BUFF_ADJUST_INTERVAL) newConn.origDataBuffer = newConn.dataBuffer runtime.SetFinalizer(newConn, connectionFinalizer) @@ -419,9 +439,7 @@ func (ctn *Connection) Close() { ctn.conn = nil // put the data buffer back in the pool in case it gets used again - if len(ctn.dataBuffer) >= DefaultBufferSize && len(ctn.dataBuffer) <= MaxBufferSize { - bufPool.Put(ctn.dataBuffer) - } + buffPool.Put(ctn.dataBuffer) ctn.dataBuffer = nil ctn.origDataBuffer = nil @@ -510,15 +528,42 @@ func (ctn *Connection) isIdle() bool { return ctn.idleTimeout > 0 && time.Now().After(ctn.idleDeadline) } +func selectWithinRange[T int | uint | int64 | uint64](min, val, max T) T { + if val < min { + return min + } else if val > max { + return max + } + return val +} + // refresh extends the idle deadline of the connection. func (ctn *Connection) refresh() { - ctn.idleDeadline = time.Now().Add(ctn.idleTimeout) + now := time.Now() + ctn.idleDeadline = now.Add(ctn.idleTimeout) if ctn.inflater != nil { ctn.inflater.Close() } ctn.compressed = false ctn.inflater = nil ctn.dataBuffer = ctn.origDataBuffer + + // adjust buffer size + if now.After(ctn.bufferAdjustDeadline) { + ctn.bufferAdjustDeadline = now.Add(_BUFF_ADJUST_INTERVAL) + newBuffSize := selectWithinRange(MinBufferSize, int(ctn.buffHist.Median()), PoolCutOffBufferSize) + ctn.buffHist.Reset() + // Do not go lower than 1K and larger than max allowed buffer size + if newBuffSize != len(ctn.dataBuffer) { + ctn.origDataBuffer = nil + // put the current buffer back in the pool + buffPool.Put(ctn.dataBuffer) + + // Get a new one from the pool + ctn.dataBuffer = buffPool.Get(int(newBuffSize)) + ctn.origDataBuffer = ctn.dataBuffer + } + } } // initInflater sets up the zlib inflater to read compressed data from the connection diff --git a/delete_command.go b/delete_command.go index 62745970..940bb293 100644 --- a/delete_command.go +++ b/delete_command.go @@ -114,7 +114,6 @@ func (cmd *deleteCommand) Execute() Error { } func (cmd *deleteCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/execute_command.go b/execute_command.go index 109fdd44..bddc04ab 100644 --- a/execute_command.go +++ b/execute_command.go @@ -83,7 +83,6 @@ func (cmd *executeCommand) Execute() Error { } func (cmd *executeCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/exists_command.go b/exists_command.go index 0a4db9c8..97f0b031 100644 --- a/exists_command.go +++ b/exists_command.go @@ -108,7 +108,6 @@ func (cmd *existsCommand) Execute() Error { } func (cmd *existsCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/key.go b/key.go index 20e7f46d..3b62bf0d 100644 --- a/key.go +++ b/key.go @@ -87,9 +87,9 @@ func (ky *Key) String() string { } if ky.userKey != nil { - return fmt.Sprintf("%s:%s:%s:%v", ky.namespace, ky.setName, ky.userKey.String(), Buffer.BytesToHexString(ky.digest[:])) + return fmt.Sprintf("%s:%s:%s:%v", ky.namespace, ky.setName, ky.userKey.String(), fmt.Sprintf("% 02x", ky.digest[:])) } - return fmt.Sprintf("%s:%s::%v", ky.namespace, ky.setName, Buffer.BytesToHexString(ky.digest[:])) + return fmt.Sprintf("%s:%s::%v", ky.namespace, ky.setName, fmt.Sprintf("% 02x", ky.digest[:])) } // NewKey initializes a key from namespace, optional set name and user key. diff --git a/operate_command.go b/operate_command.go index 762dbf49..fc14d102 100644 --- a/operate_command.go +++ b/operate_command.go @@ -71,7 +71,6 @@ func (cmd *operateCommand) Execute() Error { } func (cmd *operateCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/proxy_query_partition_command.go b/proxy_query_partition_command.go index 51ca0c28..b6de35b2 100644 --- a/proxy_query_partition_command.go +++ b/proxy_query_partition_command.go @@ -74,7 +74,6 @@ func (cmd *grpcQueryPartitionCommand) Execute() Error { func (cmd *grpcQueryPartitionCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.recordset.signalEnd() - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/proxy_scan_command.go b/proxy_scan_command.go index 8adf6084..411c1091 100644 --- a/proxy_scan_command.go +++ b/proxy_scan_command.go @@ -75,7 +75,6 @@ func (cmd *grpcScanPartitionCommand) Execute() Error { func (cmd *grpcScanPartitionCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.recordset.signalEnd() - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/read_command.go b/read_command.go index ff5d5bcf..cdfecee0 100644 --- a/read_command.go +++ b/read_command.go @@ -269,7 +269,6 @@ func (cmd *readCommand) Execute() Error { } func (cmd *readCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/read_header_command.go b/read_header_command.go index 67cb5307..322386ca 100644 --- a/read_header_command.go +++ b/read_header_command.go @@ -105,7 +105,6 @@ func (cmd *readHeaderCommand) Execute() Error { } func (cmd *readHeaderCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/server_command.go b/server_command.go index de2e7b12..f31c0a2b 100644 --- a/server_command.go +++ b/server_command.go @@ -99,7 +99,6 @@ func (cmd *serverCommand) Execute() Error { } func (cmd *serverCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/touch_command.go b/touch_command.go index 0f7cbb24..0bc86076 100644 --- a/touch_command.go +++ b/touch_command.go @@ -141,7 +141,6 @@ func (cmd *touchCommand) Execute() Error { } func (cmd *touchCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) diff --git a/types/histogram/bench_histogram_test.go b/types/histogram/bench_histogram_test.go new file mode 100644 index 00000000..3820793d --- /dev/null +++ b/types/histogram/bench_histogram_test.go @@ -0,0 +1,83 @@ +// 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 histogram_test + +import ( + "testing" + + "github.com/aerospike/aerospike-client-go/v7/internal/histogram" +) + +var ( + _median int + _medianu64 uint64 +) + +func Benchmark_Histogram_Linear_Add(b *testing.B) { + h := histogram.NewLinear[int](5, 10) + for i := 0; i < b.N; i++ { + h.Add(i) + } +} + +func Benchmark_Histogram_Linear_Median(b *testing.B) { + h := histogram.NewLinear[int](50, 101) + for i := 0; i < 10000; i++ { + h.Add(i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _median = h.Median() + } +} + +func Benchmark_Histogram_Log_Add(b *testing.B) { + h := histogram.NewExponential[int](2, 10) + for i := 0; i < b.N; i++ { + h.Add(i) + } +} + +func Benchmark_Histogram_Log_Median(b *testing.B) { + h := histogram.NewExponential[int](2, 32) + for i := 0; i < 100000; i++ { + h.Add(i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _median = h.Median() + } +} + +func Benchmark_Histogram_Log2_Add(b *testing.B) { + h := histogram.NewLog2(10) + for i := 0; i < b.N; i++ { + h.Add(uint64(i)) + } +} + +func Benchmark_Histogram_Log2_Median(b *testing.B) { + h := histogram.NewLog2(32) + for i := 0; i < 100000; i++ { + h.Add(uint64(i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _medianu64 = h.Median() + } +} diff --git a/types/histogram/histogram.go b/types/histogram/histogram.go new file mode 100644 index 00000000..6d12585b --- /dev/null +++ b/types/histogram/histogram.go @@ -0,0 +1,126 @@ +package histogram + +import ( + "fmt" + "math" + "strings" +) + +type HistType byte + +const ( + Linear HistType = iota + Exponential +) + +type hvals interface { + int | uint | + int64 | int32 | int16 | int8 | + uint64 | uint32 | uint16 | uint8 | + float64 | float32 +} + +type Histogram[T hvals] struct { + htype HistType + base T + + Buckets []uint // slot -> count + Min, Max T + Sum float64 + Count uint +} + +func NewLinear[T hvals](base T, buckets int) *Histogram[T] { + return &Histogram[T]{ + htype: Linear, + base: base, + Buckets: make([]uint, buckets), + } +} + +func NewExponential[T hvals](base T, buckets int) *Histogram[T] { + return &Histogram[T]{ + htype: Exponential, + base: base, + Buckets: make([]uint, buckets), + } +} + +func (h *Histogram[T]) Reset() { + for i := range h.Buckets { + h.Buckets[i] = 0 + } + + h.Min = 0 + h.Max = 0 + h.Sum = 0 + h.Count = 0 +} + +func (h *Histogram[T]) String() string { + res := new(strings.Builder) + switch h.htype { + case Linear: + for i := 0; i < len(h.Buckets)-1; i++ { + v := float64(h.base) * float64(i) + fmt.Fprintf(res, "[%v, %v) => %d\n", v, v+float64(h.base), h.Buckets[i]) + } + fmt.Fprintf(res, "[%v, inf) => %d\n", float64(h.base)*float64(len(h.Buckets)-1), h.Buckets[len(h.Buckets)-1]) + case Exponential: + fmt.Fprintf(res, "[0, %v) => %d\n", float64(h.base), h.Buckets[0]) + for i := 1; i < len(h.Buckets)-1; i++ { + v := math.Pow(float64(h.base), float64(i)) + fmt.Fprintf(res, "[%v, %v) => %d\n", v, v*float64(h.base), h.Buckets[i]) + } + fmt.Fprintf(res, "[%v, inf) => %d\n", math.Pow(float64(h.base), float64(len(h.Buckets))-1), h.Buckets[len(h.Buckets)-1]) + } + return res.String() +} + +func (h *Histogram[T]) Median() T { + var s uint = 0 + c := h.Count / 2 + for i, bv := range h.Buckets { + s += bv + if s >= c { + // found the bucket + if h.htype == Linear { + return T(i+1) * h.base + } + return T(math.Pow(float64(h.base), float64(i+1))) + } + } + return h.Max +} + +func (h *Histogram[T]) Add(v T) { + if h.Count == 0 { + h.Max = v + h.Min = v + } else { + if v > h.Max { + h.Max = v + } else if v < h.Min { + h.Min = v + } + } + + h.Sum += float64(v) + h.Count++ + + var slot int + switch h.htype { + case Linear: + slot = int(math.Floor(float64(v / T(h.base)))) + case Exponential: + slot = int(math.Floor(math.Log(float64(v)) / math.Log(float64(h.base)))) + } + + if slot >= len(h.Buckets) { + h.Buckets[len(h.Buckets)-1]++ + } else if slot < 0 { + h.Buckets[0]++ + } else { + h.Buckets[slot]++ + } +} diff --git a/types/histogram/histogram_test.go b/types/histogram/histogram_test.go new file mode 100644 index 00000000..962a2732 --- /dev/null +++ b/types/histogram/histogram_test.go @@ -0,0 +1,150 @@ +// 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 histogram_test + +import ( + "testing" + + "github.com/aerospike/aerospike-client-go/v7/internal/histogram" + + gg "github.com/onsi/ginkgo/v2" + gm "github.com/onsi/gomega" +) + +func TestHistogram(t *testing.T) { + gm.RegisterFailHandler(gg.Fail) + gg.RunSpecs(t, "Histogram Suite") +} + +var _ = gg.Describe("Histogram", func() { + + gg.Context("Integer Values", func() { + + gg.Context("Linear", func() { + + gg.It("must make the correct histogram", func() { + l := []int{1, 1, 3, 4, 5, 5, 9, 11, 11, 11, 16, 16, 21} + h := histogram.NewLinear[int](5, 5) + + sum := 0 + for _, v := range l { + sum += v + h.Add(v) + } + + gm.Expect(h.Min).To(gm.Equal(1)) + gm.Expect(h.Max).To(gm.Equal(21)) + gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) + gm.Expect(h.Sum).To(gm.Equal(float64(sum))) + gm.Expect(h.Buckets).To(gm.Equal([]uint{4, 3, 3, 2, 1})) + }) + + gg.It("must find the correct median", func() { + l := []int{1e3, 2e3, 3e3, 4e3, 5e3, 6e3, 7e3, 8e3, 9e3, 10e3, 11e3, 12e3, 13e3} + h := histogram.NewLinear[int](1000, 10) + + sum := 0 + for _, v := range l { + sum += v + h.Add(v) + } + + gm.Expect(h.Min).To(gm.Equal(1000)) + gm.Expect(h.Max).To(gm.Equal(13000)) + gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) + gm.Expect(h.Sum).To(gm.Equal(float64(sum))) + gm.Expect(h.Buckets).To(gm.Equal([]uint{0, 1, 1, 1, 1, 1, 1, 1, 1, 5})) + gm.Expect(h.Median()).To(gm.Equal(7000)) + }) + + }) + + gg.Context("Exponential", func() { + + gg.It("must make the correct histogram", func() { + l := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + h := histogram.NewExponential[int](2, 5) + + sum := 0 + for _, v := range l { + sum += v + h.Add(v) + } + + gm.Expect(h.Min).To(gm.Equal(0)) + gm.Expect(h.Max).To(gm.Equal(20)) + gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) + gm.Expect(h.Sum).To(gm.Equal(float64(sum))) + gm.Expect(h.Buckets).To(gm.Equal([]uint{2, 2, 4, 8, 5})) + }) + + gg.It("must find the correct median", func() { + l := []int{10e3, 12e3, 3e3, 4e3, 50e3, 6e5, 75e3, 7e3, 21e3, 11e3, 113e3, 29e3, 189e3} + h := histogram.NewExponential[int](2, 18) + + sum := 0 + for _, v := range l { + sum += v + h.Add(v) + } + + gm.Expect(h.Min).To(gm.Equal(int(3e3))) + gm.Expect(h.Max).To(gm.Equal(int(600e3))) + gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) + gm.Expect(h.Sum).To(gm.Equal(float64(sum))) + gm.Expect(h.Buckets).To(gm.Equal([]uint{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 2, 1, 2, 2})) + gm.Expect(h.Median()).To(gm.Equal(1 << 14)) + }) + }) + + gg.Context("Log2Histogram", func() { + + gg.It("must make the correct histogram", func() { + l := []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} + h := histogram.NewLog2(5) + + var sum uint64 + for _, v := range l { + sum += v + h.Add(v) + } + + gm.Expect(h.Min).To(gm.Equal(uint64(0))) + gm.Expect(h.Max).To(gm.Equal(uint64(20))) + gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) + gm.Expect(h.Sum).To(gm.Equal(sum)) + gm.Expect(h.Buckets).To(gm.Equal([]uint64{2, 2, 4, 8, 5})) + }) + + gg.It("must find the correct median", func() { + l := []uint64{10e3, 12e3, 3e3, 4e3, 50e3, 6e5, 75e3, 7e3, 21e3, 11e3, 113e3, 29e3, 189e3} + h := histogram.NewLog2(18) + + var sum uint64 + for _, v := range l { + sum += v + h.Add(v) + } + + gm.Expect(h.Min).To(gm.Equal(uint64(3000))) + gm.Expect(h.Max).To(gm.Equal(uint64(600000))) + gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) + gm.Expect(h.Sum).To(gm.Equal(sum)) + gm.Expect(h.Buckets).To(gm.Equal([]uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 2, 1, 2, 2})) + gm.Expect(h.Median()).To(gm.Equal(uint64(1 << 14))) + }) + }) + }) +}) diff --git a/types/histogram/log2hist.go b/types/histogram/log2hist.go new file mode 100644 index 00000000..4badabd9 --- /dev/null +++ b/types/histogram/log2hist.go @@ -0,0 +1,124 @@ +package histogram + +import ( + "fmt" + "strings" +) + +type Log2 struct { + Buckets []uint64 // slot -> count + Min, Max uint64 + Sum uint64 + Count uint64 +} + +func NewLog2(buckets int) *Log2 { + return &Log2{ + Buckets: make([]uint64, buckets), + } +} + +func (h *Log2) Reset() { + for i := range h.Buckets { + h.Buckets[i] = 0 + } + + h.Min = 0 + h.Max = 0 + h.Sum = 0 + h.Count = 0 +} + +func (h *Log2) String() string { + res := new(strings.Builder) + + fmt.Fprintf(res, "[0, %v) => %d\n", 2, h.Buckets[0]) + for i := 1; i < len(h.Buckets)-1; i++ { + v := 2 << i + fmt.Fprintf(res, "[%v, %v) => %d\n", v, v*2, h.Buckets[i]) + } + fmt.Fprintf(res, "[%v, inf) => %d\n", 2<= c { + return 1 << (i + 1) + } + } + return h.Max +} + +func (h *Log2) Add(v uint64) { + if h.Count == 0 { + h.Max = v + h.Min = v + } else { + if v > h.Max { + h.Max = v + } else if v < h.Min { + h.Min = v + } + } + + h.Sum += v + h.Count++ + + slot := fastLog2(v) + + if slot >= len(h.Buckets) { + h.Buckets[len(h.Buckets)-1]++ + } else if slot < 0 { + h.Buckets[0]++ + } else { + h.Buckets[slot]++ + } +} + +/////////////////////////////////////////////////////////////////// + +var log2tab64 = [64]int8{ + 0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3, 61, + 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22, 4, 62, + 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21, 56, + 45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5, 63, +} + +// FastLog2 implements the FastLog2 function for uint64 values. +func fastLog2(value uint64) int { + value |= value >> 1 + value |= value >> 2 + value |= value >> 4 + value |= value >> 8 + value |= value >> 16 + value |= value >> 32 + + return int(log2tab64[(value*0x03f6eaf2cd271461)>>58]) +} diff --git a/types/buffer_pool.go b/types/pool/tiered_buffer.go similarity index 66% rename from types/buffer_pool.go rename to types/pool/tiered_buffer.go index 6fd3a979..43afff73 100644 --- a/types/buffer_pool.go +++ b/types/pool/tiered_buffer.go @@ -12,28 +12,48 @@ // See the License for the specific language governing permissions and // limitations under the License. -package types +package pool import ( "sync" ) -const maxBufSize = 16 * 1024 * 1024 // 2 * 8 MiB, server block size as of v6.3 +// TieredBufferPool is a tiered pool for the buffers. +// It will store buffers in powers of two in sub pools. +// The size of buffers will ALWAYS be powers of two, and the pool +// will throw away buffers passed to it which do not conform to this rule. +type TieredBufferPool struct { + // Min is Minimum the minimum buffer size. + Min int + + // Max is the maximum buffer is. The pool will allocate buffers of that size, + // But will not store them back. + Max int -type BufferPool struct { pools []sync.Pool } -// NewBufferPool creates a new buffer pool. +// NewTieredBufferPool creates a new buffer pool. // New buffers will be created with size and capacity of initBufferSize. // If cap(buffer) is larger than maxBufferSize when it is put back in the buffer, // it will be thrown away. This will prevent unwanted memory bloat and // set a deterministic maximum-size for the pool which will not be exceeded. -func NewBufferPool() *BufferPool { - p := &BufferPool{} +func NewTieredBufferPool(min, max int) *TieredBufferPool { + if !powerOf2(min) || !powerOf2(max) { + panic("min and max values should both be powers of 2") + } - max := fastlog2(uint64(maxBufSize)) - for i := 1; i <= max; i++ { + p := &TieredBufferPool{ + Min: min, + Max: max, + } + + buckets := fastLog2(uint64(max)) + if !powerOf2(max) { + buckets++ + } + + for i := 1; i <= buckets; i++ { blockSize := 1 << i p.pools = append(p.pools, sync.Pool{ @@ -49,15 +69,10 @@ func NewBufferPool() *BufferPool { return p } -// powerOf2 returns true if a number is an EXACT power of 2. -func powerOf2(sz int) bool { - return sz > 0 && (sz&(sz-1)) == 0 -} - // Returns the pool index based on the size of the buffer. // Will return -1 if the value falls outside of the pool range. -func (bp *BufferPool) poolIndex(sz int) int { - factor := fastlog2(uint64(sz)) +func (bp *TieredBufferPool) poolIndex(sz int) int { + factor := fastLog2(uint64(sz)) szl := factor - 1 if !powerOf2(sz) { szl++ @@ -70,12 +85,17 @@ func (bp *BufferPool) poolIndex(sz int) int { // Get returns a buffer from the pool. If sz is bigger than maxBufferSize, // a fresh buffer will be created and not taken from the pool. -func (bp *BufferPool) Get(sz int) []byte { - // Short circuit - if sz > maxBufSize { +func (bp *TieredBufferPool) Get(sz int) []byte { + // Short circuit. We know we don't have buffers this size in the pool. + if sz > bp.Max { return make([]byte, sz, sz) } + // do not allocate buffers smaller than a certain size + if sz < bp.Min { + sz = bp.Min + } + if szl := bp.poolIndex(sz); szl >= 0 { res := bp.pools[szl].Get().([]byte) origLen := 1 << (szl + 1) @@ -86,12 +106,13 @@ func (bp *BufferPool) Get(sz int) []byte { return make([]byte, sz, sz) } -// Put will put the buffer back in the pool, unless cap(buf) is bigger than -// maxBufSize, in which case it will be thrown away -func (bp *BufferPool) Put(buf []byte) { +// Put will put the buffer back in the pool, unless cap(buf) is smaller than Min +// or larger than Max, or the size of the buffer is not a power of 2 +// in which case it will be thrown away. +func (bp *TieredBufferPool) Put(buf []byte) { sz := cap(buf) // throw away random non-power of 2 buffer sizes - if powerOf2(sz) { + if len(buf) > bp.Min && len(buf) <= bp.Max && powerOf2(sz) { if szl := bp.poolIndex(sz); szl >= 0 { bp.pools[szl].Put(buf) return @@ -101,6 +122,11 @@ func (bp *BufferPool) Put(buf []byte) { /////////////////////////////////////////////////////////////////// +// powerOf2 returns true if a number is an EXACT power of 2. +func powerOf2(sz int) bool { + return sz > 0 && (sz&(sz-1)) == 0 +} + var log2tab64 = [64]int8{ 0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3, 61, 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22, 4, 62, @@ -108,8 +134,8 @@ var log2tab64 = [64]int8{ 45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5, 63, } -// fastlog2 implements the fastlog2 function for uint64 values. -func fastlog2(value uint64) int { +// fast log2 implementation +func fastLog2(value uint64) int { value |= value >> 1 value |= value >> 2 value |= value >> 4 diff --git a/utils/buffer/buffer.go b/utils/buffer/buffer.go index 9e82c8a8..e37f623f 100644 --- a/utils/buffer/buffer.go +++ b/utils/buffer/buffer.go @@ -16,7 +16,6 @@ package buffer import ( "encoding/binary" - "fmt" "math" ) @@ -53,18 +52,6 @@ func init() { Arch32Bits = (SizeOfInt == SizeOfInt32) } -// BytesToHexString converts a byte slice into a hex string -func BytesToHexString(buf []byte) string { - hlist := make([]byte, 3*len(buf)) - - for i := range buf { - hex := fmt.Sprintf("%02x ", buf[i]) - idx := i * 3 - copy(hlist[idx:], hex) - } - return string(hlist) -} - // LittleBytesToInt32 converts a slice into int32; only maximum of 4 bytes will be used func LittleBytesToInt32(buf []byte, offset int) int32 { l := len(buf[offset:]) @@ -97,12 +84,13 @@ func BytesToInt64(buf []byte, offset int) int64 { // VarBytesToInt64 will convert a 8, 4 or 2 byte slice into an int64 func VarBytesToInt64(buf []byte, offset int, len int) int64 { - if len == 8 { - return BytesToInt64(buf, offset) - } else if len == 4 { - return int64(BytesToInt32(buf, offset)) - } else if len == 2 { + switch len { + case 2: return int64(BytesToInt16(buf, offset)) + case 4: + return int64(BytesToInt32(buf, offset)) + case 8: + return BytesToInt64(buf, offset) } val := int64(0) diff --git a/value.go b/value.go index c7f4b046..bd596c0d 100644 --- a/value.go +++ b/value.go @@ -617,7 +617,7 @@ func (vl BytesValue) GetObject() interface{} { // String implements Stringer interface. func (vl BytesValue) String() string { - return Buffer.BytesToHexString(vl) + return fmt.Sprintf("% 02x", []byte(vl)) } /////////////////////////////////////////////////////////////////////////////// @@ -1136,7 +1136,7 @@ func (vl HLLValue) GetObject() interface{} { // String implements Stringer interface. func (vl HLLValue) String() string { - return Buffer.BytesToHexString([]byte(vl)) + return fmt.Sprintf("% 02x", []byte(vl)) } /////////////////////////////////////////////////////////////////////////////// @@ -1182,7 +1182,7 @@ func (vl *RawBlobValue) GetObject() interface{} { // String implements Stringer interface. func (vl *RawBlobValue) String() string { - return Buffer.BytesToHexString(vl.Data) + return fmt.Sprintf("% 02x", vl.Data) } ////////////////////////////////////////////////////////////////////////////// diff --git a/write_command.go b/write_command.go index 6456a549..d2449167 100644 --- a/write_command.go +++ b/write_command.go @@ -115,7 +115,6 @@ func (cmd *writeCommand) Execute() Error { } func (cmd *writeCommand) ExecuteGRPC(clnt *ProxyClient) Error { - cmd.dataBuffer = bufPool.Get().([]byte) defer cmd.grpcPutBufferBack() err := cmd.prepareBuffer(cmd, cmd.policy.deadline()) From f21b8ebe89faf1020b004639dae161e20b313131 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Fri, 26 Apr 2024 12:58:36 +0200 Subject: [PATCH 09/28] Linter Clean up --- batch_command_operate.go | 8 ++++---- delete_command.go | 4 ++-- error.go | 10 +++++----- execute_command.go | 4 ++-- execute_task.go | 6 +++--- exists_command.go | 4 ++-- operate_command.go | 4 ++-- proxy_auth_interceptor.go | 4 ++-- proxy_client.go | 2 +- proxy_query_partition_command.go | 8 ++++---- proxy_scan_command.go | 8 ++++---- read_command.go | 4 ++-- read_header_command.go | 4 ++-- server_command.go | 8 ++++---- touch_command.go | 4 ++-- write_command.go | 4 ++-- 16 files changed, 43 insertions(+), 43 deletions(-) diff --git a/batch_command_operate.go b/batch_command_operate.go index b05dd342..76bcc643 100644 --- a/batch_command_operate.go +++ b/batch_command_operate.go @@ -276,14 +276,14 @@ func (cmd *batchCommandOperate) ExecuteGRPC(clnt *ProxyClient) Error { return nil, e } - if res.Status != 0 { + if res.GetStatus() != 0 { e := newGrpcStatusError(res) - return res.Payload, e + return res.GetPayload(), e } - cmd.grpcEOS = !res.HasNext + cmd.grpcEOS = !res.GetHasNext() - return res.Payload, nil + return res.GetPayload(), nil } cmd.conn = newGrpcFakeConnection(nil, readCallback) diff --git a/delete_command.go b/delete_command.go index 940bb293..e3e3ae07 100644 --- a/delete_command.go +++ b/delete_command.go @@ -146,11 +146,11 @@ func (cmd *deleteCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err diff --git a/error.go b/error.go index 7cd7c7c1..1f4a8851 100644 --- a/error.go +++ b/error.go @@ -210,12 +210,12 @@ func newGrpcError(e error, messages ...string) Error { } func newGrpcStatusError(res *kvs.AerospikeResponsePayload) Error { - if res.Status >= 0 { - return newError(types.ResultCode(res.Status)).markInDoubt(res.InDoubt) + if res.GetStatus() >= 0 { + return newError(types.ResultCode(res.GetStatus())).markInDoubt(res.GetInDoubt()) } var resultCode = types.OK - switch res.Status { + switch res.GetStatus() { case -16: // BATCH_FAILED resultCode = types.BATCH_FAILED @@ -234,7 +234,7 @@ func newGrpcStatusError(res *kvs.AerospikeResponsePayload) Error { case -9: // ASYNC_QUEUE_FULL // resultCode = types.ASYNC_QUEUE_FULL - return newError(types.SERVER_ERROR, "Server ASYNC_QUEUE_FULL").markInDoubt(res.InDoubt) + return newError(types.SERVER_ERROR, "Server ASYNC_QUEUE_FULL").markInDoubt(res.GetInDoubt()) case -8: // SERVER_NOT_AVAILABLE resultCode = types.SERVER_NOT_AVAILABLE @@ -258,7 +258,7 @@ func newGrpcStatusError(res *kvs.AerospikeResponsePayload) Error { resultCode = types.COMMON_ERROR } - return newError(resultCode).markInDoubt(res.InDoubt) + return newError(resultCode).markInDoubt(res.GetInDoubt()) } // SetInDoubt sets whether it is possible that the write transaction may have completed diff --git a/execute_command.go b/execute_command.go index bddc04ab..bd4c9599 100644 --- a/execute_command.go +++ b/execute_command.go @@ -115,11 +115,11 @@ func (cmd *executeCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err diff --git a/execute_task.go b/execute_task.go index caab2f6a..fc656ca4 100644 --- a/execute_task.go +++ b/execute_task.go @@ -196,13 +196,13 @@ func (etsk *ExecuteTask) grpcIsDone() (bool, Error) { return false, e } - if res.Status != 0 { + if res.GetStatus() != 0 { e := newGrpcStatusError(res) etsk.clnt.returnGrpcConnToPool(conn) return false, e } - switch *res.BackgroundTaskStatus { + switch res.GetBackgroundTaskStatus() { case kvs.BackgroundTaskStatus_COMPLETE: etsk.clnt.returnGrpcConnToPool(conn) return true, nil @@ -211,7 +211,7 @@ func (etsk *ExecuteTask) grpcIsDone() (bool, Error) { return false, nil } - if !res.HasNext { + if !res.GetHasNext() { etsk.clnt.returnGrpcConnToPool(conn) return false, nil } diff --git a/exists_command.go b/exists_command.go index 97f0b031..010ffa53 100644 --- a/exists_command.go +++ b/exists_command.go @@ -140,11 +140,11 @@ func (cmd *existsCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err diff --git a/operate_command.go b/operate_command.go index fc14d102..8c8d3198 100644 --- a/operate_command.go +++ b/operate_command.go @@ -103,11 +103,11 @@ func (cmd *operateCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err diff --git a/proxy_auth_interceptor.go b/proxy_auth_interceptor.go index a194d977..3c76d298 100644 --- a/proxy_auth_interceptor.go +++ b/proxy_auth_interceptor.go @@ -136,7 +136,7 @@ func (interceptor *authInterceptor) login() Error { return newGrpcError(gerr, gerr.Error()) } - claims := strings.Split(res.Token, ".") + claims := strings.Split(res.GetToken(), ".") decClaims, gerr := base64.RawURLEncoding.DecodeString(claims[1]) if err != nil { return newGrpcError(err, "Invalid token encoding. Expected base64.") @@ -166,7 +166,7 @@ func (interceptor *authInterceptor) login() Error { // Set expiry based on local clock. expiry := time.Now().Add(ttl) - interceptor.fullToken = "Bearer " + res.Token + interceptor.fullToken = "Bearer " + res.GetToken() interceptor.expiry = expiry return nil diff --git a/proxy_client.go b/proxy_client.go index f96101c5..5899efce 100644 --- a/proxy_client.go +++ b/proxy_client.go @@ -327,7 +327,7 @@ func (clnt *ProxyClient) ServerVersion(policy *InfoPolicy) (string, Error) { clnt.returnGrpcConnToPool(conn) - return res.Version, nil + return res.GetVersion(), nil } //------------------------------------------------------- diff --git a/proxy_query_partition_command.go b/proxy_query_partition_command.go index b6de35b2..4cf174d7 100644 --- a/proxy_query_partition_command.go +++ b/proxy_query_partition_command.go @@ -122,15 +122,15 @@ func (cmd *grpcQueryPartitionCommand) ExecuteGRPC(clnt *ProxyClient) Error { return nil, e } - if res.Status != 0 { + if res.GetStatus() != 0 { e := newGrpcStatusError(res) cmd.recordset.sendError(e) - return res.Payload, e + return res.GetPayload(), e } - cmd.grpcEOS = !res.HasNext + cmd.grpcEOS = !res.GetHasNext() - return res.Payload, nil + return res.GetPayload(), nil } cmd.conn = newGrpcFakeConnection(nil, readCallback) diff --git a/proxy_scan_command.go b/proxy_scan_command.go index 411c1091..f3022da1 100644 --- a/proxy_scan_command.go +++ b/proxy_scan_command.go @@ -125,15 +125,15 @@ func (cmd *grpcScanPartitionCommand) ExecuteGRPC(clnt *ProxyClient) Error { return nil, e } - cmd.grpcEOS = !res.HasNext + cmd.grpcEOS = !res.GetHasNext() - if res.Status != 0 { + if res.GetStatus() != 0 { e := newGrpcStatusError(res) cmd.recordset.sendError(e) - return res.Payload, e + return res.GetPayload(), e } - return res.Payload, nil + return res.GetPayload(), nil } cmd.conn = newGrpcFakeConnection(nil, readCallback) diff --git a/read_command.go b/read_command.go index cdfecee0..45a3841a 100644 --- a/read_command.go +++ b/read_command.go @@ -301,11 +301,11 @@ func (cmd *readCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err diff --git a/read_header_command.go b/read_header_command.go index 322386ca..77c75b8e 100644 --- a/read_header_command.go +++ b/read_header_command.go @@ -137,11 +137,11 @@ func (cmd *readHeaderCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err diff --git a/server_command.go b/server_command.go index f31c0a2b..d6d188db 100644 --- a/server_command.go +++ b/server_command.go @@ -141,16 +141,16 @@ func (cmd *serverCommand) ExecuteGRPC(clnt *ProxyClient) Error { return nil, e } - if res.Status != 0 { + if res.GetStatus() != 0 { e := newGrpcStatusError(res) - return res.Payload, e + return res.GetPayload(), e } - if !res.HasNext { + if !res.GetHasNext() { return nil, errGRPCStreamEnd } - return res.Payload, nil + return res.GetPayload(), nil } cmd.conn = newGrpcFakeConnection(nil, readCallback) diff --git a/touch_command.go b/touch_command.go index 0bc86076..dc35de0e 100644 --- a/touch_command.go +++ b/touch_command.go @@ -173,11 +173,11 @@ func (cmd *touchCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err diff --git a/write_command.go b/write_command.go index d2449167..f7160663 100644 --- a/write_command.go +++ b/write_command.go @@ -147,11 +147,11 @@ func (cmd *writeCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer clnt.returnGrpcConnToPool(conn) - if res.Status != 0 { + if res.GetStatus() != 0 { return newGrpcStatusError(res) } - cmd.conn = newGrpcFakeConnection(res.Payload, nil) + cmd.conn = newGrpcFakeConnection(res.GetPayload(), nil) err = cmd.parseResult(cmd, cmd.conn) if err != nil { return err From c2e33976b1a1b4a2993d0a714a5a43a8ae0112c4 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Fri, 26 Apr 2024 13:25:54 +0200 Subject: [PATCH 10/28] Remove dependency on xrand "math/rand" is fast enough now --- bench_rand_gen_test.go | 3 ++- multi_command.go | 6 +++--- recordset.go | 5 ++--- statement.go | 6 +++--- types/rand/xor_shift128.go | 41 ++------------------------------------ 5 files changed, 12 insertions(+), 49 deletions(-) diff --git a/bench_rand_gen_test.go b/bench_rand_gen_test.go index 2d99cbf0..85493b12 100644 --- a/bench_rand_gen_test.go +++ b/bench_rand_gen_test.go @@ -57,7 +57,8 @@ func Benchmark_math_rand_synched(b *testing.B) { } func Benchmark_xor_rand_fast_pool(b *testing.B) { + r := xor.NewXorRand() for i := 0; i < b.N; i++ { - xor.Int64() + r.Int64() } } diff --git a/multi_command.go b/multi_command.go index 66bafcf5..6fb8ca7a 100644 --- a/multi_command.go +++ b/multi_command.go @@ -16,10 +16,10 @@ package aerospike import ( "fmt" + "math/rand" "reflect" "github.com/aerospike/aerospike-client-go/v7/types" - xrand "github.com/aerospike/aerospike-client-go/v7/types/rand" Buffer "github.com/aerospike/aerospike-client-go/v7/utils/buffer" ) @@ -114,11 +114,11 @@ func (cmd *baseMultiCommand) prepareRetry(ifc command, isTimeout bool) bool { } func (cmd *baseMultiCommand) getConnection(policy Policy) (*Connection, Error) { - return cmd.node.getConnectionWithHint(policy.GetBasePolicy().deadline(), policy.GetBasePolicy().socketTimeout(), byte(xrand.Int64()%256)) + return cmd.node.getConnectionWithHint(policy.GetBasePolicy().deadline(), policy.GetBasePolicy().socketTimeout(), byte(rand.Int63()&0xff)) } func (cmd *baseMultiCommand) putConnection(conn *Connection) { - cmd.node.putConnectionWithHint(conn, byte(xrand.Int64()%256)) + cmd.node.putConnectionWithHint(conn, byte(rand.Int63()&0xff)) } func (cmd *baseMultiCommand) parseResult(ifc command, conn *Connection) Error { diff --git a/recordset.go b/recordset.go index cad725fb..16b85f9e 100644 --- a/recordset.go +++ b/recordset.go @@ -16,13 +16,12 @@ package aerospike import ( "fmt" + "math/rand" "reflect" "runtime" "sync" "github.com/aerospike/aerospike-client-go/v7/internal/atomic" - - xornd "github.com/aerospike/aerospike-client-go/v7/types/rand" ) // Result is the value returned by Recordset's Results() function. @@ -70,7 +69,7 @@ func (os *objectset) TaskId() uint64 { func (os *objectset) resetTaskID() { os.chanLock.Lock() defer os.chanLock.Unlock() - os.taskID = uint64(xornd.Int64()) + os.taskID = rand.Uint64() } // Recordset encapsulates the result of Scan and Query commands. diff --git a/statement.go b/statement.go index 45a9ca7b..10c4346c 100644 --- a/statement.go +++ b/statement.go @@ -16,10 +16,10 @@ package aerospike import ( "fmt" + "math/rand" kvs "github.com/aerospike/aerospike-client-go/v7/proto/kvs" "github.com/aerospike/aerospike-client-go/v7/types" - xornd "github.com/aerospike/aerospike-client-go/v7/types/rand" ) // Statement encapsulates query statement parameters. @@ -61,7 +61,7 @@ func NewStatement(ns string, set string, binNames ...string) *Statement { SetName: set, BinNames: binNames, ReturnData: true, - TaskId: xornd.Uint64(), + TaskId: rand.Uint64(), } } @@ -118,7 +118,7 @@ func (stmt *Statement) prepare(returnData bool) { func (stmt *Statement) grpc(policy *QueryPolicy, ops []*Operation) *kvs.Statement { IndexName := stmt.IndexName // reset taskID every time - TaskId := xornd.Int64() + TaskId := rand.Int63() SetName := stmt.SetName MaxRecords := uint64(policy.MaxRecords) diff --git a/types/rand/xor_shift128.go b/types/rand/xor_shift128.go index aadcdaa7..5e37665e 100644 --- a/types/rand/xor_shift128.go +++ b/types/rand/xor_shift128.go @@ -16,52 +16,17 @@ package rand import ( "encoding/binary" - "sync" - "sync/atomic" - "time" + "math/rand" ) -const ( - poolSize = 512 -) - -// random number generator pool -var ( - pool = make([]*Xor128Rand, poolSize) - pos uint64 - rndInit = &Xor128Rand{src: [2]uint64{uint64(time.Now().UnixNano()), uint64(time.Now().UnixNano())}} -) - -func init() { - for i := range pool { - pool[i] = NewXorRand() - } -} - -// Int64 returns a random int64 number. It can be negative. -// This function uses a pool and is lockless. -func Int64() int64 { - apos := int(atomic.AddUint64(&pos, 1) % poolSize) - return pool[apos].Int64() -} - -// Uint64 returns a random uint64 number. -// This function uses a pool and is lockless. -func Uint64() uint64 { - apos := int(atomic.AddUint64(&pos, 1) % poolSize) - return pool[apos].Uint64() -} - // Xor128Rand is a random number generator type Xor128Rand struct { src [2]uint64 - l sync.Mutex } // NewXorRand creates a XOR Shift random number generator. func NewXorRand() *Xor128Rand { - t := time.Now().UnixNano() + rndInit.Int64() - return &Xor128Rand{src: [2]uint64{uint64(t), uint64(t)}} + return &Xor128Rand{src: [2]uint64{rand.Uint64(), rand.Uint64()}} } // Int64 returns a random int64 number. It can be negative. @@ -71,14 +36,12 @@ func (r *Xor128Rand) Int64() int64 { // Uint64 returns a random uint64 number. func (r *Xor128Rand) Uint64() uint64 { - r.l.Lock() s1 := r.src[0] s0 := r.src[1] r.src[0] = s0 s1 ^= s1 << 23 r.src[1] = (s1 ^ s0 ^ (s1 >> 17) ^ (s0 >> 26)) res := r.src[1] + s0 - r.l.Unlock() return res } From 648626924307b8040b1119c425e4e17b6c2d5a76 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Tue, 30 Apr 2024 23:15:30 +0200 Subject: [PATCH 11/28] [CLIENT-2702] Support Client Transaction Metrics --- batch_command.go | 4 + batch_command_delete.go | 4 + batch_command_exists.go | 4 + batch_command_get.go | 4 + batch_command_operate.go | 7 + client.go | 19 +- cluster.go | 56 +++++- command.go | 88 +++++++++ delete_command.go | 4 + execute_command.go | 4 + exists_command.go | 4 + metrics_policy.go | 61 ++++++ node.go | 2 + node_stats.go | 172 +++++++++++++++++ operate_command.go | 4 + proxy_query_partition_command.go | 4 + proxy_scan_command.go | 4 + query_command.go | 4 + query_partition_command.go | 4 + query_partitiopn_objects_command.go | 4 + read_command.go | 4 + read_header_command.go | 4 + scan_objects_command.go | 4 + scan_partition_command.go | 4 + scan_partition_objects_command.go | 4 + tools/benchmark/benchmark.go | 2 +- touch_command.go | 4 + types/histogram/bench_histogram_test.go | 8 +- types/histogram/histogram.go | 136 +++++++++++--- types/histogram/histogram_test.go | 37 +++- types/histogram/log2hist.go | 19 +- types/histogram/sync_histogram.go | 236 ++++++++++++++++++++++++ write_command.go | 4 + 33 files changed, 881 insertions(+), 42 deletions(-) create mode 100644 metrics_policy.go create mode 100644 types/histogram/sync_histogram.go diff --git a/batch_command.go b/batch_command.go index d0549536..cc3c8fb3 100644 --- a/batch_command.go +++ b/batch_command.go @@ -93,6 +93,10 @@ func (cmd *batchCommand) getPolicy(ifc command) Policy { return cmd.policy } +func (cmd *batchCommand) transactionType() transactionType { + return ttNone +} + func (cmd *batchCommand) Execute() Error { return cmd.execute(cmd) } diff --git a/batch_command_delete.go b/batch_command_delete.go index d5fad935..f9c5eecf 100644 --- a/batch_command_delete.go +++ b/batch_command_delete.go @@ -161,6 +161,10 @@ func (cmd *batchCommandDelete) parseRecord(rec *BatchRecord, key *Key, opCount i return nil } +func (cmd *batchCommandDelete) transactionType() transactionType { + return ttBatchWrite +} + func (cmd *batchCommandDelete) Execute() Error { return cmd.execute(cmd) } diff --git a/batch_command_exists.go b/batch_command_exists.go index 0ed589a6..5243136a 100644 --- a/batch_command_exists.go +++ b/batch_command_exists.go @@ -106,6 +106,10 @@ func (cmd *batchCommandExists) parseRecordResults(ifc command, receiveSize int) return true, nil } +func (cmd *batchCommandExists) transactionType() transactionType { + return ttBatchRead +} + func (cmd *batchCommandExists) Execute() Error { return cmd.execute(cmd) } diff --git a/batch_command_get.go b/batch_command_get.go index c6486505..13fd18b1 100644 --- a/batch_command_get.go +++ b/batch_command_get.go @@ -212,6 +212,10 @@ func (cmd *batchCommandGet) parseRecord(key *Key, opCount int, generation, expir return newRecord(cmd.node, key, bins, generation, expiration), nil } +func (cmd *batchCommandGet) transactionType() transactionType { + return ttBatchRead +} + func (cmd *batchCommandGet) Execute() Error { return cmd.execute(cmd) } diff --git a/batch_command_operate.go b/batch_command_operate.go index 76bcc643..f0754ccd 100644 --- a/batch_command_operate.go +++ b/batch_command_operate.go @@ -229,6 +229,13 @@ func (cmd *batchCommandOperate) Execute() Error { return cmd.execute(cmd) } +func (cmd *batchCommandOperate) transactionType() transactionType { + if cmd.isRead() { + return ttBatchRead + } + return ttBatchWrite +} + func (cmd *batchCommandOperate) generateBatchNodes(cluster *Cluster) ([]*batchNode, Error) { return newBatchOperateNodeListIfcRetry(cluster, cmd.policy, cmd.records, cmd.sequenceAP, cmd.sequenceSC, cmd.batch) } diff --git a/client.go b/client.go index 64b77372..67a7fa52 100644 --- a/client.go +++ b/client.go @@ -1727,11 +1727,28 @@ func (clnt *Client) String() string { return "" } +// MetricsEnabled returns true if metrics are enabled for the cluster. +func (clnt *Client) MetricsEnabled() bool { + return clnt.cluster.MetricsEnabled() +} + +// EnableMetrics enables the cluster transaction metrics gathering. +// If the parameters for the histogram in the policy are the different from the one already +// on the cluster, the metrics will be reset. +func (clnt *Client) EnableMetrics(policy *MetricsPolicy) { + clnt.cluster.EnableMetrics(policy) +} + +// DisableMetrics disables the cluster transaction metrics gathering. +func (clnt *Client) DisableMetrics() { + clnt.cluster.DisableMetrics() +} + // Stats returns internal statistics regarding the inner state of the client and the cluster. func (clnt *Client) Stats() (map[string]interface{}, Error) { resStats := clnt.cluster.statsCopy() - clusterStats := nodeStats{} + clusterStats := *newNodeStats(clnt.cluster.MetricsPolicy()) for _, stats := range resStats { clusterStats.aggregate(&stats) } diff --git a/cluster.go b/cluster.go index 6fb27baa..6de03013 100644 --- a/cluster.go +++ b/cluster.go @@ -48,6 +48,10 @@ type Cluster struct { stats map[string]*nodeStats //host => stats statsLock sync.Mutex + // enable performance metrics + metricsEnabled atomic.Bool // bool + metricsPolicy atomic.Value // *MetricsPolicy + // Hints for best node for a partition partitionWriteMap atomic.Value //partitionMap @@ -367,7 +371,7 @@ func (clstr *Cluster) tend() Error { clstr.removeNodes(removeList) } - clstr.aggregateNodestats(removeList) + clstr.aggregateNodeStats(removeList) } // Add nodes in a batch. @@ -400,7 +404,7 @@ func (clstr *Cluster) tend() Error { logger.Logger.Info("Tend finished. Live node count changes from %d to %d", nodeCountBeforeTend, len(clstr.GetNodes())) } - clstr.aggregateNodestats(clstr.GetNodes()) + clstr.aggregateNodeStats(clstr.GetNodes()) // Reset connection error window for all nodes every connErrorWindow tend iterations. if clstr.clientPolicy.MaxErrorRate > 0 && clstr.tendCount%clstr.clientPolicy.ErrorRateWindow == 0 { @@ -412,7 +416,7 @@ func (clstr *Cluster) tend() Error { return nil } -func (clstr *Cluster) aggregateNodestats(nodeList []*Node) { +func (clstr *Cluster) aggregateNodeStats(nodeList []*Node) { // update stats clstr.statsLock.Lock() defer clstr.statsLock.Unlock() @@ -428,6 +432,9 @@ func (clstr *Cluster) aggregateNodestats(nodeList []*Node) { } func (clstr *Cluster) statsCopy() map[string]nodeStats { + // update the stats on the cluster object + clstr.aggregateNodeStats(clstr.GetNodes()) + clstr.statsLock.Lock() defer clstr.statsLock.Unlock() @@ -1035,3 +1042,46 @@ func (clstr *Cluster) WarmUp(count int) (int, Error) { } return cnt.Get(), nil } + +// MetricsEnabled returns true if metrics are enabled for the cluster. +func (clstr *Cluster) MetricsPolicy() *MetricsPolicy { + res := clstr.metricsPolicy.Load() + if res != nil { + return res.(*MetricsPolicy) + } + return nil +} + +// MetricsEnabled returns true if metrics are enabled for the cluster. +func (clstr *Cluster) MetricsEnabled() bool { + return clstr.metricsEnabled.Load() +} + +// EnableMetrics enables the cluster transaction metrics gathering. +// If the parameters for the histogram in the policy are the different from the one already +// on the cluster, the metrics will be reset. +func (clstr *Cluster) EnableMetrics(policy *MetricsPolicy) { + if policy == nil { + policy = DefaultMetricsPolicy() + } + + clstr.metricsPolicy.Store(policy) + clstr.metricsEnabled.Store(true) + + clstr.statsLock.Lock() + defer clstr.statsLock.Unlock() + + // reshape the histogram in case it has changed + for _, stat := range clstr.stats { + stat.reshape(policy) + } + + for _, node := range clstr.GetNodes() { + node.stats.reshape(policy) + } +} + +// DisableMetrics disables the cluster transaction metrics gathering. +func (clstr *Cluster) DisableMetrics() { + clstr.metricsEnabled.Store(false) +} diff --git a/command.go b/command.go index ee5b959d..02cfb29d 100644 --- a/command.go +++ b/command.go @@ -121,6 +121,23 @@ const ( _AS_MSG_TYPE_COMPRESSED int64 = 4 ) +type transactionType int + +const ( + ttNone transactionType = iota + ttGet + ttGetHeader + ttExists + ttPut + ttDelete + ttOperate + ttQuery + ttScan + ttUDF + ttBatchRead + ttBatchWrite +) + var ( buffPool = pool.NewTieredBufferPool(MinBufferSize, PoolCutOffBufferSize) ) @@ -137,6 +154,8 @@ type command interface { parseRecordResults(ifc command, receiveSize int) (bool, Error) prepareRetry(ifc command, isTimeout bool) bool + transactionType() transactionType + isRead() bool execute(ifc command) Error @@ -2563,6 +2582,8 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // for exponential backoff interval := policy.SleepBetweenRetries + transStart := time.Now() + notFirstIteration := false isClientTimeout := false loopCount := 0 @@ -2579,6 +2600,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time if cmd.node != nil && cmd.node.cluster != nil { cmd.node.cluster.maxRetriesExceededCount.GetAndIncrement() } + applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) return chainErrors(ErrMaxRetriesExceeded.err(), errChain).iter(cmd.commandSentCounter).setInDoubt(ifc.isRead(), cmd.commandWasSent).setNode(cmd.node) } @@ -2596,12 +2618,15 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time } if notFirstIteration { + applyTransactionRetryMetrics(cmd.node) + if !ifc.prepareRetry(ifc, isClientTimeout || (err != nil && err.Matches(types.SERVER_NOT_AVAILABLE))) { if bc, ok := ifc.(batcher); ok { // Batch may be retried in separate commands. alreadyRetried, err := bc.retryBatch(bc, cmd.node.cluster, deadline, cmd.commandSentCounter, cmd.commandWasSent) if alreadyRetried { // Batch was retried in separate subcommands. Complete this command. + applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) if err != nil { return chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) } @@ -2645,6 +2670,8 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time if err = cmd.node.validateErrorCount(); err != nil { isClientTimeout = false + applyTransactionErrorMetrics(cmd.node) + // chain the errors errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) @@ -2659,6 +2686,8 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // chain the errors errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + applyTransactionErrorMetrics(cmd.node) + // exit immediately if connection pool is exhausted and the corresponding policy option is set if policy.ExitFastOnExhaustedConnectionPool && errors.Is(err, ErrConnectionPoolExhausted) { break @@ -2682,6 +2711,8 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // Set command buffer. err = ifc.writeBuffer(ifc) if err != nil { + applyTransactionErrorMetrics(cmd.node) + // chain the errors err = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) @@ -2689,6 +2720,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // Close socket to flush out possible garbage. Do not put back in pool. cmd.conn.Close() cmd.conn = nil + applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) return err } @@ -2704,11 +2736,14 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // now that the deadline has been set in the buffer, compress the contents if err = cmd.compress(); err != nil { + applyTransactionErrorMetrics(cmd.node) return chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) } // now that the deadline has been set in the buffer, compress the contents if err = cmd.prepareBuffer(ifc, deadline); err != nil { + applyTransactionErrorMetrics(cmd.node) + applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) return chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node) } @@ -2716,6 +2751,8 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time cmd.commandWasSent = true _, err = cmd.conn.Write(cmd.dataBuffer[:cmd.dataOffset]) if err != nil { + applyTransactionErrorMetrics(cmd.node) + // chain the errors errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) @@ -2736,6 +2773,8 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // Parse results. err = ifc.parseResult(ifc, cmd.conn) if err != nil { + applyTransactionErrorMetrics(cmd.node) + // chain the errors errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) @@ -2773,9 +2812,12 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time cmd.conn = nil } + applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) return errChain.setInDoubt(ifc.isRead(), cmd.commandWasSent) } + applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) + // in case it has grown and re-allocated, it means // it was borrowed from the pool, sp put it back. if &cmd.dataBufferCompress != &cmd.conn.origDataBuffer { @@ -2839,3 +2881,49 @@ func networkError(err Error) bool { func deviceOverloadError(err Error) bool { return err.Matches(types.DEVICE_OVERLOAD) } + +func applyTransactionMetrics(node *Node, tt transactionType, tb time.Time) { + if node != nil && node.cluster.MetricsEnabled() { + applyMetrics(tt, node.stats, tb) + } +} + +func applyTransactionErrorMetrics(node *Node) { + if node != nil { + node.stats.TransactionErrorCount.GetAndIncrement() + } +} + +func applyTransactionRetryMetrics(node *Node) { + if node != nil { + node.stats.TransactionRetryCount.GetAndIncrement() + } +} + +func applyMetrics(tt transactionType, metrics *nodeStats, s time.Time) { + d := uint64(time.Since(s).Microseconds()) + switch tt { + case ttGet: + metrics.GetMetrics.Add(d) + case ttGetHeader: + metrics.GetHeaderMetrics.Add(d) + case ttExists: + metrics.ExistsMetrics.Add(d) + case ttPut: + metrics.PutMetrics.Add(d) + case ttDelete: + metrics.DeleteMetrics.Add(d) + case ttOperate: + metrics.OperateMetrics.Add(d) + case ttQuery: + metrics.QueryMetrics.Add(d) + case ttScan: + metrics.ScanMetrics.Add(d) + case ttUDF: + metrics.UDFMetrics.Add(d) + case ttBatchRead: + metrics.BatchReadMetrics.Add(d) + case ttBatchWrite: + metrics.BatchWriteMetrics.Add(d) + } +} diff --git a/delete_command.go b/delete_command.go index e3e3ae07..18b9a77f 100644 --- a/delete_command.go +++ b/delete_command.go @@ -113,6 +113,10 @@ func (cmd *deleteCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *deleteCommand) transactionType() transactionType { + return ttDelete +} + func (cmd *deleteCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() diff --git a/execute_command.go b/execute_command.go index bd4c9599..1c6c4025 100644 --- a/execute_command.go +++ b/execute_command.go @@ -82,6 +82,10 @@ func (cmd *executeCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *executeCommand) transactionType() transactionType { + return ttUDF +} + func (cmd *executeCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() diff --git a/exists_command.go b/exists_command.go index 010ffa53..c53aefc3 100644 --- a/exists_command.go +++ b/exists_command.go @@ -107,6 +107,10 @@ func (cmd *existsCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *existsCommand) transactionType() transactionType { + return ttExists +} + func (cmd *existsCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() diff --git a/metrics_policy.go b/metrics_policy.go new file mode 100644 index 00000000..f53165f2 --- /dev/null +++ b/metrics_policy.go @@ -0,0 +1,61 @@ +// 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 ( + "github.com/aerospike/aerospike-client-go/v7/types/histogram" +) + +// MetricsPolicy specifies client periodic metrics configuration. +type MetricsPolicy struct { + // Histogram type specifies if the histogram should be [histogram.Linear] or [histogram.Logarithmic]. + // + // Default: [histogram.Logarithmic] + HistogramType histogram.Type + + // LatencyColumns defines the number of elapsed time range buckets in latency histograms. + // + // Default: 24 + LatencyColumns int //= 24; + + // Depending on the type of histogram: + // + // For logarithmic histograms, the buckets are: =base^(columns-1) + // + // // LatencyColumns=5 latencyBase=8 + // <8µs <64µs <512µs <4096µs >=4096 + // + // // LatencyColumns=7 LatencyBase=4 + // <4µs <16µs <64µs <256µs <1024µs <4096 >=4096µs + // + // For linear histograms, the buckets are: =base*(column-1) + // + // // LatencyColumns=5 latencyBase=15 + // <15µs <30µs <45µs <60µs >=60µs + // + // // LatencyColumns=7 LatencyBase=5 + // <5µs <10µs <15µs <20µs <25µs <30µs >=30µs + // + // Default: 2 + LatencyBase int //= 2; +} + +func DefaultMetricsPolicy() *MetricsPolicy { + return &MetricsPolicy{ + HistogramType: histogram.Logarithmic, + LatencyColumns: 24, + LatencyBase: 2, + } +} diff --git a/node.go b/node.go index c34ed5cc..dddfb700 100644 --- a/node.go +++ b/node.go @@ -86,6 +86,8 @@ func newNode(cluster *Cluster, nv *nodeValidator) *Node { features: nv.features, + stats: *newNodeStats(cluster.MetricsPolicy()), + // Assign host to first IP alias because the server identifies nodes // by IP address (not hostname). connections: *newConnectionHeap(cluster.clientPolicy.MinConnectionsPerNode, cluster.clientPolicy.ConnectionQueueSize), diff --git a/node_stats.go b/node_stats.go index 596498fe..a4218c93 100644 --- a/node_stats.go +++ b/node_stats.go @@ -19,6 +19,7 @@ import ( "sync" iatomic "github.com/aerospike/aerospike-client-go/v7/internal/atomic" + hist "github.com/aerospike/aerospike-client-go/v7/types/histogram" ) // nodeStats keeps track of client's internal node statistics @@ -60,6 +61,54 @@ type nodeStats struct { NodeAdded iatomic.Int `json:"node-added-count"` // Total number of times nodes were removed from the client (not the same as actual nodes removed. Network disruptions between client and server may cause a node being dropped client-side) NodeRemoved iatomic.Int `json:"node-removed-count"` + + // Total number of transaction retries + TransactionRetryCount iatomic.Int `json:"transaction-retry-count"` + // Total number of transaction errors + TransactionErrorCount iatomic.Int `json:"transaction-error-count"` + + // Metrics for Get commands + GetMetrics hist.SyncHistogram[uint64] `json:"get-metrics"` + // Metrics for GetHeader commands + GetHeaderMetrics hist.SyncHistogram[uint64] `json:"get-header-metrics"` + // Metrics for Exists commands + ExistsMetrics hist.SyncHistogram[uint64] `json:"exists-metrics"` + // Metrics for Put commands + PutMetrics hist.SyncHistogram[uint64] `json:"put-metrics"` + // Metrics for Delete commands + DeleteMetrics hist.SyncHistogram[uint64] `json:"delete-metrics"` + // Metrics for Operate commands + OperateMetrics hist.SyncHistogram[uint64] `json:"operate-metrics"` + // Metrics for Query commands + QueryMetrics hist.SyncHistogram[uint64] `json:"query-metrics"` + // Metrics for Scan commands + ScanMetrics hist.SyncHistogram[uint64] `json:"scan-metrics"` + // Metrics for UDFMetrics commands + UDFMetrics hist.SyncHistogram[uint64] `json:"udf-metrics"` + // Metrics for Read only Batch commands + BatchReadMetrics hist.SyncHistogram[uint64] `json:"batch-read-metrics"` + // Metrics for Batch commands containing writes + BatchWriteMetrics hist.SyncHistogram[uint64] `json:"batch-write-metrics"` +} + +func newNodeStats(policy *MetricsPolicy) *nodeStats { + if policy == nil { + policy = DefaultMetricsPolicy() + } + + return &nodeStats{ + GetMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + GetHeaderMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + ExistsMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + PutMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + DeleteMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + OperateMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + QueryMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + ScanMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + UDFMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + BatchReadMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + BatchWriteMetrics: *hist.NewSync[uint64](policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns), + } } // latest returns the latest values to be used in aggregation and then resets the values @@ -84,6 +133,21 @@ func (ns *nodeStats) getAndReset() *nodeStats { PartitionMapUpdates: ns.PartitionMapUpdates.CloneAndSet(0), NodeAdded: ns.NodeAdded.CloneAndSet(0), NodeRemoved: ns.NodeRemoved.CloneAndSet(0), + + TransactionRetryCount: ns.TransactionRetryCount.CloneAndSet(0), + TransactionErrorCount: ns.TransactionErrorCount.CloneAndSet(0), + + GetMetrics: *ns.GetMetrics.CloneAndReset(), + GetHeaderMetrics: *ns.GetHeaderMetrics.CloneAndReset(), + ExistsMetrics: *ns.ExistsMetrics.CloneAndReset(), + PutMetrics: *ns.PutMetrics.CloneAndReset(), + DeleteMetrics: *ns.DeleteMetrics.CloneAndReset(), + OperateMetrics: *ns.OperateMetrics.CloneAndReset(), + QueryMetrics: *ns.QueryMetrics.CloneAndReset(), + ScanMetrics: *ns.ScanMetrics.CloneAndReset(), + UDFMetrics: *ns.UDFMetrics.CloneAndReset(), + BatchReadMetrics: *ns.BatchReadMetrics.CloneAndReset(), + BatchWriteMetrics: *ns.BatchWriteMetrics.CloneAndReset(), } ns.m.Unlock() @@ -111,14 +175,46 @@ func (ns *nodeStats) clone() nodeStats { PartitionMapUpdates: ns.PartitionMapUpdates.Clone(), NodeAdded: ns.NodeAdded.Clone(), NodeRemoved: ns.NodeRemoved.Clone(), + + TransactionRetryCount: ns.TransactionRetryCount.Clone(), + TransactionErrorCount: ns.TransactionErrorCount.Clone(), + + GetMetrics: *ns.GetMetrics.Clone(), + GetHeaderMetrics: *ns.GetHeaderMetrics.Clone(), + ExistsMetrics: *ns.ExistsMetrics.Clone(), + PutMetrics: *ns.PutMetrics.Clone(), + DeleteMetrics: *ns.DeleteMetrics.Clone(), + OperateMetrics: *ns.OperateMetrics.Clone(), + QueryMetrics: *ns.QueryMetrics.Clone(), + ScanMetrics: *ns.ScanMetrics.Clone(), + UDFMetrics: *ns.UDFMetrics.Clone(), + BatchReadMetrics: *ns.BatchReadMetrics.Clone(), + BatchWriteMetrics: *ns.BatchWriteMetrics.Clone(), } ns.m.Unlock() return res } +func (ns *nodeStats) reshape(policy *MetricsPolicy) { + ns.m.Lock() + ns.GetMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.GetHeaderMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.ExistsMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.PutMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.DeleteMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.OperateMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.QueryMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.ScanMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.UDFMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.BatchReadMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.BatchWriteMetrics.Reshape(policy.HistogramType, uint64(policy.LatencyBase), policy.LatencyColumns) + ns.m.Unlock() +} + func (ns *nodeStats) aggregate(newStats *nodeStats) { ns.m.Lock() + newStats.m.Lock() ns.ConnectionsAttempts.AddAndGet(newStats.ConnectionsAttempts.Get()) ns.ConnectionsSuccessful.AddAndGet(newStats.ConnectionsSuccessful.Get()) @@ -138,6 +234,22 @@ func (ns *nodeStats) aggregate(newStats *nodeStats) { ns.NodeAdded.AddAndGet(newStats.NodeAdded.Get()) ns.NodeRemoved.AddAndGet(newStats.NodeRemoved.Get()) + ns.TransactionRetryCount.AddAndGet(newStats.TransactionRetryCount.Get()) + ns.TransactionErrorCount.AddAndGet(newStats.TransactionErrorCount.Get()) + + ns.GetMetrics.Merge(&newStats.GetMetrics) + ns.GetHeaderMetrics.Merge(&newStats.GetHeaderMetrics) + ns.ExistsMetrics.Merge(&newStats.ExistsMetrics) + ns.PutMetrics.Merge(&newStats.PutMetrics) + ns.DeleteMetrics.Merge(&newStats.DeleteMetrics) + ns.OperateMetrics.Merge(&newStats.OperateMetrics) + ns.QueryMetrics.Merge(&newStats.QueryMetrics) + ns.ScanMetrics.Merge(&newStats.ScanMetrics) + ns.UDFMetrics.Merge(&newStats.UDFMetrics) + ns.BatchReadMetrics.Merge(&newStats.BatchReadMetrics) + ns.BatchWriteMetrics.Merge(&newStats.BatchWriteMetrics) + + newStats.m.Unlock() ns.m.Unlock() } @@ -160,6 +272,21 @@ func (ns nodeStats) MarshalJSON() ([]byte, error) { PartitionMapUpdates int `json:"partition-map-updates"` NodeAdded int `json:"node-added-count"` NodeRemoved int `json:"node-removed-count"` + + RetryCount int `json:"transaction-retry-count"` + ErrorCount int `json:"transaction-error-count"` + + GetMetrics hist.SyncHistogram[uint64] `json:"get-metrics"` + GetHeaderMetrics hist.SyncHistogram[uint64] `json:"get-header-metrics"` + ExistsMetrics hist.SyncHistogram[uint64] `json:"exists-metrics"` + PutMetrics hist.SyncHistogram[uint64] `json:"put-metrics"` + DeleteMetrics hist.SyncHistogram[uint64] `json:"delete-metrics"` + OperateMetrics hist.SyncHistogram[uint64] `json:"operate-metrics"` + QueryMetrics hist.SyncHistogram[uint64] `json:"query-metrics"` + ScanMetrics hist.SyncHistogram[uint64] `json:"scan-metrics"` + UDFMetrics hist.SyncHistogram[uint64] `json:"udf-metrics"` + BatchReadMetrics hist.SyncHistogram[uint64] `json:"batch-read-metrics"` + BatchWriteMetrics hist.SyncHistogram[uint64] `json:"batch-write-metrics"` }{ ns.ConnectionsAttempts.Get(), ns.ConnectionsSuccessful.Get(), @@ -178,6 +305,21 @@ func (ns nodeStats) MarshalJSON() ([]byte, error) { ns.PartitionMapUpdates.Get(), ns.NodeAdded.Get(), ns.NodeRemoved.Get(), + + ns.TransactionRetryCount.Get(), + ns.TransactionErrorCount.Get(), + + ns.GetMetrics, + ns.GetHeaderMetrics, + ns.ExistsMetrics, + ns.PutMetrics, + ns.DeleteMetrics, + ns.OperateMetrics, + ns.QueryMetrics, + ns.ScanMetrics, + ns.UDFMetrics, + ns.BatchReadMetrics, + ns.BatchWriteMetrics, }) } @@ -200,6 +342,21 @@ func (ns *nodeStats) UnmarshalJSON(data []byte) error { PartitionMapUpdates int `json:"partition-map-updates"` NodeAdded int `json:"node-added-count"` NodeRemoved int `json:"node-removed-count"` + + RetryCount int `json:"transaction-retry-count"` + ErrorCount int `json:"transaction-error-count"` + + GetMetrics hist.SyncHistogram[uint64] `json:"get-metrics"` + GetHeaderMetrics hist.SyncHistogram[uint64] `json:"get-header-metrics"` + ExistsMetrics hist.SyncHistogram[uint64] `json:"exists-metrics"` + PutMetrics hist.SyncHistogram[uint64] `json:"put-metrics"` + DeleteMetrics hist.SyncHistogram[uint64] `json:"delete-metrics"` + OperateMetrics hist.SyncHistogram[uint64] `json:"operate-metrics"` + QueryMetrics hist.SyncHistogram[uint64] `json:"query-metrics"` + ScanMetrics hist.SyncHistogram[uint64] `json:"scan-metrics"` + UDFMetrics hist.SyncHistogram[uint64] `json:"udf-metrics"` + BatchReadMetrics hist.SyncHistogram[uint64] `json:"batch-read-metrics"` + BatchWriteMetrics hist.SyncHistogram[uint64] `json:"batch-write-metrics"` }{} if err := json.Unmarshal(data, &aux); err != nil { @@ -224,5 +381,20 @@ func (ns *nodeStats) UnmarshalJSON(data []byte) error { ns.NodeAdded.Set(aux.NodeAdded) ns.NodeRemoved.Set(aux.NodeRemoved) + ns.TransactionRetryCount.Set(aux.RetryCount) + ns.TransactionErrorCount.Set(aux.ErrorCount) + + ns.GetMetrics = aux.GetMetrics + ns.GetHeaderMetrics = aux.GetHeaderMetrics + ns.ExistsMetrics = aux.ExistsMetrics + ns.PutMetrics = aux.PutMetrics + ns.DeleteMetrics = aux.DeleteMetrics + ns.OperateMetrics = aux.OperateMetrics + ns.QueryMetrics = aux.QueryMetrics + ns.ScanMetrics = aux.ScanMetrics + ns.UDFMetrics = aux.UDFMetrics + ns.BatchReadMetrics = aux.BatchReadMetrics + ns.BatchWriteMetrics = aux.BatchWriteMetrics + return nil } diff --git a/operate_command.go b/operate_command.go index 8c8d3198..abd830e3 100644 --- a/operate_command.go +++ b/operate_command.go @@ -70,6 +70,10 @@ func (cmd *operateCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *operateCommand) transactionType() transactionType { + return ttOperate +} + func (cmd *operateCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() diff --git a/proxy_query_partition_command.go b/proxy_query_partition_command.go index 4cf174d7..c569bca3 100644 --- a/proxy_query_partition_command.go +++ b/proxy_query_partition_command.go @@ -67,6 +67,10 @@ func (cmd *grpcQueryPartitionCommand) shouldRetry(e Error) bool { panic("UNREACHABLE") } +func (cmd *grpcQueryPartitionCommand) transactionType() transactionType { + return ttQuery +} + func (cmd *grpcQueryPartitionCommand) Execute() Error { panic("UNREACHABLE") } diff --git a/proxy_scan_command.go b/proxy_scan_command.go index f3022da1..88f2e605 100644 --- a/proxy_scan_command.go +++ b/proxy_scan_command.go @@ -68,6 +68,10 @@ func (cmd *grpcScanPartitionCommand) shouldRetry(e Error) bool { panic("UNREACHABLE") } +func (cmd *grpcScanPartitionCommand) transactionType() transactionType { + return ttScan +} + func (cmd *grpcScanPartitionCommand) Execute() Error { panic("UNREACHABLE") } diff --git a/query_command.go b/query_command.go index 6dd4271b..b5c2a042 100644 --- a/query_command.go +++ b/query_command.go @@ -48,6 +48,10 @@ func (cmd *queryCommand) parseResult(ifc command, conn *Connection) Error { return cmd.baseMultiCommand.parseResult(ifc, conn) } +func (cmd *queryCommand) transactionType() transactionType { + return ttQuery +} + // Execute will run the query. func (cmd *queryCommand) Execute() Error { err := cmd.execute(cmd) diff --git a/query_partition_command.go b/query_partition_command.go index 3e364691..abb0523a 100644 --- a/query_partition_command.go +++ b/query_partition_command.go @@ -51,6 +51,10 @@ func (cmd *queryPartitionCommand) shouldRetry(e Error) bool { return cmd.tracker != nil && cmd.tracker.shouldRetry(cmd.nodePartitions, e) } +func (cmd *queryPartitionCommand) transactionType() transactionType { + return ttQuery +} + func (cmd *queryPartitionCommand) Execute() Error { err := cmd.execute(cmd) if err != nil { diff --git a/query_partitiopn_objects_command.go b/query_partitiopn_objects_command.go index 008ff220..4c68a14c 100644 --- a/query_partitiopn_objects_command.go +++ b/query_partitiopn_objects_command.go @@ -50,6 +50,10 @@ func (cmd *queryPartitionObjectsCommand) shouldRetry(e Error) bool { return cmd.tracker != nil && cmd.tracker.shouldRetry(cmd.nodePartitions, e) } +func (cmd *queryPartitionObjectsCommand) transactionType() transactionType { + return ttQuery +} + func (cmd *queryPartitionObjectsCommand) Execute() Error { err := cmd.execute(cmd) if err != nil { diff --git a/read_command.go b/read_command.go index 45a3841a..87359473 100644 --- a/read_command.go +++ b/read_command.go @@ -268,6 +268,10 @@ func (cmd *readCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *readCommand) transactionType() transactionType { + return ttGet +} + func (cmd *readCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() diff --git a/read_header_command.go b/read_header_command.go index 77c75b8e..37d7f905 100644 --- a/read_header_command.go +++ b/read_header_command.go @@ -104,6 +104,10 @@ func (cmd *readHeaderCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *readHeaderCommand) transactionType() transactionType { + return ttGetHeader +} + func (cmd *readHeaderCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() diff --git a/scan_objects_command.go b/scan_objects_command.go index 662b3e7f..70c8fd46 100644 --- a/scan_objects_command.go +++ b/scan_objects_command.go @@ -59,6 +59,10 @@ func (cmd *scanObjectsCommand) parseResult(ifc command, conn *Connection) Error return cmd.baseMultiCommand.parseResult(ifc, conn) } +func (cmd *scanObjectsCommand) transactionType() transactionType { + return ttScan +} + func (cmd *scanObjectsCommand) Execute() Error { defer cmd.recordset.signalEnd() err := cmd.execute(cmd) diff --git a/scan_partition_command.go b/scan_partition_command.go index 80a3c6f3..6727c693 100644 --- a/scan_partition_command.go +++ b/scan_partition_command.go @@ -62,6 +62,10 @@ func (cmd *scanPartitionCommand) shouldRetry(e Error) bool { return cmd.tracker != nil && cmd.tracker.shouldRetry(cmd.nodePartitions, e) } +func (cmd *scanPartitionCommand) transactionType() transactionType { + return ttScan +} + func (cmd *scanPartitionCommand) Execute() Error { err := cmd.execute(cmd) if err != nil { diff --git a/scan_partition_objects_command.go b/scan_partition_objects_command.go index 92ef81de..6a734f64 100644 --- a/scan_partition_objects_command.go +++ b/scan_partition_objects_command.go @@ -54,6 +54,10 @@ func (cmd *scanPartitionObjectsCommand) shouldRetry(e Error) bool { return cmd.tracker != nil && cmd.tracker.shouldRetry(cmd.nodePartitions, e) } +func (cmd *scanPartitionObjectsCommand) transactionType() transactionType { + return ttScan +} + func (cmd *scanPartitionObjectsCommand) Execute() Error { err := cmd.execute(cmd) if err != nil { diff --git a/tools/benchmark/benchmark.go b/tools/benchmark/benchmark.go index 1e1c5a35..75f48a7a 100644 --- a/tools/benchmark/benchmark.go +++ b/tools/benchmark/benchmark.go @@ -173,7 +173,7 @@ func main() { var client as.ClientIfc if *grpc { - gclient, err := as.NewProxyClient(clientPolicy, dbHost) + gclient, err := as.NewProxyClientWithPolicyAndHost(clientPolicy, dbHost) if err != nil { logger.Fatal(err) } diff --git a/touch_command.go b/touch_command.go index dc35de0e..0c8abad9 100644 --- a/touch_command.go +++ b/touch_command.go @@ -140,6 +140,10 @@ func (cmd *touchCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *touchCommand) transactionType() transactionType { + return ttPut +} + func (cmd *touchCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() diff --git a/types/histogram/bench_histogram_test.go b/types/histogram/bench_histogram_test.go index 3820793d..48ea8824 100644 --- a/types/histogram/bench_histogram_test.go +++ b/types/histogram/bench_histogram_test.go @@ -1,4 +1,4 @@ -// Copyright 2014-2022 Aerospike, Inc. +// Copyright 2014-2024 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package histogram_test import ( "testing" - "github.com/aerospike/aerospike-client-go/v7/internal/histogram" + "github.com/aerospike/aerospike-client-go/v7/types/histogram" ) var ( @@ -26,14 +26,14 @@ var ( ) func Benchmark_Histogram_Linear_Add(b *testing.B) { - h := histogram.NewLinear[int](5, 10) + h := histogram.New[int](histogram.Linear, 5, 10) for i := 0; i < b.N; i++ { h.Add(i) } } func Benchmark_Histogram_Linear_Median(b *testing.B) { - h := histogram.NewLinear[int](50, 101) + h := histogram.New[int](histogram.Linear, 50, 101) for i := 0; i < 10000; i++ { h.Add(i) } diff --git a/types/histogram/histogram.go b/types/histogram/histogram.go index 6d12585b..bf2cb7a8 100644 --- a/types/histogram/histogram.go +++ b/types/histogram/histogram.go @@ -1,48 +1,72 @@ +// Copyright 2014-2024 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 histogram import ( + "errors" "fmt" "math" "strings" ) -type HistType byte +type Type byte const ( - Linear HistType = iota - Exponential + Linear Type = iota + Logarithmic ) type hvals interface { - int | uint | - int64 | int32 | int16 | int8 | - uint64 | uint32 | uint16 | uint8 | - float64 | float32 + ~int | ~uint | + ~int64 | ~int32 | ~int16 | ~int8 | + ~uint64 | ~uint32 | ~uint16 | ~uint8 | + ~float64 | ~float32 } type Histogram[T hvals] struct { - htype HistType + htype Type base T - Buckets []uint // slot -> count - Min, Max T - Sum float64 - Count uint + Buckets []uint64 `json:"buckets"` // slot -> count + Min T `json:"min"` + Max T `json:"max"` + Sum float64 `json:"sum"` + Count uint64 `json:"count"` +} + +func New[T hvals](htype Type, base T, buckets int) *Histogram[T] { + return &Histogram[T]{ + htype: htype, + base: base, + Buckets: make([]uint64, buckets), + } } func NewLinear[T hvals](base T, buckets int) *Histogram[T] { return &Histogram[T]{ htype: Linear, base: base, - Buckets: make([]uint, buckets), + Buckets: make([]uint64, buckets), } } func NewExponential[T hvals](base T, buckets int) *Histogram[T] { return &Histogram[T]{ - htype: Exponential, + htype: Logarithmic, base: base, - Buckets: make([]uint, buckets), + Buckets: make([]uint64, buckets), } } @@ -57,6 +81,21 @@ func (h *Histogram[T]) Reset() { h.Count = 0 } +func (h *Histogram[T]) Reshape(htype Type, base T, buckets int) { + if h.htype == htype && h.base == base && len(h.Buckets) == buckets { + return + } + + h.htype = htype + h.base = base + h.Buckets = make([]uint64, buckets) + + h.Min = 0 + h.Max = 0 + h.Sum = 0 + h.Count = 0 +} + func (h *Histogram[T]) String() string { res := new(strings.Builder) switch h.htype { @@ -66,7 +105,7 @@ func (h *Histogram[T]) String() string { fmt.Fprintf(res, "[%v, %v) => %d\n", v, v+float64(h.base), h.Buckets[i]) } fmt.Fprintf(res, "[%v, inf) => %d\n", float64(h.base)*float64(len(h.Buckets)-1), h.Buckets[len(h.Buckets)-1]) - case Exponential: + case Logarithmic: fmt.Fprintf(res, "[0, %v) => %d\n", float64(h.base), h.Buckets[0]) for i := 1; i < len(h.Buckets)-1; i++ { v := math.Pow(float64(h.base), float64(i)) @@ -77,8 +116,59 @@ func (h *Histogram[T]) String() string { return res.String() } +func (h *Histogram[T]) Clone() *Histogram[T] { + b := make([]uint64, len(h.Buckets)) + copy(b, h.Buckets) + return &Histogram[T]{ + htype: h.htype, + base: h.base, + + Buckets: b, + Min: h.Min, + Max: h.Max, + Sum: h.Sum, + Count: h.Count, + } +} + +func (h *Histogram[T]) CloneAndReset() *Histogram[T] { + res := h.Clone() + h.Reset() + return res +} + +func (h *Histogram[T]) Merge(other *Histogram[T]) error { + if h.base != other.base || h.htype != other.htype || len(h.Buckets) != len(other.Buckets) { + return errors.New("Histograms to not match") + } + + if other.Min < h.Min || h.Min == 0 { + h.Min = other.Min + } + + if other.Max > h.Max { + h.Max = other.Max + } + + h.Sum += other.Sum + h.Count += uint64(other.Count) + + for i := range h.Buckets { + h.Buckets[i] += other.Buckets[i] + } + + return nil +} + +func (h *Histogram[T]) Average() float64 { + if h.Count > 0 { + return h.Sum / float64(h.Count) + } + return 0 +} + func (h *Histogram[T]) Median() T { - var s uint = 0 + var s uint64 = 0 c := h.Count / 2 for i, bv := range h.Buckets { s += bv @@ -109,11 +199,13 @@ func (h *Histogram[T]) Add(v T) { h.Count++ var slot int - switch h.htype { - case Linear: - slot = int(math.Floor(float64(v / T(h.base)))) - case Exponential: - slot = int(math.Floor(math.Log(float64(v)) / math.Log(float64(h.base)))) + if v > 0 { + switch h.htype { + case Linear: + slot = int(math.Floor(float64(v / T(h.base)))) + case Logarithmic: + slot = int(math.Floor(math.Log(float64(v)) / math.Log(float64(h.base)))) + } } if slot >= len(h.Buckets) { diff --git a/types/histogram/histogram_test.go b/types/histogram/histogram_test.go index 962a2732..6b231ee2 100644 --- a/types/histogram/histogram_test.go +++ b/types/histogram/histogram_test.go @@ -1,4 +1,4 @@ -// Copyright 2014-2022 Aerospike, Inc. +// Copyright 2014-2024 Aerospike, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package histogram_test import ( "testing" - "github.com/aerospike/aerospike-client-go/v7/internal/histogram" + "github.com/aerospike/aerospike-client-go/v7/types/histogram" gg "github.com/onsi/ginkgo/v2" gm "github.com/onsi/gomega" @@ -36,7 +36,7 @@ var _ = gg.Describe("Histogram", func() { gg.It("must make the correct histogram", func() { l := []int{1, 1, 3, 4, 5, 5, 9, 11, 11, 11, 16, 16, 21} - h := histogram.NewLinear[int](5, 5) + h := histogram.New[int](histogram.Linear, 5, 5) sum := 0 for _, v := range l { @@ -48,12 +48,12 @@ var _ = gg.Describe("Histogram", func() { gm.Expect(h.Max).To(gm.Equal(21)) gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) gm.Expect(h.Sum).To(gm.Equal(float64(sum))) - gm.Expect(h.Buckets).To(gm.Equal([]uint{4, 3, 3, 2, 1})) + gm.Expect(h.Buckets).To(gm.Equal([]uint64{4, 3, 3, 2, 1})) }) gg.It("must find the correct median", func() { l := []int{1e3, 2e3, 3e3, 4e3, 5e3, 6e3, 7e3, 8e3, 9e3, 10e3, 11e3, 12e3, 13e3} - h := histogram.NewLinear[int](1000, 10) + h := histogram.New[int](histogram.Linear, 1000, 10) sum := 0 for _, v := range l { @@ -65,7 +65,7 @@ var _ = gg.Describe("Histogram", func() { gm.Expect(h.Max).To(gm.Equal(13000)) gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) gm.Expect(h.Sum).To(gm.Equal(float64(sum))) - gm.Expect(h.Buckets).To(gm.Equal([]uint{0, 1, 1, 1, 1, 1, 1, 1, 1, 5})) + gm.Expect(h.Buckets).To(gm.Equal([]uint64{0, 1, 1, 1, 1, 1, 1, 1, 1, 5})) gm.Expect(h.Median()).To(gm.Equal(7000)) }) @@ -75,7 +75,7 @@ var _ = gg.Describe("Histogram", func() { gg.It("must make the correct histogram", func() { l := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20} - h := histogram.NewExponential[int](2, 5) + h := histogram.New[int](histogram.Logarithmic, 2, 5) sum := 0 for _, v := range l { @@ -87,12 +87,29 @@ var _ = gg.Describe("Histogram", func() { gm.Expect(h.Max).To(gm.Equal(20)) gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) gm.Expect(h.Sum).To(gm.Equal(float64(sum))) - gm.Expect(h.Buckets).To(gm.Equal([]uint{2, 2, 4, 8, 5})) + gm.Expect(h.Buckets).To(gm.Equal([]uint64{2, 2, 4, 8, 5})) + }) + + gg.It("must make the correct histogram on barriers", func() { + l := []int{0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 255, 256, 257, 511, 512, 513, 1023, 1024, 1025} + h := histogram.New[int](histogram.Logarithmic, 4, 8) + + sum := 0 + for _, v := range l { + sum += v + h.Add(v) + } + + gm.Expect(h.Min).To(gm.Equal(0)) + gm.Expect(h.Max).To(gm.Equal(1025)) + gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) + gm.Expect(h.Sum).To(gm.Equal(float64(sum))) + gm.Expect(h.Buckets).To(gm.Equal([]uint64{4, 6, 6, 6, 6, 2, 0, 0})) }) gg.It("must find the correct median", func() { l := []int{10e3, 12e3, 3e3, 4e3, 50e3, 6e5, 75e3, 7e3, 21e3, 11e3, 113e3, 29e3, 189e3} - h := histogram.NewExponential[int](2, 18) + h := histogram.New[int](histogram.Logarithmic, 2, 18) sum := 0 for _, v := range l { @@ -104,7 +121,7 @@ var _ = gg.Describe("Histogram", func() { gm.Expect(h.Max).To(gm.Equal(int(600e3))) gm.Expect(uint64(h.Count)).To(gm.Equal(uint64(len(l)))) gm.Expect(h.Sum).To(gm.Equal(float64(sum))) - gm.Expect(h.Buckets).To(gm.Equal([]uint{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 2, 1, 2, 2})) + gm.Expect(h.Buckets).To(gm.Equal([]uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 3, 2, 1, 2, 2})) gm.Expect(h.Median()).To(gm.Equal(1 << 14)) }) }) diff --git a/types/histogram/log2hist.go b/types/histogram/log2hist.go index 4badabd9..b81cc055 100644 --- a/types/histogram/log2hist.go +++ b/types/histogram/log2hist.go @@ -1,3 +1,17 @@ +// Copyright 2014-2024 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 histogram import ( @@ -91,7 +105,10 @@ func (h *Log2) Add(v uint64) { h.Sum += v h.Count++ - slot := fastLog2(v) + var slot int + if v > 0 { + slot = fastLog2(v) + } if slot >= len(h.Buckets) { h.Buckets[len(h.Buckets)-1]++ diff --git a/types/histogram/sync_histogram.go b/types/histogram/sync_histogram.go new file mode 100644 index 00000000..45d2da76 --- /dev/null +++ b/types/histogram/sync_histogram.go @@ -0,0 +1,236 @@ +// Copyright 2014-2024 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 histogram + +import ( + "errors" + "fmt" + "math" + "strings" + "sync" +) + +type SyncHistogram[T hvals] struct { + l sync.RWMutex + htype Type + base T + + Buckets []uint64 `json:"buckets"` // slot -> count + Min T `json:"min"` + Max T `json:"max"` + Sum float64 `json:"sum"` + Count uint64 `json:"count"` +} + +func NewSync[T hvals](htype Type, base T, buckets int) *SyncHistogram[T] { + return &SyncHistogram[T]{ + htype: htype, + base: base, + Buckets: make([]uint64, buckets), + } +} + +func (h *SyncHistogram[T]) Reset() { + h.l.Lock() + for i := range h.Buckets { + h.Buckets[i] = 0 + } + + h.Min = 0 + h.Max = 0 + h.Sum = 0 + h.Count = 0 + h.l.Unlock() +} + +func (h *SyncHistogram[T]) Reshape(htype Type, base T, buckets int) { + h.l.Lock() + if h.htype == htype && h.base == base && len(h.Buckets) == buckets { + h.l.Unlock() + return + } + + h.htype = htype + h.base = base + h.Buckets = make([]uint64, buckets) + h.l.Unlock() +} + +func (h *SyncHistogram[T]) String() string { + h.l.RLock() + res := new(strings.Builder) + switch h.htype { + case Linear: + for i := 0; i < len(h.Buckets)-1; i++ { + v := float64(h.base) * float64(i) + fmt.Fprintf(res, "[%v, %v) => %d\n", v, v+float64(h.base), h.Buckets[i]) + } + fmt.Fprintf(res, "[%v, inf) => %d\n", float64(h.base)*float64(len(h.Buckets)-1), h.Buckets[len(h.Buckets)-1]) + case Logarithmic: + fmt.Fprintf(res, "[0, %v) => %d\n", float64(h.base), h.Buckets[0]) + for i := 1; i < len(h.Buckets)-1; i++ { + v := math.Pow(float64(h.base), float64(i)) + fmt.Fprintf(res, "[%v, %v) => %d\n", v, v*float64(h.base), h.Buckets[i]) + } + fmt.Fprintf(res, "[%v, inf) => %d\n", math.Pow(float64(h.base), float64(len(h.Buckets))-1), h.Buckets[len(h.Buckets)-1]) + } + h.l.RUnlock() + return res.String() +} + +func (h *SyncHistogram[T]) Clone() *SyncHistogram[T] { + h.l.Lock() + b := make([]uint64, len(h.Buckets)) + copy(b, h.Buckets) + res := &SyncHistogram[T]{ + htype: h.htype, + base: h.base, + + Buckets: b, + Min: h.Min, + Max: h.Max, + Sum: h.Sum, + Count: h.Count, + } + h.l.Unlock() + return res +} + +func (h *SyncHistogram[T]) CloneAndReset() *SyncHistogram[T] { + h.l.Lock() + b := make([]uint64, len(h.Buckets)) + copy(b, h.Buckets) + res := &SyncHistogram[T]{ + htype: h.htype, + base: h.base, + + Buckets: b, + Min: h.Min, + Max: h.Max, + Sum: h.Sum, + Count: h.Count, + } + + // Reset + for i := range h.Buckets { + h.Buckets[i] = 0 + } + + h.Min = 0 + h.Max = 0 + h.Sum = 0 + h.Count = 0 + h.l.Unlock() + return res +} + +func (h *SyncHistogram[T]) Merge(other *SyncHistogram[T]) error { + h.l.Lock() + other.l.RLock() + if h.base != other.base || h.htype != other.htype || len(h.Buckets) != len(other.Buckets) { + other.l.RUnlock() + h.l.Unlock() + return errors.New("Histograms to not match") + } + + if other.Min < h.Min || h.Min == 0 { + h.Min = other.Min + } + + if other.Max > h.Max { + h.Max = other.Max + } + + h.Sum += other.Sum + h.Count += uint64(other.Count) + + for i := range h.Buckets { + h.Buckets[i] += other.Buckets[i] + } + other.l.RUnlock() + h.l.Unlock() + + return nil +} + +func (h *SyncHistogram[T]) Average() float64 { + h.l.RLock() + if h.Count > 0 { + res := h.Sum / float64(h.Count) + h.l.RUnlock() + return res + } + h.l.RUnlock() + return 0 +} + +func (h *SyncHistogram[T]) Median() T { + h.l.RLock() + var s uint64 = 0 + c := h.Count / 2 + for i, bv := range h.Buckets { + s += bv + if s >= c { + // found the bucket + if h.htype == Linear { + res := T(i+1) * h.base + h.l.RUnlock() + return res + } + res := T(math.Pow(float64(h.base), float64(i+1))) + h.l.RUnlock() + return res + } + } + res := h.Max + h.l.RUnlock() + return res +} + +func (h *SyncHistogram[T]) Add(v T) { + h.l.Lock() + if h.Count == 0 { + h.Max = v + h.Min = v + } else { + if v > h.Max { + h.Max = v + } else if v < h.Min { + h.Min = v + } + } + + h.Sum += float64(v) + h.Count++ + + var slot int + if v > 0 { + switch h.htype { + case Linear: + slot = int(math.Floor(float64(v / T(h.base)))) + case Logarithmic: + slot = int(math.Floor(math.Log(float64(v)) / math.Log(float64(h.base)))) + } + } + + if slot >= len(h.Buckets) { + h.Buckets[len(h.Buckets)-1]++ + } else if slot < 0 { + h.Buckets[0]++ + } else { + h.Buckets[slot]++ + } + h.l.Unlock() +} diff --git a/write_command.go b/write_command.go index f7160663..f19e245f 100644 --- a/write_command.go +++ b/write_command.go @@ -114,6 +114,10 @@ func (cmd *writeCommand) Execute() Error { return cmd.execute(cmd) } +func (cmd *writeCommand) transactionType() transactionType { + return ttPut +} + func (cmd *writeCommand) ExecuteGRPC(clnt *ProxyClient) Error { defer cmd.grpcPutBufferBack() From aadfdfe36f49c401e069a5fd0a3e1a658eb10932 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Thu, 2 May 2024 22:41:53 +0200 Subject: [PATCH 12/28] [CLIENT-2238] Convert batch calls with just a few keys per node in sub-batches to Get requests If the number keys for a sub-batch to a node is equal or less then the value set in BatchPolicy.DirectGetThreshold, the client use direct get instead of batch commands to reduce the load on the server. --- batch_command.go | 2 ++ batch_command_delete.go | 29 ++++++++++++++++++++ batch_command_exists.go | 27 +++++++++++++++++++ batch_command_get.go | 36 +++++++++++++++++++++++++ batch_command_operate.go | 55 ++++++++++++++++++++++++++++++++++++++ batch_command_udf.go | 29 ++++++++++++++++++++ batch_delete_policy.go | 15 +++++++++++ batch_index_command_get.go | 41 ++++++++++++++++++++++++++++ batch_policy.go | 6 +++++ batch_read.go | 4 +++ batch_read_policy.go | 14 ++++++++++ batch_record.go | 7 +++++ batch_udf_policy.go | 14 ++++++++++ batch_write_policy.go | 17 ++++++++++++ client.go | 26 ++++++++++++------ client_reflect.go | 16 +++++++++++ cluster.go | 2 ++ command.go | 2 +- 18 files changed, 333 insertions(+), 9 deletions(-) diff --git a/batch_command.go b/batch_command.go index cc3c8fb3..93f20d75 100644 --- a/batch_command.go +++ b/batch_command.go @@ -27,6 +27,8 @@ type batcher interface { retryBatch(ifc batcher, cluster *Cluster, deadline time.Time, iteration int, commandWasSent bool) (bool, Error) generateBatchNodes(*Cluster) ([]*batchNode, Error) setSequence(int, int) + + executeSingle(*Client) Error } type batchCommand struct { diff --git a/batch_command_delete.go b/batch_command_delete.go index f9c5eecf..51e3c18f 100644 --- a/batch_command_delete.go +++ b/batch_command_delete.go @@ -165,7 +165,36 @@ func (cmd *batchCommandDelete) transactionType() transactionType { return ttBatchWrite } +func (cmd *batchCommandDelete) executeSingle(client *Client) Error { + for i, key := range cmd.keys { + res, err := client.Operate(cmd.policy.toWritePolicy(), key, DeleteOp()) + cmd.records[i].setRecord(res) + if err != nil { + cmd.records[i].setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandDelete) Execute() Error { + if len(cmd.keys) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_exists.go b/batch_command_exists.go index 5243136a..18d532f0 100644 --- a/batch_command_exists.go +++ b/batch_command_exists.go @@ -110,7 +110,34 @@ func (cmd *batchCommandExists) transactionType() transactionType { return ttBatchRead } +func (cmd *batchCommandExists) executeSingle(client *Client) Error { + var err Error + for _, offset := range cmd.batch.offsets { + cmd.existsArray[offset], err = client.Exists(&cmd.policy.BasePolicy, cmd.keys[offset]) + if err != nil { + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandExists) Execute() Error { + if len(cmd.batch.offsets) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_get.go b/batch_command_get.go index 13fd18b1..9328c353 100644 --- a/batch_command_get.go +++ b/batch_command_get.go @@ -216,7 +216,43 @@ func (cmd *batchCommandGet) transactionType() transactionType { return ttBatchRead } +func (cmd *batchCommandGet) executeSingle(client *Client) Error { + for _, offset := range cmd.batch.offsets { + var err Error + if cmd.objects == nil { + if (cmd.readAttr & _INFO1_NOBINDATA) == _INFO1_NOBINDATA { + cmd.records[offset], err = client.GetHeader(&cmd.policy.BasePolicy, cmd.keys[offset]) + } else { + cmd.records[offset], err = client.Get(&cmd.policy.BasePolicy, cmd.keys[offset], cmd.binNames...) + } + } else { + err = client.getObjectDirect(&cmd.policy.BasePolicy, cmd.keys[offset], cmd.objects[offset]) + cmd.objectsFound[offset] = err == nil + } + if err != nil { + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandGet) Execute() Error { + if len(cmd.batch.offsets) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_operate.go b/batch_command_operate.go index f0754ccd..e6421b6f 100644 --- a/batch_command_operate.go +++ b/batch_command_operate.go @@ -225,7 +225,62 @@ func (cmd *batchCommandOperate) parseRecord(key *Key, opCount int, generation, e return newRecord(cmd.node, key, bins, generation, expiration), nil } +func (cmd *batchCommandOperate) executeSingle(client *Client) Error { + var res *Record + var err Error + for _, br := range cmd.records { + + switch br := br.(type) { + case *BatchRead: + var ops []*Operation + if br.headerOnly() { + ops = []*Operation{GetHeaderOp()} + } else if len(br.BinNames) > 0 { + for i := range br.BinNames { + ops = append(ops, GetBinOp(br.BinNames[i])) + } + } else { + ops = br.Ops + } + res, err = client.Operate(br.Policy.toWritePolicy(cmd.policy), br.Key, br.Ops...) + case *BatchWrite: + res, err = client.Operate(br.policy.toWritePolicy(cmd.policy), br.Key, br.ops...) + br.setRecord(res) + case *BatchDelete: + res, err = client.Operate(br.policy.toWritePolicy(cmd.policy), br.Key, DeleteOp()) + br.setRecord(res) + case *BatchUDF: + res, err = client.execute(br.policy.toWritePolicy(cmd.policy), br.Key, br.packageName, br.functionName, br.functionArgs...) + } + + br.setRecord(res) + if err != nil { + br.BatchRec().setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + + return err + } + } + return nil +} + func (cmd *batchCommandOperate) Execute() Error { + if len(cmd.records) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_udf.go b/batch_command_udf.go index cab4af33..d774c9ea 100644 --- a/batch_command_udf.go +++ b/batch_command_udf.go @@ -174,7 +174,36 @@ func (cmd *batchCommandUDF) isRead() bool { return !cmd.attr.hasWrite } +func (cmd *batchCommandUDF) executeSingle(client *Client) Error { + for i, key := range cmd.keys { + res, err := client.execute(cmd.policy.toWritePolicy(), key, cmd.packageName, cmd.functionName, cmd.args...) + cmd.records[i].setRecord(res) + if err != nil { + cmd.records[i].setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandUDF) Execute() Error { + if len(cmd.keys) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_delete_policy.go b/batch_delete_policy.go index d4bada4a..0f66d5c7 100644 --- a/batch_delete_policy.go +++ b/batch_delete_policy.go @@ -57,3 +57,18 @@ func NewBatchDeletePolicy() *BatchDeletePolicy { GenerationPolicy: NONE, } } + +func (bdp *BatchDeletePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if bdp.FilterExpression != nil { + wp.FilterExpression = bdp.FilterExpression + } + wp.CommitLevel = bdp.CommitLevel + wp.GenerationPolicy = bdp.GenerationPolicy + wp.Generation = bdp.Generation + wp.DurableDelete = bdp.DurableDelete + wp.SendKey = bdp.SendKey + + return wp +} diff --git a/batch_index_command_get.go b/batch_index_command_get.go index fb1f013c..e6cab565 100644 --- a/batch_index_command_get.go +++ b/batch_index_command_get.go @@ -14,6 +14,8 @@ package aerospike +import "github.com/aerospike/aerospike-client-go/v7/types" + type batchIndexCommandGet struct { batchCommandGet } @@ -56,9 +58,48 @@ func (cmd *batchIndexCommandGet) writeBuffer(ifc command) Error { } func (cmd *batchIndexCommandGet) Execute() Error { + if len(cmd.batch.offsets) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } +func (cmd *batchIndexCommandGet) executeSingle(client *Client) Error { + for i, br := range cmd.indexRecords { + var ops []*Operation + if br.headerOnly() { + ops = []*Operation{GetHeaderOp()} + } else if len(br.BinNames) > 0 { + for i := range br.BinNames { + ops = append(ops, GetBinOp(br.BinNames[i])) + } + } else { + ops = br.Ops + } + res, err := client.Operate(cmd.policy.toWritePolicy(), br.Key, br.Ops...) + cmd.indexRecords[i].setRecord(res) + if err != nil { + cmd.indexRecords[i].setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchIndexCommandGet) generateBatchNodes(cluster *Cluster) ([]*batchNode, Error) { return newBatchNodeListRecords(cluster, cmd.policy, cmd.indexRecords, cmd.sequenceAP, cmd.sequenceSC, cmd.batch) } diff --git a/batch_policy.go b/batch_policy.go index eb1f73f3..9af86f48 100644 --- a/batch_policy.go +++ b/batch_policy.go @@ -117,6 +117,12 @@ func NewWriteBatchPolicy() *BatchPolicy { return res } +func (p *BatchPolicy) toWritePolicy() *WritePolicy { + wp := NewWritePolicy(0, 0) + wp.BasePolicy = p.BasePolicy + return wp +} + func (p *BatchPolicy) grpc_write() *kvs.WritePolicy { return &kvs.WritePolicy{ Replica: p.ReplicaPolicy.grpc(), diff --git a/batch_read.go b/batch_read.go index d47d8f46..f81a5d8f 100644 --- a/batch_read.go +++ b/batch_read.go @@ -153,3 +153,7 @@ func (br *BatchRead) size(parentPolicy *BasePolicy) (int, Error) { func (br *BatchRead) String() string { return fmt.Sprintf("%s: %v", br.Key, br.BinNames) } + +func (br *BatchRead) headerOnly() bool { + return br.BinNames == nil && !br.ReadAllBins +} diff --git a/batch_read_policy.go b/batch_read_policy.go index 7ca4d941..f741805a 100644 --- a/batch_read_policy.go +++ b/batch_read_policy.go @@ -53,3 +53,17 @@ func NewBatchReadPolicy() *BatchReadPolicy { ReadModeSC: ReadModeSCSession, } } + +func (brp *BatchReadPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if brp.FilterExpression != nil { + wp.FilterExpression = brp.FilterExpression + } + + wp.ReadModeAP = brp.ReadModeAP + wp.ReadModeSC = brp.ReadModeSC + wp.ReadTouchTTLPercent = brp.ReadTouchTTLPercent + + return wp +} diff --git a/batch_record.go b/batch_record.go index a9b867f5..8196e5f5 100644 --- a/batch_record.go +++ b/batch_record.go @@ -129,6 +129,13 @@ func (br *BatchRecord) setRecord(record *Record) { br.ResultCode = types.OK } +// Set error result directly. +func (br *BatchRecord) setRawError(err Error) { + br.ResultCode = err.resultCode() + br.InDoubt = err.IsInDoubt() + br.Err = err +} + // Set error result. For internal use only. func (br *BatchRecord) setError(node *Node, resultCode types.ResultCode, inDoubt bool) { br.ResultCode = resultCode diff --git a/batch_udf_policy.go b/batch_udf_policy.go index 0924bd55..e95bd29c 100644 --- a/batch_udf_policy.go +++ b/batch_udf_policy.go @@ -55,3 +55,17 @@ func NewBatchUDFPolicy() *BatchUDFPolicy { CommitLevel: COMMIT_ALL, } } + +func (bup *BatchUDFPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if bup.FilterExpression != nil { + wp.FilterExpression = bup.FilterExpression + } + wp.CommitLevel = bup.CommitLevel + wp.Expiration = bup.Expiration + wp.DurableDelete = bup.DurableDelete + wp.SendKey = bup.SendKey + + return wp +} diff --git a/batch_write_policy.go b/batch_write_policy.go index 33c7c99a..008d35dd 100644 --- a/batch_write_policy.go +++ b/batch_write_policy.go @@ -82,3 +82,20 @@ func NewBatchWritePolicy() *BatchWritePolicy { CommitLevel: COMMIT_ALL, } } + +func (bwp *BatchWritePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if bwp.FilterExpression != nil { + wp.FilterExpression = bwp.FilterExpression + } + wp.RecordExistsAction = bwp.RecordExistsAction + wp.CommitLevel = bwp.CommitLevel + wp.GenerationPolicy = bwp.GenerationPolicy + wp.Generation = bwp.Generation + wp.Expiration = bwp.Expiration + wp.DurableDelete = bwp.DurableDelete + wp.SendKey = bwp.SendKey + + return wp +} diff --git a/client.go b/client.go index 67a7fa52..bead1fb0 100644 --- a/client.go +++ b/client.go @@ -105,6 +105,9 @@ func NewClientWithPolicyAndHost(policy *ClientPolicy, hosts ...*Host) (*Client, DefaultInfoPolicy: NewInfoPolicy(), } + // back reference especially used in batch commands + cluster.client = client + runtime.SetFinalizer(client, clientFinalizer) return client, err @@ -929,18 +932,11 @@ func (clnt *Client) ListUDF(policy *BasePolicy) ([]*UDF, Error) { // This method is only supported by Aerospike 3+ servers. // If the policy is nil, the default relevant policy will be used. func (clnt *Client) Execute(policy *WritePolicy, key *Key, packageName string, functionName string, args ...Value) (interface{}, Error) { - policy = clnt.getUsableWritePolicy(policy) - command, err := newExecuteCommand(clnt.cluster, policy, key, packageName, functionName, NewValueArray(args)) + record, err := clnt.execute(policy, key, packageName, functionName, args...) if err != nil { return nil, err } - if err := command.Execute(); err != nil { - return nil, err - } - - record := command.GetRecord() - if record == nil || len(record.Bins) == 0 { return nil, nil } @@ -956,6 +952,20 @@ func (clnt *Client) Execute(policy *WritePolicy, key *Key, packageName string, f return nil, ErrUDFBadResponse.err() } +func (clnt *Client) execute(policy *WritePolicy, key *Key, packageName string, functionName string, args ...Value) (*Record, Error) { + policy = clnt.getUsableWritePolicy(policy) + command, err := newExecuteCommand(clnt.cluster, policy, key, packageName, functionName, NewValueArray(args)) + if err != nil { + return nil, err + } + + if err := command.Execute(); err != nil { + return nil, err + } + + return command.GetRecord(), nil +} + //---------------------------------------------------------- // Query/Execute (Supported by Aerospike 3+ servers only) //---------------------------------------------------------- diff --git a/client_reflect.go b/client_reflect.go index e1ab64bb..d6dd4fef 100644 --- a/client_reflect.go +++ b/client_reflect.go @@ -80,6 +80,22 @@ func (clnt *Client) GetObject(policy *BasePolicy, key *Key, obj interface{}) Err return command.Execute() } +// getObjectDirect reads a record for specified key and puts the result into the provided object. +// The policy can be used to specify timeouts. +// If the policy is nil, the default relevant policy will be used. +func (clnt *Client) getObjectDirect(policy *BasePolicy, key *Key, rval *reflect.Value) Error { + policy = clnt.getUsablePolicy(policy) + + binNames := objectMappings.getFields(rval.Type()) + command, err := newReadCommand(clnt.cluster, policy, key, binNames, nil) + if err != nil { + return err + } + + command.object = rval + return command.Execute() +} + // BatchGetObjects reads multiple record headers and bins for specified keys in one batch request. // The returned objects are in positional order with the original key array order. // If a key is not found, the positional object will not change, and the positional found boolean will be false. diff --git a/cluster.go b/cluster.go index 6de03013..0152c621 100644 --- a/cluster.go +++ b/cluster.go @@ -32,6 +32,8 @@ import ( // Cluster encapsulates the aerospike cluster nodes and manages // them. type Cluster struct { + client *Client + // Initial host nodes specified by user. seeds iatomic.SyncVal //[]*Host diff --git a/command.go b/command.go index 02cfb29d..d4571da1 100644 --- a/command.go +++ b/command.go @@ -2884,7 +2884,7 @@ func deviceOverloadError(err Error) bool { func applyTransactionMetrics(node *Node, tt transactionType, tb time.Time) { if node != nil && node.cluster.MetricsEnabled() { - applyMetrics(tt, node.stats, tb) + applyMetrics(tt, &node.stats, tb) } } From d33cb54ad4f9e564ba9d4f0697da2cec3d6648be Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Fri, 3 May 2024 00:32:28 +0200 Subject: [PATCH 13/28] [CLIENT-2905] Fix inconsistency of handling in-doubt flag --- batch_command.go | 6 +++--- batch_command_delete.go | 2 +- batch_command_operate.go | 10 +++++----- batch_command_udf.go | 2 +- command.go | 36 ++++++++++++++++++------------------ error.go | 8 +++----- 6 files changed, 31 insertions(+), 33 deletions(-) diff --git a/batch_command.go b/batch_command.go index 93f20d75..01115cdd 100644 --- a/batch_command.go +++ b/batch_command.go @@ -24,7 +24,7 @@ type batcher interface { cloneBatchCommand(batch *batchNode) batcher filteredOut() int - retryBatch(ifc batcher, cluster *Cluster, deadline time.Time, iteration int, commandWasSent bool) (bool, Error) + retryBatch(ifc batcher, cluster *Cluster, deadline time.Time, iteration int) (bool, Error) generateBatchNodes(*Cluster) ([]*batchNode, Error) setSequence(int, int) @@ -58,7 +58,7 @@ func (cmd *batchCommand) prepareRetry(ifc command, isTimeout bool) bool { return false } -func (cmd *batchCommand) retryBatch(ifc batcher, cluster *Cluster, deadline time.Time, iteration int, commandWasSent bool) (bool, Error) { +func (cmd *batchCommand) retryBatch(ifc batcher, cluster *Cluster, deadline time.Time, iteration int) (bool, Error) { // Retry requires keys for this node to be split among other nodes. // This is both recursive and exponential. batchNodes, err := ifc.generateBatchNodes(cluster) @@ -76,7 +76,7 @@ func (cmd *batchCommand) retryBatch(ifc batcher, cluster *Cluster, deadline time for _, batchNode := range batchNodes { command := ifc.cloneBatchCommand(batchNode) command.setSequence(cmd.sequenceAP, cmd.sequenceSC) - if err := command.executeAt(command, cmd.policy.GetBasePolicy(), deadline, iteration, commandWasSent); err != nil { + if err := command.executeAt(command, cmd.policy.GetBasePolicy(), deadline, iteration); err != nil { ferr = chainErrors(err, ferr) if !cmd.policy.AllowPartialResults { return false, ferr diff --git a/batch_command_delete.go b/batch_command_delete.go index 51e3c18f..edebeab6 100644 --- a/batch_command_delete.go +++ b/batch_command_delete.go @@ -108,8 +108,8 @@ func (cmd *batchCommandDelete) parseRecordResults(ifc command, receiveSize int) return false, err } } else { - cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandWasSent)) cmd.records[batchIndex].Err = chainErrors(newCustomNodeError(cmd.node, resultCode), cmd.records[batchIndex].Err) + cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandSentCounter)) } } return true, nil diff --git a/batch_command_operate.go b/batch_command_operate.go index e6421b6f..2544b8d6 100644 --- a/batch_command_operate.go +++ b/batch_command_operate.go @@ -121,7 +121,7 @@ func (cmd *batchCommandOperate) parseRecordResults(ifc command, receiveSize int) if resultCode == types.UDF_BAD_RESPONSE { rec, err := cmd.parseRecord(cmd.records[batchIndex].key(), opCount, generation, expiration) if err != nil { - cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandWasSent)) + cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandSentCounter)) return false, err } @@ -134,9 +134,9 @@ func (cmd *batchCommandOperate) parseRecordResults(ifc command, receiveSize int) // Need to store record because failure bin contains an error message. cmd.records[batchIndex].setRecord(rec) if msg, ok := msg.(string); ok && len(msg) > 0 { - cmd.records[batchIndex].setErrorWithMsg(cmd.node, resultCode, msg, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandWasSent)) + cmd.records[batchIndex].setErrorWithMsg(cmd.node, resultCode, msg, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandSentCounter)) } else { - cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandWasSent)) + cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandSentCounter)) } // If cmd is the end marker of the response, do not proceed further @@ -147,7 +147,7 @@ func (cmd *batchCommandOperate) parseRecordResults(ifc command, receiveSize int) continue } - cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandWasSent)) + cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandSentCounter)) // If cmd is the end marker of the response, do not proceed further if (info3 & _INFO3_LAST) == _INFO3_LAST { @@ -162,7 +162,7 @@ func (cmd *batchCommandOperate) parseRecordResults(ifc command, receiveSize int) if cmd.objects == nil { rec, err := cmd.parseRecord(cmd.records[batchIndex].key(), opCount, generation, expiration) if err != nil { - cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandWasSent)) + cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandSentCounter)) return false, err } cmd.records[batchIndex].setRecord(rec) diff --git a/batch_command_udf.go b/batch_command_udf.go index d774c9ea..8c626053 100644 --- a/batch_command_udf.go +++ b/batch_command_udf.go @@ -117,8 +117,8 @@ func (cmd *batchCommandUDF) parseRecordResults(ifc command, receiveSize int) (bo return false, err } } else { - cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandWasSent)) cmd.records[batchIndex].Err = chainErrors(newCustomNodeError(cmd.node, resultCode), cmd.records[batchIndex].Err) + cmd.records[batchIndex].setError(cmd.node, resultCode, cmd.batchInDoubt(cmd.attr.hasWrite, cmd.commandSentCounter)) } } return true, nil diff --git a/command.go b/command.go index d4571da1..0c0a85a1 100644 --- a/command.go +++ b/command.go @@ -159,7 +159,7 @@ type command interface { isRead() bool execute(ifc command) Error - executeAt(ifc command, policy *BasePolicy, deadline time.Time, iterations int, commandWasSent bool) Error + executeAt(ifc command, policy *BasePolicy, deadline time.Time, iterations int) Error canPutConnBack() bool @@ -2549,8 +2549,8 @@ func (cmd *baseCommand) compressedSize() int { return int(size) } -func (cmd *baseCommand) batchInDoubt(isWrite bool, commandWasSent bool) bool { - return isWrite && commandWasSent +func (cmd *baseCommand) batchInDoubt(isWrite bool, commandSentCounter int) bool { + return isWrite && commandSentCounter > 1 } func (cmd *baseCommand) isRead() bool { @@ -2575,10 +2575,10 @@ func (cmd *baseCommand) execute(ifc command) Error { policy := ifc.getPolicy(ifc).GetBasePolicy() deadline := policy.deadline() - return cmd.executeAt(ifc, policy, deadline, -1, false) + return cmd.executeAt(ifc, policy, deadline, -1) } -func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time.Time, iterations int, commandWasSent bool) (errChain Error) { +func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time.Time, iterations int) (errChain Error) { // for exponential backoff interval := policy.SleepBetweenRetries @@ -2601,7 +2601,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time cmd.node.cluster.maxRetriesExceededCount.GetAndIncrement() } applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) - return chainErrors(ErrMaxRetriesExceeded.err(), errChain).iter(cmd.commandSentCounter).setInDoubt(ifc.isRead(), cmd.commandWasSent).setNode(cmd.node) + return chainErrors(ErrMaxRetriesExceeded.err(), errChain).iter(cmd.commandSentCounter).setInDoubt(ifc.isRead(), cmd.commandSentCounter).setNode(cmd.node) } // Sleep before trying again, after the first iteration @@ -2623,19 +2623,19 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time if !ifc.prepareRetry(ifc, isClientTimeout || (err != nil && err.Matches(types.SERVER_NOT_AVAILABLE))) { if bc, ok := ifc.(batcher); ok { // Batch may be retried in separate commands. - alreadyRetried, err := bc.retryBatch(bc, cmd.node.cluster, deadline, cmd.commandSentCounter, cmd.commandWasSent) + alreadyRetried, err := bc.retryBatch(bc, cmd.node.cluster, deadline, cmd.commandSentCounter) if alreadyRetried { // Batch was retried in separate subcommands. Complete this command. applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) if err != nil { - return chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + return chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) } return nil } // chain the errors and retry if err != nil { - errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) continue } } @@ -2659,7 +2659,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // chain the errors if err != nil { - errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setInDoubt(ifc.isRead(), cmd.commandWasSent) + errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setInDoubt(ifc.isRead(), cmd.commandSentCounter) } // Node is currently inactive. Retry. @@ -2673,7 +2673,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time applyTransactionErrorMetrics(cmd.node) // chain the errors - errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) // Max error rate achieved, try again per policy continue @@ -2684,7 +2684,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time isClientTimeout = false // chain the errors - errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) applyTransactionErrorMetrics(cmd.node) @@ -2714,7 +2714,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time applyTransactionErrorMetrics(cmd.node) // chain the errors - err = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + err = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) // All runtime exceptions are considered fatal. Do not retry. // Close socket to flush out possible garbage. Do not put back in pool. @@ -2737,7 +2737,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time // now that the deadline has been set in the buffer, compress the contents if err = cmd.compress(); err != nil { applyTransactionErrorMetrics(cmd.node) - return chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + return chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) } // now that the deadline has been set in the buffer, compress the contents @@ -2754,7 +2754,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time applyTransactionErrorMetrics(cmd.node) // chain the errors - errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) isClientTimeout = false if deviceOverloadError(err) { @@ -2776,7 +2776,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time applyTransactionErrorMetrics(cmd.node) // chain the errors - errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + errChain = chainErrors(err, errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) if networkError(err) { isTimeout := errors.Is(err, ErrTimeout) @@ -2813,7 +2813,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time } applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) - return errChain.setInDoubt(ifc.isRead(), cmd.commandWasSent) + return errChain.setInDoubt(ifc.isRead(), cmd.commandSentCounter) } applyTransactionMetrics(cmd.node, ifc.transactionType(), transStart) @@ -2842,7 +2842,7 @@ func (cmd *baseCommand) executeAt(ifc command, policy *BasePolicy, deadline time if cmd.node != nil && cmd.node.cluster != nil { cmd.node.cluster.totalTimeoutExceededCount.GetAndIncrement() } - errChain = chainErrors(ErrTimeout.err(), errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandWasSent) + errChain = chainErrors(ErrTimeout.err(), errChain).iter(cmd.commandSentCounter).setNode(cmd.node).setInDoubt(ifc.isRead(), cmd.commandSentCounter) return errChain } diff --git a/error.go b/error.go index 1f4a8851..ef38028f 100644 --- a/error.go +++ b/error.go @@ -54,7 +54,7 @@ type Error interface { Trace() string iter(int) Error - setInDoubt(bool, bool) Error + setInDoubt(bool, int) Error setNode(*Node) Error markInDoubt(bool) Error markInDoubtIf(bool) Error @@ -264,10 +264,8 @@ func newGrpcStatusError(res *kvs.AerospikeResponsePayload) Error { // SetInDoubt sets whether it is possible that the write transaction may have completed // even though this error was generated. This may be the case when a // client error occurs (like timeout) after the command was sent to the server. -func (ase *AerospikeError) setInDoubt(isRead bool, commandWasSent bool) Error { - if !isRead && commandWasSent { - ase.InDoubt = true - } +func (ase *AerospikeError) setInDoubt(isRead bool, commandSentCounter int) Error { + ase.InDoubt = !isRead && (commandSentCounter > 1 || (commandSentCounter == 1 && (ase.ResultCode == types.TIMEOUT || ase.ResultCode <= 0))) return ase } From 1392e454997ee043a5ddff7975730f209355b90e Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Thu, 2 May 2024 22:41:53 +0200 Subject: [PATCH 14/28] [CLIENT-2238] Convert batch calls with just a few keys per node in sub-batches to Get requests If the number keys for a sub-batch to a node is equal or less then the value set in BatchPolicy.DirectGetThreshold, the client use direct get instead of batch commands to reduce the load on the server. --- batch_command.go | 2 + batch_command_delete.go | 44 +++++++++++++++++++--- batch_command_exists.go | 27 ++++++++++++++ batch_command_get.go | 36 ++++++++++++++++++ batch_command_operate.go | 55 ++++++++++++++++++++++++++++ batch_command_udf.go | 43 +++++++++++++++++++--- batch_delete_policy.go | 15 ++++++++ batch_index_command_get.go | 41 +++++++++++++++++++++ batch_policy.go | 6 +++ batch_read.go | 4 ++ batch_read_policy.go | 14 +++++++ batch_record.go | 7 ++++ batch_test.go | 8 ++-- batch_udf_policy.go | 14 +++++++ batch_write_policy.go | 17 +++++++++ client.go | 30 ++++++++++----- client_reflect.go | 16 ++++++++ client_test.go | 43 +++++++++++----------- cluster.go | 2 + command.go | 2 +- error_test.go | 2 +- types/{ => pool}/buffer_pool_test.go | 27 ++++++++------ 22 files changed, 395 insertions(+), 60 deletions(-) rename types/{ => pool}/buffer_pool_test.go (72%) diff --git a/batch_command.go b/batch_command.go index cc3c8fb3..93f20d75 100644 --- a/batch_command.go +++ b/batch_command.go @@ -27,6 +27,8 @@ type batcher interface { retryBatch(ifc batcher, cluster *Cluster, deadline time.Time, iteration int, commandWasSent bool) (bool, Error) generateBatchNodes(*Cluster) ([]*batchNode, Error) setSequence(int, int) + + executeSingle(*Client) Error } type batchCommand struct { diff --git a/batch_command_delete.go b/batch_command_delete.go index f9c5eecf..3f0f21ee 100644 --- a/batch_command_delete.go +++ b/batch_command_delete.go @@ -22,15 +22,17 @@ import ( type batchCommandDelete struct { batchCommand - keys []*Key - records []*BatchRecord - attr *batchAttr + batchDeletePolicy *BatchDeletePolicy + keys []*Key + records []*BatchRecord + attr *batchAttr } func newBatchCommandDelete( node *Node, batch *batchNode, policy *BatchPolicy, + batchDeletePolicy *BatchDeletePolicy, keys []*Key, records []*BatchRecord, attr *batchAttr, @@ -41,9 +43,10 @@ func newBatchCommandDelete( policy: policy, batch: batch, }, - keys: keys, - records: records, - attr: attr, + batchDeletePolicy: batchDeletePolicy, + keys: keys, + records: records, + attr: attr, } return res } @@ -165,7 +168,36 @@ func (cmd *batchCommandDelete) transactionType() transactionType { return ttBatchWrite } +func (cmd *batchCommandDelete) executeSingle(client *Client) Error { + for i, key := range cmd.keys { + res, err := client.Operate(cmd.batchDeletePolicy.toWritePolicy(cmd.policy), key, DeleteOp()) + cmd.records[i].setRecord(res) + if err != nil { + cmd.records[i].setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandDelete) Execute() Error { + if len(cmd.keys) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_exists.go b/batch_command_exists.go index 5243136a..18d532f0 100644 --- a/batch_command_exists.go +++ b/batch_command_exists.go @@ -110,7 +110,34 @@ func (cmd *batchCommandExists) transactionType() transactionType { return ttBatchRead } +func (cmd *batchCommandExists) executeSingle(client *Client) Error { + var err Error + for _, offset := range cmd.batch.offsets { + cmd.existsArray[offset], err = client.Exists(&cmd.policy.BasePolicy, cmd.keys[offset]) + if err != nil { + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandExists) Execute() Error { + if len(cmd.batch.offsets) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_get.go b/batch_command_get.go index 13fd18b1..9328c353 100644 --- a/batch_command_get.go +++ b/batch_command_get.go @@ -216,7 +216,43 @@ func (cmd *batchCommandGet) transactionType() transactionType { return ttBatchRead } +func (cmd *batchCommandGet) executeSingle(client *Client) Error { + for _, offset := range cmd.batch.offsets { + var err Error + if cmd.objects == nil { + if (cmd.readAttr & _INFO1_NOBINDATA) == _INFO1_NOBINDATA { + cmd.records[offset], err = client.GetHeader(&cmd.policy.BasePolicy, cmd.keys[offset]) + } else { + cmd.records[offset], err = client.Get(&cmd.policy.BasePolicy, cmd.keys[offset], cmd.binNames...) + } + } else { + err = client.getObjectDirect(&cmd.policy.BasePolicy, cmd.keys[offset], cmd.objects[offset]) + cmd.objectsFound[offset] = err == nil + } + if err != nil { + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandGet) Execute() Error { + if len(cmd.batch.offsets) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_operate.go b/batch_command_operate.go index f0754ccd..e6421b6f 100644 --- a/batch_command_operate.go +++ b/batch_command_operate.go @@ -225,7 +225,62 @@ func (cmd *batchCommandOperate) parseRecord(key *Key, opCount int, generation, e return newRecord(cmd.node, key, bins, generation, expiration), nil } +func (cmd *batchCommandOperate) executeSingle(client *Client) Error { + var res *Record + var err Error + for _, br := range cmd.records { + + switch br := br.(type) { + case *BatchRead: + var ops []*Operation + if br.headerOnly() { + ops = []*Operation{GetHeaderOp()} + } else if len(br.BinNames) > 0 { + for i := range br.BinNames { + ops = append(ops, GetBinOp(br.BinNames[i])) + } + } else { + ops = br.Ops + } + res, err = client.Operate(br.Policy.toWritePolicy(cmd.policy), br.Key, br.Ops...) + case *BatchWrite: + res, err = client.Operate(br.policy.toWritePolicy(cmd.policy), br.Key, br.ops...) + br.setRecord(res) + case *BatchDelete: + res, err = client.Operate(br.policy.toWritePolicy(cmd.policy), br.Key, DeleteOp()) + br.setRecord(res) + case *BatchUDF: + res, err = client.execute(br.policy.toWritePolicy(cmd.policy), br.Key, br.packageName, br.functionName, br.functionArgs...) + } + + br.setRecord(res) + if err != nil { + br.BatchRec().setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + + return err + } + } + return nil +} + func (cmd *batchCommandOperate) Execute() Error { + if len(cmd.records) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_command_udf.go b/batch_command_udf.go index cab4af33..162c8d74 100644 --- a/batch_command_udf.go +++ b/batch_command_udf.go @@ -22,18 +22,20 @@ import ( type batchCommandUDF struct { batchCommand - keys []*Key - packageName string - functionName string - args ValueArray - records []*BatchRecord - attr *batchAttr + batchUDFPolicy *BatchUDFPolicy + keys []*Key + packageName string + functionName string + args ValueArray + records []*BatchRecord + attr *batchAttr } func newBatchCommandUDF( node *Node, batch *batchNode, policy *BatchPolicy, + batchUDFPolicy *BatchUDFPolicy, keys []*Key, packageName, functionName string, @@ -174,7 +176,36 @@ func (cmd *batchCommandUDF) isRead() bool { return !cmd.attr.hasWrite } +func (cmd *batchCommandUDF) executeSingle(client *Client) Error { + for i, key := range cmd.keys { + res, err := client.execute(cmd.batchUDFPolicy.toWritePolicy(cmd.policy), key, cmd.packageName, cmd.functionName, cmd.args...) + cmd.records[i].setRecord(res) + if err != nil { + cmd.records[i].setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchCommandUDF) Execute() Error { + if len(cmd.keys) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } diff --git a/batch_delete_policy.go b/batch_delete_policy.go index d4bada4a..0f66d5c7 100644 --- a/batch_delete_policy.go +++ b/batch_delete_policy.go @@ -57,3 +57,18 @@ func NewBatchDeletePolicy() *BatchDeletePolicy { GenerationPolicy: NONE, } } + +func (bdp *BatchDeletePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if bdp.FilterExpression != nil { + wp.FilterExpression = bdp.FilterExpression + } + wp.CommitLevel = bdp.CommitLevel + wp.GenerationPolicy = bdp.GenerationPolicy + wp.Generation = bdp.Generation + wp.DurableDelete = bdp.DurableDelete + wp.SendKey = bdp.SendKey + + return wp +} diff --git a/batch_index_command_get.go b/batch_index_command_get.go index fb1f013c..e6cab565 100644 --- a/batch_index_command_get.go +++ b/batch_index_command_get.go @@ -14,6 +14,8 @@ package aerospike +import "github.com/aerospike/aerospike-client-go/v7/types" + type batchIndexCommandGet struct { batchCommandGet } @@ -56,9 +58,48 @@ func (cmd *batchIndexCommandGet) writeBuffer(ifc command) Error { } func (cmd *batchIndexCommandGet) Execute() Error { + if len(cmd.batch.offsets) == 1 { + return cmd.executeSingle(cmd.node.cluster.client) + } return cmd.execute(cmd) } +func (cmd *batchIndexCommandGet) executeSingle(client *Client) Error { + for i, br := range cmd.indexRecords { + var ops []*Operation + if br.headerOnly() { + ops = []*Operation{GetHeaderOp()} + } else if len(br.BinNames) > 0 { + for i := range br.BinNames { + ops = append(ops, GetBinOp(br.BinNames[i])) + } + } else { + ops = br.Ops + } + res, err := client.Operate(cmd.policy.toWritePolicy(), br.Key, br.Ops...) + cmd.indexRecords[i].setRecord(res) + if err != nil { + cmd.indexRecords[i].setRawError(err) + + // Key not found is NOT an error for batch requests + if err.resultCode() == types.KEY_NOT_FOUND_ERROR { + continue + } + + if err.resultCode() == types.FILTERED_OUT { + cmd.filteredOutCnt++ + continue + } + + if cmd.policy.AllowPartialResults { + continue + } + return err + } + } + return nil +} + func (cmd *batchIndexCommandGet) generateBatchNodes(cluster *Cluster) ([]*batchNode, Error) { return newBatchNodeListRecords(cluster, cmd.policy, cmd.indexRecords, cmd.sequenceAP, cmd.sequenceSC, cmd.batch) } diff --git a/batch_policy.go b/batch_policy.go index eb1f73f3..9af86f48 100644 --- a/batch_policy.go +++ b/batch_policy.go @@ -117,6 +117,12 @@ func NewWriteBatchPolicy() *BatchPolicy { return res } +func (p *BatchPolicy) toWritePolicy() *WritePolicy { + wp := NewWritePolicy(0, 0) + wp.BasePolicy = p.BasePolicy + return wp +} + func (p *BatchPolicy) grpc_write() *kvs.WritePolicy { return &kvs.WritePolicy{ Replica: p.ReplicaPolicy.grpc(), diff --git a/batch_read.go b/batch_read.go index d47d8f46..f81a5d8f 100644 --- a/batch_read.go +++ b/batch_read.go @@ -153,3 +153,7 @@ func (br *BatchRead) size(parentPolicy *BasePolicy) (int, Error) { func (br *BatchRead) String() string { return fmt.Sprintf("%s: %v", br.Key, br.BinNames) } + +func (br *BatchRead) headerOnly() bool { + return br.BinNames == nil && !br.ReadAllBins +} diff --git a/batch_read_policy.go b/batch_read_policy.go index 7ca4d941..f741805a 100644 --- a/batch_read_policy.go +++ b/batch_read_policy.go @@ -53,3 +53,17 @@ func NewBatchReadPolicy() *BatchReadPolicy { ReadModeSC: ReadModeSCSession, } } + +func (brp *BatchReadPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if brp.FilterExpression != nil { + wp.FilterExpression = brp.FilterExpression + } + + wp.ReadModeAP = brp.ReadModeAP + wp.ReadModeSC = brp.ReadModeSC + wp.ReadTouchTTLPercent = brp.ReadTouchTTLPercent + + return wp +} diff --git a/batch_record.go b/batch_record.go index a9b867f5..8196e5f5 100644 --- a/batch_record.go +++ b/batch_record.go @@ -129,6 +129,13 @@ func (br *BatchRecord) setRecord(record *Record) { br.ResultCode = types.OK } +// Set error result directly. +func (br *BatchRecord) setRawError(err Error) { + br.ResultCode = err.resultCode() + br.InDoubt = err.IsInDoubt() + br.Err = err +} + // Set error result. For internal use only. func (br *BatchRecord) setError(node *Node, resultCode types.ResultCode, inDoubt bool) { br.ResultCode = resultCode diff --git a/batch_test.go b/batch_test.go index c13e2ffc..a0ff56ae 100644 --- a/batch_test.go +++ b/batch_test.go @@ -690,10 +690,10 @@ var _ = gg.Describe("Aerospike", func() { for _, bri := range batchRecords { br := bri.BatchRec() - gm.Expect(br.InDoubt).To(gm.BeTrue()) + gm.Expect(br.InDoubt).To(gm.BeFalse()) gm.Expect(br.ResultCode).To(gm.Equal(types.UDF_BAD_RESPONSE)) gm.Expect(br.Err.Matches(types.UDF_BAD_RESPONSE)).To(gm.Equal(true)) - gm.Expect(br.Err.IsInDoubt()).To(gm.Equal(true)) + gm.Expect(br.Err.IsInDoubt()).To(gm.BeFalse()) } if nsInfo(ns, "storage-engine") == "device" { @@ -719,13 +719,13 @@ var _ = gg.Describe("Aerospike", func() { gm.Expect(err).ToNot(gm.HaveOccurred()) br := batchRecords[0].BatchRec() - gm.Expect(br.Err.IsInDoubt()).To(gm.BeTrue()) + gm.Expect(br.Err.IsInDoubt()).To(gm.BeFalse()) gm.Expect(br.ResultCode).To(gm.Equal(types.RECORD_TOO_BIG)) gm.Expect(br.Err.Matches(types.RECORD_TOO_BIG)).To(gm.Equal(true)) gm.Expect(br.Err.IsInDoubt()).To(gm.Equal(true)) br = batchRecords[1].BatchRec() - gm.Expect(br.Err.IsInDoubt()).To(gm.BeTrue()) + gm.Expect(br.Err.IsInDoubt()).To(gm.BeFalse()) gm.Expect(br.ResultCode).To(gm.Equal(types.RECORD_TOO_BIG)) gm.Expect(br.Err.Matches(types.RECORD_TOO_BIG)).To(gm.Equal(true)) gm.Expect(br.Err.IsInDoubt()).To(gm.Equal(true)) diff --git a/batch_udf_policy.go b/batch_udf_policy.go index 0924bd55..e95bd29c 100644 --- a/batch_udf_policy.go +++ b/batch_udf_policy.go @@ -55,3 +55,17 @@ func NewBatchUDFPolicy() *BatchUDFPolicy { CommitLevel: COMMIT_ALL, } } + +func (bup *BatchUDFPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if bup.FilterExpression != nil { + wp.FilterExpression = bup.FilterExpression + } + wp.CommitLevel = bup.CommitLevel + wp.Expiration = bup.Expiration + wp.DurableDelete = bup.DurableDelete + wp.SendKey = bup.SendKey + + return wp +} diff --git a/batch_write_policy.go b/batch_write_policy.go index 33c7c99a..008d35dd 100644 --- a/batch_write_policy.go +++ b/batch_write_policy.go @@ -82,3 +82,20 @@ func NewBatchWritePolicy() *BatchWritePolicy { CommitLevel: COMMIT_ALL, } } + +func (bwp *BatchWritePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { + wp := bp.toWritePolicy() + + if bwp.FilterExpression != nil { + wp.FilterExpression = bwp.FilterExpression + } + wp.RecordExistsAction = bwp.RecordExistsAction + wp.CommitLevel = bwp.CommitLevel + wp.GenerationPolicy = bwp.GenerationPolicy + wp.Generation = bwp.Generation + wp.Expiration = bwp.Expiration + wp.DurableDelete = bwp.DurableDelete + wp.SendKey = bwp.SendKey + + return wp +} diff --git a/client.go b/client.go index 67a7fa52..69e2a8c8 100644 --- a/client.go +++ b/client.go @@ -105,6 +105,9 @@ func NewClientWithPolicyAndHost(policy *ClientPolicy, hosts ...*Host) (*Client, DefaultInfoPolicy: NewInfoPolicy(), } + // back reference especially used in batch commands + cluster.client = client + runtime.SetFinalizer(client, clientFinalizer) return client, err @@ -631,7 +634,7 @@ func (clnt *Client) BatchDelete(policy *BatchPolicy, deletePolicy *BatchDeletePo return nil, err } - cmd := newBatchCommandDelete(nil, nil, policy, keys, records, attr) + cmd := newBatchCommandDelete(nil, nil, policy, deletePolicy, keys, records, attr) _, err = clnt.batchExecute(policy, batchNodes, cmd) return records, err } @@ -682,7 +685,7 @@ func (clnt *Client) BatchExecute(policy *BatchPolicy, udfPolicy *BatchUDFPolicy, return nil, err } - cmd := newBatchCommandUDF(nil, nil, policy, keys, packageName, functionName, args, records, attr) + cmd := newBatchCommandUDF(nil, nil, policy, udfPolicy, keys, packageName, functionName, args, records, attr) _, err = clnt.batchExecute(policy, batchNodes, cmd) return records, err } @@ -929,18 +932,11 @@ func (clnt *Client) ListUDF(policy *BasePolicy) ([]*UDF, Error) { // This method is only supported by Aerospike 3+ servers. // If the policy is nil, the default relevant policy will be used. func (clnt *Client) Execute(policy *WritePolicy, key *Key, packageName string, functionName string, args ...Value) (interface{}, Error) { - policy = clnt.getUsableWritePolicy(policy) - command, err := newExecuteCommand(clnt.cluster, policy, key, packageName, functionName, NewValueArray(args)) + record, err := clnt.execute(policy, key, packageName, functionName, args...) if err != nil { return nil, err } - if err := command.Execute(); err != nil { - return nil, err - } - - record := command.GetRecord() - if record == nil || len(record.Bins) == 0 { return nil, nil } @@ -956,6 +952,20 @@ func (clnt *Client) Execute(policy *WritePolicy, key *Key, packageName string, f return nil, ErrUDFBadResponse.err() } +func (clnt *Client) execute(policy *WritePolicy, key *Key, packageName string, functionName string, args ...Value) (*Record, Error) { + policy = clnt.getUsableWritePolicy(policy) + command, err := newExecuteCommand(clnt.cluster, policy, key, packageName, functionName, NewValueArray(args)) + if err != nil { + return nil, err + } + + if err := command.Execute(); err != nil { + return nil, err + } + + return command.GetRecord(), nil +} + //---------------------------------------------------------- // Query/Execute (Supported by Aerospike 3+ servers only) //---------------------------------------------------------- diff --git a/client_reflect.go b/client_reflect.go index e1ab64bb..d6dd4fef 100644 --- a/client_reflect.go +++ b/client_reflect.go @@ -80,6 +80,22 @@ func (clnt *Client) GetObject(policy *BasePolicy, key *Key, obj interface{}) Err return command.Execute() } +// getObjectDirect reads a record for specified key and puts the result into the provided object. +// The policy can be used to specify timeouts. +// If the policy is nil, the default relevant policy will be used. +func (clnt *Client) getObjectDirect(policy *BasePolicy, key *Key, rval *reflect.Value) Error { + policy = clnt.getUsablePolicy(policy) + + binNames := objectMappings.getFields(rval.Type()) + command, err := newReadCommand(clnt.cluster, policy, key, binNames, nil) + if err != nil { + return err + } + + command.object = rval + return command.Execute() +} + // BatchGetObjects reads multiple record headers and bins for specified keys in one batch request. // The returned objects are in positional order with the original key array order. // If a key is not found, the positional object will not change, and the positional found boolean will be false. diff --git a/client_test.go b/client_test.go index a0797ceb..d41df5e4 100644 --- a/client_test.go +++ b/client_test.go @@ -32,28 +32,29 @@ import ( gm "github.com/onsi/gomega" ) -func isMapOrFloat(ifc interface{}) bool { - m, ok := ifc.(map[string]interface{}) - if !ok { - _, ok1 := ifc.(float64) - _, ok2 := ifc.(float32) - _, ok3 := ifc.(int) - _, ok4 := ifc.(int64) - return ok1 || ok2 || ok3 || ok4 - } - - for _, v := range m { - switch v := v.(type) { - case float64: - return true - case map[string]interface{}: - return isMapOrFloat(v) - default: - return false +func isJsonObject(ifc interface{}) bool { + switch ifc := ifc.(type) { + case float64, float32, int, int64, uint64: + return true + case []interface{}: + for _, v := range ifc { + switch v.(type) { + case float64, float32, int, int64, uint64: + default: + return false + } } + return true + case map[string]interface{}: + for _, v := range ifc { + if !isJsonObject(v) { + return false + } + } + return true + default: + return false } - - return true } // ALL tests are isolated by SetName and Key, which are 50 random characters @@ -92,7 +93,7 @@ var _ = gg.Describe("Aerospike", func() { gm.Expect(len(stats)).To(gm.BeNumerically(">", 0)) for _, nodeStatsIfc := range stats { // make sure it's a strict map of string => float64 | string => float64 - gm.Expect(isMapOrFloat(nodeStatsIfc)).To(gm.BeTrue()) + gm.Expect(isJsonObject(nodeStatsIfc)).To(gm.BeTrue()) if nodeStats, ok := nodeStatsIfc.(map[string]interface{}); ok { gm.Expect(nodeStats["connections-attempts"].(float64)).To(gm.BeNumerically(">=", 1)) diff --git a/cluster.go b/cluster.go index 6de03013..0152c621 100644 --- a/cluster.go +++ b/cluster.go @@ -32,6 +32,8 @@ import ( // Cluster encapsulates the aerospike cluster nodes and manages // them. type Cluster struct { + client *Client + // Initial host nodes specified by user. seeds iatomic.SyncVal //[]*Host diff --git a/command.go b/command.go index 02cfb29d..d4571da1 100644 --- a/command.go +++ b/command.go @@ -2884,7 +2884,7 @@ func deviceOverloadError(err Error) bool { func applyTransactionMetrics(node *Node, tt transactionType, tb time.Time) { if node != nil && node.cluster.MetricsEnabled() { - applyMetrics(tt, node.stats, tb) + applyMetrics(tt, &node.stats, tb) } } diff --git a/error_test.go b/error_test.go index fa57970b..e541c726 100644 --- a/error_test.go +++ b/error_test.go @@ -128,7 +128,7 @@ var _ = gg.Describe("Aerospike Error Tests", func() { }) gg.It("should handle chained case", func() { - inner := newError(ast.UDF_BAD_RESPONSE).setInDoubt(false, true) + inner := newError(ast.UDF_BAD_RESPONSE).setInDoubt(false, 2) outer := newError(ast.TIMEOUT) err := chainErrors(outer, inner) diff --git a/types/buffer_pool_test.go b/types/pool/buffer_pool_test.go similarity index 72% rename from types/buffer_pool_test.go rename to types/pool/buffer_pool_test.go index 9b762cf0..5cbef9db 100644 --- a/types/buffer_pool_test.go +++ b/types/pool/buffer_pool_test.go @@ -12,30 +12,35 @@ // See the License for the specific language governing permissions and // limitations under the License. -package types +package pool import ( "math/rand" - gg "github.com/onsi/ginkgo" + gg "github.com/onsi/ginkgo/v2" gm "github.com/onsi/gomega" ) var _ = gg.Describe("BufferPool Test", func() { + const ( + Min = 1 << 10 + Max = 1 << 16 + ) + gg.Context("Any size Buffer Pool", func() { - var bp *BufferPool + bp := NewTieredBufferPool(Min, Max) check := func(sz int) { buf := bp.Get(sz) gm.Expect(len(buf)).To(gm.BeNumerically(">=", sz)) - if sz <= maxBufSize { + if sz <= Max { if powerOf2(sz) { - gm.Expect(len(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))))) - gm.Expect(cap(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))))) + gm.Expect(len(buf)).To(gm.BeNumerically("==", 1<<(fastLog2(uint64(sz))))) + gm.Expect(cap(buf)).To(gm.BeNumerically("==", 1<<(fastLog2(uint64(sz))))) } else { - gm.Expect(len(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))+1))) - gm.Expect(cap(buf)).To(gm.BeNumerically("==", 1<<(fastlog2(uint64(sz))+1))) + gm.Expect(len(buf)).To(gm.BeNumerically("==", 1<<(fastLog2(uint64(sz))+1))) + gm.Expect(cap(buf)).To(gm.BeNumerically("==", 1<<(fastLog2(uint64(sz))+1))) } } else { gm.Expect(len(buf)).To(gm.BeNumerically("==", sz)) @@ -44,7 +49,7 @@ var _ = gg.Describe("BufferPool Test", func() { } gg.It("should return a buffer with correct size", func() { - bp = NewBufferPool() + bp = NewTieredBufferPool(Min, Max) for i := 1; i < 24; i++ { check(1< Date: Fri, 3 May 2024 12:38:02 +0000 Subject: [PATCH 15/28] Update CHANGELOG --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d1fb08..e4b7af3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Change History +## May 3 2024: v7.3.0 + +This is a major feature release of the Go client and touches some of the fundamental aspects of the inner workings of it. +We suggest complete testing of your application before using it in production. + +- **New Features** + - [CLIENT-2238] Convert batch calls with just one key per node in sub-batches to Get requests. + If the number keys for a sub-batch to a node is equal or less then the value set in BatchPolicy.DirectGetThreshold, the client use direct get instead of batch commands to reduce the load on the server. + + - [CLIENT-2274] Use constant sized connection buffers and resize the connection buffers over time. + + The client would use a single buffer on the connection and would grow it + per demand in case it needed a bigger buffer, but would not shrink it. + This helped with avoiding using buffer pools and the associated + synchronization, but resulted in excessive memory use in case there were a + few large records in the results, even if they were infrequent. + This changeset does two things: + 1. Will use a memory pool for large records only. Large records + are defined as records bigger than `aerospike.PoolCutOffBufferSize`. + This is a tiered pool with different buffer sizes. The pool + uses `sync.Pool` under the cover, releasing unused buffers back to the + runtime. + 2. By using bigger `aerospike.DefaultBufferSize` values, the user can + imitate the old behavior, so no memory pool is used most of the time. + 3. Setting `aerospike.MinBufferSize` will prevent the pool using buffer sizes too small, + having to grow them frequently. + 4. Buffers are resized every 5 seconds to the median size of buffers used over the previous period, + within the above limits. + + This change should result in much lower memory use by the client. + + - [CLIENT-2702] Support Client Transaction Metrics. The native client can now track transaction latencies using histograms. Enable using the `Client.EnableMetrics` API. + +- **Improvements** + - [CLIENT-2889] Increase grpc `MaxRecvMsgSize` to handle big records for the proxy client. + - [CLIENT-2891] Export various batch operation struct fields. Resolves #247. + - Remove dependency on `xrand` sub-package since the native API is fast enough. + - Linter Clean up. + +- **Fixes** + - [CLIENT-2905] Fix inconsistency of handling in-doubt flag in errors. + - [CLIENT-2890] Support `[]MapPair` return in reflection. + This fix supports unmarshalling ordered maps into `map[K]V` and `[]MapPair` in the structs. + ## April 10 2024: v7.2.1 This release updates the dependencies to mitigate security issues. From 92f706b187c2d3cd990ff6da8219187649f31bfb Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Sun, 5 May 2024 20:07:14 +0000 Subject: [PATCH 16/28] Fix invalid pointer derefs in batch policy conversions when the underlying policy is nil --- batch_delete_policy.go | 17 +++++++++-------- batch_policy.go | 4 +++- batch_read_policy.go | 15 ++++++++------- batch_udf_policy.go | 15 ++++++++------- batch_write_policy.go | 20 +++++++++++--------- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/batch_delete_policy.go b/batch_delete_policy.go index 0f66d5c7..d73b10de 100644 --- a/batch_delete_policy.go +++ b/batch_delete_policy.go @@ -61,14 +61,15 @@ func NewBatchDeletePolicy() *BatchDeletePolicy { func (bdp *BatchDeletePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { wp := bp.toWritePolicy() - if bdp.FilterExpression != nil { - wp.FilterExpression = bdp.FilterExpression + if bdp != nil { + if bdp.FilterExpression != nil { + wp.FilterExpression = bdp.FilterExpression + } + wp.CommitLevel = bdp.CommitLevel + wp.GenerationPolicy = bdp.GenerationPolicy + wp.Generation = bdp.Generation + wp.DurableDelete = bdp.DurableDelete + wp.SendKey = bdp.SendKey } - wp.CommitLevel = bdp.CommitLevel - wp.GenerationPolicy = bdp.GenerationPolicy - wp.Generation = bdp.Generation - wp.DurableDelete = bdp.DurableDelete - wp.SendKey = bdp.SendKey - return wp } diff --git a/batch_policy.go b/batch_policy.go index 9af86f48..da49a30b 100644 --- a/batch_policy.go +++ b/batch_policy.go @@ -119,7 +119,9 @@ func NewWriteBatchPolicy() *BatchPolicy { func (p *BatchPolicy) toWritePolicy() *WritePolicy { wp := NewWritePolicy(0, 0) - wp.BasePolicy = p.BasePolicy + if p != nil { + wp.BasePolicy = p.BasePolicy + } return wp } diff --git a/batch_read_policy.go b/batch_read_policy.go index f741805a..a65b4b9a 100644 --- a/batch_read_policy.go +++ b/batch_read_policy.go @@ -57,13 +57,14 @@ func NewBatchReadPolicy() *BatchReadPolicy { func (brp *BatchReadPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { wp := bp.toWritePolicy() - if brp.FilterExpression != nil { - wp.FilterExpression = brp.FilterExpression - } - - wp.ReadModeAP = brp.ReadModeAP - wp.ReadModeSC = brp.ReadModeSC - wp.ReadTouchTTLPercent = brp.ReadTouchTTLPercent + if brp != nil { + if brp.FilterExpression != nil { + wp.FilterExpression = brp.FilterExpression + } + wp.ReadModeAP = brp.ReadModeAP + wp.ReadModeSC = brp.ReadModeSC + wp.ReadTouchTTLPercent = brp.ReadTouchTTLPercent + } return wp } diff --git a/batch_udf_policy.go b/batch_udf_policy.go index e95bd29c..f6c7ec8e 100644 --- a/batch_udf_policy.go +++ b/batch_udf_policy.go @@ -59,13 +59,14 @@ func NewBatchUDFPolicy() *BatchUDFPolicy { func (bup *BatchUDFPolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { wp := bp.toWritePolicy() - if bup.FilterExpression != nil { - wp.FilterExpression = bup.FilterExpression + if bup != nil { + if bup.FilterExpression != nil { + wp.FilterExpression = bup.FilterExpression + } + wp.CommitLevel = bup.CommitLevel + wp.Expiration = bup.Expiration + wp.DurableDelete = bup.DurableDelete + wp.SendKey = bup.SendKey } - wp.CommitLevel = bup.CommitLevel - wp.Expiration = bup.Expiration - wp.DurableDelete = bup.DurableDelete - wp.SendKey = bup.SendKey - return wp } diff --git a/batch_write_policy.go b/batch_write_policy.go index 008d35dd..606f2d2c 100644 --- a/batch_write_policy.go +++ b/batch_write_policy.go @@ -86,16 +86,18 @@ func NewBatchWritePolicy() *BatchWritePolicy { func (bwp *BatchWritePolicy) toWritePolicy(bp *BatchPolicy) *WritePolicy { wp := bp.toWritePolicy() - if bwp.FilterExpression != nil { - wp.FilterExpression = bwp.FilterExpression + if bwp != nil { + if bwp.FilterExpression != nil { + wp.FilterExpression = bwp.FilterExpression + } + wp.RecordExistsAction = bwp.RecordExistsAction + wp.CommitLevel = bwp.CommitLevel + wp.GenerationPolicy = bwp.GenerationPolicy + wp.Generation = bwp.Generation + wp.Expiration = bwp.Expiration + wp.DurableDelete = bwp.DurableDelete + wp.SendKey = bwp.SendKey } - wp.RecordExistsAction = bwp.RecordExistsAction - wp.CommitLevel = bwp.CommitLevel - wp.GenerationPolicy = bwp.GenerationPolicy - wp.Generation = bwp.Generation - wp.Expiration = bwp.Expiration - wp.DurableDelete = bwp.DurableDelete - wp.SendKey = bwp.SendKey return wp } From 2bca9224bd15b8f5be570b6ffe9ec085aecddea8 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Sun, 5 May 2024 20:09:35 +0000 Subject: [PATCH 17/28] Update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 47d1b27d..70722727 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ Makefile testdata/ Dockerfile* .dockerignore -docker-compose.yml \ No newline at end of file +docker-compose.yml +golangci.yml +cover.out \ No newline at end of file From c90afb864bbbd07958b75136f8d650712255ffff Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Sun, 5 May 2024 20:16:26 +0000 Subject: [PATCH 18/28] Fix the indoubt flag test for BatchUDF --- batch_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/batch_test.go b/batch_test.go index a0ff56ae..2169de08 100644 --- a/batch_test.go +++ b/batch_test.go @@ -722,13 +722,13 @@ var _ = gg.Describe("Aerospike", func() { gm.Expect(br.Err.IsInDoubt()).To(gm.BeFalse()) gm.Expect(br.ResultCode).To(gm.Equal(types.RECORD_TOO_BIG)) gm.Expect(br.Err.Matches(types.RECORD_TOO_BIG)).To(gm.Equal(true)) - gm.Expect(br.Err.IsInDoubt()).To(gm.Equal(true)) + gm.Expect(br.Err.IsInDoubt()).To(gm.Equal(false)) br = batchRecords[1].BatchRec() gm.Expect(br.Err.IsInDoubt()).To(gm.BeFalse()) gm.Expect(br.ResultCode).To(gm.Equal(types.RECORD_TOO_BIG)) gm.Expect(br.Err.Matches(types.RECORD_TOO_BIG)).To(gm.Equal(true)) - gm.Expect(br.Err.IsInDoubt()).To(gm.Equal(true)) + gm.Expect(br.Err.IsInDoubt()).To(gm.Equal(false)) br = batchRecords[2].BatchRec() gm.Expect(br.Err.IsInDoubt()).To(gm.BeFalse()) From be135206660b174e98db948f69d036e684be759a Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Mon, 6 May 2024 11:57:44 +0000 Subject: [PATCH 19/28] Added policy.SendKey to docs --- docs/policies.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/policies.md b/docs/policies.md index 1fa60360..d215ddf2 100644 --- a/docs/policies.md +++ b/docs/policies.md @@ -63,6 +63,10 @@ Attributes: backoff during retries. * Default: `1.0` +- `SendKey` – Qualify whether to send user defined key in addition to hash digest on both reads and writes. + If the key is sent on a write, the key will be stored with the record on server + * Default: `false` +