[Tarantool-patches] [PATCH V3 1/7] error: add a Lua traceback to error

Leonid Vasiliev lvasiliev at tarantool.org
Wed Apr 15 12:31:56 MSK 2020


Lua backtrace can be enabled for all errors using a server-wide
option, or on a per-error basis using box.error() and
box.error.new() arguments.

Co-authored-by: Vladislav Shpilevoy<v.shpilevoy at tarantool.org>

Needed for #4398

@TarantoolBot document
Title: Lua error.traceback

Lua error objects now feature 'traceback' optional attribute.
It contains Lua traceback. C part is not present here.

Traceback collection is a relatively expensive operation, so it
is disabled by default. In case you need to enable it, there are
2 options:

* Global option `traceback_enable` for `box.error.cfg` call:
```
    box.error.cfg({traceback_enable = true})
```

* Per object option, in case you want traceback only for certain
cases:
```
    box.error.new({
        code = 1000,
        reason = 'Reason',
        traceback = true/false
    })
```
---
 src/box/lua/error.cc      | 33 +++++++++++++++++++++++++-
 src/lib/core/diag.c       | 16 +++++++++++++
 src/lib/core/diag.h       | 28 ++++++++++++++++++++++
 src/lib/core/exception.cc |  1 +
 src/lua/error.c           |  9 ++++++++
 src/lua/error.lua         |  7 ++++++
 test/box/error.result     | 59 +++++++++++++++++++++++++++++++++++++++++++++++
 test/box/error.test.lua   | 33 ++++++++++++++++++++++++++
 8 files changed, 185 insertions(+), 1 deletion(-)

diff --git a/src/box/lua/error.cc b/src/box/lua/error.cc
index b2625bf..a2facf0 100644
--- a/src/box/lua/error.cc
+++ b/src/box/lua/error.cc
@@ -54,6 +54,8 @@ luaT_error_create(lua_State *L, int top_base)
 {
 	uint32_t code = 0;
 	const char *reason = NULL;
+	bool is_traceback_enabled = false;
+	bool is_traceback_specified = false;
 	const char *file = "";
 	unsigned line = 0;
 	lua_Debug info;
@@ -87,6 +89,12 @@ luaT_error_create(lua_State *L, int top_base)
 		if (reason == NULL)
 			reason = "";
 		lua_pop(L, 1);
+		lua_getfield(L, top_base, "traceback");
+		if (lua_isboolean(L, -1)) {
+			is_traceback_enabled = lua_toboolean(L, -1);
+			is_traceback_specified = true;
+		}
+		lua_pop(L, 1);
 	} else {
 		return NULL;
 	}
@@ -102,7 +110,14 @@ raise:
 		}
 		line = info.currentline;
 	}
-	return box_error_new(file, line, code, "%s", reason);
+
+	struct error *err = box_error_new(file, line, code, "%s", reason);
+	/*
+	 * Explicit traceback option overrides the global setting.
+	 */
+	if (is_traceback_specified)
+		err->is_traceback_enabled = is_traceback_enabled;
+	return err;
 }
 
 static int
@@ -180,6 +195,18 @@ luaT_error_set(struct lua_State *L)
 }
 
 static int
+luaT_error_cfg(struct lua_State *L)
+{
+	if (lua_gettop(L) < 1 || !lua_istable(L, 1))
+		return luaL_error(L, "Usage: box.error.cfg({}})");
+
+	lua_getfield(L, 1, "traceback_enable");
+	if (lua_isboolean(L, -1))
+		error_is_traceback_enabled = lua_toboolean(L, -1);
+	return 0;
+}
+
+static int
 lbox_errinj_set(struct lua_State *L)
 {
 	char *name = (char*)luaL_checkstring(L, 1);
@@ -297,6 +324,10 @@ box_lua_error_init(struct lua_State *L) {
 			lua_pushcfunction(L, luaT_error_set);
 			lua_setfield(L, -2, "set");
 		}
+		{
+			lua_pushcfunction(L, luaT_error_cfg);
+			lua_setfield(L, -2, "cfg");
+		}
 		lua_setfield(L, -2, "__index");
 	}
 	lua_setmetatable(L, -2);
diff --git a/src/lib/core/diag.c b/src/lib/core/diag.c
index e143db1..833454b 100644
--- a/src/lib/core/diag.c
+++ b/src/lib/core/diag.c
@@ -31,6 +31,8 @@
 #include "diag.h"
 #include "fiber.h"
 
+bool error_is_traceback_enabled = false;
+
 int
 error_set_prev(struct error *e, struct error *prev)
 {
@@ -97,6 +99,8 @@ error_create(struct error *e,
 	e->errmsg[0] = '\0';
 	e->cause = NULL;
 	e->effect = NULL;
+	e->traceback = NULL;
+	e->is_traceback_enabled = error_is_traceback_enabled;
 }
 
 struct diag *
@@ -120,3 +124,15 @@ error_vformat_msg(struct error *e, const char *format, va_list ap)
 	vsnprintf(e->errmsg, sizeof(e->errmsg), format, ap);
 }
 
+void
+error_set_traceback(struct error *e, const char *traceback)
+{
+	assert(e->traceback == NULL);
+	e->traceback = strdup(traceback);
+	/*
+	 * Don't try to set it again. Traceback can be NULL in case of OOM, so
+	 * it is not a reliable source of information whether need to collect a
+	 * traceback.
+	 */
+	e->is_traceback_enabled = false;
+}
diff --git a/src/lib/core/diag.h b/src/lib/core/diag.h
index 7a5454d..e918d30 100644
--- a/src/lib/core/diag.h
+++ b/src/lib/core/diag.h
@@ -42,6 +42,13 @@
 extern "C" {
 #endif /* defined(__cplusplus) */
 
+/**
+ * Flag to turn on/off traceback automatic collection. Currently
+ * it works only for Lua. Stack is collected at the moment of
+ * error object push onto the stack.
+ */
+extern bool error_is_traceback_enabled;
+
 enum {
 	DIAG_ERRMSG_MAX = 512,
 	DIAG_FILENAME_MAX = 256
@@ -110,6 +117,19 @@ struct error {
 	 */
 	struct error *cause;
 	struct error *effect;
+	/**
+	 * Optional traceback. At the moment it can contain only
+	 * Lua traceback, and only when it is requested
+	 * explicitly.
+	 */
+	char *traceback;
+	/**
+	 * Flag whether a traceback should be collected when the
+	 * error object is exposed to a user next time. When the
+	 * tracing is disabled, or it is enabled but already
+	 * collected for this error object, it becomes false.
+	 */
+	bool is_traceback_enabled;
 };
 
 static inline void
@@ -172,6 +192,14 @@ error_unlink_effect(struct error *e)
 int
 error_set_prev(struct error *e, struct error *prev);
 
+/**
+ * Set traceback of @a e error object. It can be done only once.
+ * In case of OOM the traceback is kept NULL, and can't be
+ * collected again.
+ */
+void
+error_set_traceback(struct error *e, const char *traceback);
+
 NORETURN static inline void
 error_raise(struct error *e)
 {
diff --git a/src/lib/core/exception.cc b/src/lib/core/exception.cc
index 180cb0e..1717b76 100644
--- a/src/lib/core/exception.cc
+++ b/src/lib/core/exception.cc
@@ -42,6 +42,7 @@ extern "C" {
 static void
 exception_destroy(struct error *e)
 {
+	free(e->traceback);
 	delete (Exception *) e;
 }
 
diff --git a/src/lua/error.c b/src/lua/error.c
index 18a990a..a21548c 100644
--- a/src/lua/error.c
+++ b/src/lua/error.c
@@ -85,6 +85,15 @@ luaT_pusherror(struct lua_State *L, struct error *e)
 	 * then set the finalizer.
 	 */
 	error_ref(e);
+
+	if (e->traceback == NULL && e->is_traceback_enabled) {
+		int top = lua_gettop(L);
+		luaL_traceback(L, L, NULL, 0);
+		if (lua_isstring(L, -1))
+			error_set_traceback(e, lua_tostring(L, -1));
+		lua_settop(L, top);
+	}
+
 	assert(CTID_CONST_STRUCT_ERROR_REF != 0);
 	struct error **ptr = (struct error **)
 		luaL_pushcdata(L, CTID_CONST_STRUCT_ERROR_REF);
diff --git a/src/lua/error.lua b/src/lua/error.lua
index bdc9c71..93fd1b9 100644
--- a/src/lua/error.lua
+++ b/src/lua/error.lua
@@ -26,6 +26,8 @@ struct error {
     char _errmsg[DIAG_ERRMSG_MAX];
     struct error *_cause;
     struct error *_effect;
+    char *err_traceback;
+    bool is_traceback_enabled;
 };
 
 char *
@@ -92,6 +94,10 @@ local function error_trace(err)
     }
 end
 
+local function error_traceback(err)
+    return err.err_traceback ~= ffi.nullptr and ffi.string(err.err_traceback) or nil
+end
+
 local function error_errno(err)
     local e = err._saved_errno
     if e == 0 then
@@ -131,6 +137,7 @@ local error_fields = {
     ["trace"]       = error_trace;
     ["errno"]       = error_errno;
     ["prev"]        = error_prev;
+    ["traceback"]   = error_traceback;
 }
 
 local function error_unpack(err)
diff --git a/test/box/error.result b/test/box/error.result
index df6d0eb..2502d88 100644
--- a/test/box/error.result
+++ b/test/box/error.result
@@ -831,3 +831,62 @@ assert(box.error.last() == e1)
  | ---
  | - true
  | ...
+
+--
+-- gh-4398: Lua traceback for errors.
+--
+function t1(traceback, throw)                                                   \
+    local opts = {code = 0, reason = 'Reason', traceback = traceback}           \
+    if throw then                                                               \
+        box.error(opts)                                                         \
+    else                                                                        \
+        return box.error.new(opts)                                              \
+    end                                                                         \
+end
+ | ---
+ | ...
+function t2(...) return t1(...), nil end
+ | ---
+ | ...
+function t3(...) return t2(...), nil end
+ | ---
+ | ...
+
+function check_trace(trace)                                                     \
+    local t1loc = trace:find('t1')                                              \
+    local t2loc = trace:find('t2')                                              \
+    local t3loc = trace:find('t3')                                              \
+    return t1loc < t2loc and t2loc < t3loc or {t1loc, t2loc, t3loc, trace}      \
+end
+ | ---
+ | ...
+
+check_trace(t3(true, false):unpack().traceback)
+ | ---
+ | - true
+ | ...
+
+box.error.cfg{traceback_enable = true}
+ | ---
+ | ...
+-- Explicit 'traceback = false' overrides the global setting.
+t3(false, false):unpack().traceback
+ | ---
+ | - null
+ | ...
+-- When explicit option is not specified, global setting works.
+check_trace(t3(nil, false):unpack().traceback)
+ | ---
+ | - true
+ | ...
+
+box.error.cfg{traceback_enable = false}
+ | ---
+ | ...
+_, e = pcall(t3, true, true)
+ | ---
+ | ...
+check_trace(e:unpack().traceback)
+ | ---
+ | - true
+ | ...
diff --git a/test/box/error.test.lua b/test/box/error.test.lua
index 41baed5..6f12716 100644
--- a/test/box/error.test.lua
+++ b/test/box/error.test.lua
@@ -229,3 +229,36 @@ box.error({code = 111, reason = "err"})
 box.error.last()
 box.error(e1)
 assert(box.error.last() == e1)
+
+--
+-- gh-4398: Lua traceback for errors.
+--
+function t1(traceback, throw)                                                   \
+    local opts = {code = 0, reason = 'Reason', traceback = traceback}           \
+    if throw then                                                               \
+        box.error(opts)                                                         \
+    else                                                                        \
+        return box.error.new(opts)                                              \
+    end                                                                         \
+end
+function t2(...) return t1(...), nil end
+function t3(...) return t2(...), nil end
+
+function check_trace(trace)                                                     \
+    local t1loc = trace:find('t1')                                              \
+    local t2loc = trace:find('t2')                                              \
+    local t3loc = trace:find('t3')                                              \
+    return t1loc < t2loc and t2loc < t3loc or {t1loc, t2loc, t3loc, trace}      \
+end
+
+check_trace(t3(true, false):unpack().traceback)
+
+box.error.cfg{traceback_enable = true}
+-- Explicit 'traceback = false' overrides the global setting.
+t3(false, false):unpack().traceback
+-- When explicit option is not specified, global setting works.
+check_trace(t3(nil, false):unpack().traceback)
+
+box.error.cfg{traceback_enable = false}
+_, e = pcall(t3, true, true)
+check_trace(e:unpack().traceback)
-- 
2.7.4



More information about the Tarantool-patches mailing list