Skip to content
This repository was archived by the owner on Mar 9, 2025. It is now read-only.

Commit be1cfc4

Browse files
partially resolve #100 (router_map_callrw) (#114)
* custom msgpackv5 decoders for router_map_callrw * new method 'RouterMapCallRW[T]'
1 parent 0a8677f commit be1cfc4

File tree

4 files changed

+164
-62
lines changed

4 files changed

+164
-62
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ CHANGES:
1212
* Decode 'vshard.storage.call' response manually into struct vshardStorageCallResponseProto using DecodeMsgpack interface to reduce allocations (partially #61, #100).
1313
* Remove `mapstructure` tag from StorageCallVShardError.
1414
* Update benchmarks in README files.
15+
* Package `mapstructure` is completely removed from direct dependencies list.
1516

1617
FEATURES:
1718

1819
* Add pause between requests in buckets discovering. Configured by config DiscoveryWorkStep, default is 10ms.
1920
* Add ReplicaUUID to the StorageCallVShardError struct.
21+
* New method 'RouterMapCallRW[T]' to replace the deprecated one 'RouterMapCallRWImpl'.
2022

2123
REFACTOR:
2224

@@ -26,9 +28,11 @@ REFACTOR:
2628
* Remove bucketStatError type, use StorageCallVShardError type instead.
2729
* Add custom msgpackv5 decoder for 'vshard.storage.bucket_stat' response (partially #100).
2830
* Add custom msgpackv5 decoder for 'BucketStatInfo', since msgpackv5 library has an issue (see commit content).
31+
* Add custom msgpackv5 decoder for 'RouterMapCallRW'.
2932

3033
TESTS:
3134
* Rename bootstrap_test.go -> tarantool_test.go and new test in this file.
35+
* Test for new 'RouterMapCallRW[T]'.
3236

3337
## v1.2.0
3438

api.go

Lines changed: 152 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"time"
77

88
"github.com/google/uuid"
9-
"github.com/mitchellh/mapstructure"
109

1110
"github.com/tarantool/go-tarantool/v2"
1211
"github.com/tarantool/go-tarantool/v2/pool"
@@ -338,15 +337,154 @@ func (r *Router) RouterCallImpl(ctx context.Context,
338337
}
339338
}
340339

340+
// RouterMapCallRWOptions sets options for RouterMapCallRW.
341+
type RouterMapCallRWOptions struct {
342+
// Timeout defines timeout for RouterMapCallRW.
343+
Timeout time.Duration
344+
}
345+
346+
type storageMapResponseProto[T any] struct {
347+
ok bool
348+
value T
349+
err StorageCallVShardError
350+
}
351+
352+
func (r *storageMapResponseProto[T]) DecodeMsgpack(d *msgpack.Decoder) error {
353+
// proto for 'storage_map' method
354+
// https://github.com/tarantool/vshard/blob/8d299bfecff8bc656056658350ad48c829f9ad3f/vshard/storage/init.lua#L3158
355+
respArrayLen, err := d.DecodeArrayLen()
356+
if err != nil {
357+
return err
358+
}
359+
360+
if respArrayLen == 0 {
361+
return fmt.Errorf("protocol violation: invalid array length: %d", respArrayLen)
362+
}
363+
364+
code, err := d.PeekCode()
365+
if err != nil {
366+
return err
367+
}
368+
369+
if code == msgpcode.Nil {
370+
err = d.DecodeNil()
371+
if err != nil {
372+
return err
373+
}
374+
375+
if respArrayLen != 2 {
376+
return fmt.Errorf("protocol violation: length is %d on vshard error case", respArrayLen)
377+
}
378+
379+
err = d.Decode(&r.err)
380+
if err != nil {
381+
return fmt.Errorf("failed to decode storage vshard error: %w", err)
382+
}
383+
384+
return nil
385+
}
386+
387+
isOk, err := d.DecodeBool()
388+
if err != nil {
389+
return err
390+
}
391+
392+
if !isOk {
393+
return fmt.Errorf("protocol violation: isOk=false")
394+
}
395+
396+
switch respArrayLen {
397+
case 1:
398+
break
399+
case 2:
400+
err = d.Decode(&r.value)
401+
if err != nil {
402+
return fmt.Errorf("can't decode value %T: %w", r.value, err)
403+
}
404+
default:
405+
return fmt.Errorf("protocol violation: invalid array length when no vshard error: %d", respArrayLen)
406+
}
407+
408+
r.ok = true
409+
410+
return nil
411+
}
412+
413+
type storageRefResponseProto struct {
414+
err error
415+
bucketCount uint64
416+
}
417+
418+
func (r *storageRefResponseProto) DecodeMsgpack(d *msgpack.Decoder) error {
419+
respArrayLen, err := d.DecodeArrayLen()
420+
if err != nil {
421+
return err
422+
}
423+
424+
if respArrayLen == 0 {
425+
return fmt.Errorf("protocol violation: invalid array length: %d", respArrayLen)
426+
}
427+
428+
code, err := d.PeekCode()
429+
if err != nil {
430+
return err
431+
}
432+
433+
if code == msgpcode.Nil {
434+
err = d.DecodeNil()
435+
if err != nil {
436+
return err
437+
}
438+
439+
if respArrayLen != 2 {
440+
return fmt.Errorf("protocol violation: length is %d on error case", respArrayLen)
441+
}
442+
443+
// The possible variations of error here are fully unknown yet for us, e.g:
444+
// vshard error, assert error or some other type of error. So this question requires research.
445+
// So we do not decode it to some known error format, because we don't use it anyway.
446+
decodedError, err := d.DecodeInterface()
447+
if err != nil {
448+
return err
449+
}
450+
451+
// convert empty interface into error
452+
r.err = fmt.Errorf("%v", decodedError)
453+
454+
return nil
455+
}
456+
457+
r.bucketCount, err = d.DecodeUint64()
458+
if err != nil {
459+
return err
460+
}
461+
462+
return nil
463+
}
464+
341465
// RouterMapCallRWImpl perform call function on all masters in the cluster
342466
// with a guarantee that in case of success it was executed with all
343467
// buckets being accessible for reads and writes.
468+
// Deprecated: RouterMapCallRWImpl is deprecated.
469+
// Use more general RouterMapCallRW instead.
344470
func (r *Router) RouterMapCallRWImpl(
345471
ctx context.Context,
346472
fnc string,
347473
args interface{},
348474
opts CallOpts,
349475
) (map[uuid.UUID]interface{}, error) {
476+
return RouterMapCallRW[interface{}](r, ctx, fnc, args, RouterMapCallRWOptions{Timeout: opts.Timeout})
477+
}
478+
479+
// RouterMapCallRW is a consistent Map-Reduce. The given function is called on all masters in the
480+
// cluster with a guarantee that in case of success it was executed with all
481+
// buckets being accessible for reads and writes.
482+
// T is a return type of user defined function 'fnc'.
483+
// We define it as a distinct function, not a Router method, because golang limitations,
484+
// see: https://github.com/golang/go/issues/49085.
485+
func RouterMapCallRW[T any](r *Router, ctx context.Context,
486+
fnc string, args interface{}, opts RouterMapCallRWOptions,
487+
) (map[uuid.UUID]T, error) {
350488
const vshardStorageServiceCall = "vshard.storage._call"
351489

352490
timeout := CallTimeoutMin
@@ -399,32 +537,17 @@ func (r *Router) RouterMapCallRWImpl(
399537
// proto for 'storage_ref' method:
400538
// https://github.com/tarantool/vshard/blob/dfa2cc8a2aff221d5f421298851a9a229b2e0434/vshard/storage/init.lua#L3137
401539
for _, rsFuture := range rsFutures {
402-
respData, err := rsFuture.future.Get()
403-
if err != nil {
404-
return nil, fmt.Errorf("rs {%s} storage_ref err: %v", rsFuture.uuid, err)
405-
}
406-
407-
if len(respData) < 1 {
408-
return nil, fmt.Errorf("protocol violation: storage_ref: expected len(respData) 1 or 2, got: %d", len(respData))
409-
}
540+
var storageRefResponse storageRefResponseProto
410541

411-
if respData[0] == nil {
412-
if len(respData) != 2 {
413-
return nil, fmt.Errorf("protocol vioaltion: storage_ref: expected len(respData) = 2 when respData[0] == nil, got %d", len((respData)))
414-
}
415-
416-
// The possible variations of error in respData[1] are fully unknown yet for us, this question requires research.
417-
// So we do not convert respData[1] to some known error format, because we don't use it anyway.
418-
return nil, fmt.Errorf("storage_ref failed on %v: %v", rsFuture.uuid, respData[1])
542+
if err := rsFuture.future.GetTyped(&storageRefResponse); err != nil {
543+
return nil, fmt.Errorf("rs {%s} storage_ref err: %v", rsFuture.uuid, err)
419544
}
420545

421-
var bucketCount uint64
422-
err = rsFuture.future.GetTyped(&[]interface{}{&bucketCount})
423-
if err != nil {
424-
return nil, err
546+
if storageRefResponse.err != nil {
547+
return nil, fmt.Errorf("storage_ref failed on %v: %v", rsFuture.uuid, storageRefResponse.err)
425548
}
426549

427-
totalBucketCount += bucketCount
550+
totalBucketCount += storageRefResponse.bucketCount
428551
}
429552

430553
if totalBucketCount != r.cfg.TotalBucketCount {
@@ -449,52 +572,20 @@ func (r *Router) RouterMapCallRWImpl(
449572
}
450573

451574
// map stage: get their responses
452-
idToResult := make(map[uuid.UUID]interface{})
453-
// proto for 'storage_map' method:
454-
// https://github.com/tarantool/vshard/blob/8d299bfecff8bc656056658350ad48c829f9ad3f/vshard/storage/init.lua#L3158
575+
idToResult := make(map[uuid.UUID]T)
455576
for _, rsFuture := range rsFutures {
456-
respData, err := rsFuture.future.Get()
457-
if err != nil {
458-
return nil, fmt.Errorf("rs {%s} storage_map err: %v", rsFuture.uuid, err)
459-
}
460-
461-
if len(respData) < 1 {
462-
return nil, fmt.Errorf("protocol violation: invalid respData length: must be >= 1, current: %d", len(respData))
463-
}
464-
465-
if respData[0] == nil {
466-
if len(respData) != 2 {
467-
return nil, fmt.Errorf("protocol violation: invalid respData length when respData[0] == nil, must be = 2, current: %d", len(respData))
468-
}
469-
470-
var assertError assertError
471-
err = mapstructure.Decode(respData[1], &assertError)
472-
if err != nil {
473-
// We could not decode respData[1] as assertError, so return respData[1] as is, add info why we could not decode.
474-
return nil, fmt.Errorf("storage_map failed on %v: %+v (decoding to assertError failed %v)", rsFuture.uuid, respData[1], err)
475-
}
577+
var storageMapResponse storageMapResponseProto[T]
476578

477-
return nil, fmt.Errorf("storage_map failed on %v: %+v", rsFuture.uuid, assertError)
478-
}
479-
480-
var isVShardRespOk bool
481-
err = rsFuture.future.GetTyped(&[]interface{}{&isVShardRespOk})
579+
err := rsFuture.future.GetTyped(&storageMapResponse)
482580
if err != nil {
483-
return nil, fmt.Errorf("can't decode isVShardRespOk for storage_map response: %v", err)
581+
return nil, fmt.Errorf("rs {%s} storage_map err: %v", rsFuture.uuid, err)
484582
}
485583

486-
if !isVShardRespOk {
487-
return nil, fmt.Errorf("protocol violation: isVShardRespOk = false from storage_map: replicaset %v", rsFuture.uuid)
584+
if !storageMapResponse.ok {
585+
return nil, fmt.Errorf("storage_map failed on %v: %+v", rsFuture.uuid, storageMapResponse.err)
488586
}
489587

490-
switch l := len(respData); l {
491-
case 1:
492-
idToResult[rsFuture.uuid] = nil
493-
case 2:
494-
idToResult[rsFuture.uuid] = respData[1]
495-
default:
496-
return nil, fmt.Errorf("protocol vioaltion: invalid respData when respData[0] == true, expected 1 or 2, got %d", l)
497-
}
588+
idToResult[rsFuture.uuid] = storageMapResponse.value
498589
}
499590

500591
r.metrics().RequestDuration(time.Since(timeStart), true, true)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ go 1.20
44

55
require (
66
github.com/google/uuid v1.6.0
7-
github.com/mitchellh/mapstructure v1.5.0
87
github.com/snksoft/crc v1.1.0
98
github.com/spf13/viper v1.18.2
109
github.com/stretchr/testify v1.9.0
@@ -21,6 +20,7 @@ require (
2120
github.com/hashicorp/hcl v1.0.0 // indirect
2221
github.com/json-iterator/go v1.1.12 // indirect
2322
github.com/magiconair/properties v1.8.7 // indirect
23+
github.com/mitchellh/mapstructure v1.5.0 // indirect
2424
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
2525
github.com/modern-go/reflect2 v1.0.2 // indirect
2626
github.com/pelletier/go-toml/v2 v2.1.0 // indirect

tests/tnt/routermap_call_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ func TestRouterMapCall(t *testing.T) {
4343
require.Equalf(t, arg, v, "RouterMapCallRWImpl value ok for %v", k)
4444
}
4545

46+
echoArgs = []interface{}{1}
47+
respInt, err := vshardrouter.RouterMapCallRW[int](router, ctx, "echo", echoArgs, vshardrouter.RouterMapCallRWOptions{})
48+
require.NoError(t, err, "RouterMapCallRW[int] echo finished with no err")
49+
for k, v := range respInt {
50+
require.Equalf(t, 1, v, "RouterMapCallRW[int] value ok for %v", k)
51+
}
52+
4653
// RouterMapCallRWImpl returns only one value
4754
echoArgs = []interface{}{arg, "arg2"}
4855
resp, err = router.RouterMapCallRWImpl(ctx, "echo", echoArgs, callOpts)

0 commit comments

Comments
 (0)