Skip to content

Commit 7862077

Browse files
committed
fix: allow read-only operations when all masters are down
Read-only operations (get, select, pairs, count, min, max) used to fail with connection errors if all masters in the cluster were unavailable, even when healthy replicas were up and failover hadn't processed yet. To resolve this, the following improvements were made: - Introduced a `read_only` flag to `utils.get_space[s]` to fetch cluster schema from any healthy replica if masters are down. - Updated `get`, `select`, `pairs`, `count`, `min`, `max` to use this new flag. - Rewrote `call.any` to iterate through all replicasets and utilize vshard's `callro` instead of `call` to fetch metadata from replicas.
1 parent 6aaa70f commit 7862077

20 files changed

Lines changed: 266 additions & 31 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Fixed
11+
* Allow read-only operations (`get`, `select`, `pairs`, `count`, `min`, `max`)
12+
to execute successfully using healthy replicas when all cluster masters are down.
13+
1014
## [1.7.4] - 12-02-26
1115

1216
### Fixed

crud/borders.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ local function call_get_border_on_router(vshard_router, border_name, space_name,
7272
fetch_latest_metadata = '?boolean',
7373
})
7474

75-
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, opts.timeout)
75+
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, {
76+
timeout = opts.timeout,
77+
read_only = opts.mode ~= 'write',
78+
})
7679
if err ~= nil then
7780
return nil, BorderError:new("An error occurred during the operation: %s", err), const.NEED_SCHEMA_RELOAD
7881
end

crud/common/call.lua

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,19 +270,33 @@ function call.any(vshard_router, func_name, func_args, opts)
270270
if replicasets == nil then
271271
return nil, CallError:new("Failed to get router replicasets: %s", err.err)
272272
end
273-
local replicaset_id, replicaset = next(replicasets)
274273

275-
local res, err = call_with_retry_and_recovery(vshard_router, replicaset, 'call',
276-
func_name, func_args, {timeout = timeout}, false)
277-
if err ~= nil then
278-
return nil, wrap_vshard_err(vshard_router, err, func_name, replicaset_id)
279-
end
274+
local last_replicaset_id = nil
275+
local last_err = nil
280276

281-
if res == box.NULL then
282-
return nil
277+
local deadline = fiber_clock() + timeout
278+
279+
for replicaset_id, replicaset in pairs(replicasets) do
280+
local wait_timeout = deadline - fiber_clock()
281+
if wait_timeout < 0 then
282+
wait_timeout = 0
283+
end
284+
285+
local res, err = call_with_retry_and_recovery(vshard_router, replicaset, 'callro',
286+
func_name, func_args, {timeout = wait_timeout}, false)
287+
288+
if err == nil then
289+
if res == box.NULL then
290+
return nil
291+
end
292+
return res
293+
end
294+
295+
last_replicaset_id = replicaset_id
296+
last_err = err
283297
end
284298

285-
return res
299+
return nil, wrap_vshard_err(vshard_router, last_err, func_name, last_replicaset_id)
286300
end
287301

288302
return call

crud/common/utils.lua

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,44 @@ local function get_replicaset_by_replica_id(replicasets, id)
123123
return nil, nil
124124
end
125125

126-
function utils.get_spaces(vshard_router, timeout, replica_id)
127-
local replicasets, replicaset, replicaset_id, master
126+
local function find_any_healthy_replica_conn(replicasets)
127+
for _, replicaset in pairs(replicasets) do
128+
for _, replica in pairs(replicaset.replicas) do
129+
if replica:is_connected() then
130+
return replica.conn
131+
end
132+
end
133+
end
134+
return nil
135+
end
136+
137+
--- Returns all spaces existing in the schema.
138+
--
139+
-- @function get_spaces
140+
--
141+
-- @param table vshard_router
142+
-- A vshard router instance to route requests.
143+
--
144+
-- @tparam ?number opts.timeout
145+
-- Function call timeout. If not specified, `const.DEFAULT_VSHARD_CALL_TIMEOUT` is used.
146+
--
147+
-- @tparam ?boolean opts.read_only
148+
-- If true, the function will try to fetch spaces from any available healthy replica.
149+
--
150+
-- @tparam ?string opts.replica_id
151+
-- The exact replica ID to fetch the replicaset from.
152+
--
153+
-- @return[1] table map of spaces
154+
-- @return[1] nil
155+
-- @return[1] number schema version
156+
-- @treturn[2] nil
157+
-- @treturn[2] table Error description
158+
function utils.get_spaces(vshard_router, opts)
159+
local replicasets, replicaset, replicaset_id, master, ro_replica_conn
160+
161+
opts = opts or {}
128162

129-
timeout = timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
163+
local timeout = opts.timeout or const.DEFAULT_VSHARD_CALL_TIMEOUT
130164
local deadline = fiber.clock() + timeout
131165
local iter_sleep = math.min(timeout / 100, 0.1)
132166
while (
@@ -136,12 +170,12 @@ function utils.get_spaces(vshard_router, timeout, replica_id)
136170
) do
137171
-- Try to get master with timeout.
138172
replicasets = vshard_router:routeall()
139-
if replica_id ~= nil then
173+
if opts.replica_id ~= nil then
140174
-- Get the same replica on which the last DML operation was performed.
141175
-- This approach is temporary and is related to [1], [2].
142176
-- [1] https://github.com/tarantool/crud/issues/236
143177
-- [2] https://github.com/tarantool/crud/issues/361
144-
replicaset_id, replicaset = get_replicaset_by_replica_id(replicasets, replica_id)
178+
replicaset_id, replicaset = get_replicaset_by_replica_id(replicasets, opts.replica_id)
145179
break
146180
else
147181
replicaset_id, replicaset = next(replicasets)
@@ -155,9 +189,22 @@ function utils.get_spaces(vshard_router, timeout, replica_id)
155189
end
156190
end
157191

192+
-- If the master check above didn't succeed (or master is dead),
193+
-- and this is a read-only operation, try to find any available healthy replica.
194+
if opts.read_only then
195+
ro_replica_conn = find_any_healthy_replica_conn(replicasets)
196+
if ro_replica_conn ~= nil then
197+
break
198+
end
199+
end
200+
158201
fiber.sleep(iter_sleep)
159202
end
160203

204+
if opts.read_only and ro_replica_conn ~= nil then
205+
return ro_replica_conn.space, nil, ro_replica_conn.schema_version
206+
end
207+
161208
if replicaset == nil then
162209
return nil, GetSpaceError:new(
163210
'The router returned empty replicasets: ' ..
@@ -184,8 +231,32 @@ function utils.get_spaces(vshard_router, timeout, replica_id)
184231
return master.conn.space, nil, master.conn.schema_version
185232
end
186233

187-
function utils.get_space(space_name, vshard_router, timeout, replica_id)
188-
local spaces, err, schema_version = utils.get_spaces(vshard_router, timeout, replica_id)
234+
--- Returns a specific space by its name.
235+
--
236+
-- @function get_space
237+
--
238+
-- @param string space_name
239+
-- A space name to fetch.
240+
--
241+
-- @param table vshard_router
242+
-- A vshard router instance to route requests.
243+
--
244+
-- @tparam ?number opts.timeout
245+
-- Function call timeout.
246+
--
247+
-- @tparam ?boolean opts.read_only
248+
-- If true, the function will try to fetch the space from any available healthy replica.
249+
--
250+
-- @tparam ?string opts.replica_id
251+
-- The exact replica ID to fetch the replicaset from.
252+
--
253+
-- @return[1] table space object
254+
-- @return[1] nil
255+
-- @return[1] number schema version
256+
-- @treturn[2] nil
257+
-- @treturn[2] table Error description
258+
function utils.get_space(space_name, vshard_router, opts)
259+
local spaces, err, schema_version = utils.get_spaces(vshard_router, opts)
189260

190261
if spaces == nil then
191262
return nil, err

crud/count.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ local function call_count_on_router(vshard_router, space_name, user_conditions,
131131
return nil, CountError:new("Failed to parse conditions: %s", err)
132132
end
133133

134-
local space, err = utils.get_space(space_name, vshard_router, opts.timeout)
134+
local space, err = utils.get_space(space_name, vshard_router, {
135+
timeout = opts.timeout,
136+
read_only = opts.mode ~= 'write',
137+
})
135138
if err ~= nil then
136139
return nil, CountError:new("An error occurred during the operation: %s", err), const.NEED_SCHEMA_RELOAD
137140
end

crud/delete.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ local function call_delete_on_router(vshard_router, space_name, key, opts)
8181
fetch_latest_metadata = '?boolean',
8282
})
8383

84-
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, opts.timeout)
84+
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, {timeout = opts.timeout})
8585
if err ~= nil then
8686
return nil, DeleteError:new("An error occurred during the operation: %s", err), const.NEED_SCHEMA_RELOAD
8787
end

crud/get.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ local function call_get_on_router(vshard_router, space_name, key, opts)
8282
fetch_latest_metadata = '?boolean',
8383
})
8484

85-
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, opts.timeout)
85+
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, {
86+
timeout = opts.timeout,
87+
read_only = opts.mode ~= 'write',
88+
})
8689
if err ~= nil then
8790
return nil, GetError:new("An error occurred during the operation: %s", err), const.NEED_SCHEMA_RELOAD
8891
end

crud/insert.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ local function call_insert_on_router(vshard_router, space_name, original_tuple,
8585
fetch_latest_metadata = '?boolean',
8686
})
8787

88-
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, opts.timeout)
88+
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, {timeout = opts.timeout})
8989
if err ~= nil then
9090
return nil, InsertError:new("An error occurred during the operation: %s", err), const.NEED_SCHEMA_RELOAD
9191
end

crud/insert_many.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ local function call_insert_many_on_router(vshard_router, space_name, original_tu
183183
fetch_latest_metadata = '?boolean',
184184
})
185185

186-
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, opts.timeout)
186+
local space, err, netbox_schema_version = utils.get_space(space_name, vshard_router, {timeout = opts.timeout})
187187
if err ~= nil then
188188
return nil, {
189189
InsertManyError:new("An error occurred during the operation: %s", err)

crud/len.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function len.call(space_name, opts)
5353
return nil, LenError:new(err)
5454
end
5555

56-
local space, err = utils.get_space(space_name, vshard_router, opts.timeout)
56+
local space, err = utils.get_space(space_name, vshard_router, {timeout = opts.timeout})
5757
if err ~= nil then
5858
return nil, LenError:new("An error occurred during the operation: %s", err)
5959
end

0 commit comments

Comments
 (0)