[Tarantool-patches] [PATCH v3 17/20] sql: introduce holder for prepared statemets

Nikita Pettik korablev at tarantool.org
Fri Dec 20 15:47:22 MSK 2019


This patch introduces holder (as data structure) to handle prepared
statements and a set of interface functions (insert, delete, find) to
operate on it. Holder under the hood is implemented as a global hash
(keys are values of hash function applied to the original string containing
SQL query; values are pointer to wrappers around compiled VDBE objects) and
GC queue. Each entry in hash has reference counter. When it reaches 0
value, entry is moved to GC queue. In case prepared statements holder is
out of memory, it launches GC process: each entry in GC queue is deleted
and all resources are released. Such approach allows to avoid workload
spikes on session's disconnect (since on such event all statements must
be deallocated).
Each session is extended with local hash to map statement ids available
for it. That is, session is allowed to execute and deallocate only
statements which are previously prepared in scope of this session.
On the other hand, global hash makes it possible to share same prepared
statement object among different sessions.
Size of cache is regulated via box.cfg{sql_cache_size} parameter.

Part of #2592
---
 src/box/CMakeLists.txt          |   1 +
 src/box/box.cc                  |  26 ++++
 src/box/box.h                   |   3 +
 src/box/errcode.h               |   1 +
 src/box/lua/cfg.cc              |   9 ++
 src/box/lua/load_cfg.lua        |   3 +
 src/box/session.cc              |  35 +++++
 src/box/session.h               |  17 +++
 src/box/sql.c                   |   3 +
 src/box/sql_stmt_cache.c        | 289 ++++++++++++++++++++++++++++++++++++++++
 src/box/sql_stmt_cache.h        | 145 ++++++++++++++++++++
 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 +
 16 files changed, 562 insertions(+), 18 deletions(-)
 create mode 100644 src/box/sql_stmt_cache.c
 create mode 100644 src/box/sql_stmt_cache.h

diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 5cd5cba81..763bc3a4c 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
+    sql_stmt_cache.c
     wal.c
     call.c
     merger.c
diff --git a/src/box/box.cc b/src/box/box.cc
index b119c927b..2b0cfa32d 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 "sql_stmt_cache.h"
 
 static char status[64] = "unknown";
 
@@ -599,6 +600,17 @@ box_check_vinyl_options(void)
 	}
 }
 
+static int
+box_check_sql_cache_size(int size)
+{
+	if (size < 0) {
+		diag_set(ClientError, ER_CFG, "sql_cache_size",
+			 "must be non-negative");
+		return -1;
+	}
+	return 0;
+}
+
 void
 box_check_config()
 {
@@ -620,6 +632,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 +899,17 @@ box_set_net_msg_max(void)
 				IPROTO_FIBER_POOL_SIZE_FACTOR);
 }
 
+int
+box_set_prepared_stmt_cache_size(void)
+{
+	int cache_sz = cfg_geti("sql_cache_size");
+	if (box_check_sql_cache_size(cache_sz) != 0)
+		return -1;
+	if (sql_stmt_cache_set_size(cache_sz) != 0)
+		return -1;
+	return 0;
+}
+
 /* }}} configuration bindings */
 
 /**
@@ -2096,6 +2120,8 @@ box_cfg_xc(void)
 	box_check_instance_uuid(&instance_uuid);
 	box_check_replicaset_uuid(&replicaset_uuid);
 
+	if (box_set_prepared_stmt_cache_size() != 0)
+		diag_raise();
 	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..6806f1fc2 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -236,6 +236,9 @@ void box_set_replication_sync_timeout(void);
 void box_set_replication_skip_conflict(void);
 void box_set_net_msg_max(void);
 
+int
+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..439af3cf9 100644
--- a/src/box/lua/cfg.cc
+++ b/src/box/lua/cfg.cc
@@ -274,6 +274,14 @@ lbox_cfg_set_net_msg_max(struct lua_State *L)
 	return 0;
 }
 
+static int
+lbox_set_prepared_stmt_cache_size(struct lua_State *L)
+{
+	if (box_set_prepared_stmt_cache_size() != 0)
+		luaT_error(L);
+	return 0;
+}
+
 static int
 lbox_cfg_set_worker_pool_threads(struct lua_State *L)
 {
@@ -378,6 +386,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/session.cc b/src/box/session.cc
index 461d1cf25..881318252 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 "sql_stmt_cache.h"
 
 const char *session_type_strs[] = {
 	"background",
@@ -141,6 +142,7 @@ session_create(enum session_type type)
 	session_set_type(session, type);
 	session->sql_flags = default_flags;
 	session->sql_default_engine = SQL_STORAGE_ENGINE_MEMTX;
+	session->sql_stmts = NULL;
 
 	/* For on_connect triggers. */
 	credentials_create(&session->credentials, guest_user);
@@ -178,6 +180,38 @@ session_create_on_demand()
 	return s;
 }
 
+bool
+session_check_stmt_id(struct session *session, uint32_t stmt_id)
+{
+	if (session->sql_stmts == NULL)
+		return false;
+	mh_int_t i = mh_i32ptr_find(session->sql_stmts, stmt_id, NULL);
+	return i != mh_end(session->sql_stmts);
+}
+
+int
+session_add_stmt_id(struct session *session, uint32_t id)
+{
+	if (session->sql_stmts == NULL) {
+		session->sql_stmts = mh_i32ptr_new();
+		if (session->sql_stmts == NULL) {
+			diag_set(OutOfMemory, 0, "mh_i32ptr_new",
+				 "session stmt hash");
+			return -1;
+		}
+	}
+	return sql_session_stmt_hash_add_id(session->sql_stmts, id);
+}
+
+void
+session_remove_stmt_id(struct session *session, uint32_t stmt_id)
+{
+	assert(session->sql_stmts != NULL);
+	mh_int_t i = mh_i32ptr_find(session->sql_stmts, stmt_id, NULL);
+	assert(i != mh_end(session->sql_stmts));
+	mh_i32ptr_del(session->sql_stmts, i, NULL);
+}
+
 /**
  * To quickly switch to admin user when executing
  * on_connect/on_disconnect triggers in iproto.
@@ -227,6 +261,7 @@ session_destroy(struct session *session)
 	struct mh_i64ptr_node_t node = { session->id, NULL };
 	mh_i64ptr_remove(session_registry, &node, NULL);
 	credentials_destroy(&session->credentials);
+	sql_session_stmt_hash_erase(session->sql_stmts);
 	mempool_free(&session_pool, session);
 }
 
diff --git a/src/box/session.h b/src/box/session.h
index eff3d7a67..6dfc7cba5 100644
--- a/src/box/session.h
+++ b/src/box/session.h
@@ -101,6 +101,11 @@ struct session {
 	const struct session_vtab *vtab;
 	/** Session metadata. */
 	union session_meta meta;
+	/**
+	 * ID of statements prepared in current session.
+	 * This map is allocated on demand.
+	 */
+	struct mh_i32ptr_t *sql_stmts;
 	/** Session user id and global grants */
 	struct credentials credentials;
 	/** Trigger for fiber on_stop to cleanup created on-demand session */
@@ -267,6 +272,18 @@ session_storage_cleanup(int sid);
 struct session *
 session_create(enum session_type type);
 
+/** Return true if given statement id belongs to the session. */
+bool
+session_check_stmt_id(struct session *session, uint32_t stmt_id);
+
+/** Add prepared statement ID to the session hash. */
+int
+session_add_stmt_id(struct session *session, uint32_t stmt_id);
+
+/** Remove prepared statement ID from the session hash. */
+void
+session_remove_stmt_id(struct session *session, uint32_t stmt_id);
+
 /**
  * Destroy a session.
  * Must be called by the networking layer on disconnect.
diff --git a/src/box/sql.c b/src/box/sql.c
index f1df55571..455fabeef 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 "sql_stmt_cache.h"
 
 static sql *db = NULL;
 
@@ -74,6 +75,8 @@ sql_init()
 	if (sql_init_db(&db) != 0)
 		panic("failed to initialize SQL subsystem");
 
+	sql_stmt_cache_init();
+
 	assert(db != NULL);
 }
 
diff --git a/src/box/sql_stmt_cache.c b/src/box/sql_stmt_cache.c
new file mode 100644
index 000000000..742e4135c
--- /dev/null
+++ b/src/box/sql_stmt_cache.c
@@ -0,0 +1,289 @@
+/*
+ * 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 <COPYRIGHT HOLDER> ``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
+ * <COPYRIGHT HOLDER> 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 "sql_stmt_cache.h"
+
+#include "assoc.h"
+#include "error.h"
+#include "execute.h"
+#include "diag.h"
+
+static struct sql_stmt_cache sql_stmt_cache;
+
+void
+sql_stmt_cache_init()
+{
+	sql_stmt_cache.hash = mh_i32ptr_new();
+	if (sql_stmt_cache.hash == NULL)
+		panic("out of memory");
+	sql_stmt_cache.mem_quota = 0;
+	sql_stmt_cache.mem_used = 0;
+	rlist_create(&sql_stmt_cache.gc_queue);
+}
+
+static size_t
+sql_cache_entry_sizeof(struct sql_stmt *stmt)
+{
+	return sql_stmt_est_size(stmt) + sizeof(struct stmt_cache_entry);
+}
+
+static void
+sql_cache_entry_delete(struct stmt_cache_entry *entry)
+{
+	assert(entry->refs == 0);
+	assert(! sql_stmt_busy(entry->stmt));
+	sql_stmt_finalize(entry->stmt);
+	TRASH(entry);
+	free(entry);
+}
+
+/**
+ * Remove statement entry from cache: firstly delete from hash,
+ * than remove from LRU list and account cache size changes,
+ * finally release occupied memory.
+ */
+static void
+sql_stmt_cache_delete(struct stmt_cache_entry *entry)
+{
+	if (sql_stmt_cache.last_found == entry)
+		sql_stmt_cache.last_found = NULL;
+	rlist_del(&entry->link);
+	sql_stmt_cache.mem_used -= sql_cache_entry_sizeof(entry->stmt);
+	sql_cache_entry_delete(entry);
+}
+
+static struct stmt_cache_entry *
+stmt_cache_find_entry(uint32_t stmt_id)
+{
+	if (sql_stmt_cache.last_found != NULL) {
+		const char *sql_str =
+			sql_stmt_query_str(sql_stmt_cache.last_found->stmt);
+		uint32_t last_stmt_id = sql_stmt_calculate_id(sql_str,
+							 strlen(sql_str));
+		if (last_stmt_id == stmt_id)
+			return sql_stmt_cache.last_found;
+		/* Fallthrough to slow hash search. */
+	}
+	struct mh_i32ptr_t *hash = sql_stmt_cache.hash;
+	mh_int_t stmt = mh_i32ptr_find(hash, stmt_id, NULL);
+	if (stmt == mh_end(hash))
+		return NULL;
+	struct stmt_cache_entry *entry = mh_i32ptr_node(hash, stmt)->val;
+	if (entry == NULL)
+		return NULL;
+	sql_stmt_cache.last_found = entry;
+	return entry;
+}
+
+static void
+sql_stmt_cache_gc()
+{
+	struct stmt_cache_entry *entry, *next;
+	rlist_foreach_entry_safe(entry, &sql_stmt_cache.gc_queue, link, next)
+		sql_stmt_cache_delete(entry);
+	assert(rlist_empty(&sql_stmt_cache.gc_queue));
+}
+
+/**
+ * Allocate new cache entry containing given prepared statement.
+ * Add it to the LRU cache list. Account cache size enlargement.
+ */
+static struct stmt_cache_entry *
+sql_cache_entry_new(struct sql_stmt *stmt)
+{
+	struct stmt_cache_entry *entry = malloc(sizeof(*entry));
+	if (entry == NULL) {
+		diag_set(OutOfMemory, sizeof(*entry), "malloc",
+			 "struct stmt_cache_entry");
+		return NULL;
+	}
+	entry->stmt = stmt;
+	entry->refs = 0;
+	return entry;
+}
+
+/**
+ * Return true if used memory (accounting new entry) for SQL
+ * prepared statement cache does not exceed the limit.
+ */
+static bool
+sql_cache_check_new_entry_size(size_t size)
+{
+	return (sql_stmt_cache.mem_used + size <= sql_stmt_cache.mem_quota);
+}
+
+static void
+sql_stmt_cache_entry_unref(struct stmt_cache_entry *entry)
+{
+	assert((int64_t)entry->refs - 1 >= 0);
+	if (--entry->refs == 0) {
+		/*
+		 * Remove entry from hash and add it to gc queue.
+		 * Resources are to be released in the nearest
+		 * GC cycle (see sql_stmt_cache_insert()).
+		 */
+		struct sql_stmt_cache *cache = &sql_stmt_cache;
+		const char *sql_str = sql_stmt_query_str(entry->stmt);
+		uint32_t stmt_id = sql_stmt_calculate_id(sql_str,
+							 strlen(sql_str));
+		mh_int_t i = mh_i32ptr_find(cache->hash, stmt_id, NULL);
+		assert(i != mh_end(cache->hash));
+		mh_i32ptr_del(cache->hash, i, NULL);
+		rlist_add(&sql_stmt_cache.gc_queue, &entry->link);
+		if (sql_stmt_cache.last_found == entry)
+			sql_stmt_cache.last_found = NULL;
+	}
+}
+
+void
+sql_session_stmt_hash_erase(struct mh_i32ptr_t *hash)
+{
+	if (hash == NULL)
+		return;
+	mh_int_t i;
+	struct stmt_cache_entry *entry;
+	mh_foreach(hash, i) {
+		entry = (struct stmt_cache_entry *)
+			mh_i32ptr_node(hash, i)->val;
+		sql_stmt_cache_entry_unref(entry);
+	}
+	mh_i32ptr_delete(hash);
+}
+
+int
+sql_session_stmt_hash_add_id(struct mh_i32ptr_t *hash, uint32_t stmt_id)
+{
+	struct stmt_cache_entry *entry = stmt_cache_find_entry(stmt_id);
+	const struct mh_i32ptr_node_t id_node = { stmt_id, entry };
+	struct mh_i32ptr_node_t *old_node = NULL;
+	mh_int_t i = mh_i32ptr_put(hash, &id_node, &old_node, NULL);
+	if (i == mh_end(hash)) {
+		diag_set(OutOfMemory, 0, "mh_i32ptr_put", "mh_i32ptr_node");
+		return -1;
+	}
+	assert(old_node == NULL);
+	entry->refs++;
+	return 0;
+}
+
+uint32_t
+sql_stmt_calculate_id(const char *sql_str, size_t len)
+{
+	return mh_strn_hash(sql_str, len);
+}
+
+void
+sql_stmt_unref(uint32_t stmt_id)
+{
+	struct stmt_cache_entry *entry = stmt_cache_find_entry(stmt_id);
+	assert(entry != NULL);
+	sql_stmt_cache_entry_unref(entry);
+}
+
+int
+sql_stmt_cache_update(struct sql_stmt *old_stmt, struct sql_stmt *new_stmt)
+{
+	const char *sql_str = sql_stmt_query_str(old_stmt);
+	uint32_t stmt_id = sql_stmt_calculate_id(sql_str, strlen(sql_str));
+	struct stmt_cache_entry *entry = stmt_cache_find_entry(stmt_id);
+	uint32_t ref_count = entry->refs;
+	sql_stmt_cache_delete(entry);
+	if (sql_stmt_cache_insert(new_stmt) != 0) {
+		sql_stmt_finalize(new_stmt);
+		return -1;
+	}
+	/* Restore reference counter. */
+	entry = stmt_cache_find_entry(stmt_id);
+	entry->refs = ref_count;
+	return 0;
+}
+
+int
+sql_stmt_cache_insert(struct sql_stmt *stmt)
+{
+	assert(stmt != NULL);
+	struct sql_stmt_cache *cache = &sql_stmt_cache;
+	size_t new_entry_size = sql_cache_entry_sizeof(stmt);
+
+	if (! sql_cache_check_new_entry_size(new_entry_size))
+		sql_stmt_cache_gc();
+	/*
+	 * Test memory limit again. Raise an error if it is
+	 * still overcrowded.
+	 */
+	if (! sql_cache_check_new_entry_size(new_entry_size)) {
+		diag_set(ClientError, ER_SQL_PREPARE, "Memory limit for SQL "\
+			"prepared statements has been reached. Please, deallocate "\
+			"active statements or increase SQL cache size.");
+		return -1;
+	}
+	struct mh_i32ptr_t *hash = cache->hash;
+	struct stmt_cache_entry *entry = sql_cache_entry_new(stmt);
+	if (entry == NULL)
+		return -1;
+	const char *sql_str = sql_stmt_query_str(stmt);
+	uint32_t stmt_id = sql_stmt_calculate_id(sql_str, strlen(sql_str));
+	assert(sql_stmt_cache_find(stmt_id) == NULL);
+	const struct mh_i32ptr_node_t id_node = { stmt_id, entry };
+	struct mh_i32ptr_node_t *old_node = NULL;
+	mh_int_t i = mh_i32ptr_put(hash, &id_node, &old_node, NULL);
+	if (i == mh_end(hash)) {
+		sql_cache_entry_delete(entry);
+		diag_set(OutOfMemory, 0, "mh_i32ptr_put", "mh_i32ptr_node");
+		return -1;
+	}
+	assert(old_node == NULL);
+	sql_stmt_cache.mem_used += sql_cache_entry_sizeof(stmt);
+	return 0;
+}
+
+struct sql_stmt *
+sql_stmt_cache_find(uint32_t stmt_id)
+{
+	struct stmt_cache_entry *entry = stmt_cache_find_entry(stmt_id);
+	if (entry == NULL)
+		return NULL;
+	return entry->stmt;
+}
+
+int
+sql_stmt_cache_set_size(size_t size)
+{
+	if (sql_stmt_cache.mem_used > size)
+		sql_stmt_cache_gc();
+	if (sql_stmt_cache.mem_used > size) {
+		diag_set(ClientError, ER_SQL_PREPARE, "Can't reduce memory "\
+			 "limit for SQL prepared statements: please, deallocate "\
+			 "active statements");
+		return -1;
+	}
+	sql_stmt_cache.mem_quota = size;
+	return 0;
+}
diff --git a/src/box/sql_stmt_cache.h b/src/box/sql_stmt_cache.h
new file mode 100644
index 000000000..f3935a27f
--- /dev/null
+++ b/src/box/sql_stmt_cache.h
@@ -0,0 +1,145 @@
+#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 <COPYRIGHT HOLDER> ``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
+ * <COPYRIGHT HOLDER> 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 <stdint.h>
+#include <stdio.h>
+
+#include "small/rlist.h"
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+struct sql_stmt;
+struct mh_i64ptr_t;
+
+struct stmt_cache_entry {
+	/** Prepared statement itself. */
+	struct sql_stmt *stmt;
+	/**
+	 * Link to the next entry. All statements are to be
+	 * evicted on the next gc cycle.
+	 */
+	struct rlist link;
+	/**
+	 * Reference counter. If it is == 0, entry gets
+	 * into GC queue.
+	 */
+	uint32_t refs;
+};
+
+/**
+ * Global prepared statements holder.
+ */
+struct sql_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 id -> struct stmt_cahce_entry hash.*/
+	struct mh_i32ptr_t *hash;
+	/**
+	 * After deallocation statements are not deleted, but
+	 * moved to this list. GC process is triggered only
+	 * when memory limit has reached. It allows to reduce
+	 * workload on session's disconnect.
+	 */
+	struct rlist gc_queue;
+	/**
+	 * Last result of sql_stmt_cache_find() invocation.
+	 * Since during processing prepared statement it
+	 * may require to find the same statement several
+	 * times.
+	 */
+	struct stmt_cache_entry *last_found;
+};
+
+/**
+ * Initialize global cache for prepared statements. Called once
+ * during database setup (in sql_init()).
+ */
+void
+sql_stmt_cache_init();
+
+/**
+ * Erase session local hash: unref statements belong to this
+ * session and deallocate hash itself.
+ * @hash is assumed to be member of struct session @sql_stmts.
+ */
+void
+sql_session_stmt_hash_erase(struct mh_i32ptr_t *hash);
+
+/**
+ * Add entry corresponding to prepared statement with given ID
+ * to session-local hash and increase its ref counter.
+ * @hash is assumed to be member of struct session @sql_stmts.
+ */
+int
+sql_session_stmt_hash_add_id(struct mh_i32ptr_t *hash, uint32_t stmt_id);
+
+/**
+ * Prepared statement ID is supposed to be hash value
+ * of the original SQL query string.
+ */
+uint32_t
+sql_stmt_calculate_id(const char *sql_str, size_t len);
+
+/** Unref prepared statement entry in global holder. */
+void
+sql_stmt_unref(uint32_t stmt_id);
+
+int
+sql_stmt_cache_update(struct sql_stmt *old_stmt, struct sql_stmt *new_stmt);
+
+/**
+ * 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_stmt_cache_insert(struct sql_stmt *stmt);
+
+/** Find entry by SQL string. In case of search fails it returns NULL. */
+struct sql_stmt *
+sql_stmt_cache_find(uint32_t stmt_id);
+
+
+/** Set prepared cache size limit. */
+int
+sql_stmt_cache_set_size(size_t size);
+
+#if defined(__cplusplus)
+} /* extern "C" { */
+#endif
+
+#endif
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..331f5e986 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 = box.cfg.sql_cache_size}
+ | ---
+ | ...
 
 --------------------------------------------------------------------------------
 -- Test of default cfg options
diff --git a/test/box/cfg.test.lua b/test/box/cfg.test.lua
index 56ccb6767..e6a90d770 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 = box.cfg.sql_cache_size}
 
 --------------------------------------------------------------------------------
 -- Test of default cfg options
diff --git a/test/box/misc.result b/test/box/misc.result
index d2a20307a..7e5d28b70 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -554,6 +554,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



More information about the Tarantool-patches mailing list