Skip to content

Commit cb5e2c1

Browse files
committed
Improve safety and error handling of setting shared dict data.
See 18F/api.data.gov#385 This replaces "set" calls with "safe_set" for the "active_config" shared dict to prevent the possibility of publishing new API configuration removing old API configuration when it exceeds the available memory allocated for the shared dict. We also handle the possibility of "safe_set" still unpublishing the old configuration when it exceeds the available memory, which should ensure that the full API configuration never gets unpublished. This also adds more explicit error and warning logging to all the shared dict "set" calls.
1 parent 00b6d7b commit cb5e2c1

File tree

14 files changed

+310
-44
lines changed

14 files changed

+310
-44
lines changed

src/api-umbrella/proxy/hooks/init.lua

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ local incr_err
1212
WORKER_GROUP_ID, incr_err = ngx.shared.active_config:incr("worker_group_id", 1)
1313
if incr_err == "not found" then
1414
WORKER_GROUP_ID = 1
15-
local success, set_err = ngx.shared.active_config:set("worker_group_id", 1)
16-
if not success then
17-
ngx.log(ngx.ERR, "worker_group_id set err: ", set_err)
15+
local set_ok, set_err = ngx.shared.active_config:safe_set("worker_group_id", 1)
16+
if not set_ok then
17+
ngx.log(ngx.ERR, "failed to set 'worker_group_id' in 'active_config' shared dict: ", set_err)
1818
return
1919
end
2020
elseif incr_err then
@@ -23,4 +23,7 @@ end
2323

2424
ngx.shared.stats:delete("distributed_last_fetched_at")
2525
ngx.shared.api_users:delete("last_fetched_at")
26-
ngx.shared.active_config:set("elasticsearch_templates_created", false)
26+
local set_ok, set_err = ngx.shared.active_config:safe_set("elasticsearch_templates_created", false)
27+
if not set_ok then
28+
ngx.log(ngx.ERR, "failed to set 'elasticsearch_templates_created' in 'active_config' shared dict: ", set_err)
29+
end

src/api-umbrella/proxy/hooks/init_preload_modules.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ require "api-umbrella.utils.mongo"
4747
require "api-umbrella.utils.mustache_unescape"
4848
require "api-umbrella.utils.nillify_json_nulls"
4949
require "api-umbrella.utils.nillify_yaml_nulls"
50+
require "api-umbrella.utils.packed_shared_dict"
5051
require "api-umbrella.utils.random_num"
5152
require "api-umbrella.utils.random_seed"
5253
require "api-umbrella.utils.random_token"

src/api-umbrella/proxy/hooks/rewrite.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ local api_matcher = require "api-umbrella.proxy.middleware.api_matcher"
22
local config = require "api-umbrella.proxy.models.file_config"
33
local error_handler = require "api-umbrella.proxy.error_handler"
44
local host_normalize = require "api-umbrella.utils.host_normalize"
5+
local packed_shared_dict = require "api-umbrella.utils.packed_shared_dict"
56
local redirect_matches_to_https = require "api-umbrella.utils.redirect_matches_to_https"
6-
local utils = require "api-umbrella.proxy.utils"
77
local website_matcher = require "api-umbrella.proxy.middleware.website_matcher"
88

9-
local get_packed = utils.get_packed
9+
local get_packed = packed_shared_dict.get_packed
1010
local ngx_var = ngx.var
1111

1212
-- Determine the protocol/scheme and port this connection represents, based on

src/api-umbrella/proxy/jobs/distributed_rate_limit_puller.lua

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ local _M = {}
22

33
local interval_lock = require "api-umbrella.utils.interval_lock"
44
local mongo = require "api-umbrella.utils.mongo"
5+
local packed_shared_dict = require "api-umbrella.utils.packed_shared_dict"
56
local types = require "pl.types"
6-
local utils = require "api-umbrella.proxy.utils"
77

8-
local get_packed = utils.get_packed
8+
local get_packed = packed_shared_dict.get_packed
99
local is_empty = types.is_empty
10-
local set_packed = utils.set_packed
10+
local set_packed = packed_shared_dict.set_packed
1111

1212
local delay = 0.25 -- in seconds
1313

@@ -39,7 +39,12 @@ local function do_check()
3939
for index, result in ipairs(results) do
4040
if skip == 0 and index == 1 then
4141
if result["ts"] and result["ts"]["$timestamp"] then
42-
set_packed(ngx.shared.stats, "distributed_last_fetched_timestamp", result["ts"]["$timestamp"])
42+
local set_ok, set_err, set_forcible = set_packed(ngx.shared.stats, "distributed_last_fetched_timestamp", result["ts"]["$timestamp"])
43+
if not set_ok then
44+
ngx.log(ngx.ERR, "failed to set 'distributed_last_fetched_timestamp' in 'stats' shared dict: ", set_err)
45+
elseif set_forcible then
46+
ngx.log(ngx.WARN, "forcibly set 'distributed_last_fetched_timestamp' in 'stats' shared dict (shared dict may be too small)")
47+
end
4348
end
4449
end
4550

@@ -54,16 +59,18 @@ local function do_check()
5459
ttl = 3600
5560
end
5661

57-
local _, set_err = ngx.shared.stats:set(key, distributed_count, ttl)
58-
if set_err then
59-
ngx.log(ngx.ERR, "failed to set rate limit key", set_err)
62+
local set_ok, set_err, set_forcible = ngx.shared.stats:set(key, distributed_count, ttl)
63+
if not set_ok then
64+
ngx.log(ngx.ERR, "failed to set rate limit key in 'stats' shared dict: ", set_err)
65+
elseif set_forcible then
66+
ngx.log(ngx.WARN, "forcibly set rate limit key in 'stats' shared dict (shared dict may be too small)")
6067
end
6168
end
6269
elseif distributed_count > local_count then
6370
local incr = distributed_count - local_count
6471
local _, incr_err = ngx.shared.stats:incr(key, incr)
6572
if incr_err then
66-
ngx.log(ngx.ERR, "failed to increment rate limit key", incr_err)
73+
ngx.log(ngx.ERR, "failed to increment rate limit key: ", incr_err)
6774
end
6875
end
6976
end
@@ -73,7 +80,12 @@ local function do_check()
7380
until is_empty(results)
7481

7582
if success then
76-
ngx.shared.stats:set("distributed_last_pulled_at", current_fetch_time)
83+
local set_ok, set_err, set_forcible = ngx.shared.stats:set("distributed_last_pulled_at", current_fetch_time)
84+
if not set_ok then
85+
ngx.log(ngx.ERR, "failed to set 'distributed_last_pulled_at' in 'stats' shared dict: ", set_err)
86+
elseif set_forcible then
87+
ngx.log(ngx.WARN, "forcibly set 'distributed_last_pulled_at' in 'stats' shared dict (shared dict may be too small)")
88+
end
7789
end
7890
end
7991

src/api-umbrella/proxy/jobs/distributed_rate_limit_pusher.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ local function do_check()
8484
end
8585

8686
if success then
87-
ngx.shared.stats:set("distributed_last_pushed_at", current_save_time)
87+
local set_ok, set_err, set_forcible = ngx.shared.stats:set("distributed_last_pushed_at", current_save_time)
88+
if not set_ok then
89+
ngx.log(ngx.ERR, "failed to set 'distributed_last_pushed_at' in 'stats' shared dict: ", set_err)
90+
elseif set_forcible then
91+
ngx.log(ngx.WARN, "forcibly set 'distributed_last_pushed_at' in 'stats' shared dict (shared dict may be too small)")
92+
end
8893
end
8994
end
9095

src/api-umbrella/proxy/jobs/elasticsearch_setup.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ function _M.create_templates()
5050
end
5151
end
5252

53-
ngx.shared.active_config:set("elasticsearch_templates_created", true)
53+
local set_ok, set_err = ngx.shared.active_config:safe_set("elasticsearch_templates_created", true)
54+
if not set_ok then
55+
ngx.log(ngx.ERR, "failed to set 'elasticsearch_templates_created' in 'active_config' shared dict: ", set_err)
56+
end
5457
end
5558

5659
function _M.create_aliases()

src/api-umbrella/proxy/jobs/load_api_users.lua

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ local _M = {}
22

33
local interval_lock = require "api-umbrella.utils.interval_lock"
44
local mongo = require "api-umbrella.utils.mongo"
5+
local packed_shared_dict = require "api-umbrella.utils.packed_shared_dict"
56
local types = require "pl.types"
6-
local utils = require "api-umbrella.proxy.utils"
77

8-
local get_packed = utils.get_packed
8+
local get_packed = packed_shared_dict.get_packed
99
local is_empty = types.is_empty
10-
local set_packed = utils.set_packed
10+
local set_packed = packed_shared_dict.set_packed
1111

1212
local api_users = ngx.shared.api_users
1313

@@ -41,7 +41,12 @@ local function do_check()
4141
for index, result in ipairs(results) do
4242
if skip == 0 and index == 1 then
4343
if result["ts"] and result["ts"]["$timestamp"] then
44-
set_packed(api_users, "distributed_last_fetched_timestamp", result["ts"]["$timestamp"])
44+
local set_ok, set_err, set_forcible = set_packed(api_users, "distributed_last_fetched_timestamp", result["ts"]["$timestamp"])
45+
if not set_ok then
46+
ngx.log(ngx.ERR, "failed to set 'distributed_last_fetched_timestamp' in 'api_users' shared dict: ", set_err)
47+
elseif set_forcible then
48+
ngx.log(ngx.WARN, "forcibly set 'distributed_last_fetched_timestamp' in 'api_users' shared dict (shared dict may be too small)")
49+
end
4550
end
4651
end
4752

@@ -55,7 +60,12 @@ local function do_check()
5560
until is_empty(results)
5661

5762
if success then
58-
api_users:set("last_fetched_at", current_fetch_time)
63+
local set_ok, set_err, set_forcible = api_users:set("last_fetched_at", current_fetch_time)
64+
if not set_ok then
65+
ngx.log(ngx.ERR, "failed to set 'last_fetched_at' in 'api_users' shared dict: ", set_err)
66+
elseif set_forcible then
67+
ngx.log(ngx.WARN, "forcibly set 'last_fetched_at' in 'api_users' shared dict (shared dict may be too small)")
68+
end
5969
end
6070
end
6171

src/api-umbrella/proxy/jobs/load_db_config.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ local function do_check()
5252
end
5353

5454
if last_fetched_at then
55-
ngx.shared.active_config:set("db_config_last_fetched_at", last_fetched_at)
55+
local set_ok, set_err = ngx.shared.active_config:safe_set("db_config_last_fetched_at", last_fetched_at)
56+
if not set_ok then
57+
ngx.log(ngx.ERR, "failed to set 'db_config_last_fetched_at' in 'active_config' shared dict: ", set_err)
58+
end
5659
end
5760
end
5861

src/api-umbrella/proxy/log_utils.lua

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,12 @@ function _M.cache_new_city_geocode(data)
218218
-- Only cache the first city location per startup to prevent lots of indexing
219219
-- churn re-indexing the same city.
220220
if not ngx.shared.geocode_city_cache:get(id) then
221-
ngx.shared.geocode_city_cache:set(id, true)
221+
local set_ok, set_err, set_forcible = ngx.shared.geocode_city_cache:set(id, true)
222+
if not set_ok then
223+
ngx.log(ngx.ERR, "failed to set city in 'geocode_city_cache' shared dict: ", set_err)
224+
elseif set_forcible then
225+
ngx.log(ngx.WARN, "forcibly set city in 'geocode_city_cache' shared dict (shared dict may be too small)")
226+
end
222227

223228
-- Perform the actual cache call in a timer because the http library isn't
224229
-- supported directly in the log_by_lua context.

src/api-umbrella/proxy/models/active_config.lua

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local escape_regex = require "api-umbrella.utils.escape_regex"
44
local host_normalize = require "api-umbrella.utils.host_normalize"
55
local json_encode = require "api-umbrella.utils.json_encode"
66
local mustache_unescape = require "api-umbrella.utils.mustache_unescape"
7+
local packed_shared_dict = require "api-umbrella.utils.packed_shared_dict"
78
local plutils = require "pl.utils"
89
local random_token = require "api-umbrella.utils.random_token"
910
local startswith = require("pl.stringx").startswith
@@ -14,7 +15,7 @@ local xpcall_error_handler = require "api-umbrella.utils.xpcall_error_handler"
1415
local append_array = utils.append_array
1516
local cache_computed_settings = utils.cache_computed_settings
1617
local deepcopy = tablex.deepcopy
17-
local set_packed = utils.set_packed
18+
local safe_set_packed = packed_shared_dict.safe_set_packed
1819
local size = tablex.size
1920
local split = plutils.split
2021

@@ -256,10 +257,51 @@ function _M.set(db_config)
256257
local website_backends = get_combined_website_backends(file_config, db_config)
257258

258259
local active_config = build_active_config(apis, website_backends)
259-
set_packed(ngx.shared.active_config, "packed_data", active_config)
260-
ngx.shared.active_config:set("db_version", db_config["version"])
261-
ngx.shared.active_config:set("file_version", file_config["version"])
262-
ngx.shared.active_config:set("worker_group_setup_complete:" .. WORKER_GROUP_ID, true)
260+
local previous_packed_config = ngx.shared.active_config:get("packed_data")
261+
262+
local set_ok, set_err = safe_set_packed(ngx.shared.active_config, "packed_data", active_config)
263+
if not set_ok then
264+
ngx.log(ngx.ERR, "failed to set 'packed_data' in 'active_config' shared dict: ", set_err)
265+
266+
-- If the new config exceeds the amount of available space, `safe_set` will
267+
-- still result in the previous value for `packed_data` getting removed
268+
-- (see https://github.com/openresty/lua-nginx-module/issues/1365). This
269+
-- effectively unpublishes all configuration, which can be disruptive if
270+
-- your new config happens to exceed the allocated space.
271+
--
272+
-- So to more safely handle this scenario, revert `packed_data` back to the
273+
-- previously set value (that presumably fits in memory) so that the
274+
-- previous config remains in place. The new configuration will go
275+
-- unpublished, but this at least keeps the system up in the previous
276+
-- state.
277+
--
278+
-- When this occurs, we will go ahead and set the `db_version` and
279+
-- `file_version` to the new versions, even though this isn't entirely
280+
-- accurate (since we're reverting to the previous config). But by
281+
-- pretending the data was successfully set, this prevents the system from
282+
-- looping indefinitely and trying to set the config over and over to a
283+
-- version that won't fit in memory. In this situation, there's not much
284+
-- else we can do, since the shdict memory needs to be increased.
285+
set_ok, set_err = ngx.shared.active_config:safe_set("packed_data", previous_packed_config)
286+
if not set_ok then
287+
ngx.log(ngx.ERR, "failed to set 'packed_data' in 'active_config' shared dict: ", set_err)
288+
end
289+
end
290+
291+
set_ok, set_err = ngx.shared.active_config:safe_set("db_version", db_config["version"])
292+
if not set_ok then
293+
ngx.log(ngx.ERR, "failed to set 'db_version' in 'active_config' shared dict: ", set_err)
294+
end
295+
296+
set_ok, set_err = ngx.shared.active_config:safe_set("file_version", file_config["version"])
297+
if not set_ok then
298+
ngx.log(ngx.ERR, "failed to set 'file_version' in 'active_config' shared dict: ", set_err)
299+
end
300+
301+
set_ok, set_err = ngx.shared.active_config:safe_set("worker_group_setup_complete:" .. WORKER_GROUP_ID, true)
302+
if not set_ok then
303+
ngx.log(ngx.ERR, "failed to set 'worker_group_setup_complete' in 'active_config' shared dict: ", set_err)
304+
end
263305
end
264306

265307
return _M

src/api-umbrella/proxy/utils.lua

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
local _M = {}
22

3-
local cmsgpack = require "cmsgpack"
43
local json_null = require("cjson").null
54
local plutils = require "pl.utils"
65
local stringx = require "pl.stringx"
@@ -10,11 +9,9 @@ local types = require "pl.types"
109
local escape = plutils.escape
1110
local gsub = ngx.re.gsub
1211
local is_empty = types.is_empty
13-
local pack = cmsgpack.pack
1412
local split = plutils.split
1513
local strip = stringx.strip
1614
local table_keys = tablex.keys
17-
local unpack = cmsgpack.unpack
1815

1916
-- Append an array to the end of the destination array.
2017
--
@@ -48,17 +45,6 @@ function _M.base_url()
4845
return base
4946
end
5047

51-
function _M.get_packed(dict, key)
52-
local packed = dict:get(key)
53-
if packed then
54-
return unpack(packed)
55-
end
56-
end
57-
58-
function _M.set_packed(dict, key, value)
59-
return dict:set(key, pack(value))
60-
end
61-
6248
function _M.pick_where_present(dict, keys)
6349
local selected = {}
6450

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
local cmsgpack = require "cmsgpack"
2+
3+
local pack = cmsgpack.pack
4+
local unpack = cmsgpack.unpack
5+
6+
local _M = {}
7+
8+
function _M.get_packed(dict, key)
9+
local packed = dict:get(key)
10+
if packed then
11+
return unpack(packed)
12+
end
13+
end
14+
15+
function _M.set_packed(dict, key, value)
16+
return dict:set(key, pack(value))
17+
end
18+
19+
function _M.safe_set_packed(dict, key, value)
20+
return dict:safe_set(key, pack(value))
21+
end
22+
23+
return _M

0 commit comments

Comments
 (0)