git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 1/5] promise: add promise pattern to track success/error from operations

From
PGPhilip Peterson via GitGitGadget <gitgitgadget@gmail.com>
Date
Feb 18, 2024, 07:33 UTC
Message-ID
<be270db2ff5c63612356c4ef2fafdbe1724b5b71.1708241613.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.1666.git.git.1708241612.gitgitgadget@gmail.com>
From: Philip Peterson <philip.c.peterson@gmail.com>

Introduce a promise paradigm. A promise starts off in the pending state, and represents an asynchronous (or synchronous) action that will eventually end in either a successful result or a failure result. If a failure result, an error message may be provided.

This allows us to represent tasks which may fail, while deferring any control flow actions or error printing that may occur in relation to said task.

Signed-off-by: Philip Peterson <philip.c.peterson@gmail.com>
---
 Makefile  |  1 +
 promise.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 promise.h | 71 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 161 insertions(+)
 create mode 100644 promise.c
 create mode 100644 promise.h
diff --git a/Makefile b/Makefile
index 78e874099d9..4851eb2d822 100644
--- a/Makefile
+++ b/Makefile
@@ -1109,6 +1109,7 @@ LIB_OBJS += preload-index.o
 LIB_OBJS += pretty.o
 LIB_OBJS += prio-queue.o
 LIB_OBJS += progress.o
+LIB_OBJS += promise.o
 LIB_OBJS += promisor-remote.o
 LIB_OBJS += prompt.o
 LIB_OBJS += protocol.o
diff --git a/promise.c b/promise.c
new file mode 100644
index 00000000000..58ed8b67880
--- /dev/null
+++ b/promise.c
@@ -0,0 +1,89 @@
+/*
+ * Generic implementation of callbacks with await checking.
+ */
+#include "promise.h"
+
+void promise_assert_finished(struct promise_t *p) {
+	if (p->state == PROMISE_UNRESOLVED) {
+		BUG("expected promise to have been resolved/rejected");
+	}
+}
+
+void promise_assert_failure(struct promise_t *p) {
+	if (p->state != PROMISE_FAILURE) {
+		BUG("expected promise to have been rejected");
+	}
+}
+
+void promise_resolve(struct promise_t *p, int status) {
+	if (p->state != PROMISE_UNRESOLVED) {
+		BUG("promise was already resolved/rejected");
+		return;
+	}
+	p->result.success_result = status;
+	p->state = PROMISE_SUCCESS;
+}
+
+void promise_reject(struct promise_t *p, int status, const char* fmt, ...) {
+	va_list args;
+	if (p->state != PROMISE_UNRESOLVED) {
+		BUG("promise was already resolved/rejected");
+		return;
+	}
+	p->result.failure_result.status = status;
+
+	strbuf_init(&p->result.failure_result.message, 0);
+
+	va_start(args, fmt);
+	strbuf_vaddf(&p->result.failure_result.message, fmt, args);
+	va_end(args);
+
+	p->state = PROMISE_FAILURE;
+}
+
+struct promise_t *promise_init(void) {
+	// Promises are allocated on the heap, because they represent potentially long-running tasks,
+	// and a stack-allocated value might not live long enough.
+	struct promise_t *new_promise = xmalloc(sizeof(struct promise_t));
+	struct failure_result_t failure_result;
+
+	new_promise->state = PROMISE_UNRESOLVED;
+	failure_result.status = 0;
+	new_promise->result.failure_result = failure_result;
+
+	return new_promise;
+}
+
+/**
+ * Outputs an error message and size from a failed promise. The error message must be
+ * free()'ed by the caller. Calling this function is not allowed if the promise is not
+ * failed.
+ *
+ * Argument `size` may be omitted by passing in NULL.
+ *
+ * Note that although *error_message is null-terminated, its size may be larger
+ * than the terminated string, and its actual size is indicated by *size.
+ */
+void promise_copy_error(struct promise_t *p, char **error_message, size_t *size) {
+	size_t local_size;
+	promise_assert_failure(p);
+
+	*error_message = strbuf_detach(&p->result.failure_result.message, &local_size);
+	if (size) {
+		*size = local_size;
+	}
+
+	// We are only doing a copy, not a consume, so we need to put the error message back
+	// the way we found it.
+	strbuf_add(&p->result.failure_result.message, *error_message, strlen(*error_message));
+}
+
+/**
+ * Fully deallocates the promise as well as the error message, if any.
+ */
+void promise_release(struct promise_t *p) {
+	if (p->state == PROMISE_FAILURE) {
+		strbuf_release(&p->result.failure_result.message);
+	}
+	free(p);
+}
diff --git a/promise.h b/promise.h
new file mode 100644
index 00000000000..c5500eba986
--- /dev/null
+++ b/promise.h
@@ -0,0 +1,71 @@
+#ifndef PROMISE_H
+#define PROMISE_H
+
+#include "git-compat-util.h"
+#include "strbuf.h"
+
+enum promise_state {
+	PROMISE_UNRESOLVED = 0,
+	PROMISE_SUCCESS = 1,
+	PROMISE_FAILURE = 2,
+};
+
+typedef int success_result_t;
+
+struct failure_result_t {
+	int status;
+	struct strbuf message;
+};
+
+struct promise_t {
+	enum promise_state state;
+	union {
+		success_result_t success_result;
+		struct failure_result_t failure_result;
+	} result;
+};
+
+// Function to assert that a promise has been resolved
+void promise_assert_finished(struct promise_t *p);
+
+// Function to assert that a promise has been rejected
+void promise_assert_failure(struct promise_t *p);
+
+// Function to resolve a promise with a success result
+void promise_resolve(struct promise_t *p, int status);
+
+// Function to reject a promise with a failure result and an optional formatted error message
+void promise_reject(struct promise_t *p, int status, const char* fmt, ...);
+
+// Function to create a new promise
+struct promise_t *promise_init(void);
+
+// Copies the error out of a failed promise
+void promise_copy_error(struct promise_t *promise, char **error_message, size_t *size);
+
+// Fully deallocates the promise
+void promise_release(struct promise_t *promise);
+
+#define PROMISE_SUCCEED(p, errcode) do { \
+	promise_resolve(p, errcode); \
+	return; \
+} while (0)
+
+#define PROMISE_THROW(p, errcode, ...) do { \
+	promise_reject(p, errcode, __VA_ARGS__); \
+	return; \
+} while (0)
+
+#define PROMISE_BUBBLE_UP(dst, src, ...) do { \
+	if (strlen(src->result.failure_result.message.buf) != 0) { \
+		strbuf_insertf(&src->result.failure_result.message, 0, "\n\t"); \
+		strbuf_insertf(&src->result.failure_result.message, 0, _("caused by:")); \
+		strbuf_insertf(&src->result.failure_result.message, 0, "\n"); \
+		strbuf_insertf(&src->result.failure_result.message, 0, __VA_ARGS__); \
+	} \
+	promise_reject(dst, src->result.failure_result.status, "%s", src->result.failure_result.message.buf); \
+	promise_release(src); \
+	return; \
+} while (0)
+
+#endif
-- 
gitgitgadget
Previous: Philip Peterson via GitGitGadgetNext: Philip Peterson via GitGitGadget
Message 2 of 13 in “promise: introduce promises to track success or error”
  1. 0/5 promise: introduce promises to track success or errorPhilip Peterson via GitGitGadget, Feb 18, 2024
  2. 1/5 promise: add promise pattern to track success/error from operationsPhilip Peterson via GitGitGadget, Feb 18, 2024
  3. 3/5 apply: update t4012 test suitePhilip Peterson via GitGitGadget, Feb 18, 2024
  4. 2/5 apply: use new promise structures in git-apply logic as a proving groundPhilip Peterson via GitGitGadget, Feb 18, 2024
  5. 4/5 apply: pass through quiet flag to fix t4150Philip Peterson via GitGitGadget, Feb 18, 2024
  6. 5/5 am: update test t4254 by adding the new error textPhilip Peterson via GitGitGadget, Feb 18, 2024
  7. Phillip WoodFeb 19, 2024
  8. Jeff KingFeb 20, 2024
  9. Phillip WoodFeb 20, 2024
  10. Patrick SteinhardtFeb 20, 2024
  11. Junio C HamanoFeb 20, 2024
  12. Jeff KingFeb 21, 2024
  13. PhilipMar 12, 2024

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.