From 93ec666abe1e0c45c22ce723734df7d161e88283 Mon Sep 17 00:00:00 2001
From: Hiroaki Nakamura <hnakamur@gmail.com>
Date: Tue, 30 Jul 2019 18:46:38 +0900
Subject: [PATCH 1/3] Add ngx_http_lua_ffi_shdict_store_when for set_when and
 safe_set_when

---
 README.markdown           |  40 +++++
 doc/HttpLuaModule.wiki    |  32 ++++
 src/ngx_http_lua_shdict.c | 302 ++++++++++++++++++++++++++++++++++++++
 t/162-shdict-set-when.t   | 139 ++++++++++++++++++
 4 files changed, 513 insertions(+)
 create mode 100644 t/162-shdict-set-when.t

diff --git a/README.markdown b/README.markdown
index 7902550d5e..922536739a 100644
--- a/README.markdown
+++ b/README.markdown
@@ -3261,6 +3261,8 @@ Nginx API for Lua
 * [ngx.shared.DICT.get_stale](#ngxshareddictget_stale)
 * [ngx.shared.DICT.set](#ngxshareddictset)
 * [ngx.shared.DICT.safe_set](#ngxshareddictsafe_set)
+* [ngx.shared.DICT.set_when](#ngxshareddictset_when)
+* [ngx.shared.DICT.safe_set_when](#ngxshareddictsafe_set_when)
 * [ngx.shared.DICT.add](#ngxshareddictadd)
 * [ngx.shared.DICT.safe_add](#ngxshareddictsafe_add)
 * [ngx.shared.DICT.replace](#ngxshareddictreplace)
@@ -6400,6 +6402,8 @@ The resulting object `dict` has the following methods:
 * [get_stale](#ngxshareddictget_stale)
 * [set](#ngxshareddictset)
 * [safe_set](#ngxshareddictsafe_set)
+* [set_when](#ngxshareddictset_when)
+* [safe_set_when](#ngxshareddictsafe_set_when)
 * [add](#ngxshareddictadd)
 * [safe_add](#ngxshareddictsafe_add)
 * [replace](#ngxshareddictreplace)
@@ -6590,6 +6594,42 @@ See also [ngx.shared.DICT](#ngxshareddict).
 
 [Back to TOC](#nginx-api-for-lua)
 
+ngx.shared.DICT.set_when
+------------------------
+**syntax:** *success, err, forcible = ngx.shared.DICT:set(key, old_value, value, exptime?, flags?)*
+
+**context:** *init_by_lua&#42;, set_by_lua&#42;, rewrite_by_lua&#42;, access_by_lua&#42;, content_by_lua&#42;, header_filter_by_lua&#42;, body_filter_by_lua&#42;, log_by_lua&#42;, ngx.timer.&#42;, balancer_by_lua&#42;, ssl_certificate_by_lua&#42;, ssl_session_fetch_by_lua&#42;, ssl_session_store_by_lua&#42;*
+
+Just like the [set](#ngxshareddictset) method, but only stores the key-value pair into the dictionary [ngx.shared.DICT](#ngxshareddict) if the value for key is the same as `old_value`.
+
+If the `key` argument does *not* exist in the dictionary (or expired already), the `success` return value will be `true`.
+
+If the value for the key is *not* the same as `old_value`, the `success` return value will be `false` and the `err` value will be `"already modified"`.
+
+This feature was first introduced in the `v0.10.16rc1` release.
+
+See also [ngx.shared.DICT](#ngxshareddict).
+
+[Back to TOC](#nginx-api-for-lua)
+
+ngx.shared.DICT.safe_set_when
+-----------------------------
+**syntax:** *ok, err = ngx.shared.DICT:safe_set_when(key, old_value, value, exptime?, flags?)*
+
+**context:** *init_by_lua&#42;, set_by_lua&#42;, rewrite_by_lua&#42;, access_by_lua&#42;, content_by_lua&#42;, header_filter_by_lua&#42;, body_filter_by_lua&#42;, log_by_lua&#42;, ngx.timer.&#42;, balancer_by_lua&#42;, ssl_certificate_by_lua&#42;, ssl_session_fetch_by_lua&#42;, ssl_session_store_by_lua&#42;*
+
+Similar to the [set_when](#ngxshareddictset_when) method, but never overrides the (least recently used) unexpired items in the store when running out of storage in the shared memory zone. In this case, it will immediately return `nil` and the string "no memory".
+
+If the `key` argument does *not* exist in the dictionary (or expired already), the `success` return value will be `true`.
+
+If the value for the key is *not* the same as `old_value`, the `success` return value will be `false` and the `err` value will be `"already modified"`.
+
+This feature was first introduced in the `v0.10.16rc1` release.
+
+See also [ngx.shared.DICT](#ngxshareddict).
+
+[Back to TOC](#nginx-api-for-lua)
+
 ngx.shared.DICT.add
 -------------------
 
diff --git a/doc/HttpLuaModule.wiki b/doc/HttpLuaModule.wiki
index 1993b05a5d..1617f599d4 100644
--- a/doc/HttpLuaModule.wiki
+++ b/doc/HttpLuaModule.wiki
@@ -5393,6 +5393,8 @@ The resulting object <code>dict</code> has the following methods:
 * [[#ngx.shared.DICT.get_stale|get_stale]]
 * [[#ngx.shared.DICT.set|set]]
 * [[#ngx.shared.DICT.safe_set|safe_set]]
+* [[#ngx.shared.DICT.set_when|set_when]]
+* [[#ngx.shared.DICT.safe_set_when|safe_set_when]]
 * [[#ngx.shared.DICT.add|add]]
 * [[#ngx.shared.DICT.safe_add|safe_add]]
 * [[#ngx.shared.DICT.replace|replace]]
@@ -5563,6 +5565,36 @@ This feature was first introduced in the <code>v0.7.18</code> release.
 
 See also [[#ngx.shared.DICT|ngx.shared.DICT]].
 
+== ngx.shared.DICT.set_when ==
+'''syntax:''' ''success, err, forcible = ngx.shared.DICT:set(key, old_value, value, exptime?, flags?)''
+
+'''context:''' ''init_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*''
+
+Just like the [[#ngx.shared.DICT.set|set]] method, but only stores the key-value pair into the dictionary [[#ngx.shared.DICT|ngx.shared.DICT]] if the value for key is the same as <code>old_value</code>.
+
+If the <code>key</code> argument does ''not'' exist in the dictionary (or expired already), the <code>success</code> return value will be <code>true</code>.
+
+If the value for the key is ''not'' the same as <code>old_value</code>, the <code>success</code> return value will be <code>false</code> and the <code>err</code> value will be <code>"already modified"</code>.
+
+This feature was first introduced in the <code>v0.10.16rc1</code> release.
+
+See also [[#ngx.shared.DICT|ngx.shared.DICT]].
+
+== ngx.shared.DICT.safe_set_when ==
+'''syntax:''' ''ok, err = ngx.shared.DICT:safe_set_when(key, old_value, value, exptime?, flags?)''
+
+'''context:''' ''init_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*''
+
+Similar to the [[#ngx.shared.DICT.set_when|set_when]] method, but never overrides the (least recently used) unexpired items in the store when running out of storage in the shared memory zone. In this case, it will immediately return <code>nil</code> and the string "no memory".
+
+If the <code>key</code> argument does ''not'' exist in the dictionary (or expired already), the <code>success</code> return value will be <code>true</code>.
+
+If the value for the key is ''not'' the same as <code>old_value</code>, the <code>success</code> return value will be <code>false</code> and the <code>err</code> value will be <code>"already modified"</code>.
+
+This feature was first introduced in the <code>v0.10.16rc1</code> release.
+
+See also [[#ngx.shared.DICT|ngx.shared.DICT]].
+
 == ngx.shared.DICT.add ==
 
 '''syntax:''' ''success, err, forcible = ngx.shared.DICT:add(key, value, exptime?, flags?)''
diff --git a/src/ngx_http_lua_shdict.c b/src/ngx_http_lua_shdict.c
index 2fd2a94c13..e6fe6ed583 100644
--- a/src/ngx_http_lua_shdict.c
+++ b/src/ngx_http_lua_shdict.c
@@ -1569,6 +1569,308 @@ ngx_http_lua_ffi_shdict_store(ngx_shm_zone_t *zone, int op, u_char *key,
 }
 
 
+int
+ngx_http_lua_ffi_shdict_store_when(ngx_shm_zone_t *zone, int op, u_char *key,
+    size_t key_len, int old_value_type, u_char *old_str_value_buf,
+    size_t old_str_value_len, double old_num_value, int value_type,
+    u_char *str_value_buf, size_t str_value_len, double num_value,
+    long exptime, int user_flags, char **errmsg, int *forcible)
+{
+    int                          i, n;
+    u_char                       old_c, c, *p;
+    uint32_t                     hash;
+    ngx_int_t                    rc;
+    ngx_time_t                  *tp;
+    ngx_queue_t                 *queue, *q;
+    ngx_rbtree_node_t           *node;
+    ngx_http_lua_shdict_ctx_t   *ctx;
+    ngx_http_lua_shdict_node_t  *sd;
+
+    dd("exptime: %ld", exptime);
+
+    ctx = zone->data;
+
+    *forcible = 0;
+
+    hash = ngx_crc32_short(key, key_len);
+
+    switch (old_value_type) {
+
+    case SHDICT_TSTRING:
+        /* do nothing */
+        break;
+
+    case SHDICT_TNUMBER:
+        dd("num value: %lf", old_num_value);
+        old_str_value_buf = (u_char *) &old_num_value;
+        old_str_value_len = sizeof(double);
+        break;
+
+    case SHDICT_TBOOLEAN:
+        old_c = old_num_value ? 1 : 0;
+        old_str_value_buf = &old_c;
+        old_str_value_len = sizeof(u_char);
+        break;
+
+    case LUA_TNIL:
+        old_str_value_buf = NULL;
+        old_str_value_len = 0;
+        break;
+
+    default:
+        *errmsg = "unsupported old_value type";
+        return NGX_ERROR;
+    }
+
+    switch (value_type) {
+
+    case SHDICT_TSTRING:
+        /* do nothing */
+        break;
+
+    case SHDICT_TNUMBER:
+        dd("num value: %lf", num_value);
+        str_value_buf = (u_char *) &num_value;
+        str_value_len = sizeof(double);
+        break;
+
+    case SHDICT_TBOOLEAN:
+        c = num_value ? 1 : 0;
+        str_value_buf = &c;
+        str_value_len = sizeof(u_char);
+        break;
+
+    case LUA_TNIL:
+        if (op & (NGX_HTTP_LUA_SHDICT_ADD|NGX_HTTP_LUA_SHDICT_REPLACE)) {
+            *errmsg = "attempt to add or replace nil values";
+            return NGX_ERROR;
+        }
+
+        str_value_buf = NULL;
+        str_value_len = 0;
+        break;
+
+    default:
+        *errmsg = "unsupported value type";
+        return NGX_ERROR;
+    }
+
+    ngx_shmtx_lock(&ctx->shpool->mutex);
+
+#if 1
+    ngx_http_lua_shdict_expire(ctx, 1);
+#endif
+
+    rc = ngx_http_lua_shdict_lookup(zone, hash, key, key_len, &sd);
+
+    dd("lookup returns %d", (int) rc);
+
+    if (op & NGX_HTTP_LUA_SHDICT_REPLACE) {
+
+        if (rc == NGX_DECLINED || rc == NGX_DONE) {
+            ngx_shmtx_unlock(&ctx->shpool->mutex);
+            *errmsg = "not found";
+            return NGX_DECLINED;
+        }
+
+        /* rc == NGX_OK */
+
+        goto replace;
+    }
+
+    if (op & NGX_HTTP_LUA_SHDICT_ADD) {
+
+        if (rc == NGX_OK) {
+            ngx_shmtx_unlock(&ctx->shpool->mutex);
+            *errmsg = "exists";
+            return NGX_DECLINED;
+        }
+
+        if (rc == NGX_DONE) {
+            /* exists but expired */
+
+            dd("go to replace");
+            goto replace;
+        }
+
+        /* rc == NGX_DECLINED */
+
+        dd("go to insert");
+        goto insert;
+    }
+
+    if (rc == NGX_OK || rc == NGX_DONE) {
+
+        if (sd->value_type != old_value_type
+            || (size_t) sd->value_len != old_str_value_len
+            || memcmp(sd->data + sd->key_len, old_str_value_buf, sd->value_len) != 0)
+        {
+            ngx_shmtx_unlock(&ctx->shpool->mutex);
+            *errmsg = "already modified";
+            return NGX_ERROR;
+        }
+
+        if (value_type == LUA_TNIL) {
+            goto remove;
+        }
+
+replace:
+
+        if (str_value_buf
+            && str_value_len == (size_t) sd->value_len
+            && sd->value_type != SHDICT_TLIST)
+        {
+
+            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
+                           "lua shared dict set: found old entry and value "
+                           "size matched, reusing it");
+
+            ngx_queue_remove(&sd->queue);
+            ngx_queue_insert_head(&ctx->sh->lru_queue, &sd->queue);
+
+            sd->key_len = (u_short) key_len;
+
+            if (exptime > 0) {
+                tp = ngx_timeofday();
+                sd->expires = (uint64_t) tp->sec * 1000 + tp->msec
+                              + (uint64_t) exptime;
+
+            } else {
+                sd->expires = 0;
+            }
+
+            sd->user_flags = user_flags;
+
+            sd->value_len = (uint32_t) str_value_len;
+
+            dd("setting value type to %d", value_type);
+
+            sd->value_type = (uint8_t) value_type;
+
+            p = ngx_copy(sd->data, key, key_len);
+            ngx_memcpy(p, str_value_buf, str_value_len);
+
+            ngx_shmtx_unlock(&ctx->shpool->mutex);
+
+            return NGX_OK;
+        }
+
+        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
+                       "lua shared dict set: found old entry but value size "
+                       "NOT matched, removing it first");
+
+remove:
+
+        if (sd->value_type == SHDICT_TLIST) {
+            queue = ngx_http_lua_shdict_get_list_head(sd, key_len);
+
+            for (q = ngx_queue_head(queue);
+                 q != ngx_queue_sentinel(queue);
+                 q = ngx_queue_next(q))
+            {
+                p = (u_char *) ngx_queue_data(q,
+                                              ngx_http_lua_shdict_list_node_t,
+                                              queue);
+
+                ngx_slab_free_locked(ctx->shpool, p);
+            }
+        }
+
+        ngx_queue_remove(&sd->queue);
+
+        node = (ngx_rbtree_node_t *)
+                   ((u_char *) sd - offsetof(ngx_rbtree_node_t, color));
+
+        ngx_rbtree_delete(&ctx->sh->rbtree, node);
+
+        ngx_slab_free_locked(ctx->shpool, node);
+
+    }
+
+insert:
+
+    /* rc == NGX_DECLINED or value size unmatch */
+
+    if (str_value_buf == NULL) {
+        ngx_shmtx_unlock(&ctx->shpool->mutex);
+        return NGX_OK;
+    }
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
+                   "lua shared dict set: creating a new entry");
+
+    n = offsetof(ngx_rbtree_node_t, color)
+        + offsetof(ngx_http_lua_shdict_node_t, data)
+        + key_len
+        + str_value_len;
+
+    node = ngx_slab_alloc_locked(ctx->shpool, n);
+
+    if (node == NULL) {
+
+        if (op & NGX_HTTP_LUA_SHDICT_SAFE_STORE) {
+            ngx_shmtx_unlock(&ctx->shpool->mutex);
+
+            *errmsg = "no memory";
+            return NGX_ERROR;
+        }
+
+        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
+                       "lua shared dict set: overriding non-expired items "
+                       "due to memory shortage for entry \"%*s\"", key_len,
+                       key);
+
+        for (i = 0; i < 30; i++) {
+            if (ngx_http_lua_shdict_expire(ctx, 0) == 0) {
+                break;
+            }
+
+            *forcible = 1;
+
+            node = ngx_slab_alloc_locked(ctx->shpool, n);
+            if (node != NULL) {
+                goto allocated;
+            }
+        }
+
+        ngx_shmtx_unlock(&ctx->shpool->mutex);
+
+        *errmsg = "no memory";
+        return NGX_ERROR;
+    }
+
+allocated:
+
+    sd = (ngx_http_lua_shdict_node_t *) &node->color;
+
+    node->key = hash;
+    sd->key_len = (u_short) key_len;
+
+    if (exptime > 0) {
+        tp = ngx_timeofday();
+        sd->expires = (uint64_t) tp->sec * 1000 + tp->msec
+                      + (uint64_t) exptime;
+
+    } else {
+        sd->expires = 0;
+    }
+
+    sd->user_flags = user_flags;
+    sd->value_len = (uint32_t) str_value_len;
+    dd("setting value type to %d", value_type);
+    sd->value_type = (uint8_t) value_type;
+
+    p = ngx_copy(sd->data, key, key_len);
+    ngx_memcpy(p, str_value_buf, str_value_len);
+
+    ngx_rbtree_insert(&ctx->sh->rbtree, node);
+    ngx_queue_insert_head(&ctx->sh->lru_queue, &sd->queue);
+    ngx_shmtx_unlock(&ctx->shpool->mutex);
+
+    return NGX_OK;
+}
+
+
 int
 ngx_http_lua_ffi_shdict_get(ngx_shm_zone_t *zone, u_char *key,
     size_t key_len, int *value_type, u_char **str_value_buf,
diff --git a/t/162-shdict-set-when.t b/t/162-shdict-set-when.t
new file mode 100644
index 0000000000..b9701baed9
--- /dev/null
+++ b/t/162-shdict-set-when.t
@@ -0,0 +1,139 @@
+# vim:set ft= ts=4 sw=4 et fdm=marker:
+use Test::Nginx::Socket::Lua;
+
+#worker_connections(1014);
+#master_process_enabled(1);
+#log_level('warn');
+
+#repeat_each(2);
+
+plan tests => repeat_each() * (blocks() * 3);
+
+#no_diff();
+no_long_string();
+#master_on();
+#workers(2);
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: set_when success
+--- http_config
+    lua_shared_dict dogs 1m;
+--- config
+    location = /test {
+        content_by_lua '
+            local dogs = ngx.shared.dogs
+            dogs:set("foo", 32)
+            dogs:set_when("foo", 32, 33)
+            local val = dogs:get("foo")
+            ngx.say(val, " ", type(val))
+        ';
+    }
+--- request
+GET /test
+--- response_body
+33 number
+--- no_error_log
+[error]
+
+
+
+=== TEST 2: set_when fail
+--- http_config
+    lua_shared_dict dogs 1m;
+--- config
+    location = /test {
+        content_by_lua '
+            local dogs = ngx.shared.dogs
+            dogs:set("foo", 32)
+            local ok, err, forcible = dogs:set_when("foo", 32, 33)
+            ngx.say(ok, " ", err, " ", forcible)
+            local ok, err, forcible = dogs:set_when("foo", 32, 34)
+            ngx.say(ok, " ", err, " ", forcible)
+            local val = dogs:get("foo")
+            ngx.say(val, " ", type(val))
+        ';
+    }
+--- request
+GET /test
+--- response_body
+true nil false
+false already modified false
+33 number
+--- no_error_log
+[error]
+
+
+
+=== TEST 3: set_when success for expired value
+--- http_config
+    lua_shared_dict dogs 1m;
+--- config
+    location = /test {
+        content_by_lua '
+            local dogs = ngx.shared.dogs
+            dogs:set("foo", 32, 0.01)
+            ngx.sleep(0.02)
+            local ok, err, forcible = dogs:set_when("foo", 32, 33)
+            ngx.say(ok, " ", err, " ", forcible)
+            local val = dogs:get("foo")
+            ngx.say(val, " ", type(val))
+        ';
+    }
+--- request
+GET /test
+--- response_body
+true nil false
+33 number
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: set_when success for unmatched expired value
+--- http_config
+    lua_shared_dict dogs 1m;
+--- config
+    location = /test {
+        content_by_lua '
+            local dogs = ngx.shared.dogs
+            dogs:set("foo", 32, 0.01)
+            ngx.sleep(0.02)
+            local ok, err, forcible = dogs:set_when("foo", 31, 33)
+            ngx.say(ok, " ", err, " ", forcible)
+            local val = dogs:get("foo")
+            ngx.say(val, " ", type(val))
+        ';
+    }
+--- request
+GET /test
+--- response_body
+true nil false
+33 number
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: set_when success when old_value did not exist
+--- http_config
+    lua_shared_dict dogs 1m;
+--- config
+    location = /test {
+        content_by_lua '
+            local dogs = ngx.shared.dogs
+            local ok, err, forcible = dogs:set_when("foo", 32, 33)
+            ngx.say(ok, " ", err, " ", forcible)
+            local val = dogs:get("foo")
+            ngx.say(val, " ", type(val))
+        ';
+    }
+--- request
+GET /test
+--- response_body
+true nil false
+33 number
+--- no_error_log
+[error]

From 86b9f1e1f2e5dd30329a6bdf3aec09f4bf3ad85a Mon Sep 17 00:00:00 2001
From: Hiroaki Nakamura <hnakamur@gmail.com>
Date: Wed, 7 Aug 2019 15:48:49 +0000
Subject: [PATCH 2/3] Fold the line exceeding 80 columns

---
 src/ngx_http_lua_shdict.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/ngx_http_lua_shdict.c b/src/ngx_http_lua_shdict.c
index e6fe6ed583..7f5b75a037 100644
--- a/src/ngx_http_lua_shdict.c
+++ b/src/ngx_http_lua_shdict.c
@@ -1703,7 +1703,8 @@ ngx_http_lua_ffi_shdict_store_when(ngx_shm_zone_t *zone, int op, u_char *key,
 
         if (sd->value_type != old_value_type
             || (size_t) sd->value_len != old_str_value_len
-            || memcmp(sd->data + sd->key_len, old_str_value_buf, sd->value_len) != 0)
+            || memcmp(sd->data + sd->key_len, old_str_value_buf,
+                      sd->value_len) != 0)
         {
             ngx_shmtx_unlock(&ctx->shpool->mutex);
             *errmsg = "already modified";

From 7c81f9560fc509ffe807b7e02e148c8b91cd1456 Mon Sep 17 00:00:00 2001
From: Hiroaki Nakamura <hnakamur@gmail.com>
Date: Thu, 8 Aug 2019 14:07:04 +0000
Subject: [PATCH 3/3] Add 2 to shdict metatable count for set_when and
 safe_set_when

---
 t/062-count.t | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/t/062-count.t b/t/062-count.t
index b481763abf..c3153a75d3 100644
--- a/t/062-count.t
+++ b/t/062-count.t
@@ -283,7 +283,7 @@ n = 5
 --- request
 GET /test
 --- response_body
-n = 22
+n = 24
 --- no_error_log
 [error]