router: make full map_callrw with split args#644
Conversation
Serpentian
left a comment
There was a problem hiding this comment.
Well done, only one major comment (upgrade), we must address, other ones are nits and smth to think about)
| -- high-level ref functions (such as router_ref_storage_all and router_ref_ | ||
| -- storage_by_buckets). | ||
| -- | ||
| local function router_ref_send(router, timeout, args_builder, grouped_buckets) |
There was a problem hiding this comment.
Hmm, did you consider making it even more general: router_map_callrw_send and router_map_callrw_collect and also reuse them in the replicasets_map_reduce too? There we have almost the same code. Not a call to action, just something to think about.
There was a problem hiding this comment.
Yes, router_map_callrw_prepare/send/collect sounds better.
But I don't think that we can easily reuse these functions in replicasets_map_reduce because:
- We need to change api of
router_map_callrw_sendby- changing
routerparam intoreplicasets_all, asreplicasets_map_reducedoes not haveroutervariable in it. - adding extra argument -
return_raw
- changing
- The logic of sending map stage in
replicasets_map_reduceis more complex than logic ofrouter_map_callrw_send. In last one we only do RPC with no arguments or withgrouped_buckets[rs_id]and that's all. But inreplicasets_map_reducewe need to dynamically change those arguments which should be passed in RPC. Before RPC we add bucket arguments toargsand after RPC we delete them. The main problem here is that we can only easily add groupped buckets in args builder, but not delete, because the deletion is happened after RPC. If we want to do it in our newrouter_map_callrw_sendfunction we need to complicateargs_builderand usetable.deepcopy(it can slow down perf). - We need to change api of
router_map_callrw_collectby passing extrareturn_rawargument. - Also
router_map_callrw_collectwill be complicated as we should add two different ways of extracting results.
There was a problem hiding this comment.
changing router param into replicasets_all, as replicasets_map_reduce does not have router variable in it.
1.1 Yes, and it will become replicasets_map_send, which is way cleaner, since you will explicitly pass the replicasets and not use some non obvious loigic to create the list of replicasets based on optional argument grouped_buckets (which won't be needed). You can return replicasets from the router_map_prepare.
adding extra argument - return_raw
1.2. Let's just pass the opts and add is_async explicitly inside the function. And call the funciton router_map_callrw_send_async. The name should always say, what the function does.
The logic of sending map stage in replicasets_map_reduce is more complex than logic of router_map_callrw_send.
- Agree, see no other way than deepcopy.
We need to change api of router_map_callrw_collect by passing extra return_raw argument.
Also router_map_callrw_collect will be complicated as we should add two different ways of extracting results.
- You can always just create the new function
router_map_callrw_collect_rawand not pass therawto it
Again, up to you, if you think, that the current variant is better, I'm ok
kamenkremen
left a comment
There was a problem hiding this comment.
Great patch! Left some minor comments below
| local futures = {} | ||
| local opts_async = {is_async = true} | ||
| local replicasets_all = router.replicasets | ||
| local rs_ids = grouped_buckets and grouped_buckets or replicasets_all |
There was a problem hiding this comment.
Isn't that equivalent to local rs_ids = grouped_buckets or replicasets_all?
|
|
||
| -- | ||
| -- Perform Ref stage of the Ref-Map-Reduce process on a subset of all the | ||
| -- replicasets, which contains all the listed bucket IDs. |
| -- | ||
| -- Ref stage: collect. | ||
| -- | ||
| futures = futures or {} |
There was a problem hiding this comment.
futures is already either {}(because of line 820) or some table, so we don't need this
| -- Group the buckets by replicasets according to the router cache. | ||
| grouped_buckets, err = buckets_group(router, bucket_ids, timeout) | ||
| if err ~= nil then | ||
| return nil, err |
There was a problem hiding this comment.
Nit: triple space in indentation
8962951 to
8f5223c
Compare
Serpentian
left a comment
There was a problem hiding this comment.
I don't have any major comments, I think it's time to ask @Gerold103 for review, so that we can be sure, that there no major flaws, which I've missed
| local replicasets_all = router.replicasets | ||
| local deadline = fiber_clock() + timeout | ||
| if bucket_ids then | ||
| bucket_ids = bucket_ids or {} |
There was a problem hiding this comment.
Should be in the first commit
Gerold103
left a comment
There was a problem hiding this comment.
Thanks for working on this complex topic and being patient about comments 🙏.
3a2407a to
74f7492
Compare
74f7492 to
fc4a323
Compare
| local buckets = grouped_buckets[rs_id] | ||
| local target | ||
| if buckets then | ||
| args_ref[1] = 'storage_ref_make_with_buckets' |
There was a problem hiding this comment.
Nit: I'd propose to specify the whole arg table. It's very difficult to parse
7cc821a to
3eda39c
Compare
Gerold103
left a comment
There was a problem hiding this comment.
I am so sorry, but I have to write more comments 😭🥲.
| local bucket = M.bucket_refs[bucket_id] or | ||
| box.space._bucket:get{bucket_id} | ||
| if bucket then | ||
| assert(bucket.status == BACTIVE or bucket.status == BPINNED) | ||
| table.insert(res, bucket_id) |
There was a problem hiding this comment.
The problem is same as before - bucket refs have no status. This is bucket ref:
struct bucket_ref {
uint32_t ro;
uint32_t rw;
bool rw_lock;
bool ro_lock;
};
When the ref exists, you will try to do bucket.status which will throw an exception. The questions remains - how do tests pass if the code will fail always when any of these buckets have an existing ref object?
There was a problem hiding this comment.
I deleted the assertion in which we check bucket.status. The ref guarantees us that status of bucket will be ACTIVE or PINNED.
There was a problem hiding this comment.
Not exactly. Ref might also exist for a bucket in READONLY or SENDING state. Or even for SENT. But in this case it shouldn't matter. We are only checking a previously created ref. If it exists, it means a bucket couldn't change its status, I suppose. It might be readonly in theory somehow, but I can't see how at the moment. @Serpentian , to our talk about storage refs vs buckets being readable/writable.
Before this patch the main `map_callrw` ref functions such as `router_ref_storage_all` and `router_ref_storage_by_buckets` were enormous (71 and 108 lines of code). Also these functions have a large number of similar functional code blocks such as "preparing refs", "waiting refs" e.t.c. Since in tarantool#559 patch we will extend the logic of full map_callrw making it able to work with split args, the `router_ref_storage_all` can double in size. It can lead to degradation of our codebase due to less readability. To fix it we firstly determine general and repeated code blocks in ref functions: 1) `ref-prepare`: groups buckets by replicasets with router's cache, builds a table of "target" replicasets and waits necessary masters. 2) `ref-wait`: waits until future objects are ready in order to extract payload from it (responses of storages' functions). 3) `ref-process`: a custom logic for `full` or `partial` map_callrw modes which describes how we should process results from future objects. After defining the main stages of ref map_callrw functions we should unify them so that we can use them in both `router_ref_storage_all` and `router_ref_storage_by_buckets`. Needed for tarantool#559 NO_TEST=refactoring NO_DOC=refactoring
In this patch we change `status.SENDING/SENT/GARBAGE/RECEIVING/ACTIVE` on `BSENDING/BSENT/BGARBAGE/BRECEIVING/BACTIVE` to not repeat and complicate the code. Needed for tarantool#559 NO_DOC=refactoring NO_TEST=refactoring
This patch takes initialization of `rid` out to `router_map_callrw` and passes this variable to ref-functions. It is needed for future features tidiness, for example - `make full map_callrw with split args` in which the logic of `router_map_callrw` becomes more complex. Needed for tarantool#559 NO_DOC=refactoring NO_TEST=refactoring
Before this patch the `router-luatest/reload_test` checked router's services only with old routers. However in future patch (tarantoolgh-214) we need to check map_callrw with old storages. In order to make it able we: 1) change `vtest.cluster_new` so that we can pass server_config with certain ENV (LUA_PATH) variable into it. It can help us to create a new cluster on old version of vshard. 2) change `reload_router` to more general `reload_server` in order to unify the process of servers (router / storage) upgrade in `router-luatest/reload_test`. 3) unify the process of cluster creation - `create_cluster_on_specific_version` and the process of getting server's config with new ENV (LUA_PATH) variable - `get_config_for_specific_vshard_version`. Needed for tarantool#214 NO_DOC=refactoring NO_TEST=refactoring
26a0745 to
e54c676
Compare
| end | ||
| if mode == MAP_CALLRW_FULL then | ||
| replicasets_to_wait = replicasets_all | ||
| else | ||
| for rs_id, _ in pairs(grouped_buckets) do | ||
| table.insert(replicasets_to_wait, replicasets_all[rs_id]) | ||
| end | ||
| else | ||
| replicasets_to_wait = replicasets_all |
There was a problem hiding this comment.
Nit: the diff would have been smaller, if you would change the if to mode ~= MAP_CALLRW_FULL, and leave else unchanged.
| res, err = res[1], res[2] | ||
| if res == nil then | ||
| return nil, err, id | ||
| if res[1] == nil then | ||
| return nil, res[2], id |
| for rs_id, future in pairs(future_buckets) do | ||
| results[rs_id] = future:result()[1] | ||
| end |
There was a problem hiding this comment.
Since it was decided to reuse the old function storage_ref_make_with_buckets() with a new option force, it means this code will work on old storages too, but they will ignore the force option. You need to check for is_done result to make sure the ref is actually done. Otherwise you have the same problem as before when some storage isn't upgraded, and all its mentioned buckets were moved.
Or do we rely on total being not set on old storages, and then it fails further below? Still, at least an assertion of is_done would be a good enough safe guard.
There was a problem hiding this comment.
Or do we rely on total being not set on old storages, and then it fails further below?
Yes, the storage_ref_make_with_bucket on old storage can't return total amount of buckets and on router we get UNSUPPORTED error when we find out that there is no total field in the result.
Still, at least an assertion of is_done would be a good enough safe guard.
I don't think that is_done assertion can help us. When we do map_callrw on upgraded storages, we can't nicely determine which result comes from storage_ref and which comes from storage_ref_make_with_bucket because before that we unify results so that they all have a total field. And it may be that one res has total and is_done fields (storage_ref_make_with_buckets) and another has total but doesn't has is_done field (storage_ref).
We can write a small crutch)
diff --git a/vshard/router/init.lua b/vshard/router/init.lua
index bf3da49..087ef7d 100644
--- a/vshard/router/init.lua
+++ b/vshard/router/init.lua
@@ -914,6 +914,9 @@ local function router_ref_storage_all(router, bucket_ids, timeout, rid)
'full map_callrw with split args')
goto fail
end
+ if type(res.moved) == 'table' then
+ assert(res.is_done)
+ end
bucket_count = bucket_count + res.total
end
bucket_ids = router_map_callrw_process_moved(router, results)Or we can modify storage_ref_* functions so that they return its' identification. E.g. storage_ref will return {name = 'storage_ref', total = ...}
| -- | ||
| local function storage_ref_make_with_buckets(rid, timeout, bucket_ids) | ||
| local function storage_ref_make_with_buckets(rid, timeout, bucket_ids, opts) | ||
| local force = opts and opts.force or false |
There was a problem hiding this comment.
No need for or false. The effect will be the same anyway.
This patch introduces a new way of `map_callrw` execution by which we can pass some arguments to all storages and split buckets' arguments to those storages that have at least one bucket of `bucket_ids`. To achieve this we introduce a new string option - `mode` to `map_callrw` api. Also we change the logic of `router_ref_storage_all` ref function. Firstly we ref all storages and get back an amount of "moved" buckets according to the previously built router's cache. Then if there are no "moved" buckets we accumulate and check total amount of buckets on all storages and finish map_callrw ref stage. Otherwise, if there are some "moved" buckets we perform the second network hop by checking on which replicasets do the remaining "moved" buckets reside on. Closes tarantool#559 @TarantoolBot document Title: vshard: `mode` option for `router.map_callrw()` This string option regulates on which storages the user function will be executed via `map_callrw`. Possible values: 1) mode = 'partial'. In this mode user function will be executed on storages that have at least one bucket of 'bucket_ids'. The 'bucket_ids' option can be presented in two ways: like a numeric array of buckets' ids or like a map of buckets' arguments. In first one user function will only receive args, in second one it will additionally receive buckets' arguments. 2) mode = 'full'. In this mode user function will be executed with args on all storages in cluster. Also we can pass 'bucket_ids' only like a map of bucket's arguments - `bucket_ids = {id = {args}, id = {args}, ...}`. In this way the user function will additionally receive buckets' arguments on those storages that have at least one bucket of `bucket_ids`. If we didn't specify the 'mode' option, then it is set based on 'bucket_ids' option for backward compatibility. If 'bucket_ids' is presented, the mode will be 'partial' otherwise 'full'. Also now `map_callrw` ends with error in cases when we 1) pass `mode = 'full'` and a numeric array of buckets' ids - `bucket_ids = {id, id, ...}` 2) pass `mode = 'partial'` and `bucket_ids = nil`.
e54c676 to
23abb9a
Compare
This patch introduces a new way of
map_callrwexecution by which we canpass some arguments to all storages and split buckets' arguments to those
storages that have at least one bucket of
bucket_ids. To achieve this weintroduce a new string option -
modetomap_callrwapi.Also we change the logic of
router_ref_storage_allref function. Firstlywe ref all storages and get back an amount of "moved" buckets according
to the previously built router's cache. Then if there are no "moved"
buckets we accumulate and check total amount of buckets on all storages
and finish map_callrw ref stage. Otherwise, if there are some "moved"
buckets we perform the second network hop by checking on which replicasets
do the remaining "moved" buckets reside on.
Closes #559