• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tarantool / crud / 5199459215

pending completion
5199459215

push

github

DifferentialOrange
Release 1.2.0

Overview

  This release add two new flags: `noreturn` to ignore return values
  excessive transfer and encoding/decoding for insert/replace/etc
  (performance improvement up to 10% for batch requests) and
  `fetch_latest_metadata` to force fetching latest space format metadata
  right after a live migration (performance overhead may be up to 15%).

New features
  * Add `noreturn` option for operations:
    `insert`, `insert_object`, `insert_many`, `insert_object_many`,
    `replace`, `replace_object`, `replace_many`, `insert_object_many`,
    `upsert`, `upsert_object`, `upsert_many`, `upsert_object_many`,
    `update`, `delete` (#267).

Bugfixes
  * Crud DML operations returning stale schema for metadata generation.
    Now you may use `fetch_latest_metadata` flag to work with latest
    schema (#236).

1 of 1 new or added line in 1 file covered. (100.0%)

4549 of 4888 relevant lines covered (93.06%)

18261.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.38
/crud/common/utils.lua
1
local errors = require('errors')
383✔
2
local ffi = require('ffi')
383✔
3
local vshard = require('vshard')
383✔
4
local fun = require('fun')
383✔
5
local bit = require('bit')
383✔
6
local log = require('log')
383✔
7

8
local is_cartridge, cartridge = pcall(require, 'cartridge')
383✔
9
local is_cartridge_hotreload, cartridge_hotreload = pcall(require, 'cartridge.hotreload')
383✔
10

11
local const = require('crud.common.const')
383✔
12
local schema = require('crud.common.schema')
383✔
13
local dev_checks = require('crud.common.dev_checks')
383✔
14

15
local FlattenError = errors.new_class("FlattenError", {capture_stack = false})
383✔
16
local UnflattenError = errors.new_class("UnflattenError", {capture_stack = false})
383✔
17
local ParseOperationsError = errors.new_class('ParseOperationsError', {capture_stack = false})
383✔
18
local ShardingError = errors.new_class('ShardingError', {capture_stack = false})
383✔
19
local GetSpaceError = errors.new_class('GetSpaceError')
383✔
20
local GetSpaceFormatError = errors.new_class('GetSpaceFormatError', {capture_stack = false})
383✔
21
local FilterFieldsError = errors.new_class('FilterFieldsError', {capture_stack = false})
383✔
22
local NotInitializedError = errors.new_class('NotInitialized')
383✔
23
local StorageInfoError = errors.new_class('StorageInfoError')
383✔
24
local VshardRouterError = errors.new_class('VshardRouterError', {capture_stack = false})
383✔
25
local UtilsInternalError = errors.new_class('UtilsInternalError', {capture_stack = false})
383✔
26
local fiber = require('fiber')
383✔
27

28
local utils = {}
383✔
29

30
local space_format_cache = setmetatable({}, {__mode = 'k'})
383✔
31

32
-- copy from LuaJIT lj_char.c
33
local lj_char_bits = {
383✔
34
    0,
35
    1,  1,  1,  1,  1,  1,  1,  1,  1,  3,  3,  3,  3,  3,  1,  1,
36
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,
37
    2,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,
38
    152,152,152,152,152,152,152,152,152,152,  4,  4,  4,  4,  4,  4,
39
    4,176,176,176,176,176,176,160,160,160,160,160,160,160,160,160,
40
    160,160,160,160,160,160,160,160,160,160,160,  4,  4,  4,  4,132,
41
    4,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,
42
    192,192,192,192,192,192,192,192,192,192,192,  4,  4,  4,  4,  1,
43
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
44
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
45
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
46
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
47
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
48
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
49
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,
50
    128,128,128,128,128,128,128,128,128,128,128,128,128,128,128,128
51
}
52

53
local LJ_CHAR_IDENT = 0x80
383✔
54
local LJ_CHAR_DIGIT = 0x08
383✔
55

56
local LUA_KEYWORDS = {
383✔
57
    ['and'] = true,
58
    ['end'] = true,
59
    ['in'] = true,
60
    ['repeat'] = true,
61
    ['break'] = true,
62
    ['false'] = true,
63
    ['local'] = true,
64
    ['return'] = true,
65
    ['do'] = true,
66
    ['for'] = true,
67
    ['nil'] = true,
68
    ['then'] = true,
69
    ['else'] = true,
70
    ['function'] = true,
71
    ['not'] = true,
72
    ['true'] = true,
73
    ['elseif'] = true,
74
    ['if'] = true,
75
    ['or'] = true,
76
    ['until'] = true,
77
    ['while'] = true,
78
}
79

80
function utils.table_count(table)
383✔
81
    dev_checks("table")
4✔
82

83
    local cnt = 0
4✔
84
    for _, _ in pairs(table) do
23✔
85
        cnt = cnt + 1
15✔
86
    end
87

88
    return cnt
4✔
89
end
90

91
function utils.format_replicaset_error(replicaset_uuid, msg, ...)
383✔
92
    dev_checks("string", "string")
550✔
93

94
    return string.format(
550✔
95
        "Failed for %s: %s",
550✔
96
        replicaset_uuid,
550✔
97
        string.format(msg, ...)
550✔
98
    )
550✔
99
end
100

101
local function get_replicaset_by_replica_uuid(replicasets, uuid)
102
    for replicaset_uuid, replicaset in pairs(replicasets) do
54✔
103
        for replica_uuid, _ in pairs(replicaset.replicas) do
108✔
104
            if replica_uuid == uuid then
54✔
105
                return replicasets[replicaset_uuid]
18✔
106
            end
107
        end
108
    end
109

110
    return nil
×
111
end
112

113
function utils.get_space(space_name, vshard_router, timeout, replica_uuid)
383✔
114
    local replicasets, replicaset
115
    timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
163,655✔
116
    local deadline = fiber.clock() + timeout
327,310✔
117
    while (
118
        -- Break if the deadline condition is exceeded.
119
        -- Handling for deadline errors are below in the code.
120
        fiber.clock() < deadline
327,310✔
121
    ) do
163,655✔
122
        -- Try to get master with timeout.
123
        fiber.yield()
163,655✔
124
        replicasets = vshard_router:routeall()
327,310✔
125
        if replica_uuid ~= nil then
163,655✔
126
            -- Get the same replica on which the last DML operation was performed.
127
            -- This approach is temporary and is related to [1], [2].
128
            -- [1] https://github.com/tarantool/crud/issues/236
129
            -- [2] https://github.com/tarantool/crud/issues/361
130
            replicaset = get_replicaset_by_replica_uuid(replicasets, replica_uuid)
36✔
131
            break
18✔
132
        else
133
            replicaset = select(2, next(replicasets))
163,637✔
134
        end
135
        if replicaset ~= nil and
163,637✔
136
           replicaset.master ~= nil and
163,637✔
137
           replicaset.master.conn.error == nil then
163,637✔
138
            break
163,637✔
139
        end
140
    end
141

142
    if replicaset == nil then
163,655✔
143
        return nil, GetSpaceError:new(
×
144
            'The router returned empty replicasets: ' ..
×
145
            'perhaps other instances are unavailable or you have configured only the router')
×
146
    end
147

148
    if replicaset.master == nil then
163,655✔
149
        local error_msg = string.format(
×
150
            'The master was not found in replicaset %s, ' ..
×
151
            'check status of the master and repeat the operation later',
152
             replicaset.uuid)
×
153
        return nil, GetSpaceError:new(error_msg)
×
154
    end
155

156
    if replicaset.master.conn.error ~= nil then
163,655✔
157
        local error_msg = string.format(
×
158
            'The connection to the master of replicaset %s is not valid: %s',
159
             replicaset.uuid, replicaset.master.conn.error)
×
160
        return nil, GetSpaceError:new(error_msg)
×
161
    end
162

163
    local space = replicaset.master.conn.space[space_name]
163,655✔
164

165
    return space, nil, replicaset.master.conn.schema_version
163,655✔
166
end
167

168
function utils.get_space_format(space_name, vshard_router)
383✔
169
    local space, err = utils.get_space(space_name, vshard_router)
6,603✔
170
    if err ~= nil then
6,603✔
171
        return nil, GetSpaceFormatError:new("An error occurred during the operation: %s", err)
×
172
    end
173
    if space == nil then
6,603✔
174
        return nil, GetSpaceFormatError:new("Space %q doesn't exist", space_name)
302✔
175
    end
176

177
    local space_format = space:format()
6,452✔
178

179
    return space_format
6,452✔
180
end
181

182
function utils.fetch_latest_metadata_when_single_storage(space, space_name, netbox_schema_version,
383✔
183
                                                         vshard_router, opts, storage_info)
184
    -- Checking the relevance of the schema version is necessary
185
    -- to prevent the irrelevant metadata of the DML operation.
186
    -- This approach is temporary and is related to [1], [2].
187
    -- [1] https://github.com/tarantool/crud/issues/236
188
    -- [2] https://github.com/tarantool/crud/issues/361
189
    local latest_space, err
190
    assert(storage_info.replica_schema_version ~= nil,
36✔
191
           'check the replica_schema_version value from storage ' ..
18✔
192
           'for correct use of the fetch_latest_metadata opt')
18✔
193
    assert(storage_info.replica_uuid ~= nil,
36✔
194
           'check the replica_uuid value from storage ' ..
18✔
195
           'for correct use of the fetch_latest_metadata opt')
18✔
196
    assert(netbox_schema_version ~= nil,
36✔
197
           'check the netbox_schema_version value from net_box conn on router ' ..
18✔
198
           'for correct use of the fetch_latest_metadata opt')
18✔
199
    if storage_info.replica_schema_version ~= netbox_schema_version then
18✔
200
        local ok, reload_schema_err = schema.reload_schema(vshard_router)
18✔
201
        if ok then
18✔
202
            latest_space, err = utils.get_space(space_name, vshard_router,
36✔
203
                                                opts.timeout, storage_info.replica_uuid)
36✔
204
            if err ~= nil then
18✔
205
                local warn_msg = "Failed to fetch space for latest schema actualization, metadata may be outdated: %s"
×
206
                log.warn(warn_msg, err)
×
207
            end
208
            if latest_space == nil then
18✔
209
                log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
210
            end
211
        else
212
            log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
213
        end
214
    end
215
    if err == nil and latest_space ~= nil then
18✔
216
        space = latest_space
18✔
217
    end
218

219
    return space
18✔
220
end
221

222
function utils.fetch_latest_metadata_when_map_storages(space, space_name, vshard_router, opts,
383✔
223
                                                       storages_info, netbox_schema_version)
224
    -- Checking the relevance of the schema version is necessary
225
    -- to prevent the irrelevant metadata of the DML operation.
226
    -- This approach is temporary and is related to [1], [2].
227
    -- [1] https://github.com/tarantool/crud/issues/236
228
    -- [2] https://github.com/tarantool/crud/issues/361
229
    local latest_space, err
230
    for _, storage_info in pairs(storages_info) do
33✔
231
        assert(storage_info.replica_schema_version ~= nil,
32✔
232
            'check the replica_schema_version value from storage ' ..
16✔
233
            'for correct use of the fetch_latest_metadata opt')
16✔
234
        assert(netbox_schema_version ~= nil,
32✔
235
               'check the netbox_schema_version value from net_box conn on router ' ..
16✔
236
               'for correct use of the fetch_latest_metadata opt')
16✔
237
        if storage_info.replica_schema_version ~= netbox_schema_version then
16✔
238
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
15✔
239
            if ok then
15✔
240
                latest_space, err = utils.get_space(space_name, vshard_router, opts.timeout)
30✔
241
                if err ~= nil then
15✔
242
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
243
                                     "metadata may be outdated: %s"
244
                    log.warn(warn_msg, err)
×
245
                end
246
                if latest_space == nil then
15✔
247
                    log.warn("Failed to find space for latest schema actualization, metadata may be outdated")
×
248
                end
249
            else
250
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
251
            end
252
            if err == nil and latest_space ~= nil then
15✔
253
                space = latest_space
15✔
254
            end
255
            break
15✔
256
        end
257
    end
258

259
    return space
16✔
260
end
261

262
function utils.fetch_latest_metadata_for_select(space_name, vshard_router, opts,
383✔
263
                                                storages_info, iter)
264
    -- Checking the relevance of the schema version is necessary
265
    -- to prevent the irrelevant metadata of the DML operation.
266
    -- This approach is temporary and is related to [1], [2].
267
    -- [1] https://github.com/tarantool/crud/issues/236
268
    -- [2] https://github.com/tarantool/crud/issues/361
269
    for _, storage_info in pairs(storages_info) do
8✔
270
        assert(storage_info.replica_schema_version ~= nil,
8✔
271
               'check the replica_schema_version value from storage ' ..
4✔
272
               'for correct use of the fetch_latest_metadata opt')
4✔
273
        assert(iter.netbox_schema_version ~= nil,
8✔
274
               'check the netbox_schema_version value from net_box conn on router ' ..
4✔
275
               'for correct use of the fetch_latest_metadata opt')
4✔
276
        if storage_info.replica_schema_version ~= iter.netbox_schema_version then
4✔
277
            local ok, reload_schema_err = schema.reload_schema(vshard_router)
4✔
278
            if ok then
4✔
279
                local err
280
                iter.space, err = utils.get_space(space_name, vshard_router, opts.timeout)
8✔
281
                if err ~= nil then
4✔
282
                    local warn_msg = "Failed to fetch space for latest schema actualization, " ..
×
283
                                     "metadata may be outdated: %s"
284
                    log.warn(warn_msg, err)
×
285
                end
286
            else
287
                log.warn("Failed to reload schema, metadata may be outdated: %s", reload_schema_err)
×
288
            end
289
            break
290
        end
291
    end
292

293
    return iter
4✔
294
end
295

296
local function append(lines, s, ...)
297
    table.insert(lines, string.format(s, ...))
3,944✔
298
end
299

300
local flatten_functions_cache = setmetatable({}, {__mode = 'k'})
383✔
301

302
function utils.flatten(object, space_format, bucket_id, skip_nullability_check)
383✔
303
    local flatten_func = flatten_functions_cache[space_format]
6,544✔
304
    if flatten_func ~= nil then
6,544✔
305
        local data, err = flatten_func(object, bucket_id, skip_nullability_check)
6,420✔
306
        if err ~= nil then
6,420✔
307
            return nil, FlattenError:new(err)
1,000✔
308
        end
309
        return data
5,920✔
310
    end
311

312
    local lines = {}
124✔
313
    append(lines, 'local object, bucket_id, skip_nullability_check = ...')
124✔
314

315
    append(lines, 'for k in pairs(object) do')
124✔
316
    append(lines, '    if fieldmap[k] == nil then')
124✔
317
    append(lines, '        return nil, format(\'Unknown field %%q is specified\', k)')
124✔
318
    append(lines, '    end')
124✔
319
    append(lines, 'end')
124✔
320

321
    local len = #space_format
124✔
322
    append(lines, 'local result = {%s}', string.rep('NULL,', len))
124✔
323

324
    local fieldmap = {}
124✔
325

326
    for i, field in ipairs(space_format) do
742✔
327
        fieldmap[field.name] = true
618✔
328
        if field.name ~= 'bucket_id' then
618✔
329
            append(lines, 'if object[%q] ~= nil then', field.name)
494✔
330
            append(lines, '    result[%d] = object[%q]', i, field.name)
494✔
331
            if field.is_nullable ~= true then
494✔
332
                append(lines, 'elseif skip_nullability_check ~= true then')
425✔
333
                append(lines, '    return nil, \'Field %q isn\\\'t nullable' ..
850✔
334
                              ' (set skip_nullability_check_on_flatten option to true to skip check)\'',
425✔
335
                              field.name)
425✔
336
            end
337
            append(lines, 'end')
988✔
338
        else
339
            append(lines, 'if bucket_id ~= nil then')
124✔
340
            append(lines, '    result[%d] = bucket_id', i, field.name)
124✔
341
            append(lines, 'else')
124✔
342
            append(lines, '    result[%d] = object[%q]', i, field.name)
124✔
343
            append(lines, 'end')
124✔
344
        end
345
    end
346
    append(lines, 'return result')
124✔
347

348
    local code = table.concat(lines, '\n')
124✔
349
    local env = {
124✔
350
        pairs = pairs,
124✔
351
        format = string.format,
124✔
352
        fieldmap = fieldmap,
124✔
353
        NULL = box.NULL,
124✔
354
    }
355
    flatten_func = assert(load(code, nil, 't', env))
124✔
356

357
    flatten_functions_cache[space_format] = flatten_func
124✔
358
    local data, err = flatten_func(object, bucket_id, skip_nullability_check)
124✔
359
    if err ~= nil then
124✔
360
        return nil, FlattenError:new(err)
12✔
361
    end
362
    return data
118✔
363
end
364

365
function utils.unflatten(tuple, space_format)
383✔
366
    if tuple == nil then return nil end
5,836✔
367

368
    local object = {}
5,836✔
369

370
    for fieldno, field_format in ipairs(space_format) do
39,565✔
371
        local value = tuple[fieldno]
33,730✔
372

373
        if not field_format.is_nullable and value == nil then
33,730✔
374
            return nil, UnflattenError:new("Field %s isn't nullable", fieldno)
2✔
375
        end
376

377
        object[field_format.name] = value
33,729✔
378
    end
379

380
    return object
5,835✔
381
end
382

383
function utils.extract_key(tuple, key_parts)
383✔
384
    local key = {}
297,284✔
385
    for i, part in ipairs(key_parts) do
595,288✔
386
        key[i] = tuple[part.fieldno]
298,004✔
387
    end
388
    return key
297,284✔
389
end
390

391
function utils.merge_primary_key_parts(key_parts, pk_parts)
383✔
392
    local merged_parts = {}
50,172✔
393
    local key_fieldnos = {}
50,172✔
394

395
    for _, part in ipairs(key_parts) do
100,563✔
396
        table.insert(merged_parts, part)
50,391✔
397
        key_fieldnos[part.fieldno] = true
50,391✔
398
    end
399

400
    for _, pk_part in ipairs(pk_parts) do
113,061✔
401
        if not key_fieldnos[pk_part.fieldno] then
62,889✔
402
            table.insert(merged_parts, pk_part)
34,757✔
403
        end
404
    end
405

406
    return merged_parts
50,172✔
407
end
408

409
function utils.enrich_field_names_with_cmp_key(field_names, key_parts, space_format)
383✔
410
    if field_names == nil then
36,155✔
411
        return nil
36,070✔
412
    end
413

414
    local enriched_field_names = {}
85✔
415
    local key_field_names = {}
85✔
416

417
    for _, field_name in ipairs(field_names) do
251✔
418
        table.insert(enriched_field_names, field_name)
166✔
419
        key_field_names[field_name] = true
166✔
420
    end
421

422
    for _, part in ipairs(key_parts) do
223✔
423
        local field_name = space_format[part.fieldno].name
138✔
424
        if not key_field_names[field_name] then
138✔
425
            table.insert(enriched_field_names, field_name)
108✔
426
            key_field_names[field_name] = true
108✔
427
        end
428
    end
429

430
    return enriched_field_names
85✔
431
end
432

433

434
local function get_version_suffix(suffix_candidate)
435
    if type(suffix_candidate) ~= 'string' then
844✔
436
        return nil
×
437
    end
438

439
    if suffix_candidate:find('^entrypoint$')
844✔
440
    or suffix_candidate:find('^alpha%d$')
844✔
441
    or suffix_candidate:find('^beta%d$')
843✔
442
    or suffix_candidate:find('^rc%d$') then
841✔
443
        return suffix_candidate
7✔
444
    end
445

446
    return nil
837✔
447
end
448

449
utils.get_version_suffix = get_version_suffix
383✔
450

451

452
local suffix_with_digit_weight = {
383✔
453
    alpha = -3000,
454
    beta  = -2000,
455
    rc    = -1000,
456
}
457

458
local function get_version_suffix_weight(suffix)
459
    if suffix == nil then
4,834✔
460
        return 0
4,791✔
461
    end
462

463
    if suffix:find('^entrypoint$') then
43✔
464
        return -math.huge
9✔
465
    end
466

467
    for header, weight in pairs(suffix_with_digit_weight) do
100✔
468
        local pos, _, digits = suffix:find('^' .. header .. '(%d)$')
64✔
469
        if pos ~= nil then
64✔
470
            return weight + tonumber(digits)
32✔
471
        end
472
    end
473

474
    UtilsInternalError:assert(false,
4✔
475
        'Unexpected suffix %q, parse with "utils.get_version_suffix" first', suffix)
2✔
476
end
477

478
utils.get_version_suffix_weight = get_version_suffix_weight
383✔
479

480

481
local function is_version_ge(major, minor,
482
                             patch, suffix,
483
                             major_to_compare, minor_to_compare,
484
                             patch_to_compare, suffix_to_compare)
485
    major = major or 0
2,412✔
486
    minor = minor or 0
2,412✔
487
    patch = patch or 0
2,412✔
488
    local suffix_weight = get_version_suffix_weight(suffix)
2,412✔
489

490
    major_to_compare = major_to_compare or 0
2,412✔
491
    minor_to_compare = minor_to_compare or 0
2,412✔
492
    patch_to_compare = patch_to_compare or 0
2,412✔
493
    local suffix_weight_to_compare = get_version_suffix_weight(suffix_to_compare)
2,412✔
494

495
    if major > major_to_compare then return true end
2,412✔
496
    if major < major_to_compare then return false end
2,399✔
497

498
    if minor > minor_to_compare then return true end
2,388✔
499
    if minor < minor_to_compare then return false end
17✔
500

501
    if patch > patch_to_compare then return true end
12✔
502
    if patch < patch_to_compare then return false end
11✔
503

504
    if suffix_weight > suffix_weight_to_compare then return true end
10✔
505
    if suffix_weight < suffix_weight_to_compare then return false end
7✔
506

507
    return true
4✔
508
end
509

510
utils.is_version_ge = is_version_ge
383✔
511

512

513
local function is_version_in_range(major, minor,
514
                                   patch, suffix,
515
                                   major_left_side, minor_left_side,
516
                                   patch_left_side, suffix_left_side,
517
                                   major_right_side, minor_right_side,
518
                                   patch_right_side, suffix_right_side)
519
    return is_version_ge(major, minor,
4✔
520
                         patch, suffix,
2✔
521
                         major_left_side, minor_left_side,
2✔
522
                         patch_left_side, suffix_left_side)
2✔
523
       and is_version_ge(major_right_side, minor_right_side,
4✔
524
                         patch_right_side, suffix_right_side,
2✔
525
                         major, minor,
2✔
526
                         patch, suffix)
4✔
527
end
528

529
utils.is_version_in_range = is_version_in_range
383✔
530

531

532
local function get_tarantool_version()
533
    local version_parts = rawget(_G, '_TARANTOOL'):split('-', 1)
835✔
534

535
    local major_minor_patch_parts = version_parts[1]:split('.', 2)
835✔
536
    local major = tonumber(major_minor_patch_parts[1])
835✔
537
    local minor = tonumber(major_minor_patch_parts[2])
835✔
538
    local patch = tonumber(major_minor_patch_parts[3])
835✔
539

540
    local suffix = get_version_suffix(version_parts[2])
835✔
541

542
    return major, minor, patch, suffix
835✔
543
end
544

545
utils.get_tarantool_version = get_tarantool_version
383✔
546

547

548
local function tarantool_version_at_least(wanted_major, wanted_minor, wanted_patch)
549
    local major, minor, patch, suffix = get_tarantool_version()
451✔
550

551
    return is_version_ge(major, minor, patch, suffix,
451✔
552
                         wanted_major, wanted_minor, wanted_patch, nil)
451✔
553
end
554

555
utils.tarantool_version_at_least = tarantool_version_at_least
383✔
556

557

558
local enabled_tarantool_features = {}
383✔
559

560
local function determine_enabled_features()
561
    local major, minor, patch, suffix = get_tarantool_version()
383✔
562

563
    -- since Tarantool 2.3.1
564
    enabled_tarantool_features.fieldpaths = is_version_ge(major, minor, patch, suffix,
766✔
565
                                                          2, 3, 1, nil)
766✔
566

567
    -- since Tarantool 2.4.1
568
    enabled_tarantool_features.uuids = is_version_ge(major, minor, patch, suffix,
766✔
569
                                                     2, 4, 1, nil)
766✔
570

571
    -- since Tarantool 2.6.3 / 2.7.2 / 2.8.1
572
    enabled_tarantool_features.jsonpath_indexes = is_version_ge(major, minor, patch, suffix,
766✔
573
                                                                2, 8, 1, nil)
383✔
574
                                               or is_version_in_range(major, minor, patch, suffix,
383✔
575
                                                                      2, 7, 2, nil,
576
                                                                      2, 7, math.huge, nil)
×
577
                                               or is_version_in_range(major, minor, patch, suffix,
×
578
                                                                      2, 6, 3, nil,
579
                                                                      2, 6, math.huge, nil)
383✔
580

581
    -- The merger module was implemented in 2.2.1, see [1].
582
    -- However it had the critical problem [2], which leads to
583
    -- segfault at attempt to use the module from a fiber serving
584
    -- iproto request. So we don't use it in versions before the
585
    -- fix.
586
    --
587
    -- [1]: https://github.com/tarantool/tarantool/issues/3276
588
    -- [2]: https://github.com/tarantool/tarantool/issues/4954
589
    enabled_tarantool_features.builtin_merger = is_version_ge(major, minor, patch, suffix,
766✔
590
                                                              2, 6, 0, nil)
383✔
591
                                             or is_version_in_range(major, minor, patch, suffix,
383✔
592
                                                                    2, 5, 1, nil,
593
                                                                    2, 5, math.huge, nil)
×
594
                                             or is_version_in_range(major, minor, patch, suffix,
×
595
                                                                    2, 4, 2, nil,
596
                                                                    2, 4, math.huge, nil)
×
597
                                             or is_version_in_range(major, minor, patch, suffix,
×
598
                                                                    2, 3, 3, nil,
599
                                                                    2, 3, math.huge, nil)
383✔
600

601
    -- The external merger module leans on a set of relatively
602
    -- new APIs in tarantool. So it works only on tarantool
603
    -- versions, which offer those APIs.
604
    --
605
    -- See README of the module:
606
    -- https://github.com/tarantool/tuple-merger
607
    enabled_tarantool_features.external_merger = is_version_ge(major, minor, patch, suffix,
766✔
608
                                                               2, 7, 0, nil)
383✔
609
                                              or is_version_in_range(major, minor, patch, suffix,
383✔
610
                                                                     2, 6, 1, nil,
611
                                                                     2, 6, math.huge, nil)
×
612
                                              or is_version_in_range(major, minor, patch, suffix,
×
613
                                                                     2, 5, 2, nil,
614
                                                                     2, 5, math.huge, nil)
×
615
                                              or is_version_in_range(major, minor, patch, suffix,
×
616
                                                                     2, 4, 3, nil,
617
                                                                     2, 4, math.huge, nil)
×
618
                                              or is_version_in_range(major, minor, patch, suffix,
×
619
                                                                     1, 10, 8, nil,
620
                                                                     1, 10, math.huge, nil)
383✔
621
end
622

623
function utils.tarantool_supports_fieldpaths()
383✔
624
    if enabled_tarantool_features.fieldpaths == nil then
906✔
625
        determine_enabled_features()
×
626
    end
627

628
    return enabled_tarantool_features.fieldpaths
906✔
629
end
630

631
function utils.tarantool_supports_uuids()
383✔
632
    if enabled_tarantool_features.uuids == nil then
55✔
633
        determine_enabled_features()
×
634
    end
635

636
    return enabled_tarantool_features.uuids
55✔
637
end
638

639
function utils.tarantool_supports_jsonpath_indexes()
383✔
640
    if enabled_tarantool_features.jsonpath_indexes == nil then
55✔
641
        determine_enabled_features()
×
642
    end
643

644
    return enabled_tarantool_features.jsonpath_indexes
55✔
645
end
646

647
function utils.tarantool_has_builtin_merger()
383✔
648
    if enabled_tarantool_features.builtin_merger == nil then
404✔
649
        determine_enabled_features()
×
650
    end
651

652
    return enabled_tarantool_features.builtin_merger
404✔
653
end
654

655
function utils.tarantool_supports_external_merger()
383✔
656
    if enabled_tarantool_features.external_merger == nil then
404✔
657
        determine_enabled_features()
383✔
658
    end
659

660
    return enabled_tarantool_features.external_merger
404✔
661
end
662

663
local function add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id)
664
    if id < 2 or tuple[id - 1] ~= box.NULL then
×
665
        return operations
×
666
    end
667

668
    if space_format[id - 1].is_nullable and not operations_map[id - 1] then
×
669
        table.insert(operations, {'=', id - 1, box.NULL})
×
670
        return add_nullable_fields_recursive(operations, operations_map, space_format, tuple, id - 1)
×
671
    end
672

673
    return operations
×
674
end
675

676
-- Tarantool < 2.1 has no fields `box.error.NO_SUCH_FIELD_NO` and `box.error.NO_SUCH_FIELD_NAME`.
677
if tarantool_version_at_least(2, 1, 0, nil) then
766✔
678
    function utils.is_field_not_found(err_code)
383✔
679
        return err_code == box.error.NO_SUCH_FIELD_NO or err_code == box.error.NO_SUCH_FIELD_NAME
36✔
680
    end
681
else
682
    function utils.is_field_not_found(err_code)
×
683
        return err_code == box.error.NO_SUCH_FIELD
×
684
    end
685
end
686

687
local function get_operations_map(operations)
688
    local map = {}
×
689
    for _, operation in ipairs(operations) do
×
690
        map[operation[2]] = true
×
691
    end
692

693
    return map
×
694
end
695

696
function utils.add_intermediate_nullable_fields(operations, space_format, tuple)
383✔
697
    if tuple == nil then
2✔
698
        return operations
×
699
    end
700

701
    -- If tarantool doesn't supports the fieldpaths, we already
702
    -- have converted operations (see this function call in update.lua)
703
    if utils.tarantool_supports_fieldpaths() then
4✔
704
        local formatted_operations, err = utils.convert_operations(operations, space_format)
2✔
705
        if err ~= nil then
2✔
706
            return operations
2✔
707
        end
708

709
        operations = formatted_operations
×
710
    end
711

712
    -- We need this map to check if there is a field update
713
    -- operation with constant complexity
714
    local operations_map = get_operations_map(operations)
×
715
    for _, operation in ipairs(operations) do
×
716
        operations = add_nullable_fields_recursive(
×
717
            operations, operations_map,
718
            space_format, tuple, operation[2]
×
719
        )
720
    end
721

722
    table.sort(operations, function(v1, v2) return v1[2] < v2[2] end)
×
723
    return operations
×
724
end
725

726
function utils.convert_operations(user_operations, space_format)
383✔
727
    local converted_operations = {}
2✔
728

729
    for _, operation in ipairs(user_operations) do
2✔
730
        if type(operation[2]) == 'string' then
2✔
731
            local field_id
732
            for fieldno, field_format in ipairs(space_format) do
10✔
733
                if field_format.name == operation[2] then
8✔
734
                    field_id = fieldno
×
735
                    break
736
                end
737
            end
738

739
            if field_id == nil then
2✔
740
                return nil, ParseOperationsError:new(
4✔
741
                        "Space format doesn't contain field named %q", operation[2])
4✔
742
            end
743

744
            table.insert(converted_operations, {
×
745
                operation[1], field_id, operation[3]
×
746
            })
747
        else
748
            table.insert(converted_operations, operation)
×
749
        end
750
    end
751

752
    return converted_operations
×
753
end
754

755
function utils.unflatten_rows(rows, metadata)
383✔
756
    if metadata == nil then
5,344✔
757
        return nil, UnflattenError:new('Metadata is not provided')
×
758
    end
759

760
    local result = table.new(#rows, 0)
5,344✔
761
    local err
762
    for i, row in ipairs(rows) do
11,088✔
763
        result[i], err = utils.unflatten(row, metadata)
11,488✔
764
        if err ~= nil then
5,744✔
765
            return nil, err
×
766
        end
767
    end
768
    return result
5,344✔
769
end
770

771
local inverted_tarantool_iters = {
383✔
772
    [box.index.EQ] = box.index.REQ,
383✔
773
    [box.index.GT] = box.index.LT,
383✔
774
    [box.index.GE] = box.index.LE,
383✔
775
    [box.index.LT] = box.index.GT,
383✔
776
    [box.index.LE] = box.index.GE,
383✔
777
    [box.index.REQ] = box.index.EQ,
383✔
778
}
779

780
function utils.invert_tarantool_iter(iter)
383✔
781
    local inverted_iter = inverted_tarantool_iters[iter]
44✔
782
    assert(inverted_iter ~= nil, "Unsupported Tarantool iterator: " .. tostring(iter))
44✔
783
    return inverted_iter
44✔
784
end
785

786
function utils.reverse_inplace(t)
383✔
787
    for i = 1,math.floor(#t / 2) do
91✔
788
        t[i], t[#t - i + 1] = t[#t - i + 1], t[i]
43✔
789
    end
790
    return t
48✔
791
end
792

793
function utils.get_bucket_id_fieldno(space, shard_index_name)
383✔
794
    shard_index_name = shard_index_name or 'bucket_id'
297,626✔
795
    local bucket_id_index = space.index[shard_index_name]
297,626✔
796
    if bucket_id_index == nil then
297,626✔
797
        return nil, ShardingError:new('%q index is not found', shard_index_name)
24✔
798
    end
799

800
    return bucket_id_index.parts[1].fieldno
297,614✔
801
end
802

803
-- Build a map with field number as a keys and part number
804
-- as a values using index parts as a source.
805
function utils.get_index_fieldno_map(index_parts)
383✔
806
    dev_checks('table')
87✔
807

808
    local fieldno_map = {}
87✔
809
    for i, part in ipairs(index_parts) do
236✔
810
        local fieldno = part.fieldno
149✔
811
        fieldno_map[fieldno] = i
149✔
812
    end
813

814
    return fieldno_map
87✔
815
end
816

817
-- Build a map with field names as a keys and fieldno's
818
-- as a values using space format as a source.
819
function utils.get_format_fieldno_map(space_format)
383✔
820
    dev_checks('table')
36,729✔
821

822
    local fieldno_map = {}
36,729✔
823
    for fieldno, field_format in ipairs(space_format) do
185,055✔
824
        fieldno_map[field_format.name] = fieldno
148,326✔
825
    end
826

827
    return fieldno_map
36,729✔
828
end
829

830
local uuid_t = ffi.typeof('struct tt_uuid')
383✔
831
function utils.is_uuid(value)
383✔
832
    return ffi.istype(uuid_t, value)
5✔
833
end
834

835
local function get_field_format(space_format, field_name)
836
    dev_checks('table', 'string')
466✔
837

838
    local metadata = space_format_cache[space_format]
466✔
839
    if metadata ~= nil then
466✔
840
        return metadata[field_name]
446✔
841
    end
842

843
    space_format_cache[space_format] = {}
20✔
844
    for _, field in ipairs(space_format) do
134✔
845
        space_format_cache[space_format][field.name] = field
114✔
846
    end
847

848
    return space_format_cache[space_format][field_name]
20✔
849
end
850

851
local function filter_format_fields(space_format, field_names)
852
    dev_checks('table', 'table')
182✔
853

854
    local filtered_space_format = {}
182✔
855

856
    for i, field_name in ipairs(field_names) do
618✔
857
        filtered_space_format[i] = get_field_format(space_format, field_name)
932✔
858
        if filtered_space_format[i] == nil then
466✔
859
            return nil, FilterFieldsError:new(
60✔
860
                    'Space format doesn\'t contain field named %q', field_name
30✔
861
            )
60✔
862
        end
863
    end
864

865
    return filtered_space_format
152✔
866
end
867

868
function utils.get_fields_format(space_format, field_names)
383✔
869
    dev_checks('table', '?table')
35,795✔
870

871
    if field_names == nil then
35,795✔
872
        return table.copy(space_format)
35,731✔
873
    end
874

875
    local filtered_space_format, err = filter_format_fields(space_format, field_names)
64✔
876

877
    if err ~= nil then
64✔
878
        return nil, err
2✔
879
    end
880

881
    return filtered_space_format
62✔
882
end
883

884
function utils.format_result(rows, space, field_names)
383✔
885
    local result = {}
103,786✔
886
    local err
887
    local space_format = space:format()
103,786✔
888
    result.rows = rows
103,786✔
889

890
    if field_names == nil then
103,786✔
891
        result.metadata = table.copy(space_format)
207,336✔
892
        return result
103,668✔
893
    end
894

895
    result.metadata, err = filter_format_fields(space_format, field_names)
236✔
896

897
    if err ~= nil then
118✔
898
        return nil, err
28✔
899
    end
900

901
    return result
90✔
902
end
903

904
local function truncate_tuple_metadata(tuple_metadata, field_names)
905
    dev_checks('?table', 'table')
31✔
906

907
    if tuple_metadata == nil then
31✔
908
        return nil
3✔
909
    end
910

911
    local truncated_metadata = {}
28✔
912

913
    if #tuple_metadata < #field_names then
28✔
914
        return nil, FilterFieldsError:new(
×
915
                'Field names don\'t match to tuple metadata'
916
        )
917
    end
918

919
    for i, name in ipairs(field_names) do
79✔
920
        if tuple_metadata[i].name ~= name then
53✔
921
            return nil, FilterFieldsError:new(
4✔
922
                    'Field names don\'t match to tuple metadata'
923
            )
4✔
924
        end
925

926
        table.insert(truncated_metadata, tuple_metadata[i])
51✔
927
    end
928

929
    return truncated_metadata
26✔
930
end
931

932
function utils.cut_objects(objs, field_names)
383✔
933
    dev_checks('table', 'table')
5✔
934

935
    for i, obj in ipairs(objs) do
20✔
936
        objs[i] = schema.filter_obj_fields(obj, field_names)
30✔
937
    end
938

939
    return objs
5✔
940
end
941

942
function utils.cut_rows(rows, metadata, field_names)
383✔
943
    dev_checks('table', '?table', 'table')
31✔
944

945
    local truncated_metadata, err = truncate_tuple_metadata(metadata, field_names)
31✔
946

947
    if err ~= nil then
31✔
948
        return nil, err
2✔
949
    end
950

951
    for i, row in ipairs(rows) do
72✔
952
        rows[i] = schema.truncate_row_trailing_fields(row, field_names)
86✔
953
    end
954

955
    return {
29✔
956
        metadata = truncated_metadata,
29✔
957
        rows = rows,
29✔
958
    }
29✔
959
end
960

961
local function flatten_obj(vshard_router, space_name, obj, skip_nullability_check)
962
    local space_format, err = utils.get_space_format(space_name, vshard_router)
6,603✔
963
    if err ~= nil then
6,603✔
964
        return nil, FlattenError:new("Failed to get space format: %s", err), const.NEED_SCHEMA_RELOAD
302✔
965
    end
966

967
    local tuple, err = utils.flatten(obj, space_format, nil, skip_nullability_check)
6,452✔
968
    if err ~= nil then
6,452✔
969
        return nil, FlattenError:new("Object is specified in bad format: %s", err), const.NEED_SCHEMA_RELOAD
1,008✔
970
    end
971

972
    return tuple
5,948✔
973
end
974

975
function utils.flatten_obj_reload(vshard_router, space_name, obj, skip_nullability_check)
383✔
976
    return schema.wrap_func_reload(vshard_router, flatten_obj, space_name, obj, skip_nullability_check)
6,268✔
977
end
978

979
-- Merge two options map.
980
--
981
-- `opts_a` and/or `opts_b` can be `nil`.
982
--
983
-- If `opts_a.foo` and `opts_b.foo` exists, prefer `opts_b.foo`.
984
function utils.merge_options(opts_a, opts_b)
383✔
985
    return fun.chain(opts_a or {}, opts_b or {}):tomap()
11,592✔
986
end
987

988
local function lj_char_isident(n)
989
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_IDENT) == LJ_CHAR_IDENT
7,071✔
990
end
991

992
local function lj_char_isdigit(n)
993
    return bit.band(lj_char_bits[n + 2], LJ_CHAR_DIGIT) == LJ_CHAR_DIGIT
456✔
994
end
995

996
function utils.check_name_isident(name)
383✔
997
    dev_checks('string')
457✔
998

999
    -- sharding function name cannot
1000
    -- be equal to lua keyword
1001
    if LUA_KEYWORDS[name] then
457✔
1002
        return false
1✔
1003
    end
1004

1005
    -- sharding function name cannot
1006
    -- begin with a digit
1007
    local char_number = string.byte(name:sub(1,1))
912✔
1008
    if lj_char_isdigit(char_number) then
912✔
1009
        return false
1✔
1010
    end
1011

1012
    -- sharding func name must be sequence
1013
    -- of letters, digits, or underscore symbols
1014
    for i = 1, #name do
7,525✔
1015
        local char_number = string.byte(name:sub(i,i))
14,142✔
1016
        if not lj_char_isident(char_number) then
14,142✔
1017
            return false
1✔
1018
        end
1019
    end
1020

1021
    return true
454✔
1022
end
1023

1024
function utils.update_storage_call_error_description(err, func_name, replicaset_uuid)
383✔
1025
    if err == nil then
584✔
1026
        return nil
×
1027
    end
1028

1029
    if err.type == 'ClientError' and type(err.message) == 'string' then
1,429✔
1030
        if err.message == string.format("Procedure '%s' is not defined", func_name) then
843✔
1031
            if func_name:startswith('_crud.') then
16✔
1032
                err = NotInitializedError:new("Function %s is not registered: " ..
12✔
1033
                    "crud isn't initialized on replicaset %q or crud module versions mismatch " ..
6✔
1034
                    "between router and storage",
6✔
1035
                    func_name, replicaset_uuid or "Unknown")
12✔
1036
            else
1037
                err = NotInitializedError:new("Function %s is not registered", func_name)
4✔
1038
            end
1039
        end
1040
    end
1041
    return err
584✔
1042
end
1043

1044
--- Insert each value from values to list
1045
--
1046
-- @function list_extend
1047
--
1048
-- @param table list
1049
--  List to be extended
1050
--
1051
-- @param table values
1052
--  Values to be inserted to list
1053
--
1054
-- @return[1] list
1055
--  List with old values and inserted values
1056
function utils.list_extend(list, values)
383✔
1057
    dev_checks('table', 'table')
35,315✔
1058

1059
    for _, value in ipairs(values) do
128,549✔
1060
        table.insert(list, value)
93,234✔
1061
    end
1062

1063
    return list
35,315✔
1064
end
1065

1066
function utils.list_slice(list, start_index, end_index)
383✔
1067
    dev_checks('table', 'number', '?number')
48✔
1068

1069
    if end_index == nil then
48✔
1070
        end_index = table.maxn(list)
48✔
1071
    end
1072

1073
    local slice = {}
48✔
1074
    for i = start_index, end_index do
120✔
1075
        table.insert(slice, list[i])
72✔
1076
    end
1077

1078
    return slice
48✔
1079
end
1080

1081
--- Polls replicas for storage state
1082
--
1083
-- @function storage_info
1084
--
1085
-- @tparam ?number opts.timeout
1086
--  Function call timeout
1087
--
1088
-- @tparam ?string|table opts.vshard_router
1089
--  Cartridge vshard group name or vshard router instance.
1090
--
1091
-- @return a table of storage states by replica uuid.
1092
function utils.storage_info(opts)
383✔
1093
    opts = opts or {}
7✔
1094

1095
    local vshard_router, err = utils.get_vshard_router_instance(opts.vshard_router)
7✔
1096
    if err ~= nil then
7✔
1097
        return nil, StorageInfoError:new(err)
×
1098
    end
1099

1100
    local replicasets, err = vshard_router:routeall()
7✔
1101
    if replicasets == nil then
7✔
1102
        return nil, StorageInfoError:new("Failed to get router replicasets: %s", err.err)
×
1103
    end
1104

1105
    local futures_by_replicas = {}
7✔
1106
    local replica_state_by_uuid = {}
7✔
1107
    local async_opts = {is_async = true}
7✔
1108
    local timeout = opts.timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
7✔
1109

1110
    for _, replicaset in pairs(replicasets) do
28✔
1111
        for replica_uuid, replica in pairs(replicaset.replicas) do
56✔
1112
            replica_state_by_uuid[replica_uuid] = {
28✔
1113
                status = "error",
1114
                is_master = replicaset.master == replica
28✔
1115
            }
28✔
1116
            local ok, res = pcall(replica.conn.call, replica.conn, "_crud.storage_info_on_storage",
56✔
1117
                                  {}, async_opts)
28✔
1118
            if ok then
28✔
1119
                futures_by_replicas[replica_uuid] = res
26✔
1120
            else
1121
                local err_msg = string.format("Error getting storage info for %s", replica_uuid)
2✔
1122
                if res ~= nil then
2✔
1123
                    log.error("%s: %s", err_msg, res)
2✔
1124
                    replica_state_by_uuid[replica_uuid].message = tostring(res)
4✔
1125
                else
1126
                    log.error(err_msg)
×
1127
                    replica_state_by_uuid[replica_uuid].message = err_msg
×
1128
                end
1129
            end
1130
        end
1131
    end
1132

1133
    local deadline = fiber.clock() + timeout
14✔
1134
    for replica_uuid, future in pairs(futures_by_replicas) do
40✔
1135
        local wait_timeout = deadline - fiber.clock()
52✔
1136
        if wait_timeout < 0 then
26✔
1137
            wait_timeout = 0
×
1138
        end
1139

1140
        local result, err = future:wait_result(wait_timeout)
26✔
1141
        if result == nil then
26✔
1142
            future:discard()
2✔
1143
            local err_msg = string.format("Error getting storage info for %s", replica_uuid)
2✔
1144
            if err ~= nil then
2✔
1145
                if err.type == 'ClientError' and err.code == box.error.NO_SUCH_PROC then
4✔
1146
                    replica_state_by_uuid[replica_uuid].status = "uninitialized"
1✔
1147
                else
1148
                    log.error("%s: %s", err_msg, err)
1✔
1149
                    replica_state_by_uuid[replica_uuid].message = tostring(err)
2✔
1150
                end
1151
            else
1152
                log.error(err_msg)
×
1153
                replica_state_by_uuid[replica_uuid].message = err_msg
×
1154
            end
1155
        else
1156
            replica_state_by_uuid[replica_uuid].status = result[1].status or "uninitialized"
24✔
1157
        end
1158
    end
1159

1160
    return replica_state_by_uuid
7✔
1161
end
1162

1163
--- Storage status information.
1164
--
1165
-- @function storage_info_on_storage
1166
--
1167
-- @return a table with storage status.
1168
function utils.storage_info_on_storage()
383✔
1169
    return {status = "running"}
24✔
1170
end
1171

1172
local expected_vshard_api = {
383✔
1173
    'routeall', 'route', 'bucket_id_strcrc32',
1174
    'callrw', 'callro', 'callbro', 'callre',
1175
    'callbre', 'map_callrw'
1176
}
1177

1178
--- Verifies that a table has expected vshard
1179
--  router handles.
1180
local function verify_vshard_router(router)
1181
    dev_checks("table")
212✔
1182

1183
    for _, func_name in ipairs(expected_vshard_api) do
1,328✔
1184
        if type(router[func_name]) ~= 'function' then
1,204✔
1185
            return false
88✔
1186
        end
1187
    end
1188

1189
    return true
124✔
1190
end
1191

1192
--- Get a vshard router instance from a parameter.
1193
--
1194
--  If a string passed, extract router instance from
1195
--  Cartridge vshard groups. If table passed, verifies
1196
--  that a table is a vshard router instance.
1197
--
1198
-- @function get_vshard_router_instance
1199
--
1200
-- @param[opt] router name of a vshard group or a vshard router
1201
--  instance
1202
--
1203
-- @return[1] table vshard router instance
1204
-- @treturn[2] nil
1205
-- @treturn[2] table Error description
1206
function utils.get_vshard_router_instance(router)
383✔
1207
    dev_checks('?string|table')
157,139✔
1208

1209
    local router_instance
1210

1211
    if type(router) == 'string' then
157,139✔
1212
        if not is_cartridge then
124✔
1213
            return nil, VshardRouterError:new("Vshard groups are supported only in Tarantool Cartridge")
×
1214
        end
1215

1216
        local router_service = cartridge.service_get('vshard-router')
124✔
1217
        assert(router_service ~= nil)
124✔
1218

1219
        router_instance = router_service.get(router)
248✔
1220
        if router_instance == nil then
124✔
1221
            return nil, VshardRouterError:new("Vshard group %s is not found", router)
×
1222
        end
1223
    elseif type(router) == 'table' then
157,015✔
1224
        if not verify_vshard_router(router) then
424✔
1225
            return nil, VshardRouterError:new("Invalid opts.vshard_router table value, " ..
176✔
1226
                                              "a vshard router instance has been expected")
176✔
1227
        end
1228

1229
        router_instance = router
124✔
1230
    else
1231
        assert(type(router) == 'nil')
156,803✔
1232
        router_instance = vshard.router.static
156,803✔
1233

1234
        if router_instance == nil then
156,803✔
1235
            return nil, VshardRouterError:new("Default vshard group is not found and custom " ..
176✔
1236
                                              "is not specified with opts.vshard_router")
176✔
1237
        end
1238
    end
1239

1240
    return router_instance
156,963✔
1241
end
1242

1243
--- Check if Tarantool Cartridge hotreload supported
1244
--  and get its implementaion.
1245
--
1246
-- @function is_cartridge_hotreload_supported
1247
--
1248
-- @return[1] true or false
1249
-- @return[1] module table, if supported
1250
function utils.is_cartridge_hotreload_supported()
383✔
1251
    if not is_cartridge_hotreload then
516✔
1252
        return false
×
1253
    end
1254

1255
    return true, cartridge_hotreload
516✔
1256
end
1257

1258
return utils
383✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc