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

ochaton / switchover / #124

15 Mar 2025 05:41PM UTC coverage: 65.503% (-0.04%) from 65.54%
#124

push

Vladislav Grubov
ci: fix rpmbuild

5271 of 8047 relevant lines covered (65.5%)

1507.01 hits per line

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

82.38
/switchover/_tarantool.lua
1
---
2
---@class Tarantool
3
---reflects single Tarantool connection
4
---@field public _name? string Tarantool name (can be nil)
5
---@field public conn NetBoxConnection? Tarantool connection
6
---@field public endpoint string URI to Tarantool
7
---@field public masked_endpoint string URI to Tarantool (with stripped password)
8
---@field public need_discovery boolean flag to enable background fiber of discovery (default: true)
9
---@field public fqdn string retreived fqdn of Tarantool server
10
---@field public ipv4 string resolved ipv4 from endpoint of Tarantool server
11
---@field public port string listen port of Tarantool
12
---@field public discovery_timeout number discovery timeout
13
---@field public has_vshard boolean flag which is set when vshard is discovered
14
---@field public can_package_reload boolean flag which is set when package.reload is discovered
15
---@field public has_etcd boolean flag which is set when config.etcd is discovered
16
---@field public has_cartridge boolean flag which is set when cartridge is discovered
17
---@field public has_metrics boolean flag which is set when metrics is discovered
18
---@field public etcd_config table info about etcd config of the instance (endpoints, timeout, dc, etc.)
19
---@field public fetch table fetch more info
20
---@field private log_format string log format
21
---@field public rtt number? ping to instance (conn:ping())
22
---@field public spaces {name: string, len: number, bsize: number}[] space information
23
---@field public slab_stats { mem_free: number, mem_used: number, item_count: number, item_size: number, slab_count: number, slab_size: number }[] slab stats
24
---@field public runtime_info { lua: number, maxalloc: number, used: number } runtime info
25
---@field public vshard_alerts? SwitchoverTarantoolVshardAlerts kv-map of vshard alerts
26
local Tarantool = {}
183✔
27
Tarantool.__index = Tarantool
183✔
28

29
---@class SwitchoverTarantoolVshardAlerts
30
---@field storage {[1]:string, [2]: string}[]
31
---@field router {[1]:string, [2]: string}[]
32
---@field storage_status number
33
---@field router_status number
34
---@field is_storage boolean
35
---@field is_router boolean
36
---@field this_replicaset_master_uuid string?
37

38
local socket = require 'socket'
183✔
39
local G = require 'switchover._global'
183✔
40
local log = require 'log'
183✔
41
local uri = require 'uri'
183✔
42
local fun = require 'fun'
183✔
43
local json = require 'json'
183✔
44
local fiber = require 'fiber'
183✔
45
local clock = require 'clock'
183✔
46
local errno = require 'errno'
183✔
47
local netbox = require 'net.box'
183✔
48
local v = require 'switchover._semver'
183✔
49
local background = require 'switchover._background'
183✔
50
local reload_will_freeze_before = v'2.2.1'
183✔
51

52

53
local discovery_format = {
183✔
54
        {
55
                v = v"0.0.0",
366✔
56
                f = '{{endpoint}} - {{connected and "not supported version" or "not connected"}}'
×
57
        },
183✔
58
        {
59
                v = v"1.10.0",
366✔
60
                f = '{{fqdn}}:{{port}} '
×
61
                        ..'{{(etcd.dc or "").." "}}'
183✔
62
                        ..'{{info.version:gsub("^(%d+)%.(%d+)%.(%d+)%-(%d+).*$", "%1.%2.%3.%4")}} '
183✔
63
                        ..'id:{{info.id}} s:{{info.status}} v:{{info.vclock}} r:{{info.ro and "replica" or "master"}}'
183✔
64
                        ..'{{info.gc.checkpoint_is_in_progress and " snap:true" or ""}}',
183✔
65
        },
183✔
66
        {
67
                v = v"2.6.0",
366✔
68
                f = '{{fqdn}}:{{port}} {{info.version:gsub("^(%d+)%.(%d+)%.(%d+)%-(%d+).*$", "%1.%2.%3.%4")}}'
×
69
                        ..' id:{{info.id}} s:{{info.status}} v:{{info.vclock}} r:{{info.ro}} t:{{info.election.term}}'
183✔
70
                        ..' l:{{info.election.leader}} s:{{info.election.state}} m:{{cfg.election_mode}}'
183✔
71
                        ..' q:{{info.synchro.quorum}} qo:{{info.synchro.queue.owner}}',
183✔
72
        },
183✔
73
}
74

75
local function human_uptime(ts)
76
        local s = {}
1,190✔
77
        if ts > 86400 then
1,190✔
78
                local days = math.floor(ts/86400)
×
79
                ts = ts % 86400
×
80
                table.insert(s, ("%dd"):format(days))
×
81
        end
82
        if ts > 3600 then
1,190✔
83
                local hours = math.floor(ts/3600)
×
84
                ts = ts % 3600
×
85
                table.insert(s, ("%dh"):format(hours))
×
86
        end
87
        if ts > 60 then
1,190✔
88
                local mins = math.floor(ts/60)
846✔
89
                ts = ts % 60
846✔
90
                table.insert(s, ("%dm"):format(mins))
846✔
91
        end
92
        if ts > 0 then
1,190✔
93
                table.insert(s, ("%ds"):format(ts))
1,186✔
94
        end
95

96
        return table.concat(s, "")
1,190✔
97
end
98

99
local ffi = require 'ffi'
183✔
100
ffi.cdef[[
183✔
101
        struct hostent {
102
                char  *h_name;            /* official name of host */
103
                char **h_aliases;         /* alias list */
104
                int    h_addrtype;        /* host address type */
105
                int    h_length;          /* length of address */
106
                char **h_addr_list;       /* list of addresses */
107
        };
108

109
        struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
110

111
        /* Internet address */
112
        struct in_addr {
113
                uint32_t       s_addr;     /* address in network byte order */
114
        };
115
        int inet_pton(int af, const char *restrict src, void *restrict dst);
116

117
        static const int AF_INET = 2;
118
        static const int AF_INET6 = 10;
119
]]
183✔
120

121
local function fqdn(endpoint)
122
        local u = uri.parse(endpoint)
773✔
123
        if not u.ipv4 then
773✔
124
                return u.host
9✔
125
        end
126

127
        local in_addr = ffi.new('struct in_addr', {})
764✔
128
        if 1 ~= ffi.C.inet_pton(ffi.C.AF_INET, u.ipv4, in_addr) then
764✔
129
                log.warn('Cant build inet_pton(%s, %s) => %s', ffi.C.AF_INET, u.ipv4, errno.strerror(errno()))
×
130
                return u.host
×
131
        end
132

133
        local hostent = ffi.C.gethostbyaddr(in_addr, ffi.sizeof(in_addr), ffi.C.AF_INET)
764✔
134
        if hostent == nil then
764✔
135
                log.warn('Cant gethostbyaddr(%s, %s, %s) => %s',
×
136
                        in_addr, ffi.sizeof(in_addr), ffi.C.AF_INET, errno.strerror(errno()))
×
137
                return u.host
×
138
        end
139

140
        local host = ffi.string(hostent.h_name):gsub("(%.[^.]+)", "")
764✔
141
        return host
764✔
142
end
143

144
local function ipv4(endpoint)
145
        local _uri = uri.parse(endpoint)
773✔
146
        if _uri.ipv4 then
773✔
147
                return _uri.ipv4
764✔
148
        end
149

150
        local resolv, err = socket.getaddrinfo(_uri.host, 0, G.dns_timeout, {
9✔
151
                type = 'SOCK_STREAM',
152
                family = 'AF_INET',
153
                protocol = 'tcp',
154
        })
155

156
        if not resolv then
9✔
157
                log.warn("Failed to resolve %s => %s", _uri.host, err)
9✔
158
                return
9✔
159
        end
160

161
        return resolv[1].host
×
162
end
163

164
---
165
---@return table
166
function Tarantool:__serialize()
183✔
167
        return {
1,498✔
168
                endpoint = self.endpoint:gsub("^([^:]+):([^@]+)@", function(u) return u..':@' end),
1,498✔
169
                connected = self.connected,
1,498✔
170
                info = self.connected and self:info(),
2,688✔
171
                cfg  = self.connected and self:cfg(),
2,688✔
172
                slab = self.connected and self:info() and self.slab_info,
2,688✔
173
                stat = self.connected and self:info() and self.stat_info,
2,688✔
174
                etcd = self.connected and self:etcd(),
2,688✔
175
                fqdn = self.fqdn,
1,498✔
176
                port = self.port,
1,498✔
177
                n_upstreams = self.connected and #self:followed_upstreams(),
2,688✔
178
                n_downstreams = self.connected and #self:followed_downstreams(),
2,688✔
179
                uptime = self.connected and human_uptime(self:info().uptime),
3,878✔
180
                vclock = self.connected and self:jvclock(),
2,688✔
181
                wall_clock = self.connected and self.wall_clock,
1,498✔
182
                n_fibers = self.connected and self.n_fibers,
1,498✔
183
        }
1,498✔
184
end
185

186
---Serializes entire tarantool info
187
---@return table
188
function Tarantool:serialize()
183✔
189
        return Tarantool.__serialize(self)
28✔
190
end
191

192
---
193
---__tostring metamethod for pretty print. Now calls `short_status`
194
---@see Tarantool.short_status
195
function Tarantool:__tostring()
183✔
196
        local vars = setmetatable(Tarantool.__serialize(self), { __index = _G })
2,940✔
197
        local format
198
        if self.log_format then
1,470✔
199
                format = self.log_format
1,470✔
200
        elseif self.connected then
×
201
                format = fun.range(1, #discovery_format)
×
202
                        :map(function(i) return discovery_format[i] end)
×
203
                        :take_while(function(vf) return vf.v <= self:version() end)
×
204
                        :reduce(function(_, vf) --[[@return {v:string, f:string}]] return vf end).f
×
205
        else
206
                format = discovery_format[1].f
×
207
        end
208
        return (format:gsub('{{([^{}]+)}}', function(var)
2,940✔
209
        local fnc = ([[return %s]]):format(var)
14,700✔
210
                local exec = assert(loadstring(fnc))
14,700✔
211
                setfenv(exec, vars)
14,700✔
212

213
                local ok, val = pcall(exec)
14,700✔
214
                if not ok then
14,700✔
215
                        log.verbose("Failed to execute %q: %s", var, val)
2,108✔
216
                        return '?'
2,108✔
217
                end
218
                if type(val) == 'table' then
12,592✔
219
                        val = json.encode(val)
×
220
                end
221
                return tostring(val)
12,592✔
222
        end))
223
end
224

225
---
226
---Basically pretty print of usefull instance information
227
---@return string
228
function Tarantool:short_status()
183✔
229
        return self:__tostring()
134✔
230
end
231

232
---
233
---on_connect callback is called when connection (re)established to tarantool
234
---calls `start_discovery` method
235
function Tarantool:on_connect()
183✔
236
        self.connected = true
598✔
237
        log.verbose("Connected %s", self)
598✔
238
        if self.need_discovery then
598✔
239
                self:start_discovery()
598✔
240
        end
241
end
242

243
---
244
---@class TarantoolOpts
245
---@field public async boolean `default:nil` issues asynchronous connection (net.box.connect {wait_connected=false})
246
---@field public discovery_timeout number|nil `default:0.1` timeout of refreshing tarantool info.
247
---@field public fetch table which fields should be fetched from Tarantool during discovery
248
---@field public log_format string
249
---@field public need_discovery boolean flag which enabled background discovery (default: true)
250
---@field public on_connect fun(tnt:Tarantool):nil on_connect callback
251

252
---
253
---Constructor of the class.
254
---Creates new object and connects to specified endpoint
255
---@param endpoint string endpoint `<host>:<port>` to the Tarantool instance
256
---@param opts TarantoolOpts
257
---@return Tarantool
258
function Tarantool:new(endpoint, opts)
183✔
259
        log.verbose("Connecting to %s, async=%s", endpoint, opts.async)
773✔
260
        local conn = netbox.connect(endpoint, {
1,546✔
261
                wait_connected = opts.async ~= true,
773✔
262
                reconnect_after = 0.1,
263
        })
264
        local tnt = setmetatable({
1,546✔
265
                conn = conn,
773✔
266
                endpoint = endpoint,
773✔
267
                fqdn = fqdn(endpoint),
1,546✔
268
                port = uri.parse(endpoint).service,
1,546✔
269
                ipv4 = ipv4(endpoint),
1,546✔
270
                connected = false,
271
                masked_endpoint = ("{host}:{service}"):gsub("{(%w+)}", uri.parse(endpoint)),
1,546✔
272
                need_discovery = opts.need_discovery ~= false,
773✔
273
                discovery_timeout = opts.discovery_timeout or 0.1,
773✔
274
                log_format = opts.log_format,
773✔
275
                fetch = opts.fetch,
773✔
276
        }, self)
773✔
277
        conn:on_connect(function(_)
1,546✔
278
                fiber.create(function()
1,528✔
279
                        assert(tnt.conn, "conn must be present")
764✔
280
                        fiber.name("c/"..tnt.masked_endpoint)
764✔
281
                        if opts.on_connect then
764✔
282
                                opts.on_connect(tnt)
764✔
283
                        end
284
                        if tnt.conn then tnt:on_connect() end
764✔
285
                end)
286
        end)
287
        conn:on_disconnect(function()
1,546✔
288
                tnt.connected = false
240✔
289
        end)
290
        if not opts.async then
773✔
291
                log.verbose("calling on_connect for %s", tnt.masked_endpoint);
×
292
                (opts.on_connect or Tarantool.on_connect)(tnt)
×
293
        end
294
        return tnt
773✔
295
end
296

297
---
298
---Spawns separate fiber `discovery_f` where constantly calls self:_get method
299
---@see Tarantool#_get
300
function Tarantool:start_discovery()
183✔
301
        self.discovery_f = background {
1,196✔
302
                name = 'd/' .. self.masked_endpoint,
598✔
303
                wait = false,
304
                restart = false,
305
                run_interval = self.discovery_timeout,
598✔
306
                args = { self },
598✔
307
                run_while = function ()
308
                        return self.connected == true and not self.destroyed
6,389✔
309
                end,
310
                func = function(_, tnt)
311
                        tnt:_get{ update = true }
3,281✔
312
                end,
313
                teardown = function()
314
                        log.verbose("Leaving discovery fiber of %s", self)
129✔
315
                end,
316
        }
598✔
317
        self.pinger_f = background {
1,196✔
318
                name = 'p/' .. self.masked_endpoint,
598✔
319
                wait = false,
320
                restart = false,
321
                run_interval = 0.1,
322
                args = { self },
598✔
323
                run_while = function ()
324
                        return self.connected == true and not self.destroyed
6,553✔
325
                end,
326
                func = function(_, tnt)
327
                        local t = clock.time()
3,347✔
328
                        if tnt.conn:ping({ timeout = math.max(0.1, self.discovery_timeout) }) then
6,553✔
329
                                tnt.rtt = clock.time()-t
3,189✔
330
                        end
331
                end,
332
                teardown = function()
333
                        log.verbose("Leaving discovery fiber of %s", self)
129✔
334
                end,
335
        }
598✔
336
        assert(self.discovery_f, "discovery job not created")
598✔
337
end
338

339
---
340
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
341
---@return number replica_id ID of the replica `box.info.id`
342
function Tarantool:id(opts)
183✔
343
        return tonumber(self:info(opts).id) or 0
12,322✔
344
end
345

346
---
347
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
348
---@return string instance_uuid UUID of the replica `box.info.uuid`
349
function Tarantool:uuid(opts)
183✔
350
        return self:info(opts).uuid
7,162✔
351
end
352

353
---
354
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
355
---@return  string status of the replica `box.info.status`
356
function Tarantool:status(opts)
183✔
357
        return self:info(opts).status
70✔
358
end
359

360
---
361
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
362
---@return string replicaset_uuid replicaset_uuid of the replica `box.info.cluster.uuid`
363
function Tarantool:cluster_uuid(opts)
183✔
364
        return self:info(opts).cluster.uuid
8✔
365
end
366

367
---
368
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
369
---@return string version tarantool version ex: 1.10.10-0-gxxxxxx
370
function Tarantool:version(opts)
183✔
371
        return v(self:info(opts).version)
362✔
372
end
373

374
---sets name for connection
375
---@param new_name string
376
function Tarantool:setname(new_name)
183✔
377
        self._name = new_name
331✔
378
end
379

380
---
381
---@return string
382
function Tarantool:name()
183✔
383
        return ("%s (%s)"):format(self._name or 'unknown', self.masked_endpoint)
221✔
384
end
385

386
---
387
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
388
---@return string jsoned_vclock `json.encode(box.info.vclock)`
389
function Tarantool:jvclock(opts)
183✔
390
        local max_id = 0
1,292✔
391
        local vclock = self:info(opts).vclock
2,584✔
392
        for id, _ in pairs(vclock) do
3,583✔
393
                if max_id < id then max_id = id end
2,291✔
394
        end
395
        local r = table.new(max_id, 0)
1,292✔
396
        for i = 1, max_id do
3,583✔
397
                if vclock[i] then
2,291✔
398
                        r[i] = vclock[i]
2,291✔
399
                else
400
                        r[i] = ''
×
401
                end
402
        end
403
        return json.encode(setmetatable(r, {__serialize='seq'}))
1,292✔
404
end
405

406
---
407
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
408
---@return integer[] vclock `box.info.vclock`
409
function Tarantool:vclock(opts)
183✔
410
        return self:info(opts).vclock
1,608✔
411
end
412

413
---
414
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
415
---@return number sign returns sum of lsn excluding vclock[0]
416
function Tarantool:signature(opts)
183✔
417
        return fun.sum(self:vclock(opts))
×
418
end
419

420
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
421
---@return string[] list of box.cfg.replication (may be empty)
422
function Tarantool:replication(opts)
183✔
423
        return self:cfg(opts).replication or {}
1,196✔
424
end
425

426
---
427
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
428
---@return boolean ro returns `box.info.ro`
429
function Tarantool:ro(opts)
183✔
430
        return self:info(opts).ro
708✔
431
end
432

433
---
434
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
435
---@return string role evaluates role master/replica based on `box.info.ro`
436
function Tarantool:role(opts)
183✔
437
        return self:info(opts).ro == true and 'replica' or 'master'
1,994✔
438
end
439

440
---
441
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
442
---@return table election returns `box.info.election`
443
function Tarantool:election(opts)
183✔
444
        return self:info(opts).election
204✔
445
end
446

447
---
448
---@param instance_uuid string UUID of the upstream
449
---@return ReplicaUpstreamInfo? upstream returns information about upstream identified by `instance_uuid`
450
function Tarantool:upstream(instance_uuid)
183✔
451
        return fun.iter(self:upstreams()):grep(function(u)
1,496✔
452
                return u.upstream and u.uuid == instance_uuid
550✔
453
        end):nth(1)
748✔
454
end
455

456
---
457
---@param instance_uuid string UUID of the downstream
458
---@return ReplicaDownstreamInfo? downstream returns information about downstream identified by `instance_uuid`
459
function Tarantool:downstream(instance_uuid)
183✔
460
        return fun.iter(self:downstreams()):grep(function(u)
×
461
                return u.downstream and u.downstream.uuid == instance_uuid
×
462
        end):nth(1)
×
463
end
464

465
---
466
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
467
---@return ReplicaUpstreamInfo[] upstreams returns list of all upstreams of this instance `box.info.replication.?.upstream`
468
function Tarantool:upstreams(opts)
183✔
469
        local ret = {}
1,915✔
470
        for _, r in pairs(self:info(opts).replication) do
8,768✔
471
                if r.upstream and r.id ~= self:id() then
7,957✔
472
                        table.insert(ret, r)
3,019✔
473
                end
474
        end
475
        table.sort(ret, function(a, b) return a.id < b.id end)
3,122✔
476
        return ret
1,915✔
477
end
478

479
---
480
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
481
---@return ReplicaDownstreamInfo[] downstreams returns list of all downstreams of this instance `box.info.replication.?.downstream`
482
function Tarantool:downstreams(opts)
183✔
483
        local ret = {}
1,335✔
484
        for _, r in pairs(self:info(opts).replication) do
6,027✔
485
                if r.downstream and r.id ~= self:id() then
5,377✔
486
                        table.insert(ret, r)
2,020✔
487
                end
488
        end
489
        table.sort(ret, function(a, b) return a.id < b.id end)
2,108✔
490
        return ret
1,335✔
491
end
492

493
---
494
---@param opts? SwitchoverTarantoolRefreshOptions refresh options
495
---@return ReplicaDownstreamInfo[] downstreams returns list of all followed downstreams of this instance `box.info.replication.?.downstream`
496
function Tarantool:followed_downstreams(opts)
183✔
497
        local ret = {}
1,333✔
498
        for _, d in ipairs(self:downstreams(opts)) do
4,682✔
499
                if d.downstream and d.downstream.status == 'follow' then
2,016✔
500
                        table.insert(ret, d)
1,979✔
501
                end
502
        end
503
        return ret
1,333✔
504
end
505

506
---
507
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
508
---@return ReplicaUpstreamInfo[] upstreams returns list of all followed upstreams of this instance `box.info.replication.?.upstream`
509
function Tarantool:followed_upstreams(opts)
183✔
510
        local ret = {}
1,343✔
511
        for _, d in ipairs(self:upstreams(opts)) do
4,720✔
512
                if d.upstream and d.upstream.status == 'follow' then
2,034✔
513
                        table.insert(ret, d)
1,996✔
514
                end
515
        end
516
        return ret
1,343✔
517
end
518

519
---@class ReplicaUpstreamInfo:ReplicaInfo
520
---@field upstream UpstreamInfo
521

522
---@class ReplicaDownstreamInfo:ReplicaInfo
523
---@field downstream DownstreamInfo
524

525
---
526
---@param instance_uuid string UUID of the upstream
527
---@return ReplicaUpstreamInfo? upstream returns upstream info identified with `instance_uuid` (if it is followed)
528
function Tarantool:replicates_from(instance_uuid)
183✔
529
        return fun.iter(self:upstreams()):grep(function(r)
792✔
530
                return r.uuid == instance_uuid and r.upstream.status == 'follow'
311✔
531
        end):nth(1)
396✔
532
end
533

534
---
535
---@param instance_uuid string UUID of the downstream
536
---@return ReplicaDownstreamInfo? downstream returns downstream info identified with `instance_uuid` (if it is followed)
537
function Tarantool:replicated_by(instance_uuid)
183✔
538
        return fun.iter(self:downstreams()):grep(function(r)
8✔
539
                return r.uuid == instance_uuid and r.downstream.status == 'follow'
3✔
540
        end):nth(1)
4✔
541
end
542

543
---
544
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
545
---@return BoxInfo info returns `box.info`
546
function Tarantool:info(opts)
183✔
547
        self:_get(opts)
22,873✔
548
        return self.cached_info
22,873✔
549
end
550

551
---
552
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
553
---@return BoxCfg cfg returns `box.cfg`
554
function Tarantool:cfg(opts)
183✔
555
        self:_get(opts)
2,583✔
556
        return self.cached_cfg
2,583✔
557
end
558

559
---
560
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
561
---@return table etcd returns `config.get('etcd')`
562
function Tarantool:etcd(opts)
183✔
563
        self:_get(opts)
1,292✔
564
        return self.etcd_config
1,292✔
565
end
566

567
---Checks if package.reload() will freeze on certain Tarantool versions
568
---@param opts? SwitchoverTarantoolRefreshOptions
569
---@return boolean
570
function Tarantool:reload_will_freeze(opts)
183✔
571
        self:_get(opts)
49✔
572
        if self:version() >= reload_will_freeze_before then
98✔
573
                return false
49✔
574
        end
575
        -- node will freeze on package.reload during box.snapshot() on rw node
576
        return self:info().gc.checkpoint_is_in_progress == true
×
577
end
578

579
---Checks that vshard.storage() is rw (box.info.ro==false) but vshard didn't update it's topology
580
---@param opts? SwitchoverTarantoolRefreshOptions
581
---@return boolean
582
function Tarantool:vshard_storage_rw_but_ro(opts)
183✔
583
        self:_get(opts)
28✔
584
        -- no master => no cry
585
        if self:ro() then
56✔
586
                return false
8✔
587
        end
588
        -- no vshard => no cry
589
        if not self.has_vshard then
20✔
590
                return false
×
591
        end
592
        -- no vshard storage => no cry
593
        if not self.vshard_alerts.is_storage then
20✔
594
                return false
12✔
595
        end
596
        -- tt is vshard storage and it's uuid is the same as master.uuid in vshard topology
597
        -- (NON_MASTER issue)
598
        if tostring(self.vshard_alerts.this_replicaset_master_uuid):lower() == self:uuid():lower() then
32✔
599
                return false
7✔
600
        end
601
        -- Okay :(( vshard has malformed configuration
602
        return true
1✔
603
end
604

605
---@class SwitchoverTarantoolRefreshOptions
606
---@field public update boolean if set to `true` refreshes cache from connection
607

608
---
609
---@param opts SwitchoverTarantoolRefreshOptions|nil refresh options
610
---Refreshes info about remote Tarantool:
611
---box.info
612
---box.cfg
613
---has package.reload, has vshard, has config.etcd, has cartridge
614
function Tarantool:_get(opts)
183✔
615
        opts = opts or {}
30,106✔
616
        if not self.cached_info or not self.cached_cfg or (opts or {}).update then
30,106✔
617
                assert(self.conn, "connection is not established")
4,070✔
618
                log.verbose("Tarantool._get(%s, %s)", self.endpoint, json.encode(opts))
4,070✔
619
                local info_memory, stat_net_info, info_gc
620
                self.cached_info,
×
621
                self.cached_cfg,
×
622
                self.can_package_reload,
×
623
                self.has_vshard,
×
624
                self.vshard_alerts,
×
625
                self.has_etcd,
×
626
                self.etcd_config,
×
627
                self.has_cartridge,
×
628
                self.has_metrics,
×
629
                self.cluster_len,
×
630
                self.cluster_max_id,
×
631
                info_memory,
632
                self.runtime_info,
×
633
                info_gc,
634
                self.slab_info,
×
635
                self.stat_info,
×
636
                stat_net_info,
637
                self.n_fibers,
×
638
                self.metrics_export,
×
639
                self.fiber_info,
×
640
                self.bucket_count,
×
641
                self.spaces,
×
642
                self.slab_stats,
×
643
                self.wall_clock
644
                        =
×
645
                self.conn:eval ([[
8,140✔
646
                        local fetch = ... or {}
647
                        local has_etcd = false
648
                        local has_vshard = rawget(_G, 'vshard') ~= nil
649
                        local is_storage = has_vshard and vshard.storage and vshard.storage.internal and vshard.storage.internal.current_cfg
650
                        local is_router = has_vshard and vshard.router and vshard.router.internal and vshard.router.internal.current_cfg
651
                        local etcd_config = {}
652
                        if type(package.loaded.config) == 'table' and package.loaded.config.etcd and package.loaded.config._flat then
653
                                has_etcd = true
654
                                etcd_config = package.loaded.config.get('etcd')
655
                        end
656
                        return
657
                                box.info,
658
                                box.cfg,
659
                                type(package.reload) ~= 'nil',
660
                                has_vshard,
661
                                fetch.vshard_alerts and has_vshard
662
                                        and {
663
                                                storage = is_storage and vshard.storage.info().alerts,
664
                                                router = is_router and vshard.router.info().alerts,
665
                                                storage_status = is_storage and vshard.storage.info().status,
666
                                                router_status = is_router and vshard.router.info().status,
667
                                                is_storage = is_storage ~= nil,
668
                                                is_router = is_router ~= nil,
669
                                                this_replicaset_master_uuid = is_storage
670
                                                        and vshard.storage.internal.this_replicaset
671
                                                        and vshard.storage.internal.this_replicaset.master
672
                                                        and vshard.storage.internal.this_replicaset.master.uuid,
673
                                        }
674
                                        or {},
675
                                has_etcd,
676
                                etcd_config,
677
                                type(package.loaded.cartridge) ~= 'nil',
678
                                type(package.loaded.metrics) ~= 'nil',
679
                                box.space._cluster:len(),
680
                                box.space._cluster.index[0]:max()[1],
681
                                box.info.memory(),
682
                                box.runtime.info(),
683
                                box.info.gc(),
684
                                box.slab.info(),
685
                                box.stat(),
686
                                box.stat.net(),
687
                                require 'fun'.length(require 'fiber'.info()),
688
                                type(package.loaded.metrics) ~= 'nil'
689
                                        and fetch.metrics
690
                                        and select(2, xpcall(function() require 'json'.decode(require 'metrics.plugins.json'.export()) end, function() return {} end))
691
                                        or box.NULL,
692
                                fetch.fiber_info
693
                                        and require 'fiber'.info() or box.NULL,
694
                                box.space._bucket
695
                                        and box.space._bucket:len()
696
                                        or 'no',
697
                                fetch.spaces
698
                                        and require 'fun'.iter(box.space)
699
                                                        :grep(function(k,s) return type(k) == 'number' and k>511 and next(s.index) end)
700
                                                        :map(function(_, s)
701
                                                                return {
702
                                                                        name=s.name,
703
                                                                        id=s.id,
704
                                                                        len=s:len(),
705
                                                                        bsize=s:bsize(),
706
                                                                        isize=require'fun'.range(0, #s.index)
707
                                                                                :map(function(i) return s.index[i]:bsize() end)
708
                                                                                :sum(),
709
                                                                        engine=s.engine
710
                                                                }
711
                                                        end)
712
                                                        :totable()
713
                                        or {},
714
                                fetch.slabs and box.slab.stats() or {},
715
                                require 'clock'.time()
716
                ]], {self.fetch}, {})
7,967✔
717
                self.cached_info.memory = info_memory
3,897✔
718
                self.cached_info.gc = info_gc
3,897✔
719
                self.stat_info.net = stat_net_info
3,897✔
720
        end
721
        return self.cached_info, self.cached_cfg
29,933✔
722
end
723

724
---
725
---force_promote calls `box.cfg{read_only=false} box.ctl.promote()` on remote
726
---@param args {allow_vshard: boolean}
727
---@return boolean is_safe, string? error_msg
728
function Tarantool:force_promote(args)
183✔
729
        if self.has_vshard and not args.allow_vshard then
4✔
730
                error("Cant force_promote: instance is in vshard cluster", 2)
×
731
        end
732
        log.warn("Executing force_promote on %s", self)
4✔
733
        local ok, err = pcall(function()
8✔
734
                self.cached_info, self.cached_cfg = self.conn:eval[[
4✔
735
                        box.cfg{ read_only = false }
736
                        if box.ctl.promote
737
                                and box.info.synchro
738
                                and box.info.synchro.queue
739
                                and box.info.synchro.queue.owner ~= 0
740
                                and box.info.synchro.queue.owner ~= box.info.id
741
                        then
742
                                box.ctl.promote()
743
                        end
744
                        return box.info, box.cfg
745
                ]]
8✔
746
        end)
747

748
        if not ok then
4✔
749
                return false, err
×
750
        end
751

752
        if not self:ro() then
8✔
753
                return true
4✔
754
        else
755
                return true, "candidate still ro"
×
756
        end
757
end
758

759
---Executes cartridge.failover_promote({ [replicaset_uuid] = instance_uuid })
760
---@param timeout number eval timeout
761
---@return unknown
762
function Tarantool:cartridge_promote(timeout)
183✔
763
        assert(self.has_cartridge, "instance must have cartridge")
×
764
        return self.conn:eval([[ local map = ... return require 'cartridge'.failover_promote(map) ]], {
×
765
                {[self:cluster_uuid()] = self:uuid()},
×
766
        }, { timeout = timeout })
×
767
end
768

769
---
770
---package_reload calls `package.reload()` on remote
771
---
772
---Raises if Tarantool cannot package_reload
773
---@param timeout number?
774
---@return boolean
775
function Tarantool:package_reload(timeout)
183✔
776
        if not self.can_package_reload then
96✔
777
                error(("Instance %s does not support package.reload"):format(self.endpoint), 0)
×
778
        end
779

780
        log.warn("Calling package.reload on %s", self)
96✔
781
        self.cached_info, self.cached_cfg = self.conn:eval([[
192✔
782
                package.reload()
783
                return box.info, box.cfg
784
        ]], {}, { timeout = timeout })
192✔
785
        return true
96✔
786
end
787

788
---Restarts replication on instance
789
---@param timeout number?
790
---@return true|false
791
---@return unknown|nil
792
function Tarantool:restart_replication(timeout)
183✔
793
        log.warn("Calling restart-replication on %s", self)
16✔
794

795
        timeout = tonumber(timeout) or 30
16✔
796
        timeout = math.max(timeout, 3)
16✔
797
        timeout = math.min(timeout, 30)
16✔
798

799
        local ok, err = pcall(function()
32✔
800
                self.conn:eval([[
32✔
801
                        repl = box.cfg.replication
802
                        box.cfg{ replication = {} }
803
                        box.cfg{ replication = repl }
804
                        repl = nil
805
                ]], {}, { timeout = timeout })
16✔
806
        end)
807

808
        if not ok then
16✔
809
                log.error("restart-replication failed on instance %s : %s", self, err)
×
810
        end
811
        return ok, err
16✔
812
end
813

814
function Tarantool:package_reload_check_freeze(allow_freeze)
183✔
815
        if self:ro() == false and self:reload_will_freeze() and not allow_freeze then
54✔
816
                return false
×
817
        end
818
        return self:package_reload()
23✔
819
end
820

821
---
822
---@return table
823
function Tarantool:raft()
183✔
824
        return {
×
825
                mode            = self:cfg().election_mode,
826
                quorum          = self:cfg().replication_synchro_quorum,
827
                election_quorum = math.ceil((self.cluster_len + 1)/2),
828
                state           = self:election().state,
829
                leader          = self:election().leader,
830
                term            = self:election().term,
831
        }
832
end
833

834
function Tarantool:shadow()
183✔
835
        return setmetatable({
×
836
                connected = true,
837
                endpoint = self.endpoint,
838
                cached_info = self.cached_info,
839
                cached_cfg = self.cached_cfg,
840
                rtt        = self.rtt,
841

842
                _name = self._name,
843

844
                has_vshard = self.has_vshard,
845
                vshard_alerts = self.vshard_alerts,
846
                has_etcd = self.has_etcd,
847
                etcd_config = self.etcd_config,
848
                has_cartridge = self.has_cartridge,
849
                has_metrics = self.has_metrics,
850
                cluster_len = self.cluster_len,
851
                cluster_max_id = self.cluster_max_id,
852
                runtime_info = self.runtime_info,
853
                slab_info = self.slab_info,
854
                stat_info = self.stat_info,
855
                n_fibers = self.n_fibers,
856
                metrics_export = self.metrics_export,
857
                fiber_info = self.fiber_info,
858
                bucket_count = self.bucket_count,
859
                spaces = self.spaces,
860
                slab_stats = self.slab_stats,
861
                wall_clock = self.wall_clock,
862
        }, Tarantool)
×
863
end
864

865
---Destroys tarantool connection
866
function Tarantool:destroy()
183✔
867
        log.verbose("destroying connection to instance: %s/%s", self.masked_endpoint, tostring(self.discovery_f))
321✔
868
        self.destroyed = true
248✔
869
        self:destroy_background()
248✔
870
        if self.conn then
248✔
871
                if type(self.conn.opts) == 'table' then
248✔
872
                        self.conn.opts.reconnect_after = nil
248✔
873
                end
874
                self.conn:close()
248✔
875
                self.conn = nil
248✔
876
        end
877
        self.connected = nil
248✔
878
end
879

880
function Tarantool:destroy_background()
183✔
881
        log.verbose("destroying background: %s/%s", self.masked_endpoint, tostring(self.discovery_f))
333✔
882
        if self.discovery_f then
254✔
883
                self.discovery_f:shutdown('force')
79✔
884
                self.discovery_f = nil
79✔
885
        end
886
        log.verbose("destroying background: %s/%s", self.masked_endpoint, tostring(self.pinger_f))
333✔
887
        if self.pinger_f then
254✔
888
                self.pinger_f:shutdown('force')
79✔
889
                self.pinger_f = nil
79✔
890
        end
891
end
892

893
return setmetatable(Tarantool, { __call = Tarantool.new })
183✔
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