[asterisk-commits] dlee: trunk r396119 - in /trunk: include/asterisk/ main/ res/ res/ari/ tests/

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Fri Aug 2 09:27:37 CDT 2013


Author: dlee
Date: Fri Aug  2 09:27:35 2013
New Revision: 396119

URL: http://svnview.digium.com/svn/asterisk?view=rev&rev=396119
Log:
Address JSON thread safety issues.

In tracking down some unit tests failures, I ended up reading the fine
print[1] regarding Jansson's thread safety.

In short:
 1. Ref-counting is non-atomic.
 2. json_dumps() and friends are not thread safe.

This patch adds locking where necessary to our ast_json_* wrapper API,
with documentation in json.h describing the thread safety limitations of
the API.

 [1]: http://www.digip.org/jansson/doc/2.4/portability.html#thread-safety

Modified:
    trunk/include/asterisk/json.h
    trunk/main/cel.c
    trunk/main/json.c
    trunk/res/ari/ari_websockets.c
    trunk/res/res_sorcery_astdb.c
    trunk/tests/test_json.c

Modified: trunk/include/asterisk/json.h
URL: http://svnview.digium.com/svn/asterisk/trunk/include/asterisk/json.h?view=diff&rev=396119&r1=396118&r2=396119
==============================================================================
--- trunk/include/asterisk/json.h (original)
+++ trunk/include/asterisk/json.h Fri Aug  2 09:27:35 2013
@@ -42,6 +42,37 @@
  * In the cases where you have a need to introduce intermediate objects, just
  * wrap them with json_ref() when passing them to other \c ast_json_*()
  * functions.
+ *
+ * \par Thread Safety
+ *
+ * Jansson (as of 2.4) provides fairly weak thread safety guarantees. The
+ * Asterisk wrapper improves upon that slightly. The remaining refcounting
+ * problems are issues when slicing/sharing/mixing instances between JSON
+ * objects and arrays, which we avoid.
+ *
+ * The \c ast_json_dump_* functions are thread safe for multiple concurrent
+ * dumps of the same object, so long as the concurrent dumps start from the same
+ * \c root object. But if an object is shared by other JSON objects/arrays, then
+ * concurrent dumps of the outer objects/arrays are not thread safe. This can be
+ * avoided by using ast_json_deep_copy() when sharing JSON instances between
+ * objects.
+ *
+ * The ast_json_ref() and ast_json_unref() functions are thread safe. Since the
+ * Asterisk wrapper exclusively uses the reference stealing API, Jansson won't
+ * be performing many refcount modifications behind our backs. There are a few
+ * exceptions.
+ *
+ * The first is the transitive json_decref() that occurs when \ref
+ * AST_JSON_OBJECT and \ref AST_JSON_ARRAY instances are deleted. This can be
+ * avoided by using ast_json_deep_copy() when sharing JSON instances between
+ * objects.
+ *
+ * The second is when using the reference borrowing specifier in
+ * ast_json_pack() (capital \c O). This can be avoided by using the reference
+ * stealing specifier (lowercase \c o) and wrapping the JSON object parameter
+ * with ast_json_ref() for an explicit ref-bump.
+ *
+ * \par Example code
  *
  * \code
  *	// Example of how to use the Asterisk JSON API
@@ -105,6 +136,20 @@
  * ast_free().
  */
 void ast_json_reset_alloc_funcs(void);
+
+/*!
+ * \brief Asterisk's custom JSON allocator. Exposed for use by unit tests.
+ * \since 12.0.0
+ * \internal
+ */
+void *ast_json_malloc(size_t size);
+
+/*!
+ * \brief Asterisk's custom JSON allocator. Exposed for use by unit tests.
+ * \since 12.0.0
+ * \internal
+ */
+void ast_json_free(void *p);
 
 /*!
  * \struct ast_json
@@ -683,13 +728,23 @@
 	AST_JSON_PRETTY,
 };
 
+/*!
+ * \brief Encode a JSON value to a compact string.
+ * \since 12.0.0
+ *
+ * Returned string must be freed by calling ast_json_free().
+ *
+ * \param root JSON value.
+ * \return String encoding of \a root.
+ * \return \c NULL on error.
+ */
 #define ast_json_dump_string(root) ast_json_dump_string_format(root, AST_JSON_COMPACT)
 
 /*!
  * \brief Encode a JSON value to a string.
  * \since 12.0.0
  *
- * Returned string must be freed by calling ast_free().
+ * Returned string must be freed by calling ast_json_free().
  *
  * \param root JSON value.
  * \param format encoding format type.

Modified: trunk/main/cel.c
URL: http://svnview.digium.com/svn/asterisk/trunk/main/cel.c?view=diff&rev=396119&r1=396118&r2=396119
==============================================================================
--- trunk/main/cel.c (original)
+++ trunk/main/cel.c Fri Aug  2 09:27:35 2013
@@ -638,7 +638,7 @@
 		struct ast_json *extra, const char *peer_name)
 {
 	struct timeval eventtime = ast_tvnow();
-	RAII_VAR(char *, extra_txt, NULL, ast_free);
+	RAII_VAR(char *, extra_txt, NULL, ast_json_free);
 	if (extra) {
 		extra_txt = ast_json_dump_string(extra);
 	}

Modified: trunk/main/json.c
URL: http://svnview.digium.com/svn/asterisk/trunk/main/json.c?view=diff&rev=396119&r1=396118&r2=396119
==============================================================================
--- trunk/main/json.c (original)
+++ trunk/main/json.c Fri Aug  2 09:27:35 2013
@@ -20,8 +20,8 @@
  *
  * \brief JSON abstraction layer.
  *
- * This is a very thin wrapper around the Jansson API. For more details on it, see its
- * docs at http://www.digip.org/jansson/doc/2.4/apiref.html.
+ * This is a very thin wrapper around the Jansson API. For more details on it,
+ * see its docs at http://www.digip.org/jansson/doc/2.4/apiref.html.
  *
  * \author David M. Lee, II <dlee at digium.com>
  */
@@ -46,20 +46,148 @@
 #include <jansson.h>
 #include <time.h>
 
+/*! \brief Magic number, for safety checks. */
+#define JSON_MAGIC 0x1541992
+
+/*! \brief Internal structure for allocated memory blocks */
+struct json_mem {
+	/*! Magic number, for safety checks */
+	uint32_t magic;
+	/*! Mutext for locking this memory block */
+	ast_mutex_t mutex;
+	/*! Linked list pointer for the free list */
+	AST_LIST_ENTRY(json_mem) list;
+	/*! Data section of the allocation; void pointer for proper alignment */
+	void *data[];
+};
+
+/*! \brief Free a \ref json_mem block. */
+static void json_mem_free(struct json_mem *mem)
+{
+	mem->magic = 0;
+	ast_mutex_destroy(&mem->mutex);
+	ast_free(mem);
+}
+
 /*!
- * \brief Function wrapper around ast_malloc macro.
- */
-static void *json_malloc(size_t size)
-{
-	return ast_malloc(size);
+ * \brief Get the \ref json_mem block for a pointer allocated via
+ * ast_json_malloc().
+ *
+ * This function properly handles Jansson singletons (null, true, false), and
+ * \c NULL.
+ *
+ * \param p Pointer, usually to a \c json_t or \ref ast_json.
+ * \return \ref json_mem object with extra allocation info.
+ */
+static inline struct json_mem *to_json_mem(void *p)
+{
+	struct json_mem *mem;
+	/* Avoid ref'ing the singleton values */
+	if (p == NULL || p == json_null() || p == json_true() ||
+		p == json_false()) {
+		return NULL;
+	}
+	mem = (struct json_mem *)((char *) (p) - sizeof(*mem));
+	ast_assert(mem->magic == JSON_MAGIC);
+	return mem;
 }
 
 /*!
- * \brief Function wrapper around ast_free macro.
- */
-static void json_free(void *p)
-{
-	ast_free(p);
+ * \brief Lock an \ref ast_json instance.
+ *
+ * If \a json is an immutable singleton (null, true, false), this function
+ * safely ignores it and returns \c NULL. Otherwise, \a json must have been
+ * allocates using ast_json_malloc().
+ *
+ * \param json JSON instance to lock.
+ * \return \ref Corresponding \ref json_mem block.
+ * \return \c NULL if \a json was not allocated.
+ */
+static struct json_mem *json_mem_lock(struct ast_json *json)
+{
+	struct json_mem *mem = to_json_mem(json);
+	if (!mem) {
+		return NULL;
+	}
+	ast_mutex_lock(&mem->mutex);
+	return mem;
+}
+
+/*!
+ * \brief Unlock a \ref json_mem instance.
+ *
+ * \param mem \ref json_mem, usually returned from json_mem_lock().
+ */
+static void json_mem_unlock(struct json_mem *mem)
+{
+	if (!mem) {
+		return;
+	}
+	ast_mutex_unlock(&mem->mutex);
+}
+
+/*!
+ * \brief Scoped lock for a \ref ast_json instance.
+ *
+ * \param json JSON instance to lock.
+ */
+#define SCOPED_JSON_LOCK(json)				\
+	RAII_VAR(struct json_mem *, __mem_ ## __LINE__, \
+		json_mem_lock(json), json_mem_unlock)
+
+void *ast_json_malloc(size_t size)
+{
+	struct json_mem *mem = ast_malloc(size + sizeof(*mem));
+	if (!mem) {
+		return NULL;
+	}
+	mem->magic = JSON_MAGIC;
+	ast_mutex_init(&mem->mutex);
+	return mem->data;
+}
+
+AST_THREADSTORAGE(json_free_list_ts);
+
+/*!
+ * \brief Struct for a linked list of \ref json_mem.
+ */
+AST_LIST_HEAD_NOLOCK(json_mem_list, json_mem);
+
+/*!
+ * \brief Thread local list of \ref json_mem blocks to free at the end of an
+ * unref.
+ */
+static struct json_mem_list *json_free_list(void)
+{
+	return ast_threadstorage_get(&json_free_list_ts,
+		sizeof(struct json_mem_list));
+}
+
+void ast_json_free(void *p)
+{
+	struct json_mem *mem;
+	struct json_mem_list *free_list;
+	mem = to_json_mem(p);
+
+	if (!mem) {
+		return;
+	}
+
+	/* Since the unref is holding a lock in mem, we can't free it
+	 * immediately. Store it off on a thread local list to be freed by
+	 * ast_json_unref().
+	 */
+	free_list = json_free_list();
+	if (!free_list) {
+		ast_log(LOG_ERROR, "Error allocating free list\n");
+		ast_assert(0);
+		/* It's not ideal to free the memory immediately, but that's the
+		 * best we can do if the threadlocal allocation fails */
+		json_mem_free(mem);
+		return;
+	}
+
+	AST_LIST_INSERT_HEAD(free_list, mem, list);
 }
 
 void ast_json_set_alloc_funcs(void *(*malloc_fn)(size_t), void (*free_fn)(void*))
@@ -69,21 +197,41 @@
 
 void ast_json_reset_alloc_funcs(void)
 {
-	json_set_alloc_funcs(json_malloc, json_free);
+	json_set_alloc_funcs(ast_json_malloc, ast_json_free);
 }
 
 struct ast_json *ast_json_ref(struct ast_json *json)
 {
+	/* Jansson refcounting is non-atomic; lock it. */
+	SCOPED_JSON_LOCK(json);
 	json_incref((json_t *)json);
 	return json;
 }
 
 void ast_json_unref(struct ast_json *json)
 {
-	if (!json) {
+	struct json_mem_list *free_list;
+	struct json_mem *mem;
+
+	/* Jansson refcounting is non-atomic; lock it. */
+	{
+		SCOPED_JSON_LOCK(json);
+		if (!json) {
+			return;
+		}
+		json_decref((json_t *)json);
+	}
+
+	/* Now free any objects that were ast_json_free()'s while the lock was
+	 * held */
+	free_list = json_free_list();
+	if (!free_list) {
 		return;
 	}
-	json_decref((json_t *)json);
+
+	while ((mem = AST_LIST_REMOVE_HEAD(free_list, list))) {
+		json_mem_free(mem);
+	}
 }
 
 enum ast_json_type ast_json_typeof(const struct ast_json *json)
@@ -383,6 +531,10 @@
 
 char *ast_json_dump_string_format(struct ast_json *root, enum ast_json_encoding_format format)
 {
+	/* Jansson's json_dump*, even though it's a read operation, isn't
+	 * thread safe for concurrent reads. Locking is necessary.
+	 * See http://www.digip.org/jansson/doc/2.4/portability.html#thread-safety. */
+	SCOPED_JSON_LOCK(root);
 	return json_dumps((json_t *)root, dump_flags(format));
 }
 
@@ -419,12 +571,20 @@
 
 int ast_json_dump_str_format(struct ast_json *root, struct ast_str **dst, enum ast_json_encoding_format format)
 {
+	/* Jansson's json_dump*, even though it's a read operation, isn't
+	 * thread safe for concurrent reads. Locking is necessary.
+	 * See http://www.digip.org/jansson/doc/2.4/portability.html#thread-safety. */
+	SCOPED_JSON_LOCK(root);
 	return json_dump_callback((json_t *)root, write_to_ast_str, dst, dump_flags(format));
 }
 
 
 int ast_json_dump_file_format(struct ast_json *root, FILE *output, enum ast_json_encoding_format format)
 {
+	/* Jansson's json_dump*, even though it's a read operation, isn't
+	 * thread safe for concurrent reads. Locking is necessary.
+	 * See http://www.digip.org/jansson/doc/2.4/portability.html#thread-safety. */
+	SCOPED_JSON_LOCK(root);
 	if (!root || !output) {
 		return -1;
 	}
@@ -432,6 +592,10 @@
 }
 int ast_json_dump_new_file_format(struct ast_json *root, const char *path, enum ast_json_encoding_format format)
 {
+	/* Jansson's json_dump*, even though it's a read operation, isn't
+	 * thread safe for concurrent reads. Locking is necessary.
+	 * See http://www.digip.org/jansson/doc/2.4/portability.html#thread-safety. */
+	SCOPED_JSON_LOCK(root);
 	if (!root || !path) {
 		return -1;
 	}

Modified: trunk/res/ari/ari_websockets.c
URL: http://svnview.digium.com/svn/asterisk/trunk/res/ari/ari_websockets.c?view=diff&rev=396119&r1=396118&r2=396119
==============================================================================
--- trunk/res/ari/ari_websockets.c (original)
+++ trunk/res/ari/ari_websockets.c Fri Aug  2 09:27:35 2013
@@ -142,7 +142,7 @@
 int ast_ari_websocket_session_write(struct ast_ari_websocket_session *session,
 	struct ast_json *message)
 {
-	RAII_VAR(char *, str, NULL, ast_free);
+	RAII_VAR(char *, str, NULL, ast_json_free);
 
 #ifdef AST_DEVMODE
 	if (!session->validator(message)) {

Modified: trunk/res/res_sorcery_astdb.c
URL: http://svnview.digium.com/svn/asterisk/trunk/res/res_sorcery_astdb.c?view=diff&rev=396119&r1=396118&r2=396119
==============================================================================
--- trunk/res/res_sorcery_astdb.c (original)
+++ trunk/res/res_sorcery_astdb.c Fri Aug  2 09:27:35 2013
@@ -126,7 +126,7 @@
 static int sorcery_astdb_create(const struct ast_sorcery *sorcery, void *data, void *object)
 {
 	RAII_VAR(struct ast_json *, objset, ast_sorcery_objectset_json_create(sorcery, object), ast_json_unref);
-	RAII_VAR(char *, value, NULL, ast_free_ptr);
+	RAII_VAR(char *, value, NULL, ast_json_free);
 	const char *prefix = data;
 	char family[strlen(prefix) + strlen(ast_sorcery_object_get_type(object)) + 2];
 

Modified: trunk/tests/test_json.c
URL: http://svnview.digium.com/svn/asterisk/trunk/tests/test_json.c?view=diff&rev=396119&r1=396118&r2=396119
==============================================================================
--- trunk/tests/test_json.c (original)
+++ trunk/tests/test_json.c Fri Aug  2 09:27:35 2013
@@ -55,7 +55,7 @@
  */
 static void *json_debug_malloc(size_t size)
 {
-	void *p = ast_malloc(size);
+	void *p = ast_json_malloc(size);
 	if (p) {
 		++alloc_count;
 	}
@@ -67,7 +67,7 @@
 	if (p) {
 		--alloc_count;
 	}
-	ast_free(p);
+	ast_json_free(p);
 }
 
 static int json_test_init(struct ast_test_info *info, struct ast_test *test)




More information about the asterisk-commits mailing list