From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from smtpng1.m.smailru.net (smtpng1.m.smailru.net [94.100.181.251]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id 2673C4696CC for ; Thu, 21 Nov 2019 00:28:34 +0300 (MSK) From: Nikita Pettik Date: Thu, 21 Nov 2019 00:28:13 +0300 Message-Id: <22cf7005dc67cbc1db717ebbd6aee79660479d84.1574277369.git.korablev@tarantool.org> In-Reply-To: References: In-Reply-To: References: Subject: [Tarantool-patches] [PATCH v2 14/16] sql: introduce cache for prepared statemets List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , To: tarantool-patches@dev.tarantool.org Cc: v.shpilevoy@tarantool.org This patch introduces cache (as data structure) to handle prepared statements and a set of interface functions (insert, delete, find) to operate on it. Cache under the hood uses LRU eviction policy. It is implemented as a hash table with string keys (which contain original SQL statements) and prepared statements (struct sql_stmt which is an alias for struct Vdbe) as values. To realise LRU strategy we maintain list of nodes: head is the newest prepared statement, tail a candidate to be evicted. Size of cache is regulated via box.cfg{sql_cache_size} parameter. Cache is global to all sessions. To erase session manually, one can set its size to 0. Default cache size is assumed to be 5 Mb. Part of #2592 --- src/box/CMakeLists.txt | 1 + src/box/box.cc | 20 +++++ src/box/box.h | 1 + src/box/errcode.h | 1 + src/box/lua/cfg.cc | 12 +++ src/box/lua/load_cfg.lua | 3 + src/box/prep_stmt.c | 186 ++++++++++++++++++++++++++++++++++++++++ src/box/prep_stmt.h | 112 ++++++++++++++++++++++++ src/box/session.cc | 1 + src/box/sql.c | 3 + test/app-tap/init_script.result | 37 ++++---- test/box/admin.result | 2 + test/box/cfg.result | 7 ++ test/box/cfg.test.lua | 1 + test/box/misc.result | 1 + 15 files changed, 370 insertions(+), 18 deletions(-) create mode 100644 src/box/prep_stmt.c create mode 100644 src/box/prep_stmt.h diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt index 5cd5cba81..8be3d982d 100644 --- a/src/box/CMakeLists.txt +++ b/src/box/CMakeLists.txt @@ -126,6 +126,7 @@ add_library(box STATIC sql.c bind.c execute.c + prep_stmt.c wal.c call.c merger.c diff --git a/src/box/box.cc b/src/box/box.cc index b119c927b..7d0b36f13 100644 --- a/src/box/box.cc +++ b/src/box/box.cc @@ -74,6 +74,7 @@ #include "call.h" #include "func.h" #include "sequence.h" +#include "prep_stmt.h" static char status[64] = "unknown"; @@ -599,6 +600,15 @@ box_check_vinyl_options(void) } } +static void +box_check_sql_cache_size(int size) +{ + if (size < 0) { + tnt_raise(ClientError, ER_CFG, "sql_cache_size", + "must be non-negative"); + } +} + void box_check_config() { @@ -620,6 +630,7 @@ box_check_config() box_check_memtx_memory(cfg_geti64("memtx_memory")); box_check_memtx_min_tuple_size(cfg_geti64("memtx_min_tuple_size")); box_check_vinyl_options(); + box_check_sql_cache_size(cfg_geti("sql_cache_size")); } /* @@ -886,6 +897,14 @@ box_set_net_msg_max(void) IPROTO_FIBER_POOL_SIZE_FACTOR); } +void +box_set_prepared_stmt_cache_size(void) +{ + int cache_sz = cfg_geti("sql_cache_size"); + box_check_sql_cache_size(cache_sz); + sql_prepared_stmt_cache_set_size(cache_sz); +} + /* }}} configuration bindings */ /** @@ -2096,6 +2115,7 @@ box_cfg_xc(void) box_check_instance_uuid(&instance_uuid); box_check_replicaset_uuid(&replicaset_uuid); + box_set_prepared_stmt_cache_size(); box_set_net_msg_max(); box_set_readahead(); box_set_too_long_threshold(); diff --git a/src/box/box.h b/src/box/box.h index ccd527bd5..f2e88c8a9 100644 --- a/src/box/box.h +++ b/src/box/box.h @@ -235,6 +235,7 @@ void box_set_replication_sync_lag(void); void box_set_replication_sync_timeout(void); void box_set_replication_skip_conflict(void); void box_set_net_msg_max(void); +void box_set_prepared_stmt_cache_size(void); extern "C" { #endif /* defined(__cplusplus) */ diff --git a/src/box/errcode.h b/src/box/errcode.h index c660b1c70..ee44f61b3 100644 --- a/src/box/errcode.h +++ b/src/box/errcode.h @@ -258,6 +258,7 @@ struct errcode_record { /*203 */_(ER_BOOTSTRAP_READONLY, "Trying to bootstrap a local read-only instance as master") \ /*204 */_(ER_SQL_FUNC_WRONG_RET_COUNT, "SQL expects exactly one argument returned from %s, got %d")\ /*205 */_(ER_FUNC_INVALID_RETURN_TYPE, "Function '%s' returned value of invalid type: expected %s got %s") \ + /*206 */_(ER_SQL_PREPARE, "Failed to prepare SQL statement: %s") \ /* * !IMPORTANT! Please follow instructions at start of the file diff --git a/src/box/lua/cfg.cc b/src/box/lua/cfg.cc index 4884ce013..42070fe49 100644 --- a/src/box/lua/cfg.cc +++ b/src/box/lua/cfg.cc @@ -274,6 +274,17 @@ lbox_cfg_set_net_msg_max(struct lua_State *L) return 0; } +static int +lbox_set_prepared_stmt_cache_size(struct lua_State *L) +{ + try { + box_set_prepared_stmt_cache_size(); + } catch (Exception *) { + luaT_error(L); + } + return 0; +} + static int lbox_cfg_set_worker_pool_threads(struct lua_State *L) { @@ -378,6 +389,7 @@ box_lua_cfg_init(struct lua_State *L) {"cfg_set_replication_sync_timeout", lbox_cfg_set_replication_sync_timeout}, {"cfg_set_replication_skip_conflict", lbox_cfg_set_replication_skip_conflict}, {"cfg_set_net_msg_max", lbox_cfg_set_net_msg_max}, + {"cfg_set_sql_cache_size", lbox_set_prepared_stmt_cache_size}, {NULL, NULL} }; diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua index 85617c8f0..4463f989c 100644 --- a/src/box/lua/load_cfg.lua +++ b/src/box/lua/load_cfg.lua @@ -81,6 +81,7 @@ local default_cfg = { feedback_host = "https://feedback.tarantool.io", feedback_interval = 3600, net_msg_max = 768, + sql_cache_size = 5 * 1024 * 1024, } -- types of available options @@ -144,6 +145,7 @@ local template_cfg = { feedback_host = 'string', feedback_interval = 'number', net_msg_max = 'number', + sql_cache_size = 'number', } local function normalize_uri(port) @@ -250,6 +252,7 @@ local dynamic_cfg = { instance_uuid = check_instance_uuid, replicaset_uuid = check_replicaset_uuid, net_msg_max = private.cfg_set_net_msg_max, + sql_cache_size = private.cfg_set_sql_cache_size, } -- diff --git a/src/box/prep_stmt.c b/src/box/prep_stmt.c new file mode 100644 index 000000000..fe3b8244e --- /dev/null +++ b/src/box/prep_stmt.c @@ -0,0 +1,186 @@ +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include "prep_stmt.h" + +#include "assoc.h" +#include "error.h" +#include "execute.h" +#include "session.h" + +/** Default cache size is 5 Mb. */ +size_t prep_stmt_cache_size = 5 * 1024 * 1024; + +static struct prep_stmt_cache prep_stmt_cache; + +void +sql_prepared_stmt_cache_init() +{ + prep_stmt_cache.hash = mh_strnptr_new(); + if (prep_stmt_cache.hash == NULL) + panic("out of memory"); + prep_stmt_cache.mem_quota = prep_stmt_cache_size; + prep_stmt_cache.mem_used = 0; + rlist_create(&prep_stmt_cache.cache_lru); +} + +static size_t +sql_cache_node_sizeof(struct sql_stmt *stmt) +{ + return sql_stmt_sizeof(stmt) + sizeof(struct stmt_cache_node *); +} + +static void +sql_cache_node_delete(struct prep_stmt_cache *cache, + struct stmt_cache_node *node) +{ + cache->mem_used -= sql_cache_node_sizeof(node->stmt); + rlist_del(&node->in_lru); + sql_finalize(node->stmt); + TRASH(node); + free(node); +} + +void +sql_prepared_stmt_cache_delete(struct stmt_cache_node *node) +{ + struct prep_stmt_cache *cache = &prep_stmt_cache; + const char *sql_str = sql_stmt_query_str(node->stmt); + mh_int_t hash_id = + mh_strnptr_find_inp(cache->hash, sql_str, strlen(sql_str)); + assert(hash_id != mh_end(cache->hash)); + mh_strnptr_del(cache->hash, hash_id, NULL); + sql_cache_node_delete(cache, node); +} + +void +sql_cache_stmt_refresh(struct stmt_cache_node *node) +{ + rlist_move_entry(&prep_stmt_cache.cache_lru, node, in_lru); +} + +static void +sql_prepared_stmt_cache_gc() +{ + if (rlist_empty(&prep_stmt_cache.cache_lru)) { + assert(prep_stmt_cache.mem_used == 0); + return; + } + struct stmt_cache_node *node = + rlist_last_entry(&prep_stmt_cache.cache_lru, struct stmt_cache_node, + in_lru); + /* + * TODO: instead of following simple LRU rule it could turn + * out to be reasonable to also account value of reference + * counters. + */ + sql_prepared_stmt_cache_delete(node); +} + +/** + * Allocate new cache node containing given prepared statement. + * Add it to the LRU cache list. Account cache size enlargement. + */ +static struct stmt_cache_node * +sql_cache_node_new(struct sql_stmt *stmt) +{ + struct stmt_cache_node *node = malloc(sizeof(*node)); + if (node == NULL) { + diag_set(OutOfMemory, sizeof(*node), "malloc", + "struct stmt_cache_node"); + return NULL; + } + node->stmt = stmt; + rlist_add(&prep_stmt_cache.cache_lru, &node->in_lru); + prep_stmt_cache.mem_used += sql_cache_node_sizeof(stmt); + return node; +} + +/** + * Return true if used memory (accounting new node) for SQL + * prepared statement cache does not exceed the limit. + */ +static bool +sql_cache_check_new_node_size(size_t size) +{ + return prep_stmt_cache.mem_used + size <= prep_stmt_cache.mem_quota; +} + +int +sql_prepared_stmt_cache_insert(struct sql_stmt *stmt) +{ + assert(stmt != NULL); + struct prep_stmt_cache *cache = &prep_stmt_cache; + size_t new_node_size = sql_cache_node_sizeof(stmt); + if (new_node_size > prep_stmt_cache.mem_quota) { + diag_set(ClientError, ER_SQL_PREPARE, "size of statement "\ + "exceeds cache memory limit. Please, increase SQL "\ + "cache size"); + return -1; + } + while (! sql_cache_check_new_node_size(new_node_size)) + sql_prepared_stmt_cache_gc(); + struct mh_strnptr_t *hash = cache->hash; + const char *sql_str = sql_stmt_query_str(stmt); + assert(sql_prepared_stmt_cache_find(sql_str) == NULL); + struct stmt_cache_node *cache_node = sql_cache_node_new(stmt); + if (cache_node == NULL) + return -1; + uint32_t str_hash = mh_strn_hash(sql_str, strlen(sql_str)); + const struct mh_strnptr_node_t hash_node = { sql_str, strlen(sql_str), + str_hash, cache_node }; + struct mh_strnptr_node_t *old_node = NULL; + mh_int_t i = mh_strnptr_put(hash, &hash_node, &old_node, NULL); + if (i == mh_end(hash)) { + sql_cache_node_delete(cache, cache_node); + diag_set(OutOfMemory, 0, "mh_strnptr_put", "mh_strnptr_node"); + return -1; + } + assert(old_node == NULL); + return 0; +} + +struct stmt_cache_node * +sql_prepared_stmt_cache_find(const char *sql_str) +{ + struct mh_strnptr_t *hash = prep_stmt_cache.hash; + mh_int_t stmt = mh_strnptr_find_inp(hash, sql_str, strlen(sql_str)); + if (stmt == mh_end(hash)) + return NULL; + return mh_strnptr_node(hash, stmt)->val; +} + +void +sql_prepared_stmt_cache_set_size(size_t size) +{ + prep_stmt_cache.mem_quota = size; + while (prep_stmt_cache.mem_used > size) + sql_prepared_stmt_cache_gc(); +} diff --git a/src/box/prep_stmt.h b/src/box/prep_stmt.h new file mode 100644 index 000000000..31f3ff52b --- /dev/null +++ b/src/box/prep_stmt.h @@ -0,0 +1,112 @@ +#ifndef INCLUDES_PREP_STMT_H +#define INCLUDES_PREP_STMT_H +/* + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include +#include + +#include "small/rlist.h" + +#if defined(__cplusplus) +extern "C" { +#endif + +extern size_t prep_stmt_cache_size; + +/** + * Global prepared statement cache which follows LRU + * eviction policy. Implemented as hash + * and double linked list. + */ +struct prep_stmt_cache { + /** Size of memory currently occupied by prepared statements. */ + size_t mem_used; + /** + * Max memory size that can be used for cache. + */ + size_t mem_quota; + /** Query hash -> struct prepared_stmt hash.*/ + struct mh_strnptr_t *hash; + struct rlist cache_lru; +}; + +struct stmt_cache_node { + /** Prepared statement itself. */ + struct sql_stmt *stmt; + /** + * Link to the next node. Head is the newest, tail is + * a candidate to be evicted. + */ + struct rlist in_lru; +}; + +struct sql_stmt; + +/** + * Initialize global cache for prepared statements. Called once + * in sql_init(). + */ +void +sql_prepared_stmt_cache_init(); + +/** Remove statement node from cache and release all resources. */ +void +sql_prepared_stmt_cache_delete(struct stmt_cache_node *node); + +/** + * Account LRU cache node as the newest one (i.e. move to the HEAD + * of LRU list). + */ +void +sql_cache_stmt_refresh(struct stmt_cache_node *node); + +/** + * Save prepared statement to the prepared statement cache. + * Account cache size change. If the cache is full (i.e. memory + * quota is exceeded) diag error is raised. In case of success + * return id of prepared statement via output parameter @id. + */ +int +sql_prepared_stmt_cache_insert(struct sql_stmt *stmt); + +/** Find entry by SQL string. In case of search fails it returns NULL. */ +struct stmt_cache_node * +sql_prepared_stmt_cache_find(const char *sql_str); + +/** Set @prep_stmt_cache_size value. */ +void +sql_prepared_stmt_cache_set_size(size_t size); + +#if defined(__cplusplus) +} /* extern "C" { */ +#endif + +#endif diff --git a/src/box/session.cc b/src/box/session.cc index 461d1cf25..fe33ae6b6 100644 --- a/src/box/session.cc +++ b/src/box/session.cc @@ -36,6 +36,7 @@ #include "user.h" #include "error.h" #include "tt_static.h" +#include "execute.h" const char *session_type_strs[] = { "background", diff --git a/src/box/sql.c b/src/box/sql.c index f1df55571..3a991ccd5 100644 --- a/src/box/sql.c +++ b/src/box/sql.c @@ -54,6 +54,7 @@ #include "iproto_constants.h" #include "fk_constraint.h" #include "mpstream.h" +#include "prep_stmt.h" static sql *db = NULL; @@ -74,6 +75,8 @@ sql_init() if (sql_init_db(&db) != 0) panic("failed to initialize SQL subsystem"); + sql_prepared_stmt_cache_init(); + assert(db != NULL); } diff --git a/test/app-tap/init_script.result b/test/app-tap/init_script.result index 799297ba0..551a0bbeb 100644 --- a/test/app-tap/init_script.result +++ b/test/app-tap/init_script.result @@ -31,24 +31,25 @@ box.cfg 26 replication_sync_timeout:300 27 replication_timeout:1 28 slab_alloc_factor:1.05 -29 strip_core:true -30 too_long_threshold:0.5 -31 vinyl_bloom_fpr:0.05 -32 vinyl_cache:134217728 -33 vinyl_dir:. -34 vinyl_max_tuple_size:1048576 -35 vinyl_memory:134217728 -36 vinyl_page_size:8192 -37 vinyl_read_threads:1 -38 vinyl_run_count_per_level:2 -39 vinyl_run_size_ratio:3.5 -40 vinyl_timeout:60 -41 vinyl_write_threads:4 -42 wal_dir:. -43 wal_dir_rescan_delay:2 -44 wal_max_size:268435456 -45 wal_mode:write -46 worker_pool_threads:4 +29 sql_cache_size:5242880 +30 strip_core:true +31 too_long_threshold:0.5 +32 vinyl_bloom_fpr:0.05 +33 vinyl_cache:134217728 +34 vinyl_dir:. +35 vinyl_max_tuple_size:1048576 +36 vinyl_memory:134217728 +37 vinyl_page_size:8192 +38 vinyl_read_threads:1 +39 vinyl_run_count_per_level:2 +40 vinyl_run_size_ratio:3.5 +41 vinyl_timeout:60 +42 vinyl_write_threads:4 +43 wal_dir:. +44 wal_dir_rescan_delay:2 +45 wal_max_size:268435456 +46 wal_mode:write +47 worker_pool_threads:4 -- -- Test insert from detached fiber -- diff --git a/test/box/admin.result b/test/box/admin.result index 6126f3a97..852c1cde8 100644 --- a/test/box/admin.result +++ b/test/box/admin.result @@ -83,6 +83,8 @@ cfg_filter(box.cfg) - 1 - - slab_alloc_factor - 1.05 + - - sql_cache_size + - 5242880 - - strip_core - true - - too_long_threshold diff --git a/test/box/cfg.result b/test/box/cfg.result index 5370bb870..9542e6375 100644 --- a/test/box/cfg.result +++ b/test/box/cfg.result @@ -71,6 +71,8 @@ cfg_filter(box.cfg) | - 1 | - - slab_alloc_factor | - 1.05 + | - - sql_cache_size + | - 5242880 | - - strip_core | - true | - - too_long_threshold @@ -170,6 +172,8 @@ cfg_filter(box.cfg) | - 1 | - - slab_alloc_factor | - 1.05 + | - - sql_cache_size + | - 5242880 | - - strip_core | - true | - - too_long_threshold @@ -315,6 +319,9 @@ box.cfg{memtx_memory = box.cfg.memtx_memory} box.cfg{vinyl_memory = box.cfg.vinyl_memory} | --- | ... +box.cfg{sql_cache_size = 1024} + | --- + | ... -------------------------------------------------------------------------------- -- Test of default cfg options diff --git a/test/box/cfg.test.lua b/test/box/cfg.test.lua index 56ccb6767..e129568e6 100644 --- a/test/box/cfg.test.lua +++ b/test/box/cfg.test.lua @@ -51,6 +51,7 @@ box.cfg{replicaset_uuid = '12345678-0123-5678-1234-abcdefabcdef'} box.cfg{memtx_memory = box.cfg.memtx_memory} box.cfg{vinyl_memory = box.cfg.vinyl_memory} +box.cfg{sql_cache_size = 1024} -------------------------------------------------------------------------------- -- Test of default cfg options diff --git a/test/box/misc.result b/test/box/misc.result index b2930515b..78ffbf1dc 100644 --- a/test/box/misc.result +++ b/test/box/misc.result @@ -535,6 +535,7 @@ t; 203: box.error.BOOTSTRAP_READONLY 204: box.error.SQL_FUNC_WRONG_RET_COUNT 205: box.error.FUNC_INVALID_RETURN_TYPE + 206: box.error.SQL_PREPARE ... test_run:cmd("setopt delimiter ''"); --- -- 2.15.1