Skip to content

Commit

Permalink
improve: use optimistic locking to avoid concurrency problem in admin…
Browse files Browse the repository at this point in the history
… PATCH APIs.

There is a potential concurrency problem in all admin PATCH APIs when
two patch requests come in simultaneously, in such case, the patched
result of the first applied request will be overridden, also the
probability is tidy, but from the perspective of software's robust,
that's not what we wanna to see.

In this commit, we use the optimistic locking to avoid this problem, for
the example aforementioned, the second PATCH request will failure, and
it's up to the user to retry this PATCH request again.

The optimistic locking mechanism in ETCD v2 APIs is showed by arg
`prevIndex`.

Signed-off-by: tokers <zchao1995@gmail.com>
  • Loading branch information
tokers committed Sep 17, 2020
1 parent e93cdbd commit c2e2e95
Show file tree
Hide file tree
Showing 12 changed files with 90 additions and 31 deletions.
4 changes: 2 additions & 2 deletions apisix/admin/global_rules.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ function _M.patch(id, conf, sub_path)
core.json.delay_encode(res_old, true))

local node_value = res_old.body.node.value
local modified_index = res_old.body.node.modifiedIndex

if sub_path and sub_path ~= "" then
local code, err, node_val = core.table.patch(node_value, sub_path, conf)
Expand All @@ -153,8 +154,7 @@ function _M.patch(id, conf, sub_path)
return 400, err
end

-- TODO: this is not safe, we need to use compare-set
local res, err = core.etcd.set(key, node_value)
local res, err = core.etcd.atomic_set(key, node_value, nil, modified_index)
if not res then
core.log.error("failed to set new global rule[", key, "]: ", err)
return 500, {error_msg = err}
Expand Down
4 changes: 2 additions & 2 deletions apisix/admin/routes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ function _M.patch(id, conf, sub_path, args)
core.json.delay_encode(res_old, true))

local node_value = res_old.body.node.value
local modified_index = res_old.body.node.modifiedIndex

if sub_path and sub_path ~= "" then
local code, err, node_val = core.table.patch(node_value, sub_path, conf)
Expand All @@ -259,8 +260,7 @@ function _M.patch(id, conf, sub_path, args)
return 400, err
end

-- TODO: this is not safe, we need to use compare-set
local res, err = core.etcd.set(key, node_value, args.ttl)
local res, err = core.etcd.atomic_set(key, node_value, args.ttl, modified_index)
if not res then
core.log.error("failed to set new route[", key, "] to etcd: ", err)
return 500, {error_msg = err}
Expand Down
4 changes: 2 additions & 2 deletions apisix/admin/services.lua
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ function _M.patch(id, conf, sub_path)
core.json.delay_encode(res_old, true))

local node_value = res_old.body.node.value
local modified_index = res_old.body.node.modifiedIndex

if sub_path and sub_path ~= "" then
local code, err, node_val = core.table.patch(node_value, sub_path, conf)
Expand All @@ -237,8 +238,7 @@ function _M.patch(id, conf, sub_path)
return 400, err
end

-- TODO: this is not safe, we need to use compare-set
local res, err = core.etcd.set(key, node_value)
local res, err = core.etcd.atomic_set(key, node_value, nil, modified_index)
if not res then
core.log.error("failed to set new service[", key, "]: ", err)
return 500, {error_msg = err}
Expand Down
4 changes: 2 additions & 2 deletions apisix/admin/ssl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ function _M.patch(id, conf)


local node_value = res_old.body.node.value
local modified_index = res_old.body.node.modifiedIndex

node_value = core.table.merge(node_value, conf);

Expand All @@ -208,8 +209,7 @@ function _M.patch(id, conf)
return 400, err
end

-- TODO: this is not safe, we need to use compare-set
local res, err = core.etcd.set(key, node_value)
local res, err = core.etcd.atomic_set(key, node_value, nil, modified_index)
if not res then
core.log.error("failed to set new ssl[", key, "] to etcd: ", err)
return 500, {error_msg = err}
Expand Down
4 changes: 2 additions & 2 deletions apisix/admin/upstreams.lua
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ function _M.patch(id, conf, sub_path)
core.json.delay_encode(res_old, true))

local new_value = res_old.body.node.value
local modified_index = res_old.body.node.modifiedIndex

if sub_path and sub_path ~= "" then
local code, err, node_val = core.table.patch(new_value, sub_path, conf)
Expand All @@ -271,8 +272,7 @@ function _M.patch(id, conf, sub_path)
return 400, err
end

-- TODO: this is not safe, we need to use compare-set
local res, err = core.etcd.set(key, new_value)
local res, err = core.etcd.atomic_set(key, new_value, nil, modified_index)
if not res then
core.log.error("failed to set new upstream[", key, "]: ", err)
return 500, {error_msg = err}
Expand Down
59 changes: 59 additions & 0 deletions apisix/core/etcd.lua
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,65 @@ end
_M.set = set


function _M.atomic_set(key, value, ttl, mod_revision)
local etcd_cli, prefix, err = new()
if not etcd_cli then
return nil, err
end

local lease_id
if ttl then
local data, grant_err = etcd_cli:grant(tonumber(ttl))
if not data then
return nil, grant_err
end

lease_id = data.body.ID
end

key = prefix .. key

local compare = {
{
key = key,
target = "MOD",
result = "EQUAL",
mod_revision = mod_revision,
}
}

local success = {
{
requestPut = {
key = key,
value = value,
lease = lease_id,
}
}
}

local res, err = etcd_cli:txn(compare, success)
if not res then
return nil, err
end

if not res.body.succeeded then
return nil, "value changed before overwritten"
end

res.headers["X-Etcd-Index"] = res.body.header.revision
-- etcd v3 set would not return kv info
res.body.action = "compareAndSwap"
res.body.node = {
key = key,
value = value,
}
res.status = 201

return res, nil
end


function _M.push(key, value, ttl)
local etcd_cli, prefix, err = new()
if not etcd_cli then
Expand Down
4 changes: 2 additions & 2 deletions t/admin/global-rules.t
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ passed
},
"key": "/apisix/global_rules/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -235,7 +235,7 @@ passed
},
"key": "/apisix/global_rules/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down
14 changes: 7 additions & 7 deletions t/admin/routes.t
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ passed
},
"key": "/apisix/routes/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -1056,7 +1056,7 @@ passed
},
"key": "/apisix/routes/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -1090,7 +1090,7 @@ passed
},
"key": "/apisix/routes/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -1124,7 +1124,7 @@ passed
},
"key": "/apisix/routes/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -1158,7 +1158,7 @@ passed
},
"key": "/apisix/routes/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -1190,7 +1190,7 @@ passed
},
"key": "/apisix/routes/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -1242,7 +1242,7 @@ passed
},
"key": "/apisix/routes/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down
4 changes: 2 additions & 2 deletions t/admin/services-string-id.t
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ GET /t
},
"key": "/apisix/services/5eeb3dc90f747328b2930b0b"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -697,7 +697,7 @@ passed
},
"key": "/apisix/services/5eeb3dc90f747328b2930b0b"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down
8 changes: 4 additions & 4 deletions t/admin/services.t
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ GET /t
},
"key": "/apisix/services/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -697,7 +697,7 @@ passed
},
"key": "/apisix/services/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -788,7 +788,7 @@ passed
},
"key": "/apisix/services/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -826,7 +826,7 @@ passed
},
"key": "/apisix/services/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down
8 changes: 4 additions & 4 deletions t/admin/upstream.t
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ GET /t
},
"key": "/apisix/upstreams/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -710,7 +710,7 @@ passed
},
"key": "/apisix/upstreams/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -833,7 +833,7 @@ passed
},
"key": "/apisix/upstreams/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -869,7 +869,7 @@ passed
},
"key": "/apisix/upstreams/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down
4 changes: 2 additions & 2 deletions t/router/radixtree-sni.t
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ location /t {
},
"key": "/apisix/ssl/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down Expand Up @@ -670,7 +670,7 @@ location /t {
},
"key": "/apisix/ssl/1"
},
"action": "set"
"action": "compareAndSwap"
}]]
)

Expand Down

0 comments on commit c2e2e95

Please sign in to comment.